From 7481ceb798714ef9b4cc1a34c47d25297a476446 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 21 Sep 2026 01:58:31 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=20String=20locale=20?= =?UTF-8?q?=E4=B8=89=E6=96=B9=E6=B3=95=E4=B8=8E=20localeCompare=20?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/string/locale.rs | 527 ++++++++++++++++++ crates/oxide_builtins/src/string/mod.rs | 7 +- crates/oxide_kernel/src/builtin/bind.rs | 3 + crates/oxide_kernel/src/builtin/methods.rs | 3 + crates/oxide_vm/src/bindings/bind_string.rs | 3 + .../oxide_vm/tests/builtins_string_tests.rs | 108 ++++ 6 files changed, 649 insertions(+), 2 deletions(-) create mode 100644 crates/oxide_builtins/src/string/locale.rs diff --git a/crates/oxide_builtins/src/string/locale.rs b/crates/oxide_builtins/src/string/locale.rs new file mode 100644 index 00000000..1cc45ef5 --- /dev/null +++ b/crates/oxide_builtins/src/string/locale.rs @@ -0,0 +1,527 @@ +//! String locale 三方法:toLocaleLowerCase / toLocaleUpperCase / localeCompare。 +//! +//! toLocaleLowerCase 与 toLowerCase 同映射表,增量仅 Final Sigma 条件映射 +//! (σ/ς 判定按原始单元序列、双向跳过 Mn/Cf 找最近可见字符); +//! toLocaleUpperCase 与 toUpperCase 同表无条件映射;localeCompare 基本档 = +//! NFC 归一后 UTF-16 单元序比较,第二参缺省/undefined 落 "undefined" 串。 + +use std::cmp::Ordering; + +use oxide_runtime_api::{NativeResult, VmHost}; +use oxide_types::value::JsValue; + +use crate::builtins_debug; + +use super::common::try_string; +use super::{as_units, map_well_formed_segments, this_units}; + +// ── Mn/Cf 判定表 ───────────────────────────────────────────────────────── +// +// Final Sigma 条件映射的"不可见"判定:Mn(非连接修饰符号)与 Cf(格式字符) +// 全集范围表(UCD 快照,367 段,起点升序)。表取全集而非语料钉点,防语料 +// 扩充翻车;仅当未来 Unicode 版本新增 Mn/Cf 码点且语料随之扩充时需重生成。 + +const MN_CF_RANGES: &[(u32, u32)] = &[ + (0xad, 0xad), + (0x300, 0x36f), + (0x483, 0x487), + (0x591, 0x5bd), + (0x5bf, 0x5bf), + (0x5c1, 0x5c2), + (0x5c4, 0x5c5), + (0x5c7, 0x5c7), + (0x600, 0x605), + (0x610, 0x61a), + (0x61c, 0x61c), + (0x64b, 0x65f), + (0x670, 0x670), + (0x6d6, 0x6dc), + (0x6dd, 0x6dd), + (0x6df, 0x6e4), + (0x6e7, 0x6e8), + (0x6ea, 0x6ed), + (0x70f, 0x70f), + (0x711, 0x711), + (0x730, 0x74a), + (0x7a6, 0x7b0), + (0x7eb, 0x7f3), + (0x7fd, 0x7fd), + (0x816, 0x819), + (0x81b, 0x823), + (0x825, 0x827), + (0x829, 0x82d), + (0x859, 0x85b), + (0x890, 0x891), + (0x898, 0x89f), + (0x8ca, 0x8e1), + (0x8e2, 0x8e2), + (0x8e3, 0x902), + (0x93a, 0x93a), + (0x93c, 0x93c), + (0x941, 0x948), + (0x94d, 0x94d), + (0x951, 0x957), + (0x962, 0x963), + (0x981, 0x981), + (0x9bc, 0x9bc), + (0x9c1, 0x9c4), + (0x9cd, 0x9cd), + (0x9e2, 0x9e3), + (0x9fe, 0x9fe), + (0xa01, 0xa02), + (0xa3c, 0xa3c), + (0xa41, 0xa42), + (0xa47, 0xa48), + (0xa4b, 0xa4d), + (0xa51, 0xa51), + (0xa70, 0xa71), + (0xa75, 0xa75), + (0xa81, 0xa82), + (0xabc, 0xabc), + (0xac1, 0xac5), + (0xac7, 0xac8), + (0xacd, 0xacd), + (0xae2, 0xae3), + (0xafa, 0xaff), + (0xb01, 0xb01), + (0xb3c, 0xb3c), + (0xb3f, 0xb3f), + (0xb41, 0xb44), + (0xb4d, 0xb4d), + (0xb55, 0xb56), + (0xb62, 0xb63), + (0xb82, 0xb82), + (0xbc0, 0xbc0), + (0xbcd, 0xbcd), + (0xc00, 0xc00), + (0xc04, 0xc04), + (0xc3c, 0xc3c), + (0xc3e, 0xc40), + (0xc46, 0xc48), + (0xc4a, 0xc4d), + (0xc55, 0xc56), + (0xc62, 0xc63), + (0xc81, 0xc81), + (0xcbc, 0xcbc), + (0xcbf, 0xcbf), + (0xcc6, 0xcc6), + (0xccc, 0xccd), + (0xce2, 0xce3), + (0xd00, 0xd01), + (0xd3b, 0xd3c), + (0xd41, 0xd44), + (0xd4d, 0xd4d), + (0xd62, 0xd63), + (0xd81, 0xd81), + (0xdca, 0xdca), + (0xdd2, 0xdd4), + (0xdd6, 0xdd6), + (0xe31, 0xe31), + (0xe34, 0xe3a), + (0xe47, 0xe4e), + (0xeb1, 0xeb1), + (0xeb4, 0xebc), + (0xec8, 0xece), + (0xf18, 0xf19), + (0xf35, 0xf35), + (0xf37, 0xf37), + (0xf39, 0xf39), + (0xf71, 0xf7e), + (0xf80, 0xf84), + (0xf86, 0xf87), + (0xf8d, 0xf97), + (0xf99, 0xfbc), + (0xfc6, 0xfc6), + (0x102d, 0x1030), + (0x1032, 0x1037), + (0x1039, 0x103a), + (0x103d, 0x103e), + (0x1058, 0x1059), + (0x105e, 0x1060), + (0x1071, 0x1074), + (0x1082, 0x1082), + (0x1085, 0x1086), + (0x108d, 0x108d), + (0x109d, 0x109d), + (0x135d, 0x135f), + (0x1712, 0x1714), + (0x1732, 0x1733), + (0x1752, 0x1753), + (0x1772, 0x1773), + (0x17b4, 0x17b5), + (0x17b7, 0x17bd), + (0x17c6, 0x17c6), + (0x17c9, 0x17d3), + (0x17dd, 0x17dd), + (0x180b, 0x180d), + (0x180e, 0x180e), + (0x180f, 0x180f), + (0x1885, 0x1886), + (0x18a9, 0x18a9), + (0x1920, 0x1922), + (0x1927, 0x1928), + (0x1932, 0x1932), + (0x1939, 0x193b), + (0x1a17, 0x1a18), + (0x1a1b, 0x1a1b), + (0x1a56, 0x1a56), + (0x1a58, 0x1a5e), + (0x1a60, 0x1a60), + (0x1a62, 0x1a62), + (0x1a65, 0x1a6c), + (0x1a73, 0x1a7c), + (0x1a7f, 0x1a7f), + (0x1ab0, 0x1abd), + (0x1abf, 0x1ace), + (0x1b00, 0x1b03), + (0x1b34, 0x1b34), + (0x1b36, 0x1b3a), + (0x1b3c, 0x1b3c), + (0x1b42, 0x1b42), + (0x1b6b, 0x1b73), + (0x1b80, 0x1b81), + (0x1ba2, 0x1ba5), + (0x1ba8, 0x1ba9), + (0x1bab, 0x1bad), + (0x1be6, 0x1be6), + (0x1be8, 0x1be9), + (0x1bed, 0x1bed), + (0x1bef, 0x1bf1), + (0x1c2c, 0x1c33), + (0x1c36, 0x1c37), + (0x1cd0, 0x1cd2), + (0x1cd4, 0x1ce0), + (0x1ce2, 0x1ce8), + (0x1ced, 0x1ced), + (0x1cf4, 0x1cf4), + (0x1cf8, 0x1cf9), + (0x1dc0, 0x1dff), + (0x200b, 0x200f), + (0x202a, 0x202e), + (0x2060, 0x2064), + (0x2066, 0x206f), + (0x20d0, 0x20dc), + (0x20e1, 0x20e1), + (0x20e5, 0x20f0), + (0x2cef, 0x2cf1), + (0x2d7f, 0x2d7f), + (0x2de0, 0x2dff), + (0x302a, 0x302d), + (0x3099, 0x309a), + (0xa66f, 0xa66f), + (0xa674, 0xa67d), + (0xa69e, 0xa69f), + (0xa6f0, 0xa6f1), + (0xa802, 0xa802), + (0xa806, 0xa806), + (0xa80b, 0xa80b), + (0xa825, 0xa826), + (0xa82c, 0xa82c), + (0xa8c4, 0xa8c5), + (0xa8e0, 0xa8f1), + (0xa8ff, 0xa8ff), + (0xa926, 0xa92d), + (0xa947, 0xa951), + (0xa980, 0xa982), + (0xa9b3, 0xa9b3), + (0xa9b6, 0xa9b9), + (0xa9bc, 0xa9bd), + (0xa9e5, 0xa9e5), + (0xaa29, 0xaa2e), + (0xaa31, 0xaa32), + (0xaa35, 0xaa36), + (0xaa43, 0xaa43), + (0xaa4c, 0xaa4c), + (0xaa7c, 0xaa7c), + (0xaab0, 0xaab0), + (0xaab2, 0xaab4), + (0xaab7, 0xaab8), + (0xaabe, 0xaabf), + (0xaac1, 0xaac1), + (0xaaec, 0xaaed), + (0xaaf6, 0xaaf6), + (0xabe5, 0xabe5), + (0xabe8, 0xabe8), + (0xabed, 0xabed), + (0xfb1e, 0xfb1e), + (0xfe00, 0xfe0f), + (0xfe20, 0xfe2f), + (0xfeff, 0xfeff), + (0xfff9, 0xfffb), + (0x101fd, 0x101fd), + (0x102e0, 0x102e0), + (0x10376, 0x1037a), + (0x10a01, 0x10a03), + (0x10a05, 0x10a06), + (0x10a0c, 0x10a0f), + (0x10a38, 0x10a3a), + (0x10a3f, 0x10a3f), + (0x10ae5, 0x10ae6), + (0x10d24, 0x10d27), + (0x10eab, 0x10eac), + (0x10efd, 0x10eff), + (0x10f46, 0x10f50), + (0x10f82, 0x10f85), + (0x11001, 0x11001), + (0x11038, 0x11046), + (0x11070, 0x11070), + (0x11073, 0x11074), + (0x1107f, 0x11081), + (0x110b3, 0x110b6), + (0x110b9, 0x110ba), + (0x110bd, 0x110bd), + (0x110c2, 0x110c2), + (0x110cd, 0x110cd), + (0x11100, 0x11102), + (0x11127, 0x1112b), + (0x1112d, 0x11134), + (0x11173, 0x11173), + (0x11180, 0x11181), + (0x111b6, 0x111be), + (0x111c9, 0x111cc), + (0x111cf, 0x111cf), + (0x1122f, 0x11231), + (0x11234, 0x11234), + (0x11236, 0x11237), + (0x1123e, 0x1123e), + (0x11241, 0x11241), + (0x112df, 0x112df), + (0x112e3, 0x112ea), + (0x11300, 0x11301), + (0x1133b, 0x1133c), + (0x11340, 0x11340), + (0x11366, 0x1136c), + (0x11370, 0x11374), + (0x11438, 0x1143f), + (0x11442, 0x11444), + (0x11446, 0x11446), + (0x1145e, 0x1145e), + (0x114b3, 0x114b8), + (0x114ba, 0x114ba), + (0x114bf, 0x114c0), + (0x114c2, 0x114c3), + (0x115b2, 0x115b5), + (0x115bc, 0x115bd), + (0x115bf, 0x115c0), + (0x115dc, 0x115dd), + (0x11633, 0x1163a), + (0x1163d, 0x1163d), + (0x1163f, 0x11640), + (0x116ab, 0x116ab), + (0x116ad, 0x116ad), + (0x116b0, 0x116b5), + (0x116b7, 0x116b7), + (0x1171d, 0x1171f), + (0x11722, 0x11725), + (0x11727, 0x1172b), + (0x1182f, 0x11837), + (0x11839, 0x1183a), + (0x1193b, 0x1193c), + (0x1193e, 0x1193e), + (0x11943, 0x11943), + (0x119d4, 0x119d7), + (0x119da, 0x119db), + (0x119e0, 0x119e0), + (0x11a01, 0x11a0a), + (0x11a33, 0x11a38), + (0x11a3b, 0x11a3e), + (0x11a47, 0x11a47), + (0x11a51, 0x11a56), + (0x11a59, 0x11a5b), + (0x11a8a, 0x11a96), + (0x11a98, 0x11a99), + (0x11c30, 0x11c36), + (0x11c38, 0x11c3d), + (0x11c3f, 0x11c3f), + (0x11c92, 0x11ca7), + (0x11caa, 0x11cb0), + (0x11cb2, 0x11cb3), + (0x11cb5, 0x11cb6), + (0x11d31, 0x11d36), + (0x11d3a, 0x11d3a), + (0x11d3c, 0x11d3d), + (0x11d3f, 0x11d45), + (0x11d47, 0x11d47), + (0x11d90, 0x11d91), + (0x11d95, 0x11d95), + (0x11d97, 0x11d97), + (0x11ef3, 0x11ef4), + (0x11f00, 0x11f01), + (0x11f36, 0x11f3a), + (0x11f40, 0x11f40), + (0x11f42, 0x11f42), + (0x13430, 0x1343f), + (0x13440, 0x13440), + (0x13447, 0x13455), + (0x16af0, 0x16af4), + (0x16b30, 0x16b36), + (0x16f4f, 0x16f4f), + (0x16f8f, 0x16f92), + (0x16fe4, 0x16fe4), + (0x1bc9d, 0x1bc9e), + (0x1bca0, 0x1bca3), + (0x1cf00, 0x1cf2d), + (0x1cf30, 0x1cf46), + (0x1d167, 0x1d169), + (0x1d173, 0x1d17a), + (0x1d17b, 0x1d182), + (0x1d185, 0x1d18b), + (0x1d1aa, 0x1d1ad), + (0x1d242, 0x1d244), + (0x1da00, 0x1da36), + (0x1da3b, 0x1da6c), + (0x1da75, 0x1da75), + (0x1da84, 0x1da84), + (0x1da9b, 0x1da9f), + (0x1daa1, 0x1daaf), + (0x1e000, 0x1e006), + (0x1e008, 0x1e018), + (0x1e01b, 0x1e021), + (0x1e023, 0x1e024), + (0x1e026, 0x1e02a), + (0x1e08f, 0x1e08f), + (0x1e130, 0x1e136), + (0x1e2ae, 0x1e2ae), + (0x1e2ec, 0x1e2ef), + (0x1e4ec, 0x1e4ef), + (0x1e8d0, 0x1e8d6), + (0x1e944, 0x1e94a), + (0xe0001, 0xe0001), + (0xe0020, 0xe007f), + (0xe0100, 0xe01ef), +]; + +/// 码点是否属 Mn/Cf(范围表二分判定,起点升序)。 +fn is_mn_cf(cp: u32) -> bool { + let idx = MN_CF_RANGES.partition_point(|&(lo, _)| lo <= cp); + idx > 0 && cp <= MN_CF_RANGES[idx - 1].1 +} + +// ── Final Sigma 条件映射 ────────────────────────────────────────────────── + +/// Final Sigma 条件小写映射:逐码点小写,其中 U+03A3(Σ)按条件规则取 σ/ς。 +/// +/// σ/ς 判定按原始单元序列(非变换后):后向(索引升方向)跳过 Mn/Cf 单元 +/// 存在可见字符 → σ;后向无且前向(索引降方向)跳过 Mn/Cf 存在可见字符 → ς; +/// 前后均无可见字符 → σ。孤立 surrogate 原样透传(与 toLowerCase 同口径)。 +fn final_sigma_lower(units: &[u16]) -> Vec { + let mut out = Vec::with_capacity(units.len()); + let mut i = 0; + while i < units.len() { + let u = units[i]; + if u == 0x03A3 { + // 双向跳过 Mn/Cf 找最近可见字符(含行尾前的任意非 Mn/Cf 字符)。 + let mut j = i + 1; + while j < units.len() && is_mn_cf(units[j] as u32) { + j += 1; + } + let after_visible = j < units.len(); + let mut j = i; + while j > 0 && is_mn_cf(units[j - 1] as u32) { + j -= 1; + } + let before_visible = j > 0; + out.push(if after_visible || !before_visible { 0x03C3 } else { 0x03C2 }); + i += 1; + continue; + } + if (0xD800..=0xDBFF).contains(&u) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { + // 代理对整体解码为码点小写后回写 UTF-16。 + let cp = (((u as u32) - 0xD800) << 10 | (units[i + 1] as u32 - 0xDC00)) + 0x10000; + let c = char::from_u32(cp).expect("合法代理对必解码为合法码点"); + out.extend(c.to_lowercase().collect::().encode_utf16()); + i += 2; + continue; + } + if (0xD800..=0xDFFF).contains(&u) { + out.push(u); + } else if let Some(c) = char::from_u32(u as u32) { + out.extend(c.to_lowercase().collect::().encode_utf16()); + } + i += 1; + } + out +} + +// ── 三方法 ─────────────────────────────────────────────────────────────── + +/// `String.prototype.toLocaleLowerCase`:locale 无关小写(与 toLowerCase 同表, +/// 增量 Final Sigma 条件映射)。 +pub fn string_to_locale_lower_case(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.toLocaleLowerCase called with {} args", args.len()); + let s = try_string!(this_units(vm, args)).into_owned(); + let out = final_sigma_lower(&s); + NativeResult::Ok(vm.new_string_units_owned(out)) +} + +/// `String.prototype.toLocaleUpperCase`:locale 无关大写(与 toUpperCase 同表, +/// 良形段映射,孤立 surrogate 单元原样保留)。 +pub fn string_to_locale_upper_case(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.toLocaleUpperCase called with {} args", args.len()); + let s = try_string!(this_units(vm, args)); + let out = map_well_formed_segments(&s, |seg| seg.to_uppercase()); + NativeResult::Ok(vm.new_string_units_owned(out)) +} + +/// `String.prototype.localeCompare(compareString)`:基本档比较——两侧 NFC 归一后 +/// 按 UTF-16 单元序(良形文本下与码元序等价)返回 -1/0/1。 +/// +/// # 边界与前提 +/// - 第二参缺省或 undefined 落 "undefined" 串(规范基本档允许);其余非字符串经 +/// 完整 ToString,Symbol 抛 TypeError;this 的 null/undefined/symbol 经 +/// this_units 抛 TypeError。 +/// +/// # 注意事项 +/// - 非 NFC 等价但 UCA 可分的串按单元序判(如 "ö" vs "o"),属基本档固有边界。 +pub fn string_locale_compare(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.localeCompare called with {} args", args.len()); + use unicode_normalization::UnicodeNormalization; + let a = try_string!(this_units(vm, args)).into_owned(); + let b = if args.len() > 1 && !vm.reg(args[1]).is_undefined() { + try_string!(as_units(vm, vm.reg(args[1]))).into_owned() + } else { + "undefined".encode_utf16().collect() + }; + // NFC 归一(良形段映射,孤立 surrogate 透传,与 normalize 同路径)。 + let a = map_well_formed_segments(&a, |seg| seg.nfc().collect()); + let b = map_well_formed_segments(&b, |seg| seg.nfc().collect()); + NativeResult::Ok(JsValue::int(match a.cmp(&b) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mn_cf_table_corpus_points() { + // 语料钉死码点:Cf/Mn 在表、Lu 不属表。 + assert!(is_mn_cf(0x00AD)); + assert!(is_mn_cf(0x0345)); + assert!(is_mn_cf(0x180E)); + assert!(is_mn_cf(0x1D242)); + assert!(!is_mn_cf(0x1D4A2)); + assert!(!is_mn_cf(0x0041)); + } + + #[test] + fn final_sigma_conditional_mapping() { + // (输入单元, 期望输出):单枚/前后可见/行首行尾 Mn/Cf 环绕。 + let cases: &[(&[u16], &[u16])] = &[ + (&[0x03A3], &[0x03C3]), + (&[0x41, 0x03A3], &[0x61, 0x03C2]), + (&[0xD835, 0xDCA2, 0x03A3], &[0xD835, 0xDCA2, 0x03C2]), + (&[0x0345, 0x03A3], &[0x0345, 0x03C3]), + (&[0x41, 0x03A3, 0x2E], &[0x61, 0x03C3, 0x2E]), + (&[0x41, 0x03A3, 0x0345], &[0x61, 0x03C2, 0x0345]), + (&[0x41, 0x03A3, 0x180E], &[0x61, 0x03C2, 0x180E]), + (&[0x41, 0x03A3, 0x180E, 0x42], &[0x61, 0x03C3, 0x180E, 0x62]), + ]; + for (input, want) in cases { + assert_eq!(&final_sigma_lower(input), want); + } + } +} diff --git a/crates/oxide_builtins/src/string/mod.rs b/crates/oxide_builtins/src/string/mod.rs index 22a60881..37c7c94e 100644 --- a/crates/oxide_builtins/src/string/mod.rs +++ b/crates/oxide_builtins/src/string/mod.rs @@ -1,12 +1,15 @@ //! String 内置对象实现(constructor 与全部 String.prototype 方法)。 //! -//! 按职责分 `common` 文本通道、`basic` 基础方法族、`regex` 正则与匹配族, -//! 三族经本模块 glob 重导出,供绑定层(`oxide_builtins::string::`)与 `regexp` 模块(`crate::string::`)消费。 +//! 按职责分 `common` 文本通道、`basic` 基础方法族、`regex` 正则与匹配族、 +//! `locale` locale 三方法族,各族经本模块 glob 重导出,供绑定层 +//! (`oxide_builtins::string::`)与 `regexp` 模块(`crate::string::`)消费。 mod basic; mod common; +mod locale; mod regex; pub use basic::*; pub(crate) use common::*; +pub use locale::*; pub use regex::*; diff --git a/crates/oxide_kernel/src/builtin/bind.rs b/crates/oxide_kernel/src/builtin/bind.rs index e6c85583..1a25fc17 100644 --- a/crates/oxide_kernel/src/builtin/bind.rs +++ b/crates/oxide_kernel/src/builtin/bind.rs @@ -247,6 +247,9 @@ impl BuiltinWorld { ("substring", methods.substring, 2), ("toUpperCase", methods.to_upper_case, 0), ("toLowerCase", methods.to_lower_case, 0), + ("toLocaleUpperCase", methods.to_locale_upper_case, 0), + ("toLocaleLowerCase", methods.to_locale_lower_case, 0), + ("localeCompare", methods.locale_compare, 1), ("trim", methods.trim, 0), ("repeat", methods.repeat, 1), ("padStart", methods.pad_start, 1), diff --git a/crates/oxide_kernel/src/builtin/methods.rs b/crates/oxide_kernel/src/builtin/methods.rs index b71443c6..2ddc7657 100644 --- a/crates/oxide_kernel/src/builtin/methods.rs +++ b/crates/oxide_kernel/src/builtin/methods.rs @@ -122,6 +122,9 @@ pub struct StringMethods { pub is_well_formed: *const (), pub to_well_formed: *const (), pub from_raw: *const (), + pub to_locale_lower_case: *const (), + pub to_locale_upper_case: *const (), + pub locale_compare: *const (), } /// RegExp 原型方法的 native 函数指针集合。 diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index b3d5dd86..ce73c18c 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -19,6 +19,9 @@ pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut substring: oxide_builtins::string::string_substring:: as *const (), to_upper_case: oxide_builtins::string::string_to_upper_case:: as *const (), to_lower_case: oxide_builtins::string::string_to_lower_case:: as *const (), + to_locale_upper_case: oxide_builtins::string::string_to_locale_upper_case:: as *const (), + to_locale_lower_case: oxide_builtins::string::string_to_locale_lower_case:: as *const (), + locale_compare: oxide_builtins::string::string_locale_compare:: as *const (), trim: oxide_builtins::string::string_trim:: as *const (), repeat: oxide_builtins::string::string_repeat:: as *const (), pad_start: oxide_builtins::string::string_pad_start:: as *const (), diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 35b2128f..8d4827a0 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -1404,3 +1404,111 @@ fn unicode_brace_escape_supplementary_and_lone_surrogate() { .unwrap(); assert!(r.as_bool()); } + +#[test] +fn string_to_locale_lower_case_final_sigma() { + // Final Sigma 条件映射 21 断言面(special_casing_conditional 15 + U180E 6): + // σ/ς 按原始序列双向跳过 Mn/Cf 找最近可见字符判定。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(\"\\u03A3\".toLocaleLowerCase() === \"\\u03C3\" && \ + \"A\\u03A3\".toLocaleLowerCase() === \"a\\u03C2\" && \ + \"\\uD835\\uDCA2\\u03A3\".toLocaleLowerCase() === \"\\uD835\\uDCA2\\u03C2\" && \ + \"A.\\u03A3\".toLocaleLowerCase() === \"a.\\u03C2\" && \ + \"A\\u00AD\\u03A3\".toLocaleLowerCase() === \"a\\u00AD\\u03C2\" && \ + \"A\\uD834\\uDE42\\u03A3\".toLocaleLowerCase() === \"a\\uD834\\uDE42\\u03C2\" && \ + \"\\u0345\\u03A3\".toLocaleLowerCase() === \"\\u0345\\u03C3\" && \ + \"\\u0391\\u0345\\u03A3\".toLocaleLowerCase() === \"\\u03B1\\u0345\\u03C2\" && \ + \"A\\u03A3B\".toLocaleLowerCase() === \"a\\u03C3b\" && \ + \"A\\u03A3\\uD835\\uDCA2\".toLocaleLowerCase() === \"a\\u03C3\\uD835\\uDCA2\" && \ + \"A\\u03A3.b\".toLocaleLowerCase() === \"a\\u03C3.b\" && \ + \"A\\u03A3\\u00ADB\".toLocaleLowerCase() === \"a\\u03C3\\u00ADb\" && \ + \"A\\u03A3\\uD834\\uDE42B\".toLocaleLowerCase() === \"a\\u03C3\\uD834\\uDE42b\" && \ + \"A\\u03A3\\u0345\".toLocaleLowerCase() === \"a\\u03C2\\u0345\" && \ + \"A\\u03A3\\u0345\\u0391\".toLocaleLowerCase() === \"a\\u03C3\\u0345\\u03B1\" && \ + \"A\\u180E\\u03A3\".toLocaleLowerCase() === \"a\\u180E\\u03C2\" && \ + \"A\\u180E\\u03A3B\".toLocaleLowerCase() === \"a\\u180E\\u03C3b\" && \ + \"A\\u03A3\\u180E\".toLocaleLowerCase() === \"a\\u03C2\\u180E\" && \ + \"A\\u03A3\\u180EB\".toLocaleLowerCase() === \"a\\u03C3\\u180Eb\" && \ + \"A\\u180E\\u03A3\\u180E\".toLocaleLowerCase() === \"a\\u180E\\u03C2\\u180E\" && \ + \"A\\u180E\\u03A3\\u180EB\".toLocaleLowerCase() === \"a\\u180E\\u03C3\\u180Eb\")", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn string_to_locale_case_basic_mapping() { + // 与 toLowerCase/toUpperCase 同表:ß 小写不变、大写 SS;Σ 大写恒 Σ; + // 补充平面代理对整体映射。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "\"ß\".toLocaleLowerCase() === \"ß\" && \ + \"ß\".toLocaleUpperCase() === \"SS\" && \ + \"\\u03A3\".toLocaleUpperCase() === \"\\u03A3\" && \ + \"\\u03C3\".toLocaleLowerCase() === \"\\u03C3\" && \ + \"\\u03C3\".toLocaleUpperCase() === \"\\u03A3\" && \ + \"aB\".toLocaleUpperCase() === \"AB\" && \ + \"aB\".toLocaleLowerCase() === \"ab\" && \ + \"\\uD83D\\uDE00\".toLocaleLowerCase() === \"\\uD83D\\uDE00\"", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn string_locale_compare_canonical_and_order() { + // NFC 规范等价对全 0(含组合符重排与 Hangul 合成对);缺省/undefined/ + // "undefined" 三式等价;码元序方向与反对称。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(\"o\\u0308\".localeCompare(\"ö\") === 0 && \ + \"Ç\".localeCompare(\"C\\u0327\") === 0 && \ + \"가\".localeCompare(\"\\u1100\\u1161\") === 0 && \ + \"ô\".localeCompare(\"o\\u0302\") === 0 && \ + \"ṩ\".localeCompare(\"s\\u0323\\u0307\") === 0 && \ + \"a\".localeCompare() === \"a\".localeCompare(undefined) && \ + \"a\".localeCompare() === \"a\".localeCompare(\"undefined\") && \ + \"a\".localeCompare(\"b\") === -1 && \"b\".localeCompare(\"a\") === 1 && \ + \"h\".localeCompare(\"H\") === -\"H\".localeCompare(\"h\") && \ + \"a\".localeCompare(1) === \"a\".localeCompare(\"1\"))", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn string_locale_methods_type_errors() { + // null/undefined/symbol 接收者抛 TypeError;localeCompare 第二参 + // Symbol 抛 TypeError。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "((function(){ try { (null).toLocaleLowerCase(); return false; } catch (e) { return e.name === \"TypeError\"; } })() && \ + (function(){ try { (undefined).toLocaleUpperCase(); return false; } catch (e) { return e.name === \"TypeError\"; } })() && \ + (function(){ try { (null).localeCompare(\"a\"); return false; } catch (e) { return e.name === \"TypeError\"; } })() && \ + (function(){ try { \"a\".localeCompare(Symbol(\"s\")); return false; } catch (e) { return e.name === \"TypeError\"; } })())", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn string_locale_methods_length_and_name() { + // 绑定描述符面:length 0/0/1、name 与方法名一致。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "String.prototype.toLocaleLowerCase.length === 0 && \ + String.prototype.toLocaleUpperCase.length === 0 && \ + String.prototype.localeCompare.length === 1 && \ + String.prototype.toLocaleLowerCase.name === \"toLocaleLowerCase\" && \ + String.prototype.toLocaleUpperCase.name === \"toLocaleUpperCase\" && \ + String.prototype.localeCompare.name === \"localeCompare\"", + ) + .unwrap(); + assert!(r.as_bool()); +}