vn-money
Vietnamese money on integers, because VND has no minor unit. Format, parse, apportion without losing a single dong, and read an amount out in Vietnamese words. Standard library only, zero runtime dependencies.
pip install vn-money
from vn_money import format_vnd, parse_vnd, allocate, to_words, is_vnd
format_vnd(12345678) # '12.345.678 ₫' (U+00A0, then U+20AB)
parse_vnd("12.345.678") # 12345678
parse_vnd("1.5") # VndError, reason 'has-decimal'
allocate(1_000_000, [1, 1, 1]) # [333334, 333333, 333333] — sums back exactly
to_words(15000) # 'mười lăm nghìn'
is_vnd(0.1 + 0.2) # False
The TypeScript port is @vntoolkit/vn-money
and the two are held to the same
conformance suite — 1232 cases, read by
both. That suite is the contract; this package is one implementation of it.
Why integers
ISO 4217 gives VND a minor unit of 0. There is no half a dong, and a price that arrives with a decimal point is either an error or a different currency.
So a fractional amount is refused, not rounded:
parse_vnd("1.5") # VndError('has-decimal')
is_vnd(1.5) # False
A money parser that guesses is worse than one that fails: under a
dot-as-separator reading "1.5" is either fifteen hundred or a typo, and the two
are not interchangeable on an invoice.
The default format matches ICU byte for byte, and every formatting case in the
suite was read out of Intl.NumberFormat('vi-VN', {style: 'currency', currency: 'VND'}) rather than written by hand. vi-VN groups thousands with a dot
and separates decimals with a comma — the same two characters as en-US with the
opposite meaning, which is the single easiest way to produce a wrong number that
still looks fine.
Errors carry a reason
VndError subclasses ValueError, and .reason is a closed set of strings rather
than a message, because the conformance suite compares it across two languages and
a caller needs to branch on it:
try:
parse_vnd(user_input)
except VndError as exc:
if exc.reason == "has-decimal":
form.errors["amount"] = "VND không có phần thập phân"
empty · has-decimal · unexpected-character · out-of-range · not-integer ·
empty-shares · invalid-shares
Reading an amount out
to_words follows one stated convention among several published ones, and pins it
case by case in the suite. mốt and tư need a tens digit of at least 2, so
11, 14 and 15 are where a careless implementation shows up:
to_words(15) # 'mười lăm'
to_words(14) # 'mười bốn'
to_words(105) # 'một trăm linh năm'
to_words(1001) # 'một nghìn không trăm linh một'
to_words(-1) # 'âm một'
If your house style differs, that is an issue to open, not a case to edit.
Validation
pytest packages/vn-money-py/tests -q
python packages/vn-money-py/tests/test_vn_money.py # needs nothing installed
The parity check runs both real ports over 17091 operations and compares the answers directly, which covers the inputs nobody thought to write a case for:
node scripts/verify-money-parity.mjs
# money parity: 17091/17091 operations identical across both ports
Notes for this port
bool is rejected wherever an int is expected, even though it is a subclass:
True is not an amount, and accepting it lets a form's checkbox through as a
price. format_vnd takes keyword arguments rather than an options object, because
that is how the language already does optional arguments.
SYMBOL_SPACER is chr(0x00A0) and CURRENCY_SYMBOL is the literal ₫
(U+20AB). The spacer is built from a code point on purpose — a literal no-break
space is invisible in an editor, so it survives review and is then silently
replaced by a reformat, and the formatter stops matching ICU by one code point.
Licence
MIT
Metadata
Release files for vn-money 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vn_money-0.1.0.tar.gz | 13.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vn_money-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 25.1 kB
Release files / vn_money-0.1.0.tar.gz
| Download URL | vn_money-0.1.0.tar.gz |
|---|---|
| Size | 13.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d50615ef27e279a8fd5a0b4594c7d269bbecf8262cfbe9072620381645476ad
|
|
BLAKE2b-256 checksum How to use checksums |
dcb72aebdbe92c20bfd3721fc3380a369fe467900e517ea4c144d308bdfa435c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / vn_money-0.1.0-py3-none-any.whl
| Download URL | vn_money-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
09d69b835dc1f239e622621d33e2804b8b9550358dca0a46e6a7c4e08be6d0ce
|
|
BLAKE2b-256 checksum How to use checksums |
30a4f9fda3138f711cd16392b290676f4b2ddfc1a422323890ab403d250c3c69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|