///|
/// Lexicon construction and lookup.
///
/// Two independent rhyme books are kept:
/// * 平水韵 (106 groups) for regulated verse (近体诗);
/// * 词林正韵 (19 parts) for ci.
/// Mixing them would silently accept wrong rhymes, so the API always takes an
/// explicit `RhymeSystem`.
fn add_reading(
map : Map[Char, Array[Reading]],
ch : Char,
reading : Reading,
) -> Unit {
match map.get(ch) {
Some(existing) => {
// The upstream tables occasionally repeat a character inside one group.
let mut duplicate = false
for r in existing {
if r.group == reading.group && r.tone_class == reading.tone_class {
duplicate = true
}
}
if !duplicate {
existing.push(reading)
}
}
None => map.set(ch, [reading])
}
}
///|
fn add_group_char(
map : Map[String, Array[Char]],
group : String,
ch : Char,
) -> Unit {
match map.get(group) {
Some(existing) => existing.push(ch)
None => map.set(group, [ch])
}
}
///|
fn parse_pingshui(
readings : Map[Char, Array[Reading]],
groups : Map[String, Array[Char]],
names : Array[String],
) -> Unit {
for line in split_on(PINGSHUI_DATA, '\n') {
if line.length() == 0 {
continue
}
let parts = split_on(line, '\t')
if parts.length() < 2 {
continue
}
let group = parts[0]
let chars = parts[1]
let mut known = false
for n in names {
if n == group {
known = true
}
}
if !known {
names.push(group)
}
let tone_class = if has_prefix(group, "上平") ||
has_prefix(group, "下平") {
"平"
} else if has_prefix(group, "上声") {
"上"
} else if has_prefix(group, "去声") {
"去"
} else {
"入"
}
let tone = if tone_class == "平" { Ping } else { Ze }
for ch in chars {
add_reading(readings, ch, { tone, group, tone_class, })
add_group_char(groups, group, ch)
}
}
}
///|
fn parse_cilin(
readings : Map[Char, Array[Reading]],
groups : Map[String, Array[Char]],
names : Array[String],
) -> Unit {
for line in split_on(CILIN_DATA, '\n') {
if line.length() == 0 {
continue
}
let parts = split_on(line, '\t')
if parts.length() < 3 {
continue
}
let group = parts[0]
let mut known = false
for n in names {
if n == group {
known = true
}
}
if !known {
names.push(group)
}
let tone_name = parts[1]
let chars = parts[2]
let tone_class = if tone_name == "平声" {
"平"
} else if tone_name == "上声" {
"上"
} else if tone_name == "去声" {
"去"
} else {
"入"
}
let tone = if tone_class == "平" { Ping } else { Ze }
for ch in chars {
add_reading(readings, ch, { tone, group, tone_class, })
add_group_char(groups, group, ch)
}
}
}
///|
fn parse_tunes() -> Array[CiTune] {
let out : Array[CiTune] = []
for line in split_on(CITUNES_DATA, '\n') {
if line.length() == 0 {
continue
}
let parts = split_on(line, '\t')
if parts.length() < 5 {
continue
}
let rhymes : Array[Int32] = []
for piece in split_on(parts[4], ',') {
match parse_int_opt(piece) {
Some(n) => rhymes.push(n)
None => ()
}
}
out.push({
name: parts[0],
sketch: parts[1],
author: parts[2],
pattern: parts[3],
rhymes,
})
}
out
}
///|
pub fn Lexicon::builtin() -> Lexicon {
let pingshui : Map[Char, Array[Reading]] = Map([])
let pingshui_groups : Map[String, Array[Char]] = Map([])
let cilin : Map[Char, Array[Reading]] = Map([])
let cilin_groups : Map[String, Array[Char]] = Map([])
let pingshui_names : Array[String] = []
let cilin_names : Array[String] = []
parse_pingshui(pingshui, pingshui_groups, pingshui_names)
parse_cilin(cilin, cilin_groups, cilin_names)
{
pingshui,
cilin,
pingshui_groups,
cilin_groups,
pingshui_names,
cilin_names,
tunes: parse_tunes(),
}
}
///|
pub fn lookup(lex : Lexicon, system : RhymeSystem, ch : Char) -> Array[Reading] {
let table = match system {
Pingshui => lex.pingshui
Cilin => lex.cilin
}
match table.get(ch) {
Some(rs) => rs
None => []
}
}
///|
pub fn group_names(lex : Lexicon, system : RhymeSystem) -> Array[String] {
match system {
Pingshui => lex.pingshui_names
Cilin => lex.cilin_names
}
}
///|
pub fn group_chars(
lex : Lexicon,
system : RhymeSystem,
group : String,
) -> Array[Char] {
let table = match system {
Pingshui => lex.pingshui_groups
Cilin => lex.cilin_groups
}
match table.get(group) {
Some(cs) => cs
None => []
}
}
///|
/// All rhyme groups a character can belong to under one system.
pub fn groups_of(
lex : Lexicon,
system : RhymeSystem,
ch : Char,
) -> Array[String] {
let out : Array[String] = []
for r in lookup(lex, system, ch) {
let mut seen = false
for g in out {
if g == r.group {
seen = true
}
}
if !seen {
out.push(r.group)
}
}
out
}
///|
/// Find a group that all characters can share. Returns the lexicographically
/// smallest witness so output is deterministic.
pub fn common_group(
lex : Lexicon,
system : RhymeSystem,
chars : Array[Char],
want_ping : Bool?,
) -> String? {
if chars.length() == 0 {
return None
}
let mut candidates = groups_of(lex, system, chars[0])
let mut i = 1
while i < chars.length() {
let next : Array[String] = []
for g in candidates {
for r in lookup(lex, system, chars[i]) {
if r.group == g {
let ok = match want_ping {
Some(ping) => is_ping(r.tone) == ping
None => true
}
if ok {
next.push(g)
break
}
}
}
}
candidates = next
i = i + 1
}
let mut best : String? = None
for g in candidates {
match best {
None => best = Some(g)
Some(b) => if g < b { best = Some(g) }
}
}
best
}