// util.mbt —— 译脉·先知 2.0 (MoonBit) 编码层 + JSON 辅助
// 零依赖:仅 moonbitlang/core/json 与 moonbitlang/core/math
// 对应 D1–D8 规范的分词 / TF 向量 / 余弦与共享 JSON 构造逻辑。
// ---- 算法常量(HEBB_LR 等权重参数在 engine.mbt 定义,按 D1–D8 规范设定)----
// 长文兜底:分词 / 字符 N-gram 输出的最大 token 数。
// 防恶意/错误输入(如 5MB 文档)撑爆内存;CJK 因 bigram 折算会接近 2× char 数。
// 经验值:1 万汉字 ≈ 2 万 token,远低于工业 NLU(512 token 上限),留 4× 余量。
const MAX_TOKENS : Int = 20000
// 停用词:中文功能词 + 英文停用词(按 D1–D8 分词规范设定)
// P0 性能:数组线性扫描 → Map 集合 O(1) 查询(yimai_tokenize 对每个 token 调一次,
// 58 词 × 每 token 一次字符串比较是分词热点)。R15:仅 contains 查询、不迭代,顺序无关。
let stop_map : Map[String, Bool] = Map([
("的", true), ("了", true), ("和", true), ("是", true), ("在", true), ("我", true),
("你", true), ("他", true), ("她", true), ("它", true), ("们", true), ("这", true),
("那", true), ("有", true), ("就", true), ("不", true), ("也", true), ("都", true),
("一个", true), ("上", true), ("下", true), ("中", true), ("与", true), ("及", true),
("或", true), ("把", true), ("被", true), ("给", true), ("对", true), ("从", true),
("到", true), ("等", true), ("用", true), ("将", true), ("要", true), ("会", true),
("能", true), ("可以", true), ("通过", true), ("根据", true), ("按照", true), ("关于", true),
("进行", true), ("以及", true), ("a", true), ("an", true), ("the", true), ("of", true),
("to", true), ("and", true), ("or", true), ("in", true), ("on", true), ("for", true),
("with", true), ("is", true), ("are", true), ("be", true),
])
fn is_stop(t : String) -> Bool {
stop_map.contains(t)
}
// ---------------------------------------------------------------------------
// 字符串辅助
// ---------------------------------------------------------------------------
// 仅对 ASCII 字母做小写(中文等不受影响)
fn to_lower_ascii(s : String) -> String {
let b = StringBuilder()
for c in s.iter() {
if c >= 'A' && c <= 'Z' {
b.write_char((c.to_int() + 32).unsafe_to_char())
} else {
b.write_char(c)
}
}
b.to_string()
}
// URL 百分号解码(仅解码 %XX 形式;保留 '+' 为字面,不做 form-encoding 转换)。
// 用途:serve_static 在路径安全检查前先解码,让 %2e%2e%2f 不能绕过 ".." 字符串检查。
// 非 ASCII 字符按 UTF-8 多字节原样透传;不识别的 % 序列保留原样(保守策略)。
// 零依赖;不调 @fs / @io,纯字符串处理。
pub fn decode_pct(s : String) -> String {
let b = StringBuilder()
let chars = s.to_array()
let n = chars.length()
let mut i = 0
while i < n {
let cur = chars[i]
if cur == '%' && i + 2 < n {
// 尝试解码 %XX
let d1 = hex_nibble_char(chars[i + 1])
let d2 = hex_nibble_char(chars[i + 2])
match (d1, d2) {
(Some(n1), Some(n2)) => {
b.write_char((n1 * 16 + n2).unsafe_to_char())
i = i + 3
continue
}
_ => ()
}
}
b.write_char(cur)
i = i + 1
}
b.to_string()
}
// 单个十六进制字符 → 0..15;非 hex 返回 None
fn hex_nibble_char(c : Char) -> Option[Int] {
let v = c.to_int()
if v >= 0x30 && v <= 0x39 { Some(v - 0x30) } // 0-9
else if v >= 0x41 && v <= 0x46 { Some(v - 0x41 + 10) } // A-F
else if v >= 0x61 && v <= 0x66 { Some(v - 0x61 + 10) } // a-f
else { None }
}
// WAL 字段转义:确保控制字符分隔符 \u0001 不会出现在字段内容中。
// 转义规则(双字符):\u0002 -> \u0002\u0002, \u0001 -> \u0002\u0001
// 正常文本不会包含这两个控制字符,因此转义操作通常是零开销。
// 将 UTF-16 码点转换为 Char(控制字符走此路径)
fn u16_to_char(c : UInt16) -> Char {
c.to_int().unsafe_to_char()
}
pub fn wal_escape_field(s : String) -> String {
let b = StringBuilder()
for i = 0; i < s.length(); i = i + 1 {
let c = u16_to_char(s[i])
if c == '\u0002' {
b.write_char('\u0002')
b.write_char('\u0002')
} else if c == '\u0001' {
b.write_char('\u0002')
b.write_char('\u0001')
} else {
b.write_char(c)
}
}
b.to_string()
}
// WAL 字段反转义:与 wal_escape_field 配对。
pub fn wal_unescape_field(s : String) -> String {
let b = StringBuilder()
let mut i = 0
while i < s.length() {
let c = u16_to_char(s[i])
if c == '\u0002' && i + 1 < s.length() {
let next = u16_to_char(s[i + 1])
if next == '\u0002' {
b.write_char('\u0002')
i = i + 2
} else if next == '\u0001' {
b.write_char('\u0001')
i = i + 2
} else {
b.write_char(c)
i = i + 1
}
} else {
b.write_char(c)
i = i + 1
}
}
b.to_string()
}
// 概念抽象·角色抽取:取文本前 4 字作为粗略类别键(跨主题稳定)
// 注意:须对短文本做长度钳制,否则 text[:4] 在 length<4 时抛异常
pub fn role_of(text : String) -> String {
let n = text.length()
if n <= 4 {
text
} else {
(text[:4]).to_owned()
}
}
// 多粒度角色:前二 / 前四(保留,B2 冷启动依赖) / 前后各二
// 返回所有可用粒度键,供 role_index / role_trans / D8 回退统一使用
pub fn roles_of(text : String) -> Array[String] {
let n = text.length()
let out : Array[String] = []
if n >= 2 {
out.push(text[:2].to_owned())
if n >= 4 {
out.push(role_of(text))
out.push(text[:2].to_owned() + text[(n - 2):].to_owned())
}
}
out
}
// ---------------------------------------------------------------------------
// 编码层:tokenize -> tf_vector -> cosine
// ---------------------------------------------------------------------------
// 分词:ASCII [a-z0-9_]+ 段 + CJK 单字与相邻二元组;小写并去停用词。
// 对应 D1–D8 规范的正则分词 + 中文 unigram/bigram 展开。
pub fn yimai_tokenize(text : String) -> Array[String] {
let lower = to_lower_ascii(text)
let toks : Array[String] = []
let mut lat = StringBuilder()
let mut lat_len = 0
let mut prev_cjk = ""
// 长文兜底:超 MAX_TOKENS 即截断(CJK 算单/双字均计入);截断后剩余字符不再展开,
// 残留 lat 段也丢弃(语义已不完整,多一字少一字不影响 fuzzy/Hit 评估)。
let mut tok_count = 0
let mut truncated = false
for c in lower.iter() {
if tok_count >= MAX_TOKENS {
truncated = true
break
}
let code = c.to_int()
let is_lat = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
if is_lat {
lat.write_char(c)
lat_len = lat_len + 1
} else {
if lat_len > 0 {
toks.push(lat.to_string())
tok_count = tok_count + 1
lat = StringBuilder()
lat_len = 0
}
if code >= 0x4E00 && code <= 0x9FFF {
let s = c.to_string()
toks.push(s)
tok_count = tok_count + 1
if tok_count < MAX_TOKENS && prev_cjk != "" {
toks.push(prev_cjk + s)
tok_count = tok_count + 1
}
prev_cjk = s
} else {
prev_cjk = ""
}
}
}
if lat_len > 0 && tok_count < MAX_TOKENS {
toks.push(lat.to_string())
tok_count = tok_count + 1
}
// 截断标记:调用方可经外部状态检测(fuzzy_match 内部不计;本函数只管分词语义闭合)
let _ = truncated
let out : Array[String] = []
for t in toks {
if !is_stop(t) {
out.push(t)
}
}
out
}
// 数字/单位抽取(数字守门依赖):
// 匹配模式:十进制 / 浮点(不接单位字母,数字守门只关心「数字是否被保留」)。
// 用途:qe_score 数字一致性维度 + mqm_tags numeric_consistency 标签。
// 设计取舍:纯字符扫描,零正则依赖;千分位逗号不在 v1 范围(避免 "1,000" 误吞),下版按需扩展。
// 顺序:先按出现位置输出(确定性);同数字多次出现计多次(计数交给调用方按 Map 累加)。
// v1 不接单位:避免 "3.7V" / "100℃" 因带单位而不等于 "3.7" / "100",让数字一致性集合运算简单可靠;
// 单位一致性作为下个 MQM 标签(unit_consistency)单独跑。
pub fn numeric_tokens(text : String) -> Array[String] {
let out : Array[String] = []
let mut buf = StringBuilder()
let mut buf_has_digit = false
let mut buf_has_dot = false
let flush = () => {
if buf_has_digit {
out.push(buf.to_string())
}
buf = StringBuilder()
buf_has_digit = false
buf_has_dot = false
}
for c in text.iter() {
let is_digit = c >= '0' && c <= '9'
let is_dot = c == '.'
if is_digit {
buf.write_char(c)
buf_has_digit = true
} else if is_dot && !buf_has_dot && buf_has_digit {
// 浮点:仅当之前已有数字时认作小数点(避免 "abc." 被吞)
buf.write_char(c)
buf_has_dot = true
} else {
// 任何非数字/非小数点都立即 flush(单位不带走,简化集合对比)
flush()
}
}
flush()
out
}
// 长文分句(observe / fuzzy_match 长 query 拆分的底层工具):
// 切分依据(优先级高→低):换行 / 中文句号「。!?;」/ 英文 . ! ? ;
// 额外规则:每段超过 max_chars 时再按最近空格二次切;空段丢弃;顺序稳定。
// 用途:调用方拿到 segments 数组后逐段 observe / fuzzy_match,避免长文单点稀释信号。
// 不破坏任何现有 API;P0 范围内的「长文能力」落地方式。
pub fn split_into_segments(text : String, max_chars : Int) -> Array[String] {
let out : Array[String] = []
if text.length() == 0 {
return out
}
let mut buf = StringBuilder()
let flush_buf = () => {
let s = buf.to_string()
if s.length() > 0 {
out.push(s)
}
buf = StringBuilder()
}
for c in text.iter() {
let is_break =
c == '\n' ||
c == '。' ||
c == '!' ||
c == '?' ||
c == ';' ||
c == ';' ||
c == '!' ||
c == '?' ||
c == ':'
if is_break {
flush_buf()
} else if c == '.' {
// 英文句号:v1 简化直接当 break;二级 max_chars 兜底(避免 "3.7" "U.S." 误切伤到下个 break 字符)
flush_buf()
} else {
buf.write_char(c)
// 超 max_chars 触发二级切(按空格或全角空格切)
if buf.to_string().length() >= max_chars {
let s = buf.to_string()
// 收集 chars,找最近空格位置
let chars : Array[Char] = []
for cc in s.iter() {
chars.push(cc)
}
let mut cut_idx = -1
let mut i = chars.length() - 1
while i >= 0 {
if chars[i] == ' ' || chars[i] == ' ' {
cut_idx = i
break
}
i = i - 1
}
if cut_idx > 0 {
let head = StringBuilder()
for j = 0; j < cut_idx; j = j + 1 {
head.write_char(chars[j])
}
let tail = StringBuilder()
for j = cut_idx + 1; j < chars.length(); j = j + 1 {
tail.write_char(chars[j])
}
out.push(head.to_string())
buf = StringBuilder()
buf.write_string(tail.to_string())
}
// 找不到空格就不切(避免单词被腰斩),下个 break 字符再 flush
}
}
}
flush_buf()
out
}
// TF 向量:term -> tf(词频 / 总词数)
fn tf_vector(tokens : Array[String]) -> Map[String, Double] {
let c : Map[String, Int] = Map::from_iter(([] : Array[(String, Int)]).iter())
for t in tokens {
let v = match c.get(t) {
Some(x) => x
None => 0
}
c.set(t, v + 1)
}
let mut total = 0
for _, v in c.iter2() {
total = total + v
}
let n = if total == 0 { 1 } else { total }
let out : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
for t, v in c.iter2() {
out.set(t, v.to_double() / n.to_double())
}
out
}
// 余弦相似度(在 TF 向量上计算)
fn cosine(a : Map[String, Double], b : Map[String, Double]) -> Double {
if a.length() == 0 || b.length() == 0 {
return 0.0
}
let mut num = 0.0
for k, va in a.iter2() {
match b.get(k) {
Some(vb) => num = num + va * vb
None => ()
}
}
if num == 0.0 {
return 0.0
}
let mut na2 = 0.0
for _, v in a.iter2() {
na2 = na2 + v * v
}
let mut nb2 = 0.0
for _, v in b.iter2() {
nb2 = nb2 + v * v
}
let na = na2.sqrt()
let nb = nb2.sqrt()
if na == 0.0 || nb == 0.0 {
return 0.0
}
num / (na * nb)
}
fn clamp01(x : Double) -> Double {
if x < 0.0 {
0.0
} else if x > 1.0 {
1.0
} else {
x
}
}
// 四舍五入到 4 位小数(用于展示)
fn r4(x : Double) -> Double {
@math.round(x * 10000.0) / 10000.0
}
// Array[String] 是否包含 s
fn arr_contains(arr : Array[String], s : String) -> Bool {
for x in arr {
if x == s {
return true
}
}
false
}
// 对齐长度上限:单串字符数与动态规划表总 cell 数双限制,防止恶意长输入 OOM/CPU 耗尽
const MAX_ALIGN_CHARS : Int = 4096
const MAX_ALIGN_CELLS : Int = 4000000
// 双语字符级对齐(LCS 编辑脚本,等价于 Myers 差分的对齐结果)
// 返回对齐序列:op 0=对齐 1=删A 2=插B;i/j 为 A/B 中的字符下标
// 供术语/句段双语对齐热力图与对齐质量评估使用(纯算法,零依赖)
pub fn align_diff(a : String, b : String) -> Array[(Int, Int, Int)] {
let n = a.length()
let m = b.length()
if n > MAX_ALIGN_CHARS || m > MAX_ALIGN_CHARS || n.to_double() * m.to_double() > MAX_ALIGN_CELLS.to_double() {
return []
}
let dp : Array[Array[Int]] = []
let mut i = 0
while i <= n {
let row : Array[Int] = []
let mut j = 0
while j <= m {
row.push(0)
j = j + 1
}
dp.push(row)
i = i + 1
}
i = n - 1
while i >= 0 {
let mut j = m - 1
while j >= 0 {
if a[i] == b[j] {
dp[i][j] = dp[i + 1][j + 1] + 1
} else {
let down = dp[i + 1][j]
let right = dp[i][j + 1]
dp[i][j] = if down > right { down } else { right }
}
j = j - 1
}
i = i - 1
}
let out : Array[(Int, Int, Int)] = []
i = 0
let mut k = 0
while i < n && k < m {
if a[i] == b[k] {
out.push((0, i, k))
i = i + 1
k = k + 1
} else if dp[i + 1][k] >= dp[i][k + 1] {
out.push((1, i, k))
i = i + 1
} else {
out.push((2, i, k))
k = k + 1
}
}
while i < n {
out.push((1, i, k))
i = i + 1
}
while k < m {
out.push((2, i, k))
k = k + 1
}
out
}
// ---- TM/TB 辅助:字符串相似度与极简 TBX 解析(零依赖) ----
pub fn levenshtein(a : String, b : String) -> Int {
let n = a.length()
let m = b.length()
if n == 0 { return m }
if m == 0 { return n }
let mut prev : Array[Int] = []
let mut i = 0
while i <= m {
prev.push(i)
i = i + 1
}
let mut cur : Array[Int] = []
i = 0
while i <= m {
cur.push(0)
i = i + 1
}
let mut x = 0
while x < n {
cur[0] = x + 1
let mut y = 0
while y < m {
let cost = if a[x] == b[y] { 0 } else { 1 }
let del = cur[y] + 1
let ins = prev[y + 1] + 1
let sub = prev[y] + cost
let e = if del < ins { del } else { ins }
cur[y + 1] = if e < sub { e } else { sub }
y = y + 1
}
let tmp = prev
prev = cur
cur = tmp
x = x + 1
}
prev[m]
}
// ---- 跨语种判定(供 MQM 启发式屏蔽误报) ----
// 字符是否落在中日韩统一表意文字(CJK)主区间 / 扩展A / CJK 标点 / 全角区
fn is_cjk_char(c : Char) -> Bool {
let cp = c.to_int()
(cp >= 0x4E00 && cp <= 0x9FFF) ||
(cp >= 0x3400 && cp <= 0x4DBF) ||
(cp >= 0x3000 && cp <= 0x303F) ||
(cp >= 0xFF00 && cp <= 0xFFEF)
}
// 文本主导文字:0=混合/无法判定, 1=CJK 主导, 2=拉丁主导
fn script_kind(s : String) -> Int {
let mut cjk = 0
let mut lat = 0
for c in s.iter() {
if is_cjk_char(c) {
cjk = cjk + 1
} else {
let code = c.to_int()
if (code >= 97 && code <= 122) || (code >= 65 && code <= 90) {
lat = lat + 1
}
}
}
if cjk > lat && cjk > 0 {
1
} else if lat > cjk && lat > 0 {
2
} else {
0
}
}
// 跨语种判定:源/译文主导文字分属不同体系(如 英文→中文)时为真。
// 用于屏蔽 char_ratio / 长度比 这类仅同语种有效的启发式,避免跨语言误报。
fn cross_script(a : String, b : String) -> Bool {
let ka = script_kind(a)
let kb = script_kind(b)
if ka == 0 || kb == 0 {
false
} else {
ka != kb
}
}
pub fn char_ratio(a : String, b : String) -> Double {
let n = a.length()
let m = b.length()
if n == 0 && m == 0 { return 1.0 }
let d = levenshtein(a, b).to_double()
let mx = (if n > m { n } else { m }).to_double()
1.0 - d / mx
}
// ---------------------------------------------------------------------------
// 检索强化(S1):字符 n-gram / 集合 Dice / 首锚 token(零依赖)
// 用于 fuzzy_match 升级:2-gram 捕捉形态变体,Dice 对短文本比对 Jaccard 更宽容。
// ---------------------------------------------------------------------------
// 字符 n-gram 去重集合(不足 n 长度返回空集合;MoonBit 字符串按 UTF-16 码元索引,中文 BMP 一字符一码元)
// P1 性能:直接产出 Map(原 Array 版仅供 dice_coeff 内部去重用;现节点侧缓存 Map,
// 打分循环零分配)。key 首现顺序与原数组扫描顺序一致,仅做 contains 查询,R15 无关。
fn char_ngram_set(s : String, n : Int) -> Map[String, Bool] {
let m : Map[String, Bool] = Map([])
let len = s.length()
if n <= 0 || len < n {
return m
}
let mut i = 0
while i + n <= len {
m.set(s[i:i + n].to_owned(), true)
i = i + 1
}
m
}
// token 去重集合(保持首现顺序;Map 插入序 = 首现序,与原 unique 数组序一致)
// P1 性能:直接产出 Map 供 dice_mm O(1) 集合运算;替代原 unique 数组(调用方均改为集合消费)。
fn to_set(arr : Array[String]) -> Map[String, Bool] {
let m : Map[String, Bool] = Map([])
for x in arr {
m.set(x, true)
}
m
}
// 集合 Dice 系数 = 2|A∩B| / (|A|+|B|),∈ [0,1];短文本下比 Jaccard 更宽容
// P1 优化:双 Map 直传(查询侧每次 fuzzy_match 预建一次 + 节点侧 add_tm 时缓存),
// 打分循环内零 Map 分配(原 dice_coeff 每候选×每分项各建 2 个 Map)。
// 与旧版逐字节等价:交集计数只做 contains、长度取去重基数,不依赖迭代顺序,R15 零影响。
fn dice_mm(am : Map[String, Bool], bm : Map[String, Bool]) -> Double {
if am.length() == 0 || bm.length() == 0 {
return 0.0
}
let mut inter = 0
for k, _ in am.iter2() {
if bm.contains(k) {
inter = inter + 1
}
}
2.0 * inter.to_double() / (am.length().to_double() + bm.length().to_double())
}
// 查询首锚 token:取首个长度≥2 的 token(中文 bigram 或英文整词);无则取首 token。
// 用于位置惩罚:锚 token 命中 TM 源文则加分(句首对齐权重)。
// P0 性能:接受调用方已分词的 toks(fuzzy_match 原先对同一 query 分词 3 次——
// tf_vector / unique / first_anchor 各一次,现收敛为 1 次由调用方传入)。
fn first_anchor(toks : Array[String]) -> String {
if toks.length() == 0 {
return ""
}
for t in toks {
if t.length() >= 2 {
return t
}
}
toks[0]
}
// 字符串是否以子串 p 开头(用于 xml:lang 前缀匹配,如 en-US/en/zh-CN/zh-Hans)
fn starts_with(s : String, p : String) -> Bool {
match str_index(s, p, 0) {
Some(i) => i == 0
None => false
}
}
pub fn str_contains(s : String, sub : String) -> Bool {
match str_index(s, sub, 0) {
Some(_) => true
None => false
}
}
fn split_on(s : String, sep : String) -> Array[String] {
let parts : Array[String] = []
let n = s.length()
let m = sep.length()
if m == 0 { parts.push(s); return parts }
let mut i = 0
let mut start = 0
while i + m <= n {
let mut j = 0
let mut ok = true
while j < m {
if s[i + j] != sep[j] { ok = false; break }
j = j + 1
}
if ok {
parts.push(s[start:i].to_owned())
i = i + m
start = i
} else {
i = i + 1
}
}
parts.push(s[start:n].to_owned())
parts
}
fn str_index(s : String, sub : String, from : Int) -> Option[Int] {
let n = s.length()
let m = sub.length()
let mut i = from
while i + m <= n {
let mut j = 0
let mut ok = true
while j < m {
if s[i + j] != sub[j] { ok = false; break }
j = j + 1
}
if ok { return Some(i) }
i = i + 1
}
None
}
// 提取片段中首个 ... 文本(容忍 带属性)
fn first_tag_text(s : String, tag : String) -> String {
let open = "<" + tag
let close = "" + tag + ">"
let n = s.length()
let m = open.length()
let mut i = 0
while i + m <= n {
let mut j = 0
let mut ok = true
while j < m {
if s[i + j] != open[j] { ok = false; break }
j = j + 1
}
// 边界检查:标签名后必须是标签分隔符(> / 空白 / /),否则是 之类的前缀误匹配
if ok {
let nx = if i + m < n { s[i + m] } else { '>' }
if nx != '>' && nx != ' ' && nx != '\t' && nx != '\n' && nx != '/' { ok = false }
}
if ok {
let mut k = i + m
while k < n {
if s[k] == '>' { break }
k = k + 1
}
let cstart = k + 1
match str_index(s, close, cstart) {
Some(end) => return s[cstart:end].to_owned()
None => return ""
}
}
i = i + 1
}
""
}
// 判断字符是否为 ASCII 字母/数字(用于拉丁文术语的词边界判定)
// 注:MoonBit 字符串索引返回 UInt16(UTF-16 码元),故参数与比较均用码点数值
fn is_ascii_alnum(c : UInt16) -> Bool {
(c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57)
}
// 提取片段中 langSet 的 xml:lang / lang 属性值(如 "en-US");无则返回 ""
fn lang_attr(s : String) -> String {
match str_index(s, "xml:lang=\"", 0) {
Some(i) => {
let q = i + 10 // "xml:lang=\"" 共 10 字符,末位为前导引号,值自此开始
match str_index(s, "\"", q) {
Some(end) => return s[q:end].to_owned()
None => return ""
}
}
None => {
match str_index(s, "lang=\"", 0) {
Some(i) => {
let q = i + 6 // "lang=\"" 共 6 字符,末位为前导引号,值自此开始
match str_index(s, "\"", q) {
Some(end) => return s[q:end].to_owned()
None => return ""
}
}
None => return ""
}
}
}
}
// 术语命中:区分拉丁文(需词边界,避免 log⊂logical 误报)与 CJK(直接子串,CJK 无空格词边界)
fn term_hit(text : String, term : String) -> Bool {
if term == "" { return false }
let tl = term.length()
let n = text.length()
if n < tl { return false }
let term_is_latin = is_ascii_alnum(term[0])
if !term_is_latin {
return str_contains(text, term)
}
let mut i = 0
while i + tl <= n {
if (text[i:i + tl]).to_owned() == term {
let left_ok = if i == 0 { true } else { !is_ascii_alnum(text[i - 1]) }
let right_ok = if i + tl >= n { true } else { !is_ascii_alnum(text[i + tl]) }
if left_ok && right_ok { return true }
}
i = i + 1
}
false
}
// ---------------------------------------------------------------------------
// JSON 构造 / 访问辅助(复用 AL-world 风格,零依赖 moonbitlang/core/json)
// ---------------------------------------------------------------------------
pub fn str_json(s : String) -> Json {
s.to_json()
}
pub fn num_json(d : Double) -> Json {
d.to_json()
}
pub fn arr_json(items : Array[Json]) -> Json {
items.to_json()
}
pub fn obj(pairs : Array[(String, Json)]) -> Json {
Map::from_iter(pairs.iter()).to_json()
}
// 取对象字段为字符串,缺省 ""
// P11-A 升 pub:cmd/main/main.mbt 复用替换自含 jstr,减少跨包 helper 漂移
pub fn get_str(j : Json, key : String) -> String {
match j {
Json::Object(m) =>
match m.get(key) {
Some(Json::String(s)) => s
_ => ""
}
_ => ""
}
}
// 取对象字段为数字,缺省 0.0
// P11-A 升 pub:cmd/main/main.mbt 复用替换自含 jnum
pub fn get_num(j : Json, key : String) -> Double {
match j {
Json::Object(m) =>
match m.get(key) {
Some(Json::Number(n, ..)) => n
_ => 0.0
}
_ => 0.0
}
}
// 取对象字段为对象(Json 对象),缺省空对象
// P11-A 升 pub:cmd/main/main.mbt 复用替换自含 jarr
pub fn get_obj(j : Json, key : String) -> Json {
match j {
Json::Object(m) =>
match m.get(key) {
Some(v) =>
match v {
Json::Object(_) => v
_ => obj([])
}
_ => obj([])
}
_ => obj([])
}
}
// 取对象字段为字符串数组
fn get_str_arr(j : Json, key : String) -> Array[String] {
match j {
Json::Object(m) =>
match m.get(key) {
Some(Json::Array(a)) =>
a.filter_map(fn(t) {
match t {
Json::String(s) => Some(s)
_ => None
}
})
_ => []
}
_ => []
}
}
// 取对象任意字段为原始 Json(缺省空对象),用于原样透传数组/嵌套结构
// 取对象字段为原始 Json(任意类型:Object/Array/Number/String 均可)
// P11-A 升 pub:cmd/main/main.mbt 的 jarr 包装用它(@lib.get_obj 只对 Object 字段有效,
// 对 Array 字段会丢成空 obj,get_field 不做类型筛选)
pub fn get_field(j : Json, key : String) -> Json {
match j {
Json::Object(m) =>
match m.get(key) {
Some(v) => v
_ => obj([])
}
_ => obj([])
}
}
// Map[String, Double] -> Json 对象
fn dmap_to_json(m : Map[String, Double]) -> Json {
let o : Map[String, Json] = Map::from_iter(([] : Array[(String, Json)]).iter())
for k, v in m.iter2() {
o.set(k, num_json(v))
}
o.to_json()
}
// Map[String, Int] -> Json 对象
fn imap_to_json(m : Map[String, Int]) -> Json {
let o : Map[String, Json] = Map::from_iter(([] : Array[(String, Json)]).iter())
for k, v in m.iter2() {
o.set(k, num_json(v.to_double()))
}
o.to_json()
}
// Map[String, String] -> Json 对象
fn smap_to_json(m : Map[String, String]) -> Json {
let o : Map[String, Json] = Map::from_iter(([] : Array[(String, Json)]).iter())
for k, v in m.iter2() {
o.set(k, str_json(v))
}
o.to_json()
}
// Json 对象 -> Map[String, Double]
fn json_to_dmap(j : Json) -> Map[String, Double] {
let out : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
match j {
Json::Object(m) =>
for k, v in m.iter2() {
match v {
Json::Number(n, ..) => out.set(k, n)
_ => ()
}
}
_ => ()
}
out
}
// Json 对象 -> Map[String, String]
fn json_to_smap(j : Json) -> Map[String, String] {
let out : Map[String, String] = Map::from_iter(([] : Array[(String, String)]).iter())
match j {
Json::Object(m) =>
for k, v in m.iter2() {
match v {
Json::String(s) => out.set(k, s)
_ => ()
}
}
_ => ()
}
out
}
// ===== S4 评测:BLEU / chrF++(零依赖自实现,确定性)=====
// n-gram 总数(滑动窗口计数 = len-n+1;不足 n 为 0)。
// P0 性能:与「物化整个 n-gram 列表再取 length」等价的 O(1) 算术式,去掉中间分配。
fn ngram_total(len : Int, n : Int) -> Int {
if len >= n {
len - n + 1
} else {
0
}
}
// n-gram 计数(key = join with unit separator)
// P0 性能:直接滑动窗口 + StringBuilder 拼 key(原版先物化 Array[Array[String]]
// 再 O(n²) 字符串拼接);key 逐字节等价、首现顺序不变,R15 确定性零影响。
fn ngram_counts(toks : Array[String], n : Int) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
let len = toks.length()
let mut i = 0
while i + n <= len {
let b = StringBuilder()
let mut j = 0
while j < n {
b.write_string(toks[i + j])
b.write_string("\u001F")
j = j + 1
}
let key = b.to_string()
let cur = match counts.get(key) {
Some(v) => v
None => 0
}
counts.set(key, cur + 1)
i = i + 1
}
counts
}
// BLEU-4:modified n-gram precision(clipped)+ Add-1 平滑 + BP(Papineni 2002 口径)
pub fn bleu_score(ref_text : String, hyp_text : String) -> Double {
let ref_toks = yimai_tokenize(ref_text)
let hyp_toks = yimai_tokenize(hyp_text)
let r_len = ref_toks.length()
let c_len = hyp_toks.length()
if c_len == 0 {
return 0.0
}
let bp = if c_len > r_len { 1.0 } else { @math.exp(1.0 - r_len.to_double() / c_len.to_double()) }
let mut log_sum = 0.0
let mut orders = 0.0
let mut n = 1
while n <= 4 {
let hyp_counts = ngram_counts(hyp_toks, n)
let ref_counts = ngram_counts(ref_toks, n)
let mut clipped = 0.0
for key, hyp_c in hyp_counts.iter2() {
let ref_c = match ref_counts.get(key) {
Some(v) => v
None => 0
}
let m = if hyp_c < ref_c { hyp_c } else { ref_c }
clipped = clipped + m.to_double()
}
let total = ngram_total(c_len, n).to_double()
let p_n = if total > 0.0 { (clipped + 1.0) / (total + 1.0) } else { 1.0 }
log_sum = log_sum + @math.ln(p_n)
orders = orders + 1.0
n = n + 1
}
let bleu = bp * @math.exp(log_sum / orders)
r4(bleu)
}
// 字符串码点数组(跳过空白)
fn chars_of(s : String) -> Array[String] {
let out : Array[String] = []
for c in s.iter() {
if c != ' ' && c != '\n' && c != '\t' && c != '\r' {
out.push(c.to_string())
}
}
out
}
// 字符 n-gram 计数
// P0 性能:StringBuilder 拼 key(原版 O(n²) 字符串复制拼接);key 逐字节等价、首现顺序不变。
fn char_ngram_counts(chars : Array[String], n : Int) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
let len = chars.length()
let mut i = 0
while i + n <= len {
let b = StringBuilder()
let mut j = 0
while j < n {
b.write_string(chars[i + j])
j = j + 1
}
let key = b.to_string()
let cur = match counts.get(key) {
Some(v) => v
None => 0
}
counts.set(key, cur + 1)
i = i + 1
}
counts
}
// chrF++:字符 1..6-gram + 词 1..2-gram,F-beta(beta=2)(Popović 2017 口径)
pub fn chrf_score(ref_text : String, hyp_text : String) -> Double {
let ref_chars = chars_of(ref_text)
let hyp_chars = chars_of(hyp_text)
let ref_words = ref_text.split(" ").to_array().map(fn(x) { x.to_owned() })
let hyp_words = hyp_text.split(" ").to_array().map(fn(x) { x.to_owned() })
if ref_chars.length() == 0 && hyp_chars.length() == 0 {
return 1.0
}
if ref_chars.length() == 0 || hyp_chars.length() == 0 {
return 0.0
}
let mut sum_p = 0.0
let mut sum_r = 0.0
let mut orders = 0.0
let mut n = 1
while n <= 6 {
let hc = char_ngram_counts(hyp_chars, n)
let rc = char_ngram_counts(ref_chars, n)
let mut match_n = 0.0
for key, h in hc.iter2() {
let r = match rc.get(key) {
Some(v) => v
None => 0
}
let m = if h < r { h } else { r }
match_n = match_n + m.to_double()
}
let hyp_total = ngram_total(hyp_chars.length(), n).to_double()
let ref_total = ngram_total(ref_chars.length(), n).to_double()
sum_p = sum_p + (if hyp_total > 0.0 { match_n / hyp_total } else { 0.0 })
sum_r = sum_r + (if ref_total > 0.0 { match_n / ref_total } else { 0.0 })
orders = orders + 1.0
n = n + 1
}
let mut w = 1
while w <= 2 {
let hc = ngram_counts(hyp_words, w)
let rc = ngram_counts(ref_words, w)
let mut match_n = 0.0
for key, h in hc.iter2() {
let r = match rc.get(key) {
Some(v) => v
None => 0
}
let m = if h < r { h } else { r }
match_n = match_n + m.to_double()
}
let hyp_total = ngram_total(hyp_words.length(), w).to_double()
let ref_total = ngram_total(ref_words.length(), w).to_double()
sum_p = sum_p + (if hyp_total > 0.0 { match_n / hyp_total } else { 0.0 })
sum_r = sum_r + (if ref_total > 0.0 { match_n / ref_total } else { 0.0 })
orders = orders + 1.0
w = w + 1
}
let p = sum_p / orders
let r = sum_r / orders
if p == 0.0 && r == 0.0 {
return 0.0
}
let beta2 = 4.0
let f = (1.0 + beta2) * p * r / (beta2 * p + r)
r4(f)
}