thainum 0.5.3
thainum: ^0.5.3 copied to clipboard
A comprehensive Thai number toolkit: Thai numerals, number-to-words, baht text, formatting, Thai dates and times, and reverse parsing of Thai words to numbers.
Changelog #
0.5.3 #
- Fix web (dart2js) compilation:
formatInt/formatSatangno longer use a 64-bitint.minValueliteral (which dart2js cannot represent), detecting that edge value via negation-overflow instead. The package now compiles to web and is WASM-ready. No API or behavior change on the Dart VM.
0.5.2 #
Documentation only — no code change.
- Refreshed
example/example.dartto cover the features added across v0.3–v0.5 (tryParse*,speakDigits, thethaiDigits:flag,extractNumbers,parseDecimal, percent, qualifiers/idioms, typed money + JSON + rounding,spellShort/formatShort, Thai National/Tax ID, lottery and phone helpers, and structured error codes).
0.5.1 #
Internal performance pass (B8) — the A8th engine (the winning entry, #8 of 10, of an internal Perf-MAX optimizer competition; see the README "Performance" section). No API change, no behaviour change — output is byte-identical on every path (verified by an exhaustive baseline differential over ~1.8M format cases and ~170k parse/extract cases, plus the full 507-test suite incl. the round-trip property, golden, and go-thainum conformance vectors).
- Tokenizer rewrite —
_tokenizenow scans an integer index cursor with first-code-unit dispatch buckets instead of re-copying the tail viasubstringper token. This removes the super-linear (O(n²)) growth on long inputs (now linear) and speeds the wholeparse*family ~1.6–1.9×. extractNumbers— index-cursor matching (no per-positionsubstring) and dropping the per-token defensive list copy makes it ~7–8× faster on free text.format*native-int path —formatInt/formatSatang/formatThbbuild the grouped string with native-int code-unit buffers instead of aBigIntround-trip, folding thethaiDigitsconversion into the same pass. The money path (formatSatang/formatThb) is ~3–6× faster on the VM and disproportionately faster on dart2js/web (whereBigIntis emulated).int.minValuekeeps the exactBigIntfallback.
0.5.0 #
Additive release. All existing top-level functions keep byte-identical output
and the same exception message / toString().
-
Structured error codes + Thai messages + precise offsets (B6). New
enum ThaiNumErrorclassifies every parser/speller/baht/decimal/date/ID failure.ThaiNumExceptiongains aThaiNumError? code, aString get messageTh(Thai-language wording per code, falling back to the Englishmessagewhen no code is set), and aString describe()that renders a caret under the offending token. The Englishmessage,sourceandtoString()are unchanged, so existingcatch/assert call sites keep working. Token offsets are now threaded through the tokenizer and point at the start of the bad token in the digit-normalized input; the latentparseBahtsatang-part substring-offset bug is fixed (offsets are reported relative to the whole input, not the satang slice). Offsets are leftnullon thelenientpath, where interior characters were removed. -
Opt-in strict parse mode (B7).
parseInt/parseBigInt/parseBaht/parseDecimalacceptstrict: false(default). Withstrict: truea tens place fed byหนึ่ง(หนึ่งสิบ= 10) or by a plainสองwritten instead ofยี่(สองสิบ= 20) throwsThaiNumExceptionwithThaiNumError.nonStandardTensDigit. The default (lenient) path is unchanged and remains round-trip-safe; canonical forms (สิบ,ยี่สิบ,ยี่สิบเอ็ด) still parse understrict. -
Approximate / range qualifiers (B2). New
thaiApprox(n)→'ประมาณ…',thaiNearly(n)→'เกือบ…',thaiRange(a, b)→'…ถึง…', andthaiMoreThan(n)→'…กว่า'("-something"), composed onspell.กว่าis well-formed only on a round magnitude — a value>= 10that is a single non-zero leading digit followed by zeros (10, 20, …, 100, 500, 1000, 1000000, …);thaiMoreThanrejects anything else withThaiNumError.notRoundMagnitude(a new error code, with a Thai message). Plus the predicateisRoundMagnitude(n), anenum QualifierKind, andintextensionstoThaiApprox/toThaiNearly/toThaiMoreThan/toThaiRange/toThaiQualified.thaiApprox(100); // 'ประมาณหนึ่งร้อย' thaiNearly(100); // 'เกือบหนึ่งร้อย' thaiRange(10, 20); // 'สิบถึงยี่สิบ' thaiMoreThan(10); // 'สิบกว่า' (thaiMoreThan(11) throws — not a round magnitude) -
ครึ่ง / โหล / คู่ idioms (B3, opt-in, narrow).
spellDecimalgains an optionaluseHalfflag (defaultfalse→ output byte-identical) so an exact.5fraction reads'ครึ่ง':spellDecimal('2.5', useHalf: true)→'สองครึ่ง',spellDecimal('0.5', useHalf: true)→'ครึ่ง'. A non-.5fraction still reads digit-by-digit. New unit-word surfaceenum QuantityUnitwithquantityWord(unit)(→'ครึ่ง'/'คู่'/'โหล'/'กุรุส') andquantityValue(unit)(→0.5/2/12/144). A focused, separate parse helperparseQuantity(s)(returnsnum) recognises a bare'ครึ่ง'/'คู่'/'โหล'/'กุรุส', a Thai integer reading, and an integer + trailing'ครึ่ง'('สองครึ่ง'→2.5); it does not entangle the integer grammar or attempt general ลักษณนาม classifiers (so'สองโหล'is intentionally unsupported). PlusparseHalfBaht('ครึ่งบาท')→50satang. -
Value-type completeness + JSON on the money wrappers (B4).
Baht,Satang,BahtBigIntandSatangBigIntnowimplements Comparable<T>(compareToby the underlying value, so they sort and compare directly), gain a single-fieldcopyWith({value}), and gain atoJson()/ factoryfromJson(Object?)JSON round-trip. The wire form is documented and explicit:Baht/Satangemit anint;BahtBigInt/SatangBigIntemit the value as a decimalString(aBigIntis not a JSON-native number).fromJsonaccepts the canonical form (plus an integer-valued JSON number for theintwrappers and a bareintfor theBigIntwrappers) and throwsThaiNumException(ThaiNumError.invalidNumber, aFormatException) on any other shape. Four dependency-freeJsonConverter-shaped adapter classes —BahtConverter,SatangConverter,BahtBigIntConverter,SatangBigIntConverter— exposefromJson/toJsonlaid out exactly likepackage:json_annotation'sJsonConverter<T, S>so json_serializable / freezed users canimplements JsonConverter<...>and forward to them (or use them directly), without thainum taking any new dependency. The existing==/hashCode/toStringbehaviour is unchanged. -
Selectable baht rounding mode (B5).
bahtFromStringgains an optionalroundingparameter —enum SatangRounding { halfAwayFromZero, halfEven, truncate, ceil, floor }— applied to the fractional tail when the decimal string carries more than two decimal places. The defaulthalfAwayFromZeroreproduces the previous behaviour byte-identically (an independent oracle over the existing inputs guards this). The rounding decision is made entirely from the digit string with integer arithmetic — nodoubleis ever involved, so arbitrarily long fractions ('0.005000…0001') round correctly.halfEvenis banker's rounding for financial reporting ('0.005'→0satang,'0.015'→2);truncaterounds toward zero;ceil/floorround toward ±∞ (sign-aware on negative amounts). -
Thai lottery reading helpers (B9). New
lib/src/lottery.dart(reading + date only, no prize-checking and no data):speakLotteryNumber(sixDigits)reads a six-digit prize number digit-by-digit (อ่านเรียงตัว) viaspeakDigits, andspeakTwoDigit/speakThreeDigitread the เลขท้าย 2/3 ตัว. All three accept Arabic or Thai numerals, expose theseparator/colloquialTwooptions, and throwThaiNumExceptionif the length is wrong or a non-digit is present. Plus a draw-date convenience —isLotteryDrawDate(DateTime)andlotteryDrawDates(year, month)— for the regular 1st-and-16th Thai Government Lottery schedule (documented as the regular schedule, not the rare holiday shifts).speakLotteryNumber('123456'); // 'หนึ่ง สอง สาม สี่ ห้า หก' speakTwoDigit('07'); // 'ศูนย์ เจ็ด' isLotteryDrawDate(DateTime(2024, 6, 16)); // true -
Thai phone formatting + spoken reading (B10). New
lib/src/phone.dart:formatThaiPhonegroups a 10-digit mobile number3-3-4('0812345678'→'081-234-5678') and a 9-digit landline2-3-4(best-effort, since provincial area-code lengths vary);thaiPhoneKindclassifies intoenum ThaiPhoneKind { mobile, landline, tollFree, shortCode, unknown }conservatively (unknown when unsure);normalizeThaiPhonereturns E.164+66…(drops a single trunk0); andspeakThaiPhonereads digit-by-digit viaspeakDigits(socolloquialTwo: truereads2as'โท'). All accept separators, spaces, Thai numerals and a+66country code. MatchingStringextensions ('0812345678'.formatThaiPhone(), etc.).formatThaiPhone('0812345678'); // '081-234-5678' thaiPhoneKind('021234567'); // ThaiPhoneKind.landline normalizeThaiPhone('0812345678'); // '+66812345678' -
thainumCLI (B1). Newbin/thainum.dart—dart run thainum:thainum <cmd>or, afterdart pub global activate thainum,thainum <cmd>. Subcommandsspell <int>,baht <amount>(int or decimal string),parse <thai-words>,digits <int>,date <YYYY-MM-DD>. Flags--et=always|tensOnly(forspell),--full/--abbr(fordate),--json(emit a small JSON object), and--help. The argument parser is hand-rolled — nopackage:args, so the package graph stays dependency-free — and the CLI is a thin layer over the public API only. The command logic is a testablerunCli(List<String>) → CliResult(output + exit code) so it is driven directly from tests; unknown commands / bad input exit non-zero andThaiNumExceptionis caught and printed cleanly.thainum spell 101 # หนึ่งร้อยเอ็ด thainum spell 101 --et=tensOnly # หนึ่งร้อยหนึ่ง thainum baht 21.21 # ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์ thainum parse ยี่สิบเอ็ด --json # {"value": "21"} thainum date 2024-06-05 --full # วันพุธที่ 5 มิถุนายน พ.ศ. 2567
0.4.1 #
Additive release. All existing top-level functions keep byte-identical output.
-
spellShort/formatShort— abbreviated large-number reading (A5). Renders a value>= 1,000,000with a single "สากล พัน/ล้าน" scale unit (units a factor of 1000 apart, starting at10⁶):spellShortreads it in Thai words,formatShortgives the Arabic-numeral display form. The coefficient is computed exactly from the decimal digit string (no floating-point), rounded half-away-from-zero todecimals(default 2) with trailing zeros trimmed, and a rounding carry promotes to the next unit (999,999,999→1 พันล้าน). Below10⁶both fall back to the full reading (spell/formatInt).*BigIntforms andthaiDigits:(coefficient only) are provided;decimals < 0throwsArgumentError. Plus theint/BigIntextensionstoThaiShortWordsandtoShortString.spellShort(1500000); // 'หนึ่งจุดห้าล้าน' formatShort(2300000000); // '2.3 พันล้าน' formatShort(50000000000);// '50 พันล้าน' (scale units are 1000 apart)
0.4.0 #
Additive release. Existing top-level functions keep byte-identical default output; every new capability is new API or opt-in.
-
extractNumbers— find Thai numbers in free text. Scans left to right and returns aList<NumberMatch>(withstart/endoffsets into the source,matched,value,isWord,isDigits). Consumes the maximal valid number at each position — a run of Arabic/Thai digit characters, or the longest contiguous word-run the integer grammar accepts as one number.extractNumbers('ซื้อมา ๓ ชิ้น ราคาห้าร้อยบาท'); // [ ๓ → 3 (isDigits), ห้าร้อย → 500 (isWord) ] -
parseDecimal— inverse ofspellDecimal. Splits on'จุด'; the integer part is parsed normally and the fractional part is read digit-by-digit. Returns a canonical decimalString. AlsoString.parseThaiDecimal().parseDecimal('สิบสองจุดสามสี่'); // '12.34' -
Opt-in lenient / colloquial parsing.
parseInt,parseBigInt,parseBahtandparseDecimalgainallowColloquial(accept'นึง'as 1) andlenient(strip internal spaces, NBSP and zero-width characters) named parameters, both defaulting tofalse.parseInt('ร้อยนึง', allowColloquial: true); // 101 parseInt('ยี่สิบ เอ็ด', lenient: true); // 21 -
Thai National / Tax ID.
isValidThaiId(13-digit MOD-11 checksum),isValidThaiTaxId,parseThaiId,formatThaiId(X-XXXX-XXXXX-XX-X),classifyThaiId(ThaiIdKindfrom the leading digit, per DOPA) andspeakThaiId(digit-by-digit). MatchingStringextensions.'1-1017-00230-70-8'.isValidThaiId(); // true -
Percent reading & formatting.
percent(value, {style})reads a value as'ร้อยละ…'(PercentStyle.royalRoiLa, default) or'…เปอร์เซ็นต์'(PercentStyle.colloquialPercent);formatPercentgives the numeric display form ('25%','25.5%'). Plusnum.toThaiPercent().percent(25); // 'ร้อยละยี่สิบห้า' formatPercent(25.5); // '25.5%' -
More tests. Per-module suites for spell, extras, date (format⇄parse), colloquial-clock boundaries, and durations, plus negative-path coverage for the new APIs.
0.3.0 #
Additive release. The existing top-level functions keep byte-identical default output — every new capability is opt-in.
-
Non-throwing parsing (
tryParse*).tryParseInt,tryParseBigInt,tryParseBaht(in addition to the existingparseInt/ … ) andtryParseDatereturnnullon aFormatExceptioninstead of throwing. MatchingStringextensions:tryParseThaiInt,tryParseThaiBigInt,tryParseThaiBaht,tryParseThaiDate.tryParseInt('สิบสิบ'); // null 'ยี่สิบเอ็ด'.tryParseThaiInt(); // 21 -
speakDigits— digit-by-digit reading (อ่านเรียงตัว). Reads each digit as its own Thai word (the way phone/account numbers are read), not as a quantity. Accepts Arabic and Thai numerals; non-digits collapse to a singleseparator;colloquialTwo: truereads2as'โท'. Plus theString.speakThaiDigits()extension.speakDigits('2566'); // 'สอง ห้า หก หก' speakDigits('081-234-5678'); // 'ศูนย์ แปด หนึ่ง สอง สาม สี่ ห้า หก เจ็ด แปด' -
thaiDigits:flag on formatters and date helpers.formatInt,formatSatang,formatThb,formatDate,formatDateAbbrandformatDateFull(and their extension methods, plusSatang.toDecimal/Satang.toThb) gain an optionalthaiDigitsparameter (defaultfalse). When true, only ASCII digits become Thai numerals — commas, the decimal point, the฿symbol, the-sign and labels likeพ.ศ.stay ASCII.formatThb(2121, thaiDigits: true); // '฿๒๑.๒๑' formatInt(1234567, thaiDigits: true); // '๑,๒๓๔,๕๖๗' -
Trust signals. Added a GitHub Actions CI workflow (format / analyze / test across the
3.1.0andstableSDKs, plus a publish dry-run), declared the pure-Dartplatforms:set, and added CI + pub.dev README badges. New test suites: round-trip property tests (seeded + boundaries, bothEtModes), a hand-checked spell golden table, and data-driven negative parse tests.
0.2.0 #
Receiver-style API and typed money wrappers. No behavioural change to the existing top-level functions — they keep working, the new shapes are additive.
-
Extension methods on
int,BigInt,double,String,DateTime, andDuration. Every top-level function now has atoThaiXxx()counterpart so it reads naturally in a call chain:21.toThaiWords(); // 'ยี่สิบเอ็ด' '101'.toThaiDigits(); // '๑๐๑' 1234567.toThousandsString(); // '1,234,567' 'ยี่สิบเอ็ด'.parseThaiInt(); // 21 DateTime.utc(2024, 6, 5).toThaiDate(); // '5 มิถุนายน 2567' DateTime.utc(2024, 6, 5).buddhistYear; // 2567 (getter — property) const Duration(minutes: 90).toThaiText();Conversions are methods (
toXxx(), parens); component accessors onDateTime(buddhistYear,thaiMonthName,thaiWeekdayName, …) are getters parallel toDateTime.year/DateTime.month. -
Typed money wrappers
Baht,Satang,BahtBigInt,SatangBigInt. Each is a one-fieldconst-eligible class with value-equality. Passing a satang amount where baht is expected now becomes a type error instead of a wrong invoice:const Baht(100).toBahtText(); // 'หนึ่งร้อยบาทถ้วน' const Satang(2121).toBahtText(); // 'ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์' const Satang(2121).toDecimal(); // '21.21' const Satang(2121).toThb(); // '฿21.21'int.toBahtText()(and theBigInt/doubleversions) interpret the receiver as whole baht — the unambiguous default. Use aSatang(...)wrapper when you have satang. -
All public extensions and wrappers are exported from
package:thainum/thainum.dart— no extra import needed.
0.1.0 #
Initial release. A pure-Dart port of the go-thainum library — no Flutter and
no intl dependency (the Thai tables are bundled).
- Thai numerals —
toThaiDigits/toArabicDigitsconvert between Arabic and Thai digits (101⇄๑๐๑) in mixed text. - Spell numbers as Thai words —
spell(int),spellBigInt(BigInt),spellDecimal(String), correct to ล้านล้าน (10¹²) and beyond, with a selectableEtMode(always/tensOnly) via theSpellerclass. - Baht text (บาทตัวอักษร) —
baht(whole baht),bahtSatang,bahtBigInt,bahtSatangBigInt,bahtFromString(string-exact, 2-dp away-from-zero rounding),satangFromFloatandbahtFromDouble. - Formatting —
formatInt,formatSatang,formatThb(฿). - Reverse parsing (the flagship feature) —
parseInt,parseBigInt,parseBaht; accepts both เอ็ด and หนึ่ง forms and Thai or Arabic digits; throwsThaiNumException(aFormatException) on bad input. - Ordinals, fractions & Buddhist-Era years —
ordinal,fraction,year, plusceToBe/beToCe. - Thai dates —
monthTh/monthAbbrTh,weekdayTh/weekdayAbbrTh,buddhistYear,formatDate/formatDateAbbr/formatDateFull, andparseDate(round-trips the formatters; BE → CE). - Thai time & durations —
formatTime(formal นาฬิกา),formatClock(colloquial ตี / โมง / ทุ่ม), andformatDuration.