///|
fn input_glyphs(source : String, relaxed : Bool) -> String {
let out = StringBuilder()
for c in normalize(numeral_variants(source)).iter() {
let n = c.to_int()
out.write_char(
if n >= 0xff10 && n <= 0xff19 {
(n - 0xff10 + 48).unsafe_to_char()
} else {
match c {
'.' => '.'
'-' => '-'
_ => c
}
},
)
}
// Strict spelling deliberately excludes colloquial two and circle zero.
if !relaxed &&
(source.contains("两") || source.contains("兩") || source.contains("〇")) {
return source
}
out.to_string()
}
///|
fn cn_digit(c : Char) -> Int {
match c {
'零' => 0
'一' => 1
'二' => 2
'三' => 3
'四' => 4
'五' => 5
'六' => 6
'七' => 7
'八' => 8
'九' => 9
_ => -1
}
}
///|
fn short_group(
source : String,
relaxed : Bool,
colloquial : Bool,
) -> Int64 raise NumberError {
if source.is_empty() {
raise Invalid("empty numeral group")
}
let cs = source.to_array()
let mut sum = 0
let mut pending = -1
let mut previous = 10000
let mut zero = false
for i, c in cs {
let d = cn_digit(c)
if d >= 0 {
if pending >= 0 {
raise Invalid("adjacent group digits")
}
if d == 0 {
if zero || i == 0 {
raise Invalid("misplaced zero")
}
zero = true
} else {
pending = d
}
continue
}
let unit = match c {
'十' => 10
'百' => 100
'千' => 1000
_ => raise Invalid("unknown numeral")
}
if unit >= previous {
raise Invalid("units must descend")
}
if zero && previous <= unit * 10 {
raise Invalid("unnecessary zero")
}
if !relaxed && previous < 10000 && previous > unit * 10 && !zero {
raise Invalid("missing zero")
}
let digit = if pending >= 0 {
pending
} else if unit == 10 && (i == 0 || zero || relaxed) {
1
} else {
raise Invalid("unit needs digit")
}
sum += digit * unit
previous = unit
pending = -1
zero = false
}
if pending >= 0 {
if previous > 10 && previous < 10000 && !zero {
if relaxed && colloquial {
pending *= previous / 10
} else {
raise Invalid("omitted final unit")
}
}
sum += pending
} else if zero && (!relaxed || previous <= 10) {
raise Invalid("trailing zero")
}
if sum <= 0 || sum > 9999 {
raise Invalid("invalid numeral group")
}
sum.to_int64()
}
///|
fn below_yi(
source : String,
relaxed : Bool,
colloquial : Bool,
) -> Int64 raise NumberError {
let parts = source.split("万").collect()
if parts.length() > 2 {
raise Invalid("repeated wan")
}
if parts.length() == 1 {
return short_group(source, relaxed, colloquial)
}
let high = short_group(parts[0].to_owned(), relaxed, false)
let tail = parts[1].to_owned()
if tail.is_empty() {
return high * 10000L
}
if tail == "零" && relaxed {
return high * 10000L
}
let leading_zero = tail.has_prefix("零")
let rest = if leading_zero { tail[1:].to_owned() } else { tail }
if relaxed &&
colloquial &&
!leading_zero &&
rest.length() == 1 &&
cn_digit(rest.to_array()[0]) > 0 {
return high * 10000L + cn_digit(rest.to_array()[0]).to_int64() * 1000L
}
if !leading_zero && rest.has_prefix("十") {
raise Invalid("missing group zero")
}
let low = short_group(rest, relaxed, colloquial && !leading_zero)
if (leading_zero && low >= 1000L) ||
(!relaxed && !leading_zero && low < 1000L) {
raise Invalid("invalid wan group separator")
}
high * 10000L + low
}
///|
fn unit_integer(source : String, relaxed : Bool) -> Int64 raise NumberError {
if source.is_empty() {
raise Invalid("missing integer")
}
if source == "零" {
return 0L
}
if source.iter().all(c => cn_digit(c) >= 0) {
if !relaxed && source.length() != 1 {
raise Invalid("serial digits need normal mode")
}
let mut value = 0L
for c in source.iter() {
let digit = cn_digit(c).to_int64()
if value > (9999999999999999L - digit) / 10L {
raise Invalid("integer range")
}
value = value * 10L + digit
}
return value
}
let parts = source.split("亿").collect()
if parts.length() > 2 {
raise Invalid("repeated yi")
}
if parts.length() == 1 {
return below_yi(source, relaxed, true)
}
let high = below_yi(parts[0].to_owned(), relaxed, false)
let tail = parts[1].to_owned()
if tail.is_empty() {
return high * 100000000L
}
let leading_zero = tail.has_prefix("零")
let rest = if leading_zero { tail[1:].to_owned() } else { tail }
if rest.is_empty() {
raise Invalid("trailing yi zero")
}
let low = below_yi(rest, relaxed, true)
let cs = rest.to_array()
let final_ellipsis = relaxed &&
cs.length() >= 2 &&
cn_digit(cs[cs.length() - 1]) > 0 &&
(cs[cs.length() - 2] == '百' || cs[cs.length() - 2] == '千')
if !leading_zero && !rest.contains("万") && !final_ellipsis {
raise Invalid("missing yi group zero")
}
if leading_zero && low >= 10000000L {
raise Invalid("unnecessary yi zero")
}
high * 100000000L + low
}
///|
fn standard_integer(
value : Int64,
financial : Bool,
) -> String raise NumberError {
if value < 0L || value > 9999999999999999L {
raise Invalid("integer range")
}
if value < 100000000L {
return format_integer(value, financial~)
}
let high = value / 100000000L
let low = value % 100000000L
let mut out = format_integer(high, financial~) + "亿"
if low > 0L {
if low < 10000000L {
out += "零"
}
let mut tail = format_integer(low, financial~)
if tail.has_prefix("十") {
tail = "一" + tail
}
out += tail
}
out
}
///|
fn ascii_value(source : String) -> Int64 raise NumberError {
let mut value = 0L
if source.is_empty() {
raise Invalid("missing digits")
}
for c in source.iter() {
let d = c.to_int() - 48
if d < 0 || d > 9 || value > (9999999999999999L - d.to_int64()) / 10L {
raise Invalid("decimal integer range")
}
value = value * 10L + d.to_int64()
}
value
}
///|
fn compact_decimal(source : String) -> String {
let mut out = source
if out.contains(".") {
while out.has_suffix("0") {
out = out[:out.length() - 1].to_owned()
}
if out.has_suffix(".") {
out = out[:out.length() - 1].to_owned()
}
}
if out == "-0" {
"0"
} else {
out
}
}
///|
/// Exact textual result. normal adds serial digits and final-unit ellipsis;
/// smart also expands Arabic runs and decimal coefficients of a single unit.
pub fn cn2an(
source : String,
mode? : String = "strict",
) -> String raise NumberError {
if !["strict", "normal", "smart", "direct"].contains(mode) {
raise Invalid("unknown parsing mode")
}
if source.is_empty() || source.length() > 256 {
raise Invalid("number length 1..256")
}
let relaxed = mode != "strict"
let text = input_glyphs(source, relaxed)
if mode == "direct" {
let out = StringBuilder()
for c in text.iter() {
let d = cn_digit(c)
if d >= 0 {
out.write_char((48 + d).unsafe_to_char())
} else if c == '负' {
out.write_char('-')
} else if c == '点' {
out.write_char('.')
} else if (c >= '0' && c <= '9') || c == '-' || c == '.' {
out.write_char(c)
} else {
raise Invalid("direct mode accepts digits, sign and point only")
}
}
let result = out.to_string()
ignore(decimal_parts(result))
return result
}
let negative = text.has_prefix("负") ||
(mode == "smart" && text.has_prefix("-"))
let body = if negative { text[1:].to_owned() } else { text }
if body.is_empty() {
raise Invalid("missing number")
}
if mode == "smart" && body.contains(".") {
let cs = body.to_array()
let scale = match cs[cs.length() - 1] {
'十' => 1
'百' => 2
'千' => 3
'万' => 4
'亿' => 8
_ => 0
}
let raw = if scale > 0 { body[:body.length() - 1].to_owned() } else { body }
if raw.has_prefix("-") {
raise Invalid("multiple signs")
}
let (_, whole, fraction) = decimal_parts(raw)
let f = fraction.unwrap_or("")
if f.length() > 64 {
raise Invalid("at most 64 fractional digits")
}
let combined = whole +
f +
"0".repeat(if scale > f.length() { scale - f.length() } else { 0 })
let point = whole.length() + scale
let integer = ascii_value(combined[:point].to_owned()).to_string()
let result = (if negative { "-" } else { "" }) +
integer +
(if point < combined.length() {
"." + combined[point:].to_owned()
} else {
""
})
return compact_decimal(result)
}
let expanded = StringBuilder()
let chars = body.to_array()
let mut i = 0
let mut after_point = false
while i < chars.length() {
let c = chars[i]
if c >= '0' && c <= '9' && mode == "smart" {
let digits = StringBuilder()
while i < chars.length() && chars[i] >= '0' && chars[i] <= '9' {
digits.write_char(chars[i])
i += 1
}
let run = digits.to_string()
expanded.write_string(
if after_point || (run.length() > 1 && run.has_prefix("0")) {
format_digits(run)
} else {
standard_integer(ascii_value(run), false)
},
)
} else {
if c == '点' {
after_point = true
}
expanded.write_char(c)
i += 1
}
}
let parts = expanded.to_string().split("点").collect()
if parts.length() > 2 {
raise Invalid("multiple decimal points")
}
let whole = unit_integer(parts[0].to_owned(), relaxed)
let mut out = (if negative { "-" } else { "" }) + whole.to_string()
if parts.length() == 2 {
let fraction = parts[1].to_owned()
if fraction.length() > 64 ||
(!relaxed && (fraction.is_empty() || fraction.has_suffix("零"))) {
raise Invalid("invalid fractional spelling")
}
if !fraction.is_empty() {
out += "." + parse_digits(fraction)
}
}
compact_decimal(out)
}
///|
/// Format text using wan/yi groups, keeping exact fraction digits and signed zero.
pub fn an2cn(
source : String,
mode? : String = "low",
) -> String raise NumberError {
if !["low", "up", "rmb", "direct"].contains(mode) {
raise Invalid("unknown formatting mode")
}
let text = input_glyphs(source, true)
if mode == "direct" {
if text.is_empty() || text.length() > 256 {
raise Invalid("number length 1..256")
}
let out = StringBuilder()
let names = digits(false)
for c in text.iter() {
if c >= '0' && c <= '9' {
out.write_string(names[c.to_int() - 48])
} else if c == '-' {
out.write_string("负")
} else if c == '.' {
out.write_string("点")
} else {
raise Invalid("direct mode accepts digits, sign and point only")
}
}
return out.to_string()
}
let clean = if text.has_suffix(".") {
text[:text.length() - 1].to_owned()
} else {
text
}
if mode == "rmb" {
return format_currency(clean, rounding="truncate")
}
let (negative, whole, fraction) = decimal_parts(clean)
let financial = mode == "up"
let mut out = (if negative { "负" } else { "" }) +
standard_integer(ascii_value(whole), financial)
if fraction is Some(f) {
if f.length() > 64 {
raise Invalid("at most 64 fractional digits")
}
out += "点" + format_digits(f, financial~)
}
out
}
///|
/// Text arithmetic; rounding is reject (default), truncate, or half-up.
pub fn format_currency(
source : String,
zero_yuan? : Bool = false,
yuan? : String = "元",
whole_suffix? : String = "整",
prefix? : String = "",
rounding? : String = "reject",
parentheses? : Bool = false,
) -> String raise NumberError {
if !["reject", "truncate", "half-up"].contains(rounding) {
raise Invalid("unknown rounding mode")
}
if yuan.is_empty() ||
yuan.length() > 8 ||
whole_suffix.length() > 8 ||
prefix.length() > 32 {
raise Invalid("currency label length")
}
let (negative, whole, fraction) = decimal_parts(source)
let f = fraction.unwrap_or("")
if f.length() > 64 || (rounding == "reject" && f.length() > 2) {
raise Invalid("currency requires at most two fractional digits")
}
let mut value = ascii_value(whole)
let cents = f + "00"
let mut tail = (cents[0].to_int() - 48) * 10 + cents[1].to_int() - 48
if rounding == "half-up" && f.length() > 2 && f[2].to_int() >= 53 {
tail += 1
}
if tail == 100 {
value += 1
tail = 0
}
let names = digits(true)
let mut out = if value > 0L || zero_yuan || tail == 0 {
standard_integer(value, true) + yuan
} else {
""
}
if tail == 0 {
out += whole_suffix
} else {
if tail / 10 > 0 {
out += names[tail / 10] + "角"
} else if value > 0L {
out += "零"
}
if tail % 10 > 0 {
out += names[tail % 10] + "分"
}
}
prefix +
(if negative {
if parentheses {
"(" + out + ")"
} else {
"负" + out
}
} else {
out
})
}