///|
/// Records the notation recognized by the parser.
pub(all) enum Orthography {
PlainInput
ToneNumberInput
ToneMarkInput
} derive(Eq, Debug)
///|
/// A validated Pinyin syllable in canonical lowercase form.
pub struct Syllable {
base_value : String
initial_value : String
final_value : String
tone_value : Int
input_form : Orthography
} derive(Eq, Debug)
///|
fn syllable_ascii_slice(input : String, start : Int, end : Int) -> String {
let mut output = ""
let from = if start < 0 { 0 } else { start }
let until = if end > input.length() { input.length() } else { end }
for index = from; index < until; index = index + 1 {
output = output + input[index].unsafe_to_char().to_string()
}
output
}
///|
fn syllable_starts_with(input : String, prefix : String) -> Bool {
if prefix.length() > input.length() {
return false
}
for index = 0; index < prefix.length(); index = index + 1 {
if input[index] != prefix[index] {
return false
}
}
true
}
///|
fn syllable_ends_with(input : String, suffix : String) -> Bool {
if suffix.length() > input.length() {
return false
}
let start = input.length() - suffix.length()
for index = 0; index < suffix.length(); index = index + 1 {
if input[start + index] != suffix[index] {
return false
}
}
true
}
///|
fn marked_vowel(code : UInt16) -> (String, Int)? {
match code.to_int() {
0x0101 => Some(("a", 1))
0x00E1 => Some(("a", 2))
0x01CE => Some(("a", 3))
0x00E0 => Some(("a", 4))
0x0113 => Some(("e", 1))
0x00E9 => Some(("e", 2))
0x011B => Some(("e", 3))
0x00E8 => Some(("e", 4))
0x012B => Some(("i", 1))
0x00ED => Some(("i", 2))
0x01D0 => Some(("i", 3))
0x00EC => Some(("i", 4))
0x014D => Some(("o", 1))
0x00F3 => Some(("o", 2))
0x01D2 => Some(("o", 3))
0x00F2 => Some(("o", 4))
0x016B => Some(("u", 1))
0x00FA => Some(("u", 2))
0x01D4 => Some(("u", 3))
0x00F9 => Some(("u", 4))
0x01D6 => Some(("ü", 1))
0x01D8 => Some(("ü", 2))
0x01DA => Some(("ü", 3))
0x01DC => Some(("ü", 4))
0x0144 => Some(("n", 2))
0x0148 => Some(("n", 3))
0x01F9 => Some(("n", 4))
0x1E3F => Some(("m", 2))
0x1EBF => Some(("ê", 2))
0x1EC1 => Some(("ê", 4))
_ => None
}
}
///|
fn combining_tone(code : Int) -> Int {
match code {
0x0304 => 1
0x0301 => 2
0x030C => 3
0x0300 => 4
_ => 0
}
}
///|
fn accepts_combining_tone(code : Int) -> Bool {
code == 97 ||
code == 101 ||
code == 105 ||
code == 111 ||
code == 117 ||
code == 0x00FC ||
code == 0x00EA ||
code == 109 ||
code == 110
}
///|
fn canonical_initial(base : String) -> String {
for
candidate in [
"zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "g", "k", "h", "j",
"q", "x", "r", "z", "c", "s", "y", "w",
] {
if syllable_starts_with(base, candidate) {
return candidate
}
}
""
}
///|
fn has_syllable_nucleus(base : String) -> Bool {
for index = 0; index < base.length(); index = index + 1 {
let code = base[index].to_int()
if code == 97 ||
code == 101 ||
code == 105 ||
code == 111 ||
code == 117 ||
code == 0x00FC ||
code == 0x00EA {
return true
}
}
base == "m" ||
base == "n" ||
base == "ng" ||
syllable_ends_with(base, "m") ||
syllable_ends_with(base, "n") ||
syllable_ends_with(base, "ng")
}
///|
/// Parses plain, numbered, or precomposed tone-mark Pinyin.
pub fn parse_syllable(input : String) -> Result[Syllable, PinyinError] {
if input.length() == 0 {
return Err(InvalidSyllable(input, "empty"))
}
let mut end = input.length()
let mut numbered_tone = -1
let last = input[input.length() - 1].to_int()
if last >= 48 && last <= 53 {
numbered_tone = if last == 53 { 0 } else { last - 48 }
end = end - 1
}
let mut base = ""
let mut marked_tone = 0
let mut saw_mark = false
let mut index = 0
while index < end {
let original = input[index]
let mut code = original.to_int()
if code >= 65 && code <= 90 {
code = code + 32
}
if code == 117 && index + 1 < end && input[index + 1].to_int() == 58 {
base = base + "ü"
index = index + 2
continue
}
if code == 118 {
base = base + "ü"
index = index + 1
continue
}
let combining = combining_tone(code)
if combining > 0 {
if saw_mark {
return Err(InvalidSyllable(input, "multiple_tone_marks"))
}
if base.length() == 0 ||
!accepts_combining_tone(base[base.length() - 1].to_int()) {
return Err(InvalidSyllable(input, "invalid_combining_tone"))
}
marked_tone = combining
saw_mark = true
index = index + 1
continue
}
let normalized_code = code.to_uint16()
match marked_vowel(normalized_code) {
Some((plain, tone)) => {
if saw_mark {
return Err(InvalidSyllable(input, "multiple_tone_marks"))
}
base = base + plain
marked_tone = tone
saw_mark = true
}
None =>
if (code >= 97 && code <= 122) || code == 0x00FC || code == 0x00EA {
base = base + code.to_uint16().unsafe_to_char().to_string()
} else {
return Err(InvalidSyllable(input, "invalid_character"))
}
}
index = index + 1
}
if base.length() == 0 || !has_syllable_nucleus(base) {
return Err(InvalidSyllable(input, "missing_nucleus"))
}
if saw_mark && numbered_tone >= 0 {
return Err(InvalidSyllable(input, "conflicting_tones"))
}
let tone = if saw_mark {
marked_tone
} else if numbered_tone >= 0 {
numbered_tone
} else {
0
}
let form = if saw_mark {
ToneMarkInput
} else if numbered_tone >= 0 {
ToneNumberInput
} else {
PlainInput
}
let initial = canonical_initial(base)
let final_part = syllable_ascii_slice(base, initial.length(), base.length())
Ok({
base_value: base,
initial_value: initial,
final_value: final_part,
tone_value: tone,
input_form: form,
})
}
///|
pub fn Syllable::plain(self : Syllable) -> String {
self.base_value
}
///|
pub fn Syllable::initial(self : Syllable) -> String {
self.initial_value
}
///|
pub fn Syllable::final_part(self : Syllable) -> String {
self.final_value
}
///|
pub fn Syllable::tone_number(self : Syllable) -> Int {
self.tone_value
}
///|
pub fn Syllable::orthography(self : Syllable) -> Orthography {
self.input_form
}