Runtime verified Test scripts included

Syntax

FormatNumber(number, format[, locale])  →  string
2–3 arguments

Parameters

Name Type Required Description
number string | number Yes Value to format; a numeric string works and may carry thousands separators
format string Yes Standard pattern letter with an optional precision digit, or a custom pattern
locale string No Locale supplying separators and the currency symbol; hyphen or underscore

Example

Two decimals with thousands separators, in the reader’s own locale:

%%[
  VAR @total, @locale
  SET @total = 1234.555
  SET @locale = AttributeValue("Locale")
]%%
Total: %%=FormatNumber(@total, "N2", @locale)=%%

With de-DE that renders 1.234,56; with en-US, 1,234.56.

A precision digit after the letter overrides the default, so N0 gives a whole number:

%%=FormatNumber(1234.555, "N0")=%%

Check the pattern carefully — a typo does not raise anything, it prints:

%%=FormatNumber(1234.555, "qqqq")=%%

That renders the literal text qqqq to the reader.

Return value

string — the number rendered with the requested pattern.

The domain is open, so there is no set of literals to test for. When the pattern is not recognised the pattern itself comes back instead.

Behaviour

Pattern letters are case-insensitive. Lowercase c and n produced exactly the same output as their capitals. Neither reference mentions this.

A precision digit sets the decimal places. N0, N2 and N3 on the same input gave 1,235, 1,234.56 and 1,234.555.

An unrecognised pattern is echoed back at HTTP 200. There is no error and no empty string — the pattern string is what the reader sees, which makes a typo easy to miss in review.

D and X need a whole number. On the integer 123 they gave 123 and 7B.

A numeric string is accepted and formats identically to the number.

Negatives depend on the pattern family. N2 produced -1,234.56 with a minus sign, but C2 produced ($1,234.56) in brackets. Code that strips a leading minus to detect a negative misses the currency case entirely.

Rounding is half-up, not banker’s rounding

This is the single most likely wrong assumption, because .NET’s own Math.Round defaults the other way.

Input Pattern Result Banker’s rounding would give
2.5 N0 3 2
3.5 N0 4 4
-2.5 N0 -3 -2
1.005 N2 1.01 1.00
1.004 N2 1.00 1.00

Halves always move away from zero. For financial output that is usually what is wanted, but it must not be assumed to match a rounding step performed elsewhere in the pipeline.

Standard patterns on the same input

Every result below is 1234.555 formatted with the default locale:

Pattern Result
C $1,234.56
E 1.234555E+003
F 1234.56
G 1234.555
N 1,234.56
P 123,455.50%
R 1234.555
N0 1,235
N3 1,234.555
C0 $1,235

P treats the value as a ratio and multiplies it by 100 — passing an already-computed percentage through P inflates it a hundredfold.

Custom patterns

Pattern Input Result
0000 -1234.555 -1235
###0 -1234.555 -1235
#0.00 -1234.555 -1234.56
#,##0.00 1234.555 1,234.56
#0.00;(#0.00) -1234.555 (1234.56)

The two-section form selects its second section for negative values, which is how a bracketed negative is produced without a currency pattern.

Locales accept more names than either reference lists

Locale argument Result for N2
en-US 1,234.56
en_US 1,234.56
de-DE 1.234,56
fr-FR 1 234,56
ja-JP 1,234.56
hi-IN 1,234.56
de 1.234,56
zz-ZZ 1,234.56
"" 1,234.56

The underscore form works, a language-only code works, and an unknown code falls back to the US format rather than aborting — so a bad locale value fails silently rather than loudly. The fr-FR group separator is a non-breaking space (U+00A0), not a plain space; string comparisons against that output need to expect it.

With a currency pattern the locale also supplies the symbol and its position: C2 with de-DE gave 1.234,56 €, and C with ja-JP gave ¥1,235 — leading symbol, no decimals.

Show test script
%%[
  VAR @b, @n, @ns, @neg
  SET @b = RequestParameter("b")
  SET @n = 1234.555
  SET @ns = "1234.555"
  SET @neg = -1234.555

  /* known-good control: renders on every request, so a run of HTTP 422s
     can be told apart from a broken deploy */
  OutputLine(Concat("CTRL=[", Uppercase("ok"), "]"))

  /* every standard pattern letter, plus precision digits and lowercase */
  IF @b == "s1" THEN
    OutputLine("--- s1 start ---")
    OutputLine(Concat("C=[", FormatNumber(@n, "C"), "]"))
    OutputLine(Concat("E=[", FormatNumber(@n, "E"), "]"))
    OutputLine(Concat("F=[", FormatNumber(@n, "F"), "]"))
    OutputLine(Concat("G=[", FormatNumber(@n, "G"), "]"))
    OutputLine(Concat("N=[", FormatNumber(@n, "N"), "]"))
    OutputLine(Concat("P=[", FormatNumber(@n, "P"), "]"))
    OutputLine(Concat("N0=[", FormatNumber(@n, "N0"), "]"))
    OutputLine(Concat("N3=[", FormatNumber(@n, "N3"), "]"))
    OutputLine(Concat("C0=[", FormatNumber(@n, "C0"), "]"))
    OutputLine(Concat("lowc=[", FormatNumber(@n, "c"), "]"))
    OutputLine(Concat("lown=[", FormatNumber(@n, "n"), "]"))
    OutputLine(Concat("Dint=[", FormatNumber(123, "D"), "]"))
    OutputLine(Concat("Xint=[", FormatNumber(123, "X"), "]"))
    OutputLine(Concat("R=[", FormatNumber(@n, "R"), "]"))
    OutputLine("--- s1 done ---")
  ENDIF

  /* custom patterns, the half-up rounding cases and negative numbers */
  IF @b == "s2" THEN
    OutputLine("--- s2 start ---")
    OutputLine(Concat("zero=[", FormatNumber(@neg, "0000"), "]"))
    OutputLine(Concat("digit=[", FormatNumber(@neg, "###0"), "]"))
    OutputLine(Concat("dec=[", FormatNumber(@neg, "#0.00"), "]"))
    OutputLine(Concat("grp=[", FormatNumber(@n, "#,##0.00"), "]"))
    OutputLine(Concat("sect=[", FormatNumber(@neg, "#0.00;(#0.00)"), "]"))
    OutputLine(Concat("badfmt=[", FormatNumber(@n, "qqqq"), "]"))
    OutputLine(Concat("negN2=[", FormatNumber(@neg, "N2"), "]"))
    OutputLine(Concat("negC2=[", FormatNumber(@neg, "C2"), "]"))
    OutputLine(Concat("half25=[", FormatNumber(2.5, "N0"), "]"))
    OutputLine(Concat("half35=[", FormatNumber(3.5, "N0"), "]"))
    OutputLine(Concat("up=[", FormatNumber(1.005, "N2"), "]"))
    OutputLine(Concat("dn=[", FormatNumber(1.004, "N2"), "]"))
    OutputLine(Concat("neghalf=[", FormatNumber(-2.5, "N0"), "]"))
    OutputLine(Concat("numstr=[", FormatNumber(@ns, "N2"), "]"))
    OutputLine("--- s2 done ---")
  ENDIF

  /* locale names: hyphen and underscore, language-only, unknown, empty */
  IF @b == "s3" THEN
    OutputLine("--- s3 start ---")
    OutputLine(Concat("enUSdash=[", FormatNumber(@n, "N2", "en-US"), "]"))
    OutputLine(Concat("enUSunder=[", FormatNumber(@n, "N2", "en_US"), "]"))
    OutputLine(Concat("deDE=[", FormatNumber(@n, "N2", "de-DE"), "]"))
    OutputLine(Concat("frFR=[", FormatNumber(@n, "N2", "fr-FR"), "]"))
    OutputLine(Concat("jaJP=[", FormatNumber(@n, "N2", "ja-JP"), "]"))
    OutputLine(Concat("hiIN=[", FormatNumber(@n, "N2", "hi-IN"), "]"))
    OutputLine(Concat("deDEC2=[", FormatNumber(@n, "C2", "de-DE"), "]"))
    OutputLine(Concat("jaJPC=[", FormatNumber(@n, "C", "ja-JP"), "]"))
    OutputLine(Concat("bare=[", FormatNumber(@n, "N2", "de"), "]"))
    OutputLine(Concat("bogus=[", FormatNumber(@n, "N2", "zz-ZZ"), "]"))
    OutputLine(Concat("empt=[", FormatNumber(@n, "N2", ""), "]"))
    OutputLine("--- s3 done ---")
  ENDIF
]%%

Availability

Platform Available
Marketing Cloud Engagement Yes
Marketing Cloud Next Yes (since 67)

See also