Skip to content

CkMoney

Job: read, convert, format, round and split amounts of money the way payment processors handle them — safely, and without ever losing a cent.

Money data arrives in messy shapes: "$1,234.56", "USD 10.99", the accounting negative "(123.45)", a plain number from a database. CkMoney reads them all into decimal — the C# type built for money, which stores amounts exactly (unlike double, which stores close-enough approximations that drift in arithmetic). It also converts to and from the integer "minor units" Stripe charges in, and formats amounts exactly how PayPal expects them. When a value cannot be read, the number methods answer your fallback (0 unless you say otherwise), the string methods answer "", and Split answers an empty array — nothing throws unless you ask.

Currency codes follow ISO 4217 — the international standard of three-letter codes like "USD", "EUR", "JPY" that payment systems key on.

Quick look

using SafeCoder.CommonKit;
using SafeCoder.CommonKit.Enums;

CkMoney.Amount("$1,234.56");     // -> 1234.56
CkMoney.MinorUnits("$10.99");    // -> 1099  (the cents Stripe charges)
CkMoney.Format(10);              // -> "10.00"  (the shape PayPal expects)
CkMoney.Currency("usd");         // -> "USD"
CkMoney.Amount("banana");        // -> 0  (unreadable — the default fallback)
CkMoney.IsRounded("10.999");     // -> false  (more decimals than US dollars have)
CkMoney.IsAtLeast("$50.00", "$10.00");   // -> true
CkMoney.SameAmount("$1,234.56", 1234.56m);   // -> true
CkMoney.Multiply("$19.99", 1.0825);   // -> 21.64m  (with 8.25% tax)
CkMoney.Total(new[] { "$1,234.56", "(12.00)" });   // -> 1222.56m
CkMoney.Allocate(100.01m, new[] { 70, 30 });   // -> [70.01, 30]  (never loses a cent)
CkMoney.Divide("$100", 3);            // -> 33.33m
CkMoney.Subtract("$100.00", "$25.50");   // -> 74.50m
CkMoney.Convert("$100", 0.92m, "USD", "EUR");   // -> 92.00m  (at your rate)
CkMoney.Display(1234.5m, "EUR");      // -> "1,234.50 EUR"  (for people, not machines)
CkMoney.CurrencyOf("Romania");        // -> "RON"
CkMoney.Decimals("JPY");              // -> 0
CkMoney.NumericCode("EUR");           // -> "978"  (what a card network carries)
CkMoney.Currency("978");              // -> "EUR"
CkMoney.Explain("ten");   // -> "letters in the amount"

Methods

Amount

Read an amount from anything.

decimal Amount(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number of any type, or money text.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point. Comma declares the European mirror: dots group, the comma decides.
fallback decimal 0 What comes back when the value cannot be read.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when the value cannot be read.

Returns

When Default With throwIfInvalid: true
The value reads as an amount The exact amount: 10.99 The exact amount: 10.99
Rejected: missing, letters in the amount, or grouping that does not match the declared mark The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

A 0 answer can also be a real zero amount — set a distinctive fallback when you need to tell them apart.

Takes a number of any type, or text in the shapes real data comes in: plain "10.99", grouped "$1,234.56", the accounting negatives "(123.45)" and "123.45-". The symbols $, €, £, ¥ are ignored, and a known ISO 4217 code at either end ("USD 10.99", "10.99 EUR") is dropped. The amount comes back exactly as read — no rounding. Text naming two different codes, "USD 10.99 EUR", is rejected.

CkMoney.Amount("USD 10.99");     // -> 10.99
CkMoney.Amount("EUR 1,234.56");  // -> 1234.56
CkMoney.Amount("(123.45)");      // -> -123.45  (accounting negative)
CkMoney.Amount("123.45-");       // -> -123.45  (trailing minus)

By default a dot is the decimal point and commas group digits, as in the US. For European text ("1.234,56") you declare the mirror with the DecimalMark enum — it is never guessed, because "1.234" means two different numbers in the two notations:

CkMoney.Amount(
    input: "1.234,56",
    mark: DecimalMark.Comma
);   // -> 1234.56

CkMoney.Amount("1,23");          // -> 0  (comma decimal not declared — rejected, never misread)

A fallback replaces an unreadable value:

CkMoney.Amount(
    input: "junk",
    fallback: -1m
);   // -> -1

AmountOrNull

Null when there is nothing.

decimal? AmountOrNull(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool treatZeroAsMissing = true
)
Parameter Type Default What it does
input object? null The value: a number, or money text.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal mark in text.
treatZeroAsMissing bool true Read a zero amount as missing, so it answers null too; pass false to keep a zero as the real amount it sometimes is.

Returns

When You get
The value reads as an amount worth keeping The amount: 1234.50
Missing (null, DBNull), unreadable, or a zero with treatZeroAsMissing on null — this method never throws

Reads exactly like Amount, but answers null instead of a fallback when there is nothing usable: a missing value, unreadable text — or a zero, because in many database columns 0.00 really means "no amount". That last part is on by default and switched off with treatZeroAsMissing: false. Built for filling nullable columns honestly.

CkMoney.AmountOrNull("$1,234.50");   // -> 1234.50
CkMoney.AmountOrNull("junk");        // -> null
CkMoney.AmountOrNull("0.00");        // -> null  (zero counts as missing by default)

CkMoney.AmountOrNull(
    input: "0.00",
    treatZeroAsMissing: false
);   // -> 0.00  (a real zero, kept on request)

Processors like Stripe avoid decimal points entirely: they charge in minor units, a currency's smallest coin, as a whole number. US$10.99 is 1099 cents. The next two methods convert to and from that integer.

How many decimals a currency has comes from the built-in ISO 4217 table — and it is not always two. The Japanese yen has no smaller coin at all, so ¥1099 is 1099 minor units; the Bahraini dinar has three decimals, so 3.141 dinars is 3141.

MinorUnits

An amount into integer cents.

long MinorUnits(
    object? input = null,
    string? currency = "USD",
    int? decimals = null,
    int? scale = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    long fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
currency string? "USD" The ISO 4217 code deciding the scale — how many decimals make one minor unit.
decimals int? null How many decimals the amount may carry, in place of the currency's own — for a receiver that disagrees with ISO 4217, or a unit price kept at four. 0 to CkSettings.MaxMoneyDecimals (8); null for the currency's own.
scale int? null The power of ten the whole units are written at, when it is not the decimals — a receiver that writes a currency without decimals as if it had two (5 ISK as 500). null for the decimals.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback long 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount and the currency both read The minor units: 1099 The minor units: 1099
Rejected: an unknown currency, an unreadable amount, or one outside the range of minor units The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

More decimals than the currency has are rounded away from zero before converting.

MinorUnits converts an amount to that integer of minor units.

Most receivers count a currency's decimals the way ISO 4217 does, and the defaults are right. A few do not, and they say so in their documentation: one writes a currency that has no decimals as if it had two, so 5 ISK goes out as 500. Say it with scale, and the amount is still rounded to whole króna first. See Taking a payment for the cases known today.

CkMoney.MinorUnits(
    input: 5,
    currency: "ISK",
    scale: 2
);   // -> 500  (no decimals, written as two)
CkMoney.MinorUnits("$10.99");    // -> 1099  (USD is the default currency)

CkMoney.MinorUnits(
    input: "£99.50",
    currency: "GBP"
);   // -> 9950  (pence)

CkMoney.MinorUnits(
    input: 1099,
    currency: "JPY"
);   // -> 1099  (the yen has no decimals)

CkMoney.MinorUnits("10.005");    // -> 1001  (extra decimals round away from zero first)

FromMinorUnits

Integer cents back into an amount.

decimal FromMinorUnits(
    object? input = null,
    string? currency = "USD",
    int? decimals = null,
    int? scale = null,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The minor units: a whole number, or text holding one.
currency string? "USD" The ISO 4217 code deciding the scale.
decimals int? null How many decimals the amount may carry, in place of the currency's own — for a receiver that disagrees with ISO 4217, or a unit price kept at four. 0 to CkSettings.MaxMoneyDecimals (8); null for the currency's own.
scale int? null The power of ten the whole units are written at, when it is not the decimals — a receiver that writes a currency without decimals as if it had two (5 ISK as 500). null for the decimals.
fallback decimal 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The count and the currency both read The amount: 10.99 The amount: 10.99
Rejected: an unknown currency, or a count that is not a whole number The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

Minor units are whole by definition — a fractional count rejects rather than rounds.

Works like MinorUnits in reverse: it converts a count of minor units back into a decimal amount. Minor units are whole by definition, so FromMinorUnits rejects a fractional count like 1099.5 — that is bad data, and you get the fallback.

With scale, a count written at a finer scale than the currency's decimals reads back: 500 at scale 2 is 5 ISK. A count that would not be whole — 550 — is rejected as bad data.

CkMoney.FromMinorUnits(1099);    // -> 10.99

CkMoney.FromMinorUnits(
    input: 3141,
    currency: "BHD"
);   // -> 3.141  (the dinar has three)

Currency

Validate an ISO 4217 code.

string Currency(
    string? input = null,
    string? fallback = null,
    TextCase textCase = TextCase.Uppercase,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw code.
fallback string? null Backup code used when the value is rejected. It goes through the same cleaning and checking; a fallback that is itself rejected answers "".
textCase TextCase TextCase.Uppercase The casing the code is written in: TextCase.Lowercase for a receiver that takes "usd"; upper case, the ISO form, for any other value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is an ISO 4217 code the library knows The upper-case code: "USD" The upper-case code: "USD"
Rejected: missing, not three letters or three digits, or not a known code Empty string Throws CkMoneyException — carries Input, Expected and Reason

The table holds the active codes people are paid in — 158 of them, XCG, VED and KPW since 1.3.0 — plus the two most recently withdrawn, ANG and BGN, which stored records still carry. A long-retired code (DEM, FRF, ROL) rejects like any other word, and so do the fund and accounting codes nobody is paid in (BOV, CHE, USN, …).

Cleans a currency code and checks it against the built-in ISO 4217 table.

CkMoney.Currency("usd");         // -> "USD"
CkMoney.Currency("eur");         // -> "EUR"
CkMoney.Currency("USD", textCase: TextCase.Lowercase);   // -> "usd"
CkMoney.Currency("yen");         // -> ""  (an everyday word, not the ISO code — that's "JPY")

CkMoney.Currency(
    input: "xyz",
    fallback: "USD"
);   // -> "USD"

CurrencyOf

The currency a country uses.

string CurrencyOf(
    string? country = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
country string? null The country: an ISO 3166-1 code, two or three letters, or a name — read the way CkCountry.Alpha2 reads one.
fallback string? null A currency code that replaces an empty result, once it has been through the same validation Currency gives one.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning an empty string, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The country reads and has a currency on record The upper-case code: "RON" The upper-case code: "RON"
Rejected: not a country, or a place with no currency on record, and no usable fallback Empty string Throws CkMoneyException — carries Input, Expected and Reason

For defaulting a form or a price list from the customer's country. Where a country has more than one legal tender the one in everyday use answers: Panama is PAB, El Salvador USD, Bhutan BTN, Haiti HTG, Lesotho LSL, Namibia NAD, Uruguay UYU, Venezuela VES — and where ISO 4217 lists none, the one in everyday use: Palestine ILS, South Georgia GBP. Antarctica has a country code and no currency, so it is rejected.

CkMoney.CurrencyOf("ro");             // -> "RON"
CkMoney.CurrencyOf("Germany");        // -> "EUR"
CkMoney.CurrencyOf("JPN");            // -> "JPY"
CkMoney.CurrencyOf("Atlantis");       // -> ""

CkMoney.CurrencyOf(
    country: "Atlantis",
    fallback: "usd"
);   // -> "USD"

NumericCode

The three digits card networks carry.

string NumericCode(
    string? currency = null,
    string fallback = "",
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
currency string? null The ISO 4217 code: three letters in any casing, or three digits.
fallback string "" What comes back when the code cannot be used, as given.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when the code cannot be used.

Returns

When Default With throwIfInvalid: true
The code is one the library knows Its numeric code, three digits: "978" Its numeric code, three digits: "978"
Rejected: missing, not three letters or three digits, or not a known code The fallback (default empty) Throws CkMoneyException — carries Input, Expected and Reason

Stripe and PayPal speak letters — "eur", "EUR". A card network, an acquirer's settlement file or a bank's ISO 8583 message speaks digits: field 49 of an authorisation and EMV tag 5F2A carry 978 for the euro. NumericCode writes them, as text, because the zeros count: the Albanian lek is "008".

The way back needs no method of its own. Currency, and the currency parameter of every method, read three digits as the letters they stand for — so a value taken straight from such a message works everywhere:

CkMoney.NumericCode("eur");      // -> "978"
CkMoney.NumericCode("ALL");      // -> "008"  (three digits, the zeros kept)
CkMoney.NumericCode("lei");      // -> ""

CkMoney.Currency("978");         // -> "EUR"

CkMoney.MinorUnits(
    input: "1099",
    currency: "392"
);   // -> 1099  (392 is JPY: no decimals)

ISO hands a number on when one currency takes another's place: ANG and the XCG that replaced it in 2025 are both "532", and "532" reads as XCG, the currency in use. Digits inside an amount are never a currency — "978 10.99" is a number, not ten euros.

Decimals

How many decimals a currency has.

int Decimals(
    string? currency = null,
    int fallback = -1,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
currency string? null The ISO 4217 code, in any casing.
fallback int -1 What comes back when the code cannot be used — not 0, which is a real answer.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The code is one the library knows Its decimals, 0 to 4: 2 Its decimals, 0 to 4: 2
Rejected: missing, not three letters, or not a known code The fallback (default -1) Throws CkMoneyException — carries Input, Expected and Reason

What a form needs for its input step, and a message for its wording ("at most two decimals"). It is the number Format writes with and MinorUnits scales by.

CkMoney.Decimals("usd");   // -> 2
CkMoney.Decimals("JPY");   // -> 0   (the yen has no smaller coin)
CkMoney.Decimals("BHD");   // -> 3
CkMoney.Decimals("lei");   // -> -1  (a word, not a code)

Format

Write it the way an API expects.

string Format(
    object? input = null,
    string? currency = "USD",
    int? decimals = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
currency string? "USD" The ISO 4217 code deciding the decimals.
decimals int? null How many decimals the amount may carry, in place of the currency's own — for a receiver that disagrees with ISO 4217, or a unit price kept at four. 0 to CkSettings.MaxMoneyDecimals (8); null for the currency's own.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback string? null An amount to use when the input cannot be read, put through the same reading and formatting; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering "", when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount and the currency both read The formatted text: "10.00" The formatted text: "10.00"
Rejected: an unknown currency, or an unreadable amount with no usable fallback Empty string Throws CkMoneyException — carries Input, Expected and Reason

The fallback stands in for an unreadable amount only — an unknown currency always answers empty.

Writes an amount with exactly its currency's decimals: plain digits, a dot, no grouping, no symbol — the shape PayPal's API wants ("10.00", never "10"). Extra decimals are rounded away from zero first.

With decimals, the text has that many decimals in place of the currency's own — 0 for a receiver that refuses a decimal forint amount, 4 for a unit price.

CkMoney.Format(
    input: "1000.50",
    currency: "HUF",
    decimals: 0
);   // -> "1001"
CkMoney.Format(10);              // -> "10.00"  (USD is the default)

CkMoney.Format(
    input: "99.5",
    currency: "GBP"
);   // -> "99.50"

CkMoney.Format(
    input: 1099,
    currency: "JPY"
);   // -> "1099"  (no decimals at all)

CkMoney.Format(
    input: 3.1m,
    currency: "BHD"
);   // -> "3.100"

Display

Write it the way a person reads it.

string Display(
    object? input = null,
    string? currency = "USD",
    DecimalMark into = CkSettings.DefaultDecimalMark,
    bool showCurrency = true,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    string fallback = "",
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
currency string? "USD" The ISO 4217 code whose decimals apply. Money text that names another currency is refused.
into DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in the text written; the other one groups the digits.
showCurrency bool true Write the currency code after the amount.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in the text read.
fallback string "" What comes back when a value cannot be used, as given.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount and the currency both read The text: "1,234.50 EUR" The text: "1,234.50 EUR"
Rejected: an unknown currency, or an unreadable amount The fallback (default empty) Throws CkMoneyException — carries Input, Expected and Reason

Format is the shape for machines; this is the one for a screen, an e-mail, a PDF: digits grouped in threes, exactly the currency's decimals, the code after it. The code and not a symbol, on purpose — $ is a dozen currencies, and .NET's ToString("C") takes its symbol from the culture, so it prints a dollar sign on a euro amount. No culture is consulted here. What Display writes, Amount reads back under the same mark.

CkMoney.Display(1234.5m, "EUR");      // -> "1,234.50 EUR"
CkMoney.Display(1099, "JPY");         // -> "1,099 JPY"

CkMoney.Display(
    input: 1234.5m,
    currency: "EUR",
    into: DecimalMark.Comma
);   // -> "1.234,50 EUR"

CkMoney.Display(
    input: "(1234.5)",
    showCurrency: false
);   // -> "-1,234.50"

Round

To the currency's decimals, away from zero.

decimal Round(
    object? input = null,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
currency string? "USD" The ISO 4217 code deciding the decimals.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback decimal 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount and the currency both read The rounded amount: 10.01 The rounded amount: 10.01
Rejected: an unknown currency, or an unreadable amount The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

Rounds by CkSettings.MoneyRounding (MidpointRounding.AwayFromZero) — the same rule every CkMoney conversion uses.

Rounds an amount to its currency's decimals the accounting way: a midpoint moves away from zero, so US$10.005 becomes 10.01. (.NET's default is banker's rounding, which would round that midpoint to the nearest even digit — 10.00 — good for statistics, surprising on an invoice.)

CkMoney.Round(10.005m);          // -> 10.01  (USD is the default)

CkMoney.Round(
    input: "99.999",
    currency: "GBP"
);   // -> 100.00

CkMoney.Round(
    input: 10.5m,
    currency: "JPY"
);   // -> 11

Split

Divide without losing a cent.

decimal[] Split(
    object? input = null,
    int parts = 2,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
parts int 2 How many parts, 1 to CkSettings.MaxMoneySplitParts (1000).
currency string? "USD" The ISO 4217 code deciding the minor unit.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering an empty array, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount, the currency and the count all read The parts, in order: [33.34, 33.33, 33.33] The parts, in order: [33.34, 33.33, 33.33]
Rejected: an unreadable amount, an unknown currency, or a part count out of range Empty array — never null Throws CkMoneyException — carries Input, Expected and Reason

The amount is first rounded to the currency's decimals — parts can only differ by whole minor units.

Divides an amount into parts (2 by default, up to 1000) that always sum back to exactly the whole. Naive division of 100 by 3 gives three 33.33s and a lost cent; Split hands the extra minor units to the earlier parts instead.

CkMoney.Split(
    input: 100m,
    parts: 3
);   // -> [33.34, 33.33, 33.33]  (sums back to exactly 100.00)

CkMoney.Split(
    input: "£10",
    parts: 3,
    currency: "GBP"
);   // -> [3.34, 3.33, 3.33]

CkMoney.Split(
    input: -10m,
    parts: 3
);   // -> [-3.34, -3.33, -3.33]  (negatives split the same way)

Allocate

Split by ratio, never losing a cent.

decimal[] Allocate(
    object? input = null,
    object? ratios = null,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
ratios object? null The ratios: a list of numbers, none below zero and at least one above, up to CkSettings.MaxMoneySplitParts (1000). Only their proportions matter — 70 and 30, 7 and 3, 0.7 and 0.3 are one split.
currency string? "USD" The ISO 4217 code whose decimals apply. Money text that names another currency is refused.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning an empty array, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
Everything reads The parts, in the order of the ratios: [70.01, 30.00] The parts, in the order of the ratios: [70.01, 30.00]
Rejected: an unreadable amount or ratio, a ratio below zero, no ratios, all zeros, an unknown currency An empty array Throws CkMoneyException — carries Input, Expected and Reason

A revenue share, a discount spread over invoice lines by their value, a payment divided between accounts. Doing it with Multiply loses or invents cents — half of US$0.05, twice, is 0.03 + 0.03. Here each part gets its proportional share rounded down to the minor unit, and the units left over go one at a time to the parts that lost the most in the rounding (the largest-remainder method; a tie goes to the earlier part). A part whose ratio is zero gets nothing. Split is the same with equal ratios.

CkMoney.Allocate(100.01m, new[] { 70, 30 });      // -> [70.01, 30]
CkMoney.Allocate("$1.00", new[] { 2, 3, 1 });     // -> [0.33, 0.5, 0.17]
CkMoney.Allocate(0.05m, new[] { 1, 1 });          // -> [0.03, 0.02]  (never two 0.03s)
CkMoney.Allocate(1.00m, new[] { 0, 2, 1 });       // -> [0, 0.67, 0.33]

// a 25.00 discount over three invoice lines, by their value
CkMoney.Allocate(
    input: "25.00",
    ratios: new[] { 199.99m, 49.50m, 12.00m }
);   // -> [19.12, 4.73, 1.15]  (sums back to exactly 25.00)

IsAmount

Does it read as money at all?

bool IsAmount(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount.

Returns

When Default With throwIfInvalid: true
A number, or money text: "10.99", "$1,234.56", "(123.45)", "USD 10.99" true true
Rejected: not an amount — missing, letters, an undeclared decimal comma, an unknown code false Throws CkMoneyException — carries Input, Expected and Reason

The yes-or-no form of Amount. Zero is an amount here, whatever AmountOrNull says about it; when the value is going to be stored, read it with that method instead and check for null.

CkMoney.IsAmount("$1,234.56");   // -> true
CkMoney.IsAmount("1,23");        // -> false  (a decimal comma needs declaring)
CkMoney.IsAmount("1e5");         // -> false  (letters are junk in money)

IsCurrency

A known ISO 4217 code?

bool IsCurrency(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw code.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not a known code.

Returns

When Default With throwIfInvalid: true
Three letters in any casing that name a known code true true
Rejected: not three letters, or not a known code false Throws CkMoneyException — carries Input, Expected and Reason

The yes-or-no form of Currency; call that instead when you want the upper-case spelling back.

CkMoney.IsCurrency("usd");   // -> true
CkMoney.IsCurrency("US$");   // -> false

InCurrencies

One of the currencies we settle in?

bool InCurrencies(
    string? input = null,
    params IEnumerable<string?> codes
)
Parameter Type Default What it does
input string? null The raw code.
codes params IEnumerable<string?> — The codes to accept, in any casing. Entries that are not known codes are skipped.

Returns

When You get
The value is a known code and an entry names the same one true
It is not a known code, no entry matches, or the list is empty false — an answer, not a failure; it never throws

The value and every entry are read the way Currency reads them. A params list must be last, so there is no throwIfInvalid; call Currency with it first when you need the exception.

CkMoney.InCurrencies("usd", "USD", "EUR", "GBP");   // -> true
CkMoney.InCurrencies("JPY", "USD", "EUR", "GBP");   // -> false

IsRounded

Already at the currency's precision?

bool IsRounded(
    object? input = null,
    string? currency = "USD",
    int? decimals = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
currency string? "USD" The ISO 4217 code whose decimals apply.
decimals int? null How many decimals the amount may carry, in place of the currency's own — for a receiver that disagrees with ISO 4217, or a unit price kept at four. 0 to CkSettings.MaxMoneyDecimals (8); null for the currency's own.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount or the currency is not a known code.

Returns

When Default With throwIfInvalid: true
The amount carries no more decimals than the currency allows true true
It carries more false false
Rejected: not an amount, or an unknown currency false Throws CkMoneyException — carries Input, Expected and Reason

The pre-charge check: a processor rejects an amount it cannot express in minor units. 10.99 US dollars yes, 10.999 no; 1099 yen yes, 1099.5 no; 3.141 Bahraini dinars yes. Trailing zeros are not extra precision, so "10.990" passes. The same as Round(x) == Amount(x) in one call.

With decimals, the check is against that many: IsRounded("1000.50", "HUF", decimals: 0) is false — it fits the forint, and not a receiver that takes whole forint only.

CkMoney.IsRounded("10.99");            // -> true   (USD by default)
CkMoney.IsRounded("10.999");           // -> false
CkMoney.IsRounded("1099.5", "JPY");    // -> false

IsPositive

Above zero?

bool IsPositive(
    object? input = null,
    bool allowZero = false,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
allowZero bool false Count zero as well.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount.

Returns

When Default With throwIfInvalid: true
An amount above zero — or zero itself with allowZero true true
Zero (by default), or a negative amount, "(123.45)" included false false
Rejected: not an amount — missing, letters, an undeclared decimal comma, an unknown code false Throws CkMoneyException — carries Input, Expected and Reason

"A payment must be positive"; with allowZero: true, "a balance must not be negative". CkNumber.IsPositive refuses "$5", which is why this one exists.

CkMoney.IsPositive("$5");                       // -> true
CkMoney.IsPositive("$0.00");                    // -> false
CkMoney.IsPositive("$0.00", allowZero: true);   // -> true

IsNegative

Below zero?

bool IsNegative(
    object? input = null,
    bool allowZero = false,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
allowZero bool false Count zero as well.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount.

Returns

When Default With throwIfInvalid: true
An amount below zero — "(123.45)", "123.45-", "-$5" — or zero itself with allowZero true true
Zero (by default), or a positive amount false false
Rejected: not an amount — missing, letters, an undeclared decimal comma, an unknown code false Throws CkMoneyException — carries Input, Expected and Reason

"A refund must be negative", "a credit note". The mirror of IsPositive; IsNegative, IsZero and IsPositive partition the amounts.

CkMoney.IsNegative("(123.45)");   // -> true
CkMoney.IsNegative("$5");         // -> false

IsZero

Exactly zero?

bool IsZero(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount.

Returns

When Default With throwIfInvalid: true
An amount equal to zero: 0, "0.00", "$0", "(0.00)" true true
Any other amount false false
Rejected: not an amount — missing, letters, an undeclared decimal comma, an unknown code false Throws CkMoneyException — carries Input, Expected and Reason

AmountOrNull calls a zero amount "missing" by default; this says so out loud, for data-quality checks and for telling "nothing charged" from "not an amount".

CkMoney.IsZero("$0.00");   // -> true
CkMoney.IsZero("junk");    // -> false  (not an amount, not zero)

IsBetween

Inside a range?

bool IsBetween(
    object? input = null,
    object? min = null,
    object? max = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
min object? null The lower bound, inclusive, read the same way — "$10.00" works. null means no lower bound.
max object? null The upper bound, inclusive, read the same way. null means no upper bound.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says which value is not an amount and why — or that the bounds contradict each other — instead of answering false.

Returns

When Default With throwIfInvalid: true
The value and the given bounds read, and the value lies inside, both ends included true true
Everything reads, and the value lies outside false false
Rejected: an unreadable value or bound, or bounds that contradict each other false Throws CkMoneyException — carries Input, Expected and Reason

"Minimum order", "transfer limit". Bounds are read as amounts too, and the mark applies to all three; a null bound is open, so IsBetween(total, 10, null) is "at least ten".

CkMoney.IsBetween("$50.00", "$10.00", "$10,000.00");   // -> true
CkMoney.IsBetween("9.99", 10, 100);                    // -> false
CkMoney.IsBetween("50", 100, 10);                      // -> false  (contradictory bounds)

IsAtLeast

At least this much?

bool IsAtLeast(
    object? input = null,
    object? min = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
min object? null The lower bound, inclusive, read the same way.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when either value is not an amount.

Returns

When Default With throwIfInvalid: true
Both read, and the value is greater than or equal to min true true
Both read, and the value is below min false false
Rejected: either is not an amount false Throws CkMoneyException — carries Input, Expected and Reason

"A minimum order of $10", "a deposit of at least 100". The bound counts.

CkMoney.IsAtLeast("$10.01", "$10.00");   // -> true
CkMoney.IsAtLeast("9.99", 10);           // -> false

IsAtMost

At most this much?

bool IsAtMost(
    object? input = null,
    object? max = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
max object? null The upper bound, inclusive, read the same way.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when either value is not an amount.

Returns

When Default With throwIfInvalid: true
Both read, and the value is less than or equal to max true true
Both read, and the value is above max false false
Rejected: either is not an amount false Throws CkMoneyException — carries Input, Expected and Reason

"A transfer limit of $10,000", "a refund no larger than the payment". The bound counts.

CkMoney.IsAtMost("9,999.99", "$10,000");   // -> true
CkMoney.IsAtMost("10,000.01", 10000);      // -> false

SameAmount

The same sum, whatever the dressing?

bool SameAmount(
    object? a = null,
    object? b = null,
    decimal tolerance = 0,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a object? null The first value: a number, or money text.
b object? null The second value, read the same way.
tolerance decimal 0 How far apart the two may be and still count as the same. Below 0 acts as 0.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says which value is not an amount and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both read as amounts and differ by at most tolerance true true
Both read, and differ by more false false
Rejected: either is not an amount false Throws CkMoneyException — carries Input, Expected and Reason

The reconciliation check — "the payment equals the invoice" — comparing the amounts alone: "USD 10.99" and "10.99 EUR" are the same sum here, since the dressing is not compared. Pass a tolerance of 0.01m for feeds that round differently. Two values that are not amounts are never the same.

CkMoney.SameAmount("$1,234.56", 1234.56m);              // -> true
CkMoney.SameAmount("10.99", "10.98");                    // -> false
CkMoney.SameAmount("10.99", "10.98", tolerance: 0.01m);  // -> true

HasCurrency

Does the text say which currency?

bool HasCurrency(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or money text in any shape Amount reads.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering false, when the value is not an amount.

Returns

When Default With throwIfInvalid: true
An amount whose text carries a known code at either end or one of the symbols $ € £ ¥ true true
A bare amount, or a boxed number, which declares nothing false false
Rejected: not an amount — missing, letters, an undeclared decimal comma, an unknown code false Throws CkMoneyException — carries Input, Expected and Reason

For multi-currency intake, where a bare "10.99" is ambiguous and should be refused. It says that a currency was declared, not which: $ alone cannot tell US, Canadian and Australian dollars apart. The accounting wrapper does not hide the code, so "(USD 10.99)" declares one.

CkMoney.HasCurrency("USD 10.99");   // -> true
CkMoney.HasCurrency("$10.99");      // -> true
CkMoney.HasCurrency("10.99");       // -> false

Multiply

Tax, discounts, rounded the accounting way.

decimal Multiply(
    object? input = null,
    object? factor = null,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or money text.
factor object? null The multiplier, a rate such as 1.0825, not a percentage.
currency string? "USD" The ISO 4217 code whose decimals apply.
mark DecimalMark DecimalMark.Dot Which character is the decimal point in the amount.
fallback decimal 0 What comes back when a value cannot be read.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
The amount, the factor and the currency read The product, rounded to the currency's decimals away from zero The product
Rejected: any of them unusable The fallback Throws CkMoneyException — carries Input, Expected and Reason — Expected names which

The multiplication that goes wrong when it is done in double or rounded by halves-to-even. Rounded as Round rounds.

CkMoney.Multiply("$19.99", 1.0825);   // -> 21.64m  (the price with 8.25% tax)
CkMoney.Multiply(total, 0.9);         // -> a ten-percent discount

Divide

An amount by a number, rounded once.

decimal Divide(
    object? input = null,
    object? divisor = null,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text.
divisor object? null The divisor: a number, or text that reads as one; not zero. A plain number, not money.
currency string? "USD" The ISO 4217 code whose decimals apply. Money text that names another currency is refused.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback decimal 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
Everything reads The quotient, rounded to the currency: 33.33 The quotient, rounded to the currency: 33.33
Rejected: division by zero, an unreadable amount or divisor, an unknown currency The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

A unit price from a total and a quantity; a monthly figure from a yearly one. Multiply cannot do it exactly — a third has no exact decimal. To share an amount out so that the parts add back to the whole, use Split or Allocate: three 33.33s are 99.99.

CkMoney.Divide("$100", 3);          // -> 33.33
CkMoney.Divide(1099, 2, "JPY");     // -> 550  (whole yen)
CkMoney.Divide("10", 0);            // -> 0    (division by zero: the fallback)

Convert

To another currency, at your rate.

decimal Convert(
    object? input = null,
    object? rate = null,
    string? from = null,
    string? to = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount: a number, or text. Money text that names a currency must name from.
rate object? null The exchange rate, above zero: how many units of to one unit of from buys.
from string? null The ISO 4217 code the amount is in.
to string? null The ISO 4217 code to convert it to; its decimals decide the rounding.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback decimal 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
Everything reads The converted amount, rounded to to: 92.00 The converted amount, rounded to to: 92.00
Rejected: an unknown currency, a rate that is not above zero, an unreadable amount, text naming another currency than from The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

The library holds no rates: where they come from, and how fresh they are, is yours to decide. What it adds is the rounding for the target currency — dollars into yen lose their decimals — and the check that a value saying "100 EUR" is not converted from dollars.

CkMoney.Convert("$100", 0.92m, "USD", "EUR");      // -> 92.00
CkMoney.Convert(100, 157.35m, "USD", "JPY");       // -> 15735  (the yen has no decimals)
CkMoney.Convert("100 EUR", 0.92m, "USD", "EUR");   // -> 0  (the text says EUR, not USD)

Subtract

One amount less another.

decimal Subtract(
    object? input = null,
    object? amount = null,
    string? currency = "USD",
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The amount to take from: a number, or text.
amount object? null The amount to take away: a number, or text — money-dressed like the first.
currency string? "USD" The ISO 4217 code whose decimals apply. Money text that names another currency is refused, in either value.
mark DecimalMark CkSettings.DefaultDecimalMark (DecimalMark.Dot) Which character is the decimal point in text.
fallback decimal 0 What comes back when a value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of returning the fallback, when a value cannot be used.

Returns

When Default With throwIfInvalid: true
Both amounts and the currency read The difference, rounded to the currency: 74.50 The difference, rounded to the currency: 74.50
Rejected: an unreadable amount, an unknown currency, text naming another currency The fallback (default 0) Throws CkMoneyException — carries Input, Expected and Reason

A total less a discount, a charge less a refund. Both values are read as money, so either may carry a symbol or grouping; the difference is exact and rounded once. It goes below zero when the second is the larger. To add, use Total.

CkMoney.Subtract("$100.00", "$25.50");     // -> 74.50
CkMoney.Subtract(10, 25);                  // -> -15
CkMoney.Subtract("100 USD", "25 EUR");     // -> 0  (the second value names another currency)

Total

A column of money, summed exactly.

decimal Total(
    object? values = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    decimal fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
values object? null Any list or array of numbers or money text.
mark DecimalMark DecimalMark.Dot Which character is the decimal point.
fallback decimal 0 What comes back when the list is missing or an entry cannot be read.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkMoneyException that says why, instead of answering the fallback, when the list or an entry cannot be used.

Returns

When Default With throwIfInvalid: true
Every entry reads The exact sum; an empty list totals zero The sum
Rejected: no list, or an entry that is not an amount The fallback Throws CkMoneyException — carries Input, Expected and Reason — the reason names the entry

Strict on purpose: one entry that is not an amount rejects the whole total, since a sum that silently skipped a row is worse than none. A string is one value, not a list.

CkMoney.Total(new[] { "$1,234.56", "(12.00)", "9.99" });   // -> 1232.55m

Explain

Why not — in words, without an exception.

string Explain(
    object? input = null,
    DecimalMark mark = CkSettings.DefaultDecimalMark,
    string? currency = null
)
Parameter Type Default What it does
input object? null The value: a number, or money text.
mark DecimalMark DecimalMark.Dot Which character is the decimal point.
currency string? null The ISO 4217 code the amount is meant in. Given, the value is judged the way Format and MinorUnits judge it — known code, and no other currency named in the text. null judges the amount alone.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkMoney.Explain("ten");        // -> "letters in the amount"
CkMoney.Explain("1.234,56");   // -> "a comma after the decimal point (declare DecimalMark.Comma to read European notation)"

CkMoney.Explain(
    input: "1099 JPY",
    currency: "USD"
);   // -> "the text says JPY, the currency asked for is USD"

Good to know

  • A currency named in the text is checked, never just dropped (since 1.3.0). A code at either end of the text is dressing, and currency defaults to "USD" — so MinorUnits("1099 JPY") once answered 109900, yen scaled as dollars. Every method that is told a currency now refuses a value whose text names another; pass currency: "JPY" and it is 1099. The methods that compare or add several values — SameAmount, IsAtLeast, IsAtMost, IsBetween, Total — refuse two values that name different currencies: Total(new[] { "10 USD", "5 EUR" }) is the fallback, not 15, and SameAmount("50 EUR", "50 USD") is false. A value that names no currency goes with any. The symbols $ € £ ¥ never count: each is shared by several currencies.
  • Decimal commas are declared, never guessed. "1.234,56" reads only with mark: DecimalMark.Comma; without it the value is rejected, and dot-decimal text is rejected under Comma — so an amount is never silently misread as a different number.
  • AmountOrNull treats zero as missing by default. Pass treatZeroAsMissing: false when a zero is a real amount in your data.
  • Minor units use each currency's real ISO 4217 decimals — 2 for most, 0 for the yen family, 3 for the dinars. Never assume "times 100".
  • Round goes away from zero, not banker's-style — the accounting convention.
  • Split never loses a cent and the earlier parts carry the extras.
  • CkNumber will not read "$5" or "1,234" — money dressing is this class's job; plain numbers are CkNumber's.
  • Stray letters reject. Only known ISO codes are dropped from amount text, and "1e5" is not read as scientific notation — you get the fallback, not a surprise.
  • Everything is decimal and culture-invariant. The machine's locale is never consulted, so results are the same on every server. Pull raw values out of a payload bag with CkDictionary, then read them here.
  • The validators read money text and treat zero as an amount. IsPositive("$5") is the money spelling of CkNumber.IsPositive(5); IsZero("$0.00") is true even though AmountOrNull calls it missing. All of them answer false for anything unreadable, and every one but InCurrencies takes throwIfInvalid.
  • IsRounded is the pre-charge check. An amount with more decimals than its currency allows cannot become minor units without rounding; check it before MinorUnits, or let Round decide the rounding for you.

If you want errors instead

Every method except AmountOrNull and InCurrencies takes throwIfInvalid: true; a value that cannot be used then raises a CkMoneyException (from SafeCoder.CommonKit.Exceptions) instead of returning the fallback, "", false or the empty array. The exception carries the Input, what was Expected, and a short Reason, flattened into a one-line message. AmountOrNull never throws — null is its answer for anything unusable; when you want errors, read the value with Amount and throwIfInvalid: true instead.

CkMoney.Amount(
    input: "ten dollars",
    throwIfInvalid: true
);   // throws CkMoneyException — the message says why