(s: string)
| 103 | const MON_BARE = "january|february|march|april|june|july|august|september|october|november|december"; |
| 104 | |
| 105 | function hardNumericTokens(s: string): NumTokens { |
| 106 | let t = normNum(s); |
| 107 | const strong = new Set<string>(); |
| 108 | const years = new Set<string>(); |
| 109 | const blank = (n: number) => " ".repeat(n); |
| 110 | // 1) 中文/数字日期 → date:YYYY-M(分隔符不含 '.',小数另算;月份须 1-12; |
| 111 | // 分隔后非数字边界,避免 '2025-1234'(电话) 被截成 '2025-12') |
| 112 | t = t.replace(/(\d{4})[-/年](\d{1,2})(?!\d)/g, (full, y, mo) => { |
| 113 | const m = parseInt(mo, 10); |
| 114 | if (m >= 1 && m <= 12) { |
| 115 | strong.add(`date:${y}-${m}`); |
| 116 | return blank(full.length); |
| 117 | } |
| 118 | return full; // 非法月(如 2025-99)不当日期 |
| 119 | }); |
| 120 | // 2) 英文月份日期 → date:YYYY-M(跨语言对齐)。带日号:任意月形;裸月-年:只认全称 |
| 121 | t = t.replace(new RegExp(`\\b(${MON_RE})\\.?\\s+(\\d{1,2})(?:st|nd|rd|th)?,?\\s+(\\d{4})\\b`, "g"), (full, mon, _d, y) => { |
| 122 | strong.add(`date:${y}-${MONTHS[mon.slice(0, 3)]}`); |
| 123 | return blank(full.length); |
| 124 | }); |
| 125 | t = t.replace(new RegExp(`\\b(\\d{1,2})(?:st|nd|rd|th)?\\s+(${MON_RE})\\.?,?\\s+(\\d{4})\\b`, "g"), (full, _d, mon, y) => { |
| 126 | strong.add(`date:${y}-${MONTHS[mon.slice(0, 3)]}`); |
| 127 | return blank(full.length); |
| 128 | }); |
| 129 | t = t.replace(new RegExp(`\\b(${MON_BARE})\\s+(\\d{4})\\b`, "g"), (full, mon, y) => { |
| 130 | strong.add(`date:${y}-${MONTHS[mon.slice(0, 3)]}`); |
| 131 | return blank(full.length); |
| 132 | }); |
| 133 | // 3) 百分比 → pct:val(带符号)。前缀避免与同值普通数 / 量级撞 |
| 134 | t = t.replace(/([+\-﹣-]?)\s*(\d+(?:\.\d+)?)\s*%/g, (full, sign, v) => { |
| 135 | strong.add(`pct:${/[\-﹣-]/.test(sign) ? "-" : ""}${parseFloat(v)}`); |
| 136 | return blank(full.length); |
| 137 | }); |
| 138 | // 4) 量级 万/亿/万亿 → 纯数值串(不加前缀,与 '2,630,000,000,000' 同值互认) |
| 139 | t = t.replace(/(\d+(?:\.\d+)?)\s*(万亿|亿|万)/g, (full, v, u) => { |
| 140 | const mult = u === "万亿" ? 1e12 : u === "亿" ? 1e8 : 1e4; |
| 141 | strong.add(String(Math.round(parseFloat(v) * mult))); |
| 142 | return blank(full.length); |
| 143 | }); |
| 144 | // 5) 年份 → years(裸 4 位年份不算 strong;挖空以免下一步当普通数收割) |
| 145 | t = t.replace(/\b(?:19|20)\d{2}\b/g, (full) => { |
| 146 | years.add(full); |
| 147 | return blank(full.length); |
| 148 | }); |
| 149 | // 6) 剩余通用数字(≥3 位整数 / 小数)→ 原值串(日期/百分比/量级/年份均已挖空,无裸尾数泄漏) |
| 150 | for (const m of t.matchAll(/\d+\.\d+|\d{3,}/g)) strong.add(m[0]); |
| 151 | return { strong, years }; |
| 152 | } |
| 153 | |
| 154 | function matchHits(claim: NumTokens, block: NumTokens): { strongHit: number; yearHit: number } { |
| 155 | let strongHit = 0; |
no test coverage detected