///|
/// 一条数字格式化用例:`decimals*.tsv` 的一行。
///
/// `input` 与 `expected` 都按**字符串原样**保存:CLDR 给的输入本身就是十进制字符串,
/// 转成浮点会引入舍入误差,恰好污染本项目要度量的那个维度(见 DESIGN.md D2)。
pub(all) struct DecimalCase {
locale : String
style : NumberStyle
length : NumberLength
input : String
expected : String
/// 源文件行号(1 起,含表头行),用于报告定位。
line : Int
} derive(Debug, Eq)
///|
/// 数字套件的用例集合。
pub(all) struct DecimalCorpus {
cases : Array[DecimalCase]
} derive(Debug, Eq)
///|
pub fn DecimalCorpus::new(cases : Array[DecimalCase]) -> DecimalCorpus {
{ cases, }
}
///|
pub fn DecimalCorpus::length(self : DecimalCorpus) -> Int {
self.cases.length()
}
///|
/// 字典序比较:逐码元比较;一个是另一个的前缀时,短的排前面。
///
/// **不能直接用 `Array::sort()`**:MoonBit 的 `Compare for String` 是「先比长度、
/// 再逐码元」(见 core `builtin/string.mbt`),于是 `"en"` 会排在 `"ar-SA"` 前面。
/// 报告表格要给人看,所以这里显式给出符合直觉的顺序。
fn lexicographic_compare(a : String, b : String) -> Int {
let limit = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. Array[String] {
let seen : Map[String, Unit] = Map([])
for item in self.cases {
seen[item.locale] = ()
}
let locales = seen.keys().to_array()
locales.sort_by(lexicographic_compare)
locales
}
///|
/// 某个语言的用例(保持源文件顺序)。
pub fn DecimalCorpus::of_locale(
self : DecimalCorpus,
locale : String,
) -> Array[DecimalCase] {
self.cases.filter(item => item.locale == locale)
}
///|
/// 一条「骨架 → 模式」用例:`skeletons*.tsv` 的一行。
pub(all) struct SkeletonCase {
locale : String
calendar : String
skeleton : String
pattern : String
/// 源文件行号(1 起,含表头行)。
line : Int
} derive(Debug, Eq)
///|
/// 一条「时间输入 → 格式化输出」用例:`datetime.json` 的一个元素。
///
/// 可选字段按上游原样保留(缺省即该项未被测试),刻意不做归一化:
/// 报告要能说明「这条用例到底在测哪个能力」。
pub(all) struct DatetimeCase {
locale : String
calendar : String
/// ISO 8601 时间串,含 IANA 时区标注,例如 `2000-01-01T00:00Z[Etc/GMT]`。
input : String
expected : String
semantic_skeleton : String?
classical_skeleton : String?
semantic_skeleton_length : String?
zone_style : String?
time_length : String?
date_length : String?
hour_cycle : String?
year_style : String?
date_time_format_type : String?
/// 在源文件数组中的下标(0 起),用于报告定位。
index : Int
} derive(Debug, Eq)
///|
/// 日期套件的用例集合:骨架表 + 格式化用例。
pub(all) struct DatetimeCorpus {
skeletons : Array[SkeletonCase]
formatted : Array[DatetimeCase]
} derive(Debug, Eq)
///|
pub fn DatetimeCorpus::new(
skeletons : Array[SkeletonCase],
formatted : Array[DatetimeCase],
) -> DatetimeCorpus {
{ skeletons, formatted, }
}
///|
pub fn DatetimeCorpus::length(self : DatetimeCorpus) -> Int {
self.skeletons.length() + self.formatted.length()
}
///|
/// 涉及的语言(两个来源合并、去重后按字典序)。
pub fn DatetimeCorpus::locales(self : DatetimeCorpus) -> Array[String] {
let seen : Map[String, Unit] = Map([])
for item in self.skeletons {
seen[item.locale] = ()
}
for item in self.formatted {
seen[item.locale] = ()
}
let locales = seen.keys().to_array()
locales.sort_by(lexicographic_compare)
locales
}
///|
/// 涉及的日历(两个来源合并、去重后按字典序)。
pub fn DatetimeCorpus::calendars(self : DatetimeCorpus) -> Array[String] {
let seen : Map[String, Unit] = Map([])
for item in self.skeletons {
seen[item.calendar] = ()
}
for item in self.formatted {
seen[item.calendar] = ()
}
let calendars = seen.keys().to_array()
calendars.sort_by(lexicographic_compare)
calendars
}
///|
/// 一条 MF2 用例(`messageFormat/tests/*.json` 的一个 test)。
///
/// 上游用「**文件级默认值 + 用例级覆盖**」表达期望:例如 `syntax-errors.json` 的
/// `defaultTestProperties` 带 `expErrors`,其 133 个用例只写 `src` 就继承了它。
/// 因此这里存的是**合并后**的结果,而不是用例的原始字段。
pub(all) struct MessageCase {
/// 来源文件名,例如 `syntax.json`。
file : String
scenario : String
/// 在该文件 `tests` 数组中的下标(0 起)。
index : Int
description : String?
/// MF2 消息源码。
src : String
/// 合并默认值后的语言。
locale : String
/// 合并默认值后的 bidi 隔离设置(`None` 表示上游未指定)。
bidi_isolation : String?
/// 期望输出;`None` 表示该用例期望失败(见 `expected_errors`)。
exp : String?
/// 期望的错误类别,例如 `syntax-error`、`bad-option`。
expected_errors : Array[String]
/// 该用例期望结构化 parts 输出。当前实现普遍不产出,故单独标记而不是当作通过。
expects_parts : Bool
/// `u:` 选项标签(仅 u-options.json 使用)。
tags : Array[String]
/// 输入参数的原始 JSON 片段,交给适配器自行解析(避免在这里过度建模)。
params_json : String?
} derive(Debug, Eq)
///|
/// MF2 套件的用例集合。
pub(all) struct MessageCorpus {
cases : Array[MessageCase]
} derive(Debug, Eq)
///|
pub fn MessageCorpus::new(cases : Array[MessageCase]) -> MessageCorpus {
{ cases, }
}
///|
pub fn MessageCorpus::length(self : MessageCorpus) -> Int {
self.cases.length()
}
///|
/// 涉及的来源文件(去重后按字典序)。
pub fn MessageCorpus::files(self : MessageCorpus) -> Array[String] {
let seen : Map[String, Unit] = Map([])
for item in self.cases {
seen[item.file] = ()
}
let files = seen.keys().to_array()
files.sort_by(lexicographic_compare)
files
}
///|
/// 涉及的上游场景(去重后按字典序)。
pub fn MessageCorpus::scenarios(self : MessageCorpus) -> Array[String] {
let seen : Map[String, Unit] = Map([])
for item in self.cases {
seen[item.scenario] = ()
}
let scenarios = seen.keys().to_array()
scenarios.sort_by(lexicographic_compare)
scenarios
}
///|
/// 某个场景的用例(保持源文件顺序)。
pub fn MessageCorpus::of_scenario(
self : MessageCorpus,
scenario : String,
) -> Array[MessageCase] {
self.cases.filter(item => item.scenario == scenario)
}