use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Words2NumError(pub String);
impl Words2NumError {
fn new(msg: impl Into<String>) -> Self {
Words2NumError(msg.into())
}
}
impl fmt::Display for Words2NumError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for Words2NumError {}
#[derive(Debug, Clone, PartialEq)]
pub enum W2nValue {
Int(BigInt),
Float(f64),
Dec(BigDecimal),
}
pub type Result<T> = std::result::Result<T, Words2NumError>;
pub const NBSP: char = '\u{00A0}';
pub const NNBSP: char = '\u{202F}';
pub const THIN_SPACE: char = '\u{2009}';
pub const SPACE_LIKE: &str = " \t\u{00A0}\u{202F}\u{2009}";
fn is_py_space(c: char) -> bool {
matches!(c,
'\u{0009}'..='\u{000D}' | '\u{001C}'..='\u{001F}' | '\u{0020}'
| '\u{0085}'
| '\u{00A0}' | '\u{1680}'
| '\u{2000}'..='\u{200A}' | '\u{2028}'
| '\u{2029}'
| '\u{202F}' | '\u{205F}'
| '\u{3000}'
)
}
fn py_strip(s: &str) -> &str {
s.trim_matches(is_py_space)
}
fn py_lstrip(s: &str) -> &str {
s.trim_start_matches(is_py_space)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NumberFormat {
pub thousands: &'static str,
pub decimal: &'static str,
}
const fn nf(thousands: &'static str, decimal: &'static str) -> NumberFormat {
NumberFormat { thousands, decimal }
}
pub static NUMBER_FORMAT_DEFAULTS: &[(&str, NumberFormat)] = &[
("_default", nf(",", ".")),
("en", nf(",", ".")),
("en_GB", nf(",", ".")),
("en_IN", nf(",", ".")),
("en_NG", nf(",", ".")),
("zh", nf(",", ".")),
("zh_CN", nf(",", ".")),
("zh_HK", nf(",", ".")),
("zh_TW", nf(",", ".")),
("ja", nf(",", ".")),
("ko", nf(",", ".")),
("th", nf(",", ".")),
("vi", nf(".", ",")),
("fr", nf(" ", ",")),
("fr_BE", nf(" ", ",")),
("fr_DZ", nf(" ", ",")),
("fr_CH", nf("'", ".")),
("de", nf(".", ",")),
("es", nf(".", ",")),
("es_CO", nf(".", ",")),
("es_CR", nf(".", ",")),
("es_GT", nf(".", ",")),
("es_NI", nf(".", ",")),
("es_VE", nf(".", ",")),
("it", nf(".", ",")),
("pt", nf(".", ",")),
("pt_BR", nf(".", ",")),
("nl", nf(".", ",")),
("ro", nf(".", ",")),
("hr", nf(".", ",")),
("sl", nf(".", ",")),
("sr", nf(".", ",")),
("tr", nf(".", ",")),
("el", nf(".", ",")),
("ru", nf(" ", ",")),
("uk", nf(" ", ",")),
("be", nf(" ", ",")),
("bg", nf(" ", ",")),
("pl", nf(" ", ",")),
("cs", nf(" ", ",")),
("sk", nf(" ", ",")),
("hu", nf(" ", ",")),
("sv", nf(" ", ",")),
("no", nf(" ", ",")),
("nn", nf(" ", ",")),
("da", nf(".", ",")),
("fi", nf(" ", ",")),
("et", nf(" ", ",")),
("lt", nf(" ", ",")),
("lv", nf(" ", ",")),
("is", nf(".", ",")),
("fo", nf(".", ",")),
("ar", nf(",", ".")),
("fa", nf(",", ".")),
("he", nf(",", ".")),
];
fn lookup_format(key: &str) -> Option<NumberFormat> {
NUMBER_FORMAT_DEFAULTS
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| *v)
}
pub fn get_format(lang: &str) -> NumberFormat {
if let Some(f) = lookup_format(lang) {
return f;
}
let base = lang.split('_').next().unwrap_or("");
if let Some(f) = lookup_format(base) {
return f;
}
lookup_format("_default").expect("_default is always present")
}
pub fn parse_number_string(
s: &str,
thousands_sep: Option<&str>,
decimal_sep: Option<&str>,
lang: Option<&str>,
) -> Result<W2nValue> {
let s = py_strip(s);
if s.is_empty() {
return Err(Words2NumError::new("empty numeric string"));
}
let mut sign = 1i32;
let first = s.chars().next().expect("non-empty");
let s = if first == '+' || first == '-' {
if first == '-' {
sign = -1;
}
py_lstrip(&s[first.len_utf8()..])
} else {
s
};
if s.is_empty() {
return Err(Words2NumError::new("empty numeric string after sign"));
}
if thousands_sep.is_some() || decimal_sep.is_some() {
return Ok(apply_sign(sign, parse_with_explicit(s, thousands_sep, decimal_sep)?));
}
if let Some(lang) = lang {
let fmt = get_format(lang);
if let Ok(v) = parse_with_explicit(s, Some(fmt.thousands), Some(fmt.decimal)) {
return Ok(apply_sign(sign, v));
}
}
Ok(apply_sign(sign, auto_detect_parse(s)?))
}
fn apply_sign(sign: i32, v: W2nValue) -> W2nValue {
if sign >= 0 {
return v;
}
match v {
W2nValue::Int(i) => W2nValue::Int(-i),
W2nValue::Float(f) => W2nValue::Float(-f),
W2nValue::Dec(d) => W2nValue::Dec(-d),
}
}
fn parse_with_explicit(
s: &str,
thousands_sep: Option<&str>,
decimal_sep: Option<&str>,
) -> Result<W2nValue> {
let mut cur = s.to_string();
if let Some(t) = thousands_sep.filter(|t| !t.is_empty()) {
if t == " " || t == "\u{00A0}" || t == "\u{202F}" || t == "\u{2009}" {
cur = cur.chars().filter(|&c| !is_py_space(c)).collect();
} else {
cur = cur.replace(t, "");
}
}
if let Some(d) = decimal_sep.filter(|d| !d.is_empty()) {
if d != "." {
if cur.contains('.') {
cur = cur.replace('.', "");
}
cur = cur.replace(d, ".");
}
}
to_number(&cur)
}
fn auto_detect_parse(s: &str) -> Result<W2nValue> {
let s2: String = s
.chars()
.filter(|&c| !(is_py_space(c) || c == '\'' || c == '_'))
.collect();
let has_comma = s2.contains(',');
let has_dot = s2.contains('.');
if has_comma && has_dot {
let last_comma = last_char_index(&s2, ',').expect("has_comma");
let last_dot = last_char_index(&s2, '.').expect("has_dot");
let s3 = if last_comma > last_dot {
s2.replace('.', "").replace(',', ".")
} else {
s2.replace(',', "")
};
return to_number(&s3);
}
if has_comma {
return to_number(&resolve_single_sep(&s2, ','));
}
if has_dot {
return to_number(&resolve_single_sep(&s2, '.'));
}
to_number(&s2)
}
fn last_char_index(s: &str, target: char) -> Option<usize> {
s.chars()
.enumerate()
.filter(|(_, c)| *c == target)
.map(|(i, _)| i)
.last()
}
fn resolve_single_sep(s: &str, sep: char) -> String {
let parts: Vec<&str> = s.split(sep).collect();
if parts.len() > 2 {
return s.replace(sep, "");
}
let after = parts[1];
let after_len = after.chars().count();
let before_len = parts[0].chars().count();
if after_len == 3 && !parts[0].is_empty() && before_len <= 3 {
return if sep == '.' {
s.to_string() } else {
s.replace(',', ".")
};
}
if after_len != 3 {
return if sep == '.' {
s.to_string()
} else {
s.replace(',', ".")
};
}
s.replace(sep, "")
}
fn to_number(s: &str) -> Result<W2nValue> {
let Some(ascii) = fullmatch_number(s) else {
return Err(Words2NumError::new(format!(
"not a parseable number: {}",
py_repr(s)
)));
};
if ascii.contains('.') {
Ok(W2nValue::Float(
ascii.parse::<f64>().expect("gated by fullmatch_number"),
))
} else {
Ok(W2nValue::Int(
ascii.parse::<BigInt>().expect("gated by fullmatch_number"),
))
}
}
fn fullmatch_number(s: &str) -> Option<String> {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
let mut n = 0usize;
while let Some(d) = it.peek().copied().and_then(decimal_digit) {
out.push((b'0' + d) as char);
it.next();
n += 1;
}
if n == 0 {
return None;
}
if let Some(&c) = it.peek() {
if c != '.' {
return None;
}
it.next();
out.push('.');
let mut m = 0usize;
while let Some(d) = it.peek().copied().and_then(decimal_digit) {
out.push((b'0' + d) as char);
it.next();
m += 1;
}
if m == 0 {
return None;
}
if it.next().is_some() {
return None; }
}
Some(out)
}
const ND_RUNS: &[(u32, u32)] = &[
(0x0030, 0x0039),
(0x0660, 0x0669),
(0x06F0, 0x06F9),
(0x07C0, 0x07C9),
(0x0966, 0x096F),
(0x09E6, 0x09EF),
(0x0A66, 0x0A6F),
(0x0AE6, 0x0AEF),
(0x0B66, 0x0B6F),
(0x0BE6, 0x0BEF),
(0x0C66, 0x0C6F),
(0x0CE6, 0x0CEF),
(0x0D66, 0x0D6F),
(0x0DE6, 0x0DEF),
(0x0E50, 0x0E59),
(0x0ED0, 0x0ED9),
(0x0F20, 0x0F29),
(0x1040, 0x1049),
(0x1090, 0x1099),
(0x17E0, 0x17E9),
(0x1810, 0x1819),
(0x1946, 0x194F),
(0x19D0, 0x19D9),
(0x1A80, 0x1A89),
(0x1A90, 0x1A99),
(0x1B50, 0x1B59),
(0x1BB0, 0x1BB9),
(0x1C40, 0x1C49),
(0x1C50, 0x1C59),
(0xA620, 0xA629),
(0xA8D0, 0xA8D9),
(0xA900, 0xA909),
(0xA9D0, 0xA9D9),
(0xA9F0, 0xA9F9),
(0xAA50, 0xAA59),
(0xABF0, 0xABF9),
(0xFF10, 0xFF19),
(0x104A0, 0x104A9),
(0x10D30, 0x10D39),
(0x10D40, 0x10D49), (0x11066, 0x1106F),
(0x110F0, 0x110F9),
(0x11136, 0x1113F),
(0x111D0, 0x111D9),
(0x112F0, 0x112F9),
(0x11450, 0x11459),
(0x114D0, 0x114D9),
(0x11650, 0x11659),
(0x116C0, 0x116C9),
(0x116D0, 0x116E3), (0x11730, 0x11739),
(0x118E0, 0x118E9),
(0x11950, 0x11959),
(0x11BF0, 0x11BF9), (0x11C50, 0x11C59),
(0x11D50, 0x11D59),
(0x11DA0, 0x11DA9),
(0x11F50, 0x11F59), (0x16130, 0x16139), (0x16A60, 0x16A69),
(0x16AC0, 0x16AC9), (0x16B50, 0x16B59),
(0x16D70, 0x16D79), (0x1CCF0, 0x1CCF9), (0x1D7CE, 0x1D7FF),
(0x1E140, 0x1E149),
(0x1E2F0, 0x1E2F9),
(0x1E4F0, 0x1E4F9), (0x1E5F1, 0x1E5FA), (0x1E950, 0x1E959),
(0x1FBF0, 0x1FBF9),
];
fn decimal_digit(c: char) -> Option<u8> {
let cp = c as u32;
if c.is_ascii_digit() {
return Some((cp - 0x30) as u8);
}
let idx = ND_RUNS
.binary_search_by(|&(lo, hi)| {
if hi < cp {
Ordering::Less
} else if lo > cp {
Ordering::Greater
} else {
Ordering::Equal
}
})
.ok()?;
Some(((cp - ND_RUNS[idx].0) % 10) as u8)
}
const NONPRINTABLE_RUNS: &[(u32, u32)] = &[
(0x0000, 0x001F), (0x007F, 0x00A0), (0x00AD, 0x00AD), (0x0600, 0x0605), (0x061C, 0x061C), (0x06DD, 0x06DD), (0x070F, 0x070F), (0x0890, 0x0891), (0x08E2, 0x08E2), (0x1680, 0x1680), (0x180E, 0x180E), (0x2000, 0x200F), (0x2028, 0x202F), (0x205F, 0x2064), (0x2066, 0x206F), (0x3000, 0x3000), (0xD800, 0xF8FF), (0xFEFF, 0xFEFF), (0xFFF9, 0xFFFB), (0x110BD, 0x110BD), (0x110CD, 0x110CD), (0x13430, 0x1343F), (0x1BCA0, 0x1BCA3), (0x1D173, 0x1D17A), (0xE0001, 0xE0001), (0xE0020, 0xE007F), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD), ];
fn is_py_printable(c: char) -> bool {
let cp = c as u32;
NONPRINTABLE_RUNS
.binary_search_by(|&(lo, hi)| {
if hi < cp {
Ordering::Less
} else if lo > cp {
Ordering::Greater
} else {
Ordering::Equal
}
})
.is_err()
}
fn py_repr(s: &str) -> String {
let quote = if s.contains('\'') && !s.contains('"') {
'"'
} else {
'\''
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == quote => {
out.push('\\');
out.push(c);
}
c if is_py_printable(c) => out.push(c),
c => {
let cp = c as u32;
if cp < 0x100 {
out.push_str(&format!("\\x{:02x}", cp));
} else if cp < 0x10000 {
out.push_str(&format!("\\u{:04x}", cp));
} else {
out.push_str(&format!("\\U{:08x}", cp));
}
}
}
}
out.push(quote);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn int(n: i64) -> W2nValue {
W2nValue::Int(BigInt::from(n))
}
fn flt(f: f64) -> W2nValue {
W2nValue::Float(f)
}
fn p(s: &str) -> Result<W2nValue> {
parse_number_string(s, None, None, None)
}
fn pl(s: &str, lang: &str) -> Result<W2nValue> {
parse_number_string(s, None, None, Some(lang))
}
fn err(s: &str) -> String {
p(s).unwrap_err().0
}
#[test]
fn get_format_exact_base_and_default() {
assert_eq!(get_format("fr").thousands, " ");
assert_eq!(get_format("fr").decimal, ",");
assert_eq!(get_format("fr_CH").thousands, "'"); assert_eq!(get_format("fr_CH").decimal, ".");
assert_eq!(get_format("en_XX").thousands, ","); assert_eq!(get_format("zz"), get_format("_default"));
assert_eq!(get_format(""), get_format("_default")); assert_eq!(get_format("de").thousands, ".");
assert_eq!(get_format("vi").thousands, ".");
assert_eq!(get_format("da").thousands, "."); assert_eq!(NUMBER_FORMAT_DEFAULTS.len(), 55);
}
#[test]
fn locale_table_uses_ascii_space_only() {
for (k, f) in NUMBER_FORMAT_DEFAULTS {
for sep in [f.thousands, f.decimal] {
assert!(sep.is_ascii(), "{k} has a non-ASCII separator");
}
}
}
#[test]
fn traced_auto_detect() {
assert_eq!(p("1,234").unwrap(), flt(1.234)); assert_eq!(p("1.234").unwrap(), flt(1.234));
assert_eq!(p("12,345").unwrap(), flt(12.345));
assert_eq!(p("1234.567").unwrap(), int(1234567)); assert_eq!(p(",123").unwrap(), int(123)); assert_eq!(p("1,234.56").unwrap(), flt(1234.56)); assert_eq!(p("1.234,56").unwrap(), flt(1234.56));
assert_eq!(p("1 234 567").unwrap(), int(1234567));
assert_eq!(p("1'234.5").unwrap(), flt(1234.5)); assert_eq!(p("1_000").unwrap(), int(1000)); assert_eq!(p("1,,234").unwrap(), int(1234)); assert_eq!(p("12,345.67").unwrap(), flt(12345.67));
assert_eq!(p("1,234,567.89").unwrap(), flt(1234567.89));
}
#[test]
fn traced_space_like_separators_are_distinct() {
assert_ne!(NBSP, NNBSP);
assert_ne!(NNBSP, THIN_SPACE);
assert_eq!(NBSP as u32, 0xA0);
assert_eq!(NNBSP as u32, 0x202F);
assert_eq!(THIN_SPACE as u32, 0x2009);
assert_eq!(SPACE_LIKE.chars().count(), 5);
assert_eq!(p("1\u{202f}234,5").unwrap(), flt(1234.5)); assert_eq!(p("1\u{a0}234,5").unwrap(), flt(1234.5)); assert_eq!(p("1\u{2009}234,5").unwrap(), flt(1234.5)); }
#[test]
fn traced_sign_and_strip() {
assert_eq!(p("+42").unwrap(), int(42));
assert_eq!(p(" 42 ").unwrap(), int(42));
assert_eq!(p("- 42").unwrap(), int(-42)); assert_eq!(p("-1 234,5").unwrap(), flt(-1234.5));
assert_eq!(p("-0").unwrap(), int(0)); assert_eq!(p("\u{a0}\u{a0}42\u{a0}").unwrap(), int(42));
assert_eq!(p("\u{1c}42").unwrap(), int(42)); match p("-0.0").unwrap() {
W2nValue::Float(f) => assert!(f == 0.0 && f.is_sign_negative()),
v => panic!("expected float, got {v:?}"),
}
assert_eq!(err("+-42"), "not a parseable number: '-42'");
}
#[test]
fn traced_lang_path() {
assert_eq!(pl("1.234", "de").unwrap(), int(1234)); assert_eq!(pl("1.234", "en").unwrap(), flt(1.234)); assert_eq!(pl("1 234,5", "fr").unwrap(), flt(1234.5));
assert_eq!(pl("1,234.5", "en").unwrap(), flt(1234.5));
assert_eq!(pl("1.234,5", "de").unwrap(), flt(1234.5));
assert_eq!(pl("1.234.567", "de").unwrap(), int(1234567));
assert_eq!(pl("12.345,67", "de").unwrap(), flt(12345.67));
assert_eq!(pl("1.234.567,89", "de").unwrap(), flt(1234567.89));
assert_eq!(pl("1 234,56", "fr").unwrap(), flt(1234.56));
assert_eq!(pl("1,5", "zz").unwrap(), int(15));
assert_eq!(pl("1.2.3", "fr").unwrap(), int(123));
}
#[test]
fn traced_lang_failure_falls_through_to_auto_detect() {
assert_eq!(pl("1'234", "fr").unwrap(), int(1234));
assert_eq!(
pl("1-2", "en").unwrap_err().0,
"not a parseable number: '1-2'"
);
}
#[test]
fn traced_explicit_separators() {
let ps = |s, t, d| parse_number_string(s, t, d, None);
assert_eq!(ps("1,234", Some(","), None).unwrap(), int(1234));
assert_eq!(ps("1,234", None, Some(",")).unwrap(), flt(1.234));
assert_eq!(ps("1x234y5", Some("x"), Some("y")).unwrap(), flt(1234.5));
assert_eq!(ps("1.234", None, Some(".")).unwrap(), flt(1.234));
assert_eq!(ps("12'345.67", Some("'"), Some(".")).unwrap(), flt(12345.67));
assert_eq!(ps("1_234.56", Some("_"), None).unwrap(), flt(1234.56));
assert_eq!(ps("1.234", Some(""), None).unwrap(), flt(1.234));
assert_eq!(ps("1.234", Some(""), Some("")).unwrap(), flt(1.234));
assert_eq!(ps("1 234", Some(" "), None).unwrap(), int(1234));
assert_eq!(ps("1\u{a0}234", Some(" "), None).unwrap(), int(1234));
assert_eq!(ps("1\u{202f}234", Some("\u{202f}"), None).unwrap(), int(1234));
assert_eq!(ps("1\u{2009}234", Some("\u{2009}"), None).unwrap(), int(1234));
assert_eq!(
ps("1\u{a0}234", Some("\t"), None).unwrap_err().0,
"not a parseable number: '1\\xa0234'"
);
assert_eq!(
ps("1,234", Some("x"), Some("y")).unwrap_err().0,
"not a parseable number: '1,234'"
);
}
#[test]
fn traced_to_number_bigint_and_float() {
assert_eq!(p("0.1").unwrap(), flt(0.1));
assert_eq!(p("1.0000000000000000001").unwrap(), flt(1.0)); assert_eq!(
p(&"9".repeat(100)).unwrap(),
W2nValue::Int("9".repeat(100).parse::<BigInt>().unwrap())
);
match p(&format!("{}.5", "9".repeat(400))).unwrap() {
W2nValue::Float(f) => assert!(f.is_infinite() && f.is_sign_positive()),
v => panic!("expected inf, got {v:?}"),
}
}
#[test]
fn traced_unicode_digits() {
assert_eq!(p("\u{0662}\u{0663}").unwrap(), int(23)); assert_eq!(p("\u{0661}2").unwrap(), int(12)); assert_eq!(p("1\u{0662}3").unwrap(), int(123)); assert_eq!(p("\u{FF11}\u{FF12}\u{FF13}").unwrap(), int(123)); assert_eq!(p("\u{0967}\u{0968}\u{0969}").unwrap(), int(123)); assert_eq!(p("\u{0662}\u{0663}.\u{0665}").unwrap(), flt(23.5));
assert_eq!(decimal_digit('7'), Some(7));
assert_eq!(decimal_digit('\u{0669}'), Some(9)); assert_eq!(decimal_digit('\u{1FBF4}'), Some(4)); assert_eq!(decimal_digit('\u{1D7CE}'), Some(0)); assert_eq!(decimal_digit('\u{1D7FF}'), Some(9)); assert_eq!(decimal_digit('x'), None);
assert_eq!(decimal_digit('\u{065F}'), None); assert_eq!(decimal_digit('\u{066A}'), None); }
#[test]
fn fullmatch_matches_the_regex() {
assert_eq!(fullmatch_number("123").as_deref(), Some("123"));
assert_eq!(fullmatch_number("1.23").as_deref(), Some("1.23"));
assert_eq!(fullmatch_number("12."), None); assert_eq!(fullmatch_number(".5"), None); assert_eq!(fullmatch_number("1.2.3"), None);
assert_eq!(fullmatch_number(""), None);
assert_eq!(fullmatch_number("12a"), None);
assert_eq!(fullmatch_number("1 2"), None);
}
#[test]
fn traced_errors() {
assert_eq!(err(""), "empty numeric string");
assert_eq!(err(" "), "empty numeric string");
assert_eq!(err("-"), "empty numeric string after sign");
assert_eq!(err("+ "), "empty numeric string after sign");
assert_eq!(err("abc"), "not a parseable number: 'abc'");
assert_eq!(err("not a number"), "not a parseable number: 'notanumber'");
assert_eq!(err("12a"), "not a parseable number: '12a'");
}
#[test]
fn py_repr_matches_cpython() {
assert_eq!(py_repr("1 234"), "'1 234'");
assert_eq!(py_repr("1\u{a0}234"), "'1\\xa0234'");
assert_eq!(py_repr("ab'c"), "\"ab'c\"");
assert_eq!(py_repr("ab\"c"), "'ab\"c'");
assert_eq!(py_repr("a'b\"c"), "'a\\'b\"c'");
assert_eq!(py_repr("x\ty"), "'x\\ty'");
assert_eq!(py_repr("x\ny"), "'x\\ny'");
assert_eq!(py_repr("x\ry"), "'x\\ry'");
assert_eq!(py_repr("a\\b"), "'a\\\\b'");
assert_eq!(py_repr("\u{7f}"), "'\\x7f'");
assert_eq!(py_repr("\u{b}"), "'\\x0b'"); assert_eq!(py_repr("\u{c}"), "'\\x0c'"); assert_eq!(py_repr("\u{202f}"), "'\\u202f'");
assert_eq!(py_repr("\u{2009}"), "'\\u2009'");
assert_eq!(py_repr("\u{1c}"), "'\\x1c'");
assert_eq!(py_repr("\u{ad}"), "'\\xad'"); assert_eq!(py_repr("\u{e0001}"), "'\\U000e0001'");
assert_eq!(py_repr("\u{f0000}"), "'\\U000f0000'");
assert_eq!(py_repr("\u{1F600}"), "'\u{1F600}'"); assert_eq!(py_repr("é"), "'é'");
assert_eq!(py_repr("\u{301}"), "'\u{301}'"); assert_eq!(py_repr(""), "''");
assert_eq!(py_repr(" "), "' '"); }
#[test]
fn is_py_space_is_pythons_29_not_rusts_25() {
let n = (0u32..0x110000)
.filter_map(char::from_u32)
.filter(|&c| is_py_space(c))
.count();
assert_eq!(n, 29);
for c in ['\u{1c}', '\u{1d}', '\u{1e}', '\u{1f}'] {
assert!(is_py_space(c) && !c.is_whitespace());
}
}
#[test]
fn nd_runs_are_well_formed() {
let mut total = 0;
let mut prev_hi = 0u32;
for (i, &(lo, hi)) in ND_RUNS.iter().enumerate() {
assert!(lo <= hi);
assert!(i == 0 || lo > prev_hi, "runs must be sorted and disjoint");
assert_eq!((hi - lo + 1) % 10, 0, "every run is whole decades");
prev_hi = hi;
total += hi - lo + 1;
}
assert_eq!(total, 760); }
}