# Number

> One configurable numeric type covering integers, decimals, money and percentages. It replaced dedicated integer and money types — do not reintroduce those.

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/technical/property-types/number. Last updated 2026-07-27.

---
One configurable numeric type covering integers, decimals, money and percentages. It **replaced**
dedicated `integer` and `money` types — do not reintroduce those.

**Storage kind:** `column` · **Column:** `int` for the `integer` variant, `decimal(20,4)` otherwise

Use it for anything the code will do arithmetic on or compare numerically. Do **not** use it for an
identifier that happens to be an integer — a foreign key belongs in [`join`](https://docs.configbox.at/docs/technical/property-types/join), a country in
[`countryselect`](https://docs.configbox.at/docs/technical/property-types/geography-selects). Those store integers too, but the point of them is the
picker and the label lookup, and a `number` field is a text input.

## Settings

| Setting | Meaning |
|---|---|
| `numberType` | `integer` \| `decimal` \| `money` \| `percent`. Default `decimal` |
| `decimals` | Digits after the point. `0` for integer |
| `min` / `max` | Range, validated in `check()` |
| `signed` | Allow negatives. **Form validation only** |
| `nullable` | Keep NULL for an empty value instead of coercing it to `0` |
| `unit` | Suffix on output, e.g. `kg`, `%` |
| `symbol` | Money only: `true` for the base-currency symbol, or an explicit string |
| `thousands` | Group integer digits on output with the localised separator |

`numberType` is a **preset**: it fills the others with sensible values, and any explicitly declared
key wins over the preset.

| Variant | decimals | signed | unit | symbol | min / max |
|---|---|---|---|---|---|
| `integer` | 0 | true | — | no | — |
| `decimal` *(default)* | 2 | true | — | no | — |
| `money` | 2 | false | — | yes | — |
| `percent` | 2 | false | `%` | no | **0 / 100** |

`percent` is the only variant that presets a range. If you need a percentage that may exceed 100 —
a markup, an index — declare `'max' => null` explicitly alongside it.

## Choosing the variant

- **`integer`** — counts, quantities, positions. Stored in an `int` column, so nothing after the
  decimal point survives.
- **`decimal`** — measurements, factors, anything numeric that is not currency. The default, and
  signed, because a measurement can be negative.
- **`money`** — prices and amounts. Adds the base-currency symbol on output.
- **`percent`** — rates and discounts. Adds `%` and clamps to 0–100.

## `signed` does not make the column unsigned

It governs **validation**: `check()` refuses a negative for an unsigned field with a clear message
rather than silently flooring it. The column's signedness comes from `dataType`.

Deriving one from the other mistyped 33 money columns, because most money columns here are
deliberately signed: discounts, price overrides and import adjustments go negative. `money` presets
`signed => false` for the common case — a product price — so any money field that can legitimately
go negative must say `'signed' => true`.

Note the asymmetry between validation and normalisation: `check()` reports the negative, but a value
that reaches `normalize()` on an unsigned field is floored to zero rather than kept. Validation is
the gate; do not rely on the floor to tell you something went wrong.

## Notes

- Values are normalised from the localised decimal mark on input and stored with a dot, so the raw
  column value is always canonical. Grouping characters, currency symbols and units are stripped on
  the way in, so a pasted `"1.234,56 €"` parses.
- Empty becomes `0` unless `'nullable' => true`. On a nullable field an unset number stays NULL,
  which is what you want when "no value" and "zero" mean different things — a price ceiling, say.
- Rendering falls back to the `string` template, so `maxLength` and `style` are accepted but rarely
  meaningful.

@see string.md — `stringType => 'number'` is the old way; do not declare a new one
@see ../com_configbox_property_definition_settings.md §2
