Complete Guide to Number Formatting - Style, Compact, Precision
The number parameter type formats numbers with flexible options for style, precision, and compact notation. It supports various format styles including decimal, integer, percent, currency, and unit.
The numberFormat object controls how numbers are displayed:
{
"paramName": "amount",
"type": "number",
"numberFormat": {
"style": "decimal", // decimal, integer, percent, currency, unit
"percent": "percent", // percent, permille
"compact": "short", // "short", "long" - adds K/W/M suffix
"precisionLength": 2, // number of decimal places, -1 for auto
"precisionMode": "max", // "max" to strip trailing zeros
"unit": "kg", // unit suffix for "unit" style
"currency": "USD" // currency code for "currency" style
}
}
The style option specifies the number format type.
| Style | Description | Default Precision | Example |
|---|---|---|---|
| decimal | Standard decimal number formatting | -1 (auto) | 1,234.56 |
| integer | Integer without decimals | 0 | 1234 |
| percent | Percentage formatting (multiplied by 100) | 2 | 12.34% |
| currency | Currency formatting with symbol | 2 | $1,234.56 |
| unit | Number with custom unit suffix | -1 (auto) | 100kg |
The precisionLength option controls the number of decimal places:
| precisionLength | Behavior |
|---|---|
-1 |
Auto - uses style-specific default (integer=0, percent/currency=2, others=-1) |
0 |
No decimal places |
2 |
Exactly 2 decimal places |
3 |
Exactly 3 decimal places |
When precisionMode is set to "max", trailing zeros after the decimal point are stripped.
Input: 1234.50 with precisionLength: 2, precisionMode: "max"
Output: "1234.5" (trailing zero removed)
Input: 1234.00 with precisionLength: 2, precisionMode: "max"
Output: "1234" (decimal point also removed)
The percent option controls whether to use percent or permille:
| percent | Multiplier | Symbol | Example |
|---|---|---|---|
"percent" |
100 | % | 0.25 → "25%" |
"permille" |
1000 | ‰ | 0.25 → "250‰" |
When compact is set to "short" or "long", large numbers are abbreviated with a suffix:
// Input: 1234567 with compact: "short"
// Output: "1.234567M"
// Input: 12345 with compact: "short"
// Output: "1.2345W"
// Input: 1234 with compact: "short"
// Output: "1.234K"
// Input: 999 with compact: "short"
// Output: "999" (no compacting applied)
Different programming languages provide different levels of support for number formatting features.
| Language | Styles | Compact | Precision | Percent |
|---|---|---|---|---|
| Swift | decimal, integer, percent, currency, unit | ✅ K/W/M | ✅ | ✅ percent/permille |
| Python | decimal, integer, percent, currency, unit | ✅ K/W/M | ✅ | ✅ percent/permille |
| TypeScript | decimal, integer, percent, currency, unit | ✅ K/W/M | ✅ | ✅ percent/permille |
| Java | decimal, integer, percent, currency, unit | ✅ K/W/M | ✅ | ✅ percent/permille |
static func formatNumber(_ value: Any, _ p: I18NParam) -> String {
let numberFormat = p.numberFormat
let style = numberFormat?.style ?? "decimal"
let percentStyle = numberFormat?.percent ?? "percent"
let compact = numberFormat?.compact ?? ""
let precisionLength = numberFormat?.precisionLength ?? -1
let precisionMode = numberFormat?.precisionMode ?? ""
let unit = numberFormat?.unit ?? ""
let num = (value as? NSNumber)?.doubleValue ?? Double("\(value)") ?? 0.0
var displayNum = num
var suffix = ""
if style == "percent" {
let multiplier = percentStyle == "permille" ? 1000.0 : 100.0
displayNum = num * multiplier
}
if !compact.isEmpty && displayNum >= 1000 {
if displayNum >= 1_000_000 {
displayNum = displayNum / 1_000_000
suffix = "M"
} else if displayNum >= 10_000 {
displayNum = displayNum / 10_000
suffix = "W"
} else {
displayNum = displayNum / 1_000
suffix = "K"
}
}
let effectivePrecision = precisionLength >= 0 ? precisionLength
: style == "integer" ? 0
: (style == "percent" || style == "currency") ? 2
: -1
var formatted: String
if effectivePrecision >= 0 {
let formatter = NumberFormatter()
if style == "decimal" { formatter.numberStyle = .decimal }
if style == "integer" { formatter.numberStyle = .none }
if style == "currency" { formatter.numberStyle = .currency }
formatter.minimumFractionDigits = effectivePrecision
formatter.maximumFractionDigits = effectivePrecision
formatted = formatter.string(from: NSNumber(value: displayNum))
?? String(format: "%.\(effectivePrecision)f", displayNum)
if precisionMode == "max" && formatted.contains(".") {
while formatted.hasSuffix("0") && formatted.contains(".") {
formatted.removeLast()
}
if formatted.hasSuffix(".") { formatted.removeLast() }
}
} else {
if displayNum == Double(Int(displayNum)) && displayNum.isFinite {
formatted = String(Int(displayNum))
} else {
formatted = String(displayNum)
}
}
let body = formatted + suffix
if style == "percent" {
if percentStyle == "permille" { return body + "‰" }
return body + "%"
}
if style == "currency" {
let cf = NumberFormatter()
cf.numberStyle = .currency
return cf.string(from: NSNumber(value: displayNum)) ?? body
}
if style == "unit" { return body + unit }
return body
}
@staticmethod
def format_number(value: float, p: dict) -> str:
opts = p.get("numberFormat", {})
style = opts.get("style", "decimal")
precision = opts.get("precisionLength", -1)
precision_mode = opts.get("precisionMode", "")
compact = opts.get("compact", "")
num = float(value)
display_num = num
suffix = ""
if style == "percent":
multiplier = 1000.0 if opts.get("percent") == "permille" else 100.0
display_num = num * multiplier
if compact and display_num >= 1000:
if display_num >= 1_000_000:
display_num = display_num / 1_000_000
suffix = "M"
elif display_num >= 10_000:
display_num = display_num / 10_000
suffix = "W"
else:
display_num = display_num / 1_000
suffix = "K"
effective_precision = precision if precision >= 0 else (
0 if style == "integer" else (
2 if style in ("percent", "currency") else -1))
if effective_precision >= 0:
formatted = f"{display_num:.{effective_precision}f}"
if precision_mode == "max" and "." in formatted:
formatted = formatted.rstrip("0").rstrip(".")
else:
if display_num == int(display_num) and not math.isinf(display_num):
formatted = str(int(display_num))
else:
formatted = str(display_num)
body = formatted + suffix
if style == "percent":
return body + "%"
if style == "currency":
return (opts.get("currency") or "¥") + body
if style == "unit":
return body + (opts.get("unit") or "")
return body
{
"paramName": "amount",
"type": "number",
"numberFormat": {
"style": "decimal",
"precisionLength": 2,
"precisionMode": "max"
}
}
Input: 1234.50 → Output: "1234.5"
Input: 1234.00 → Output: "1234"
{
"paramName": "discount",
"type": "number",
"numberFormat": {
"style": "percent",
"percent": "percent",
"precisionLength": 2
}
}
Input: 0.25 → Output: "25%"
Input: 0.1234 → Output: "12.34%"
{
"paramName": "ratio",
"type": "number",
"numberFormat": {
"style": "percent",
"percent": "permille"
}
}
Input: 0.25 → Output: "250‰"
{
"paramName": "price",
"type": "number",
"numberFormat": {
"style": "currency",
"currency": "USD"
}
}
Input: 1234.56 → Output: "$1234.56" (Swift with locale)
Input: 1234.56 → Output: "¥1234.56" (Python default)
{
"paramName": "views",
"type": "number",
"numberFormat": {
"style": "decimal",
"compact": "short"
}
}
Input: 1500000 → Output: "1.5M"
Input: 15000 → Output: "1.5W"
Input: 1500 → Output: "1.5K"
{
"paramName": "weight",
"type": "number",
"numberFormat": {
"style": "unit",
"unit": "kg"
}
}
Input: 100 → Output: "100kg"
Compact and percent styles can be combined. The percent multiplier is applied first, then compact notation:
{
"paramName": "conversion",
"type": "number",
"numberFormat": {
"style": "percent",
"compact": "short"
}
}
Input: 0.15 → Output: "15%" (not compacted, below 1000 after multiplier)
Input: 1.5 → Output: "150%"