thainum 0.4.0
thainum: ^0.4.0 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.
thainum #
ชุดเครื่องมือจัดการตัวเลขภาษาไทยแบบครบวงจรสำหรับภาษา Dart — เลขไทย, อ่านเป็นคำ, บาทตัวอักษร, จัดรูปแบบ และแปลงคำกลับเป็นตัวเลข
A comprehensive Thai number toolkit for Dart — Thai numerals, number-to-words, baht text, formatting, Thai dates/times, and (uniquely) reverse parsing of Thai words back into numbers.
Pure Dart, MIT-licensed, no Flutter and no intl dependency (the Thai
tables are bundled). A faithful port of
go-thainum.
dart pub add thainum
Reverse parsing — words back into numbers (the flagship) #
Essentially nothing else turns Thai words back into numbers. thainum does:
import 'package:thainum/thainum.dart';
parseInt('ยี่สิบเอ็ด'); // 21
parseInt('หนึ่งร้อยเอ็ด'); // 101 (เอ็ด form)
parseInt('หนึ่งร้อยหนึ่ง'); // 101 (หนึ่ง form — both accepted)
parseInt('๒๑'); // 21 (Thai digits)
parseBaht('ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์'); // 2121 (satang)
parseBigInt('หนึ่งล้านล้านล้าน'); // 10^18
// Stacked ล้าน is handled correctly:
parseBigInt('หนึ่งล้านล้าน'); // 10^12
All parse failures throw ThaiNumException, which implements FormatException:
try {
parseInt('สิบสิบ'); // ascending/repeat place is invalid
} on FormatException catch (e) {
print(e); // ThaiNumException: thainum: misplaced place word ("สิบ")
}
Non-throwing parsing — tryParse* (v0.3.0+) #
When you would rather branch on a null than catch a FormatException, every
parser has a tryParse* sibling (and matching String extension):
tryParseInt('ยี่สิบเอ็ด'); // 21
tryParseInt('สิบสิบ'); // null (invalid → no throw)
tryParseBigInt('หนึ่งล้านล้าน'); // 10^12
tryParseBaht('ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์'); // 2121
tryParseDate('5 มิถุนายน 2567'); // DateTime.utc(2024, 6, 5)
'สิบสิบ'.tryParseThaiInt(); // null
Read digits one by one — speakDigits (อ่านเรียงตัว, v0.3.0+) #
Phone numbers, account numbers and PINs are read digit-by-digit, not as a quantity:
speakDigits('2566'); // 'สอง ห้า หก หก'
speakDigits('081-234-5678'); // 'ศูนย์ แปด หนึ่ง สอง สาม สี่ ห้า หก เจ็ด แปด'
'0812345678'.speakThaiDigits(); // same as above
// Thai numerals accepted; punctuation collapses to one separator.
speakDigits('๒๕๖๖'); // 'สอง ห้า หก หก'
speakDigits('2566', colloquialTwo: true); // 'โท ห้า หก หก' (2 → โท)
speakDigits('2566', separator: '-'); // 'สอง-ห้า-หก-หก'
Thai numeral output — the thaiDigits: flag (v0.3.0+) #
The formatters and date helpers take an optional thaiDigits: flag. When true
only the digits become Thai numerals — commas, the decimal point, the ฿
symbol, the - sign and labels like พ.ศ. stay ASCII:
formatInt(1234567, thaiDigits: true); // '๑,๒๓๔,๕๖๗'
formatThb(2121, thaiDigits: true); // '฿๒๑.๒๑'
formatDateFull(d, thaiDigits: true); // 'วันพุธที่ ๕ มิถุนายน พ.ศ. ๒๕๖๗'
1234567.toThousandsString(thaiDigits: true); // '๑,๒๓๔,๕๖๗'
const Satang(2121).toThb(thaiDigits: true); // '฿๒๑.๒๑'
d.toThaiDate(thaiDigits: true); // '๕ มิถุนายน ๒๕๖๗'
The default (thaiDigits: false) output is byte-identical to previous releases.
Find numbers in free text — extractNumbers (v0.4.0+) #
Scan any text for embedded Thai numbers (words and/or digit characters). Each
NumberMatch carries the value, the matched substring, its start/end
offsets into the original text (so text.substring(start, end) == matched),
and whether it came from words (isWord) or digits (isDigits).
for (final m in extractNumbers('ซื้อมา ๓ ชิ้น ราคาห้าร้อยบาท')) {
print('${m.matched} = ${m.value}');
}
// ๓ = 3 (isDigits)
// ห้าร้อย = 500 (isWord)
At each position it consumes the maximal valid number — a run of digits, or
the longest contiguous run of number-words the grammar accepts. So 'ยี่สิบเอ็ด'
is one match (21), and 'ห้าร้อยสิบสิบ' yields 'ห้าร้อยสิบ' (510) then
'สิบ' (10).
Parse decimals — parseDecimal (v0.4.0+) #
The inverse of spellDecimal: integer part parsed normally, fractional part read
digit-by-digit. Returns a canonical decimal string.
parseDecimal('สิบสองจุดสามสี่'); // '12.34'
parseDecimal('ศูนย์จุดห้า'); // '0.5'
'สิบสองจุดสามสี่'.parseThaiDecimal(); // '12.34'
Lenient / colloquial inputs (opt-in, v0.4.0+) #
parseInt, parseBigInt, parseBaht and parseDecimal take two opt-in flags
(both default false, so the strict path is unchanged):
parseInt('ร้อยนึง', allowColloquial: true); // 101 (accept นึง as 1)
parseInt('ยี่สิบ เอ็ด', lenient: true); // 21 (strip spaces/NBSP/zero-width)
Thai National / Tax ID (v0.4.0+) #
Validate (MOD-11), format, parse, classify and read a 13-digit Thai National ID (the personal Tax ID is the same number).
isValidThaiId('1-1017-00230-70-8'); // true (dashes/spaces/Thai numerals ok)
formatThaiId('1101700230708'); // '1-1017-00230-70-8'
parseThaiId('1-1017-00230-70-8'); // '1101700230708'
classifyThaiId('1101700230708'); // ThaiIdKind.thaiBornRegisteredOnTime
speakThaiId('1101700230708'); // 'หนึ่ง หนึ่ง ศูนย์ …' (digit-by-digit)
'1101700230708'.isValidThaiId(); // true (String extension)
classifyThaiId maps the leading digit to a DOPA category and returns
ThaiIdKind.unknown (rather than guessing) for leading digit 0/9 or any
non-13-digit input.
Percent (v0.4.0+) #
percent(25); // 'ร้อยละยี่สิบห้า'
percent(25.5); // 'ร้อยละยี่สิบห้าจุดห้า'
percent(25, style: PercentStyle.colloquialPercent); // 'ยี่สิบห้าเปอร์เซ็นต์'
formatPercent(25.5); // '25.5%'
25.toThaiPercent(); // 'ร้อยละยี่สิบห้า'
Receiver-style API (v0.2.0+) #
Every top-level function below is also available as an extension method on
its receiver type, so the same calls read naturally in a chain. The
extensions are exported by the same import 'package:thainum/thainum.dart';
— no extra import needed.
21.toThaiWords(); // 'ยี่สิบเอ็ด'
'101'.toThaiDigits(); // '๑๐๑'
1234567.toThousandsString(); // '1,234,567'
'ยี่สิบเอ็ด'.parseThaiInt(); // 21
const Baht(100).toBahtText(); // 'หนึ่งร้อยบาทถ้วน'
const Satang(2121).toBahtText(); // 'ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์'
const Satang(2121).toThb(); // '฿21.21'
final d = DateTime.utc(2024, 6, 5);
d.toThaiDateFull(); // 'วันพุธที่ 5 มิถุนายน พ.ศ. 2567'
d.buddhistYear; // 2567 (getter — property)
The Baht / Satang / BahtBigInt / SatangBigInt wrappers make the
money unit a compile-time guarantee: a satang amount can never be passed
where baht is expected. int.toBahtText() (and the BigInt / double
versions) interpret the receiver as whole baht — the unambiguous default.
The function API (spell(21), baht(100), parseInt(...)) keeps working
unchanged.
Features #
- Thai numerals —
toThaiDigits/toArabicDigits(101⇄๑๐๑). - Spell numbers as Thai words —
int,BigInt, and decimals, correct to ล้านล้าน (10¹²) and beyond. - Baht text (บาทตัวอักษร) — render currency amounts as the formal Thai spelling used on cheques and invoices.
- Formatting — thousands separators, satang-to-decimal, and a
฿display. - Reverse parsing — turn Thai words back into numbers and satang.
- Ordinals, fractions & Buddhist-Era years —
ordinal(ที่…),fraction(เศษ…ส่วน…),year(พุทธศักราช…), plusceToBe/beToCe. - Thai dates — Thai month/weekday names, Buddhist-Era year, three formatters,
and
parseDateto turn a Thai date string back into aDateTime. - Thai time & durations —
formatTime(formal นาฬิกา),formatClock(colloquial ตี / โมง / ทุ่ม), andformatDuration. - Money is exact — amounts are handled in integer satang (1 baht = 100
satang) or
BigInt, neverdouble. A clearly-labelled lossy float entry point exists for convenience. - EtMode — choose between the Royal-Institute-recommended
เอ็ดform and the plainหนึ่งform for trailing ones.
Quick start #
Thai numerals #
toThaiDigits('101'); // ๑๐๑
toArabicDigits('๑๐๑'); // 101
Spell numbers as Thai words #
spell(21); // ยี่สิบเอ็ด
spell(101); // หนึ่งร้อยเอ็ด
spellBigInt(BigInt.parse('1000000000000')); // หนึ่งล้านล้าน
spellDecimal('12.34'); // สิบสองจุดสามสี่
Baht text (บาทตัวอักษร) #
baht takes an amount in baht (the usual unit); use bahtSatang for
sub-baht precision (1 baht = 100 satang), or bahtFromString for a decimal
string:
baht(100); // หนึ่งร้อยบาทถ้วน (100 baht)
baht(0); // ศูนย์บาทถ้วน
bahtSatang(2121); // ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์ (21.21 baht)
bahtSatang(25); // ยี่สิบห้าสตางค์
bahtFromString('21.21'); // ยี่สิบเอ็ดบาทยี่สิบเอ็ดสตางค์ (string-exact)
There is also bahtFromDouble(double) (and the satangFromFloat helper) for
convenience, but it is lossy — prefer satang or strings for anything that
must be exact.
Formatting #
formatInt(1234567); // 1,234,567
formatSatang(2121); // 21.21
formatThb(2121); // ฿21.21
Ordinals, fractions, and Buddhist-Era years #
ordinal(21); // ที่ยี่สิบเอ็ด
fraction(3, 4); // เศษสามส่วนสี่
year(2566); // พุทธศักราชสองพันห้าร้อยหกสิบหก
ceToBe(2023); // 2566
Thai dates (เดือนไทย / วันไทย / ปี พ.ศ.) #
final d = DateTime.utc(2024, 6, 5);
formatDate(d); // 5 มิถุนายน 2567
formatDateAbbr(d); // 5 มิ.ย. 2567
formatDateFull(d); // วันพุธที่ 5 มิถุนายน พ.ศ. 2567
monthTh(6); // มิถุนายน
weekdayTh(d); // วันพุธ
buddhistYear(d); // 2567
parseDate('วันพุธที่ 5 มิถุนายน พ.ศ. 2567'); // DateTime.utc(2024, 6, 5)
Dates use the Buddhist-Era year (Gregorian + 543). Wrap a formatted string with
toThaiDigits if you want Thai numerals (e.g. ๕ มิถุนายน ๒๕๖๗).
Note: Dart's
DateTime.weekdayis Monday=1 .. Sunday=7.thainummaps that internally to the correct Thai weekday name, soweekdayTh(DateTime)just works.
Time of day and durations #
final t = DateTime.utc(2024, 1, 1, 14, 30);
formatTime(t); // สิบสี่นาฬิกาสามสิบนาที (formal)
formatClock(t); // บ่ายสองโมงครึ่ง (colloquial)
formatDuration(const Duration(minutes: 90)); // หนึ่งชั่วโมงสามสิบนาที
EtMode — เอ็ด vs หนึ่ง #
By default the library uses EtMode.always, the Royal-Institute-recommended
form where a trailing one is read เอ็ด. Use EtMode.tensOnly if you want a
trailing one to read หนึ่ง except in the tens place:
const plain = Speller(et: EtMode.tensOnly);
spell(101); // หนึ่งร้อยเอ็ด (default EtMode.always)
plain.spellInt(101); // หนึ่งร้อยหนึ่ง
Speller exposes .spellInt, .spellBigInt, .spellDecimal, .baht,
.ordinal, .fraction, and .year, so you can pick the EtMode once and reuse
it. Reverse parsing accepts both forms.
A note on money and precision #
Money is handled in integer satang (1 baht = 100 satang) or BigInt, never
double. This means there are no binary-floating-point rounding surprises in
your baht text. The *FromFloat / *FromDouble entry points are provided only
as a convenience and are documented as lossy — reach for the satang / string /
BigInt APIs whenever correctness matters.
License #
MIT © 2026 MaIII (ultramcu)