LMCassette // Refactor && Fix .clear().

This commit is contained in:
ShikiSuen 2023-12-31 01:21:22 +08:00
parent 4317c9c653
commit 1c92ab8edf
4 changed files with 278 additions and 328 deletions

View File

@ -40,314 +40,275 @@ public extension vChewingLM {
public private(set) var areCandidateKeysShiftHeld: Bool = false public private(set) var areCandidateKeysShiftHeld: Bool = false
public private(set) var supplyQuickResults: Bool = false public private(set) var supplyQuickResults: Bool = false
public private(set) var supplyPartiallyMatchedResults: Bool = false public private(set) var supplyPartiallyMatchedResults: Bool = false
/// 西 - NORM
/// 西
private static let fscale = 2.7
private var norm = 0.0 private var norm = 0.0
}
}
/// public extension vChewingLM.LMCassette {
public var wildcard: String { wildcardKey.isEmpty ? "" : wildcardKey } /// 西 - fscale
/// charDef private static let fscale = 2.7
public var count: Int { charDefMap.count } ///
/// var wildcard: String { wildcardKey.isEmpty ? "" : wildcardKey }
public var isLoaded: Bool { !charDefMap.isEmpty } /// charDef
/// 使 var count: Int { charDefMap.count }
public var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated } ///
/// var isLoaded: Bool { !charDefMap.isEmpty }
public func convertKeyToDisplay(char: String) -> String { /// 使
keyNameMap[char] ?? char var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated }
} ///
func convertKeyToDisplay(char: String) -> String {
keyNameMap[char] ?? char
}
/// CIN /// CIN
/// - Note: /// - Note:
/// - `%gen_inp` `%ename` cin /// - `%gen_inp` `%ename` cin
/// - `%ename` `%cname` CJK /// - `%ename` `%cname` CJK
/// `%sname` `%intlname` /// `%sname` `%intlname`
/// - `%encoding` Swift UTF-8 /// - `%encoding` Swift UTF-8
/// - `%selkey` /// - `%selkey`
/// - `%endkey` /// - `%endkey`
/// - `%wildcardkey` /// - `%wildcardkey`
/// - `%nullcandidate` `%quick` /// - `%nullcandidate` `%quick`
/// - `%keyname begin` `%keyname end` Swift /// - `%keyname begin` `%keyname end` Swift
/// - `%quick begin` `%quick end` value /// - `%quick begin` `%quick end` value
/// - `%chardef begin` `%chardef end` /// - `%chardef begin` `%chardef end`
/// - `%symboldef begin` `%symboldef end` /// - `%symboldef begin` `%symboldef end`
/// - `%octagram begin` `%octagram end` /// - `%octagram begin` `%octagram end`
/// ///
/// - Parameter path: /// - Parameter path:
/// - Returns: /// - Returns:
@discardableResult public mutating func open(_ path: String) -> Bool { @discardableResult mutating func open(_ path: String) -> Bool {
if isLoaded { return false } if isLoaded { return false }
let oldPath = filePath let oldPath = filePath
filePath = nil filePath = nil
if FileManager.default.fileExists(atPath: path) { if FileManager.default.fileExists(atPath: path) {
do { do {
guard let fileHandle = FileHandle(forReadingAtPath: path) else { guard let fileHandle = FileHandle(forReadingAtPath: path) else {
throw FileErrors.fileHandleError("") throw vChewingLM.FileErrors.fileHandleError("")
}
let lineReader = try LineReader(file: fileHandle)
var theMaxKeyLength = 1
var loadingKeys = false
var loadingQuickSets = false {
willSet {
supplyQuickResults = true
if !newValue, quickDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
} }
let lineReader = try LineReader(file: fileHandle) }
var theMaxKeyLength = 1 var loadingCharDefinitions = false {
var loadingKeys = false willSet {
var loadingQuickSets = false if !newValue, charDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
var loadingCharDefinitions = false }
var loadingSymbolDefinitions = false }
var loadingOctagramData = false var loadingSymbolDefinitions = false {
var keysUsedInCharDef: Set<String> = .init() willSet {
for strLine in lineReader { if !newValue, symbolDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
if strLine.starts(with: "%keyname") { }
if !loadingKeys, strLine.contains("begin") { loadingKeys = true } }
if loadingKeys, strLine.contains("end") { loadingKeys = false } var loadingOctagramData = false
} var keysUsedInCharDef: Set<String> = .init()
for strLine in lineReader {
let isTabDelimiting = strLine.contains("\t")
let cells = isTabDelimiting ? strLine.split(separator: "\t") : strLine.split(separator: " ")
guard cells.count >= 1 else { continue }
let strFirstCell = cells[0].trimmingCharacters(in: .newlines)
let strSecondCell = cells.count >= 2 ? cells[1].trimmingCharacters(in: .newlines) : nil
//
if strLine.first == "%", strFirstCell != "%" {
// %flag_disp_partial_match // %flag_disp_partial_match
if strLine == "%flag_disp_partial_match" { if strLine == "%flag_disp_partial_match" {
supplyPartiallyMatchedResults = true supplyPartiallyMatchedResults = true
supplyQuickResults = true supplyQuickResults = true
} }
// %quick guard let strSecondCell = strSecondCell else { continue }
if strLine.starts(with: "%quick") { processTags: switch strFirstCell {
supplyQuickResults = true case "%keyname" where strSecondCell == "begin": loadingKeys = true
if !loadingQuickSets, strLine.contains("begin") { case "%keyname" where strSecondCell == "end": loadingKeys = false
loadingQuickSets = true case "%quick" where strSecondCell == "begin": loadingQuickSets = true
} case "%quick" where strSecondCell == "end": loadingQuickSets = false
if loadingQuickSets, strLine.contains("end") { case "%chardef" where strSecondCell == "begin": loadingCharDefinitions = true
loadingQuickSets = false case "%chardef" where strSecondCell == "end": loadingCharDefinitions = false
if quickDefMap.keys.contains(wildcardKey) { wildcardKey = "" } case "%symboldef" where strSecondCell == "begin": loadingSymbolDefinitions = true
} case "%symboldef" where strSecondCell == "end": loadingSymbolDefinitions = false
} case "%octagram" where strSecondCell == "begin": loadingOctagramData = true
// %chardef case "%octagram" where strSecondCell == "end": loadingOctagramData = false
if strLine.starts(with: "%chardef") { case "%ename" where nameENG.isEmpty:
if !loadingCharDefinitions, strLine.contains("begin") { parseSubCells: for neta in strSecondCell.components(separatedBy: ";") {
loadingCharDefinitions = true
}
if loadingCharDefinitions, strLine.contains("end") {
loadingCharDefinitions = false
if charDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
}
}
// %symboldef
if strLine.starts(with: "%symboldef") {
if !loadingSymbolDefinitions, strLine.contains("begin") {
loadingSymbolDefinitions = true
}
if loadingSymbolDefinitions, strLine.contains("end") {
loadingSymbolDefinitions = false
if symbolDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
}
}
// %octagram
if strLine.starts(with: "%octagram") {
if !loadingOctagramData, strLine.contains("begin") {
loadingOctagramData = true
}
if loadingOctagramData, strLine.contains("end") {
loadingOctagramData = false
}
}
// Start data parsing.
let cells: [String.SubSequence] =
strLine.contains("\t") ? strLine.split(separator: "\t") : strLine.split(separator: " ")
guard cells.count >= 2 else { continue }
let strFirstCell = cells[0].trimmingCharacters(in: .newlines)
let strSecondCell = cells[1].trimmingCharacters(in: .newlines)
if loadingKeys, !cells[0].starts(with: "%keyname") {
keyNameMap[strFirstCell] = cells[1].trimmingCharacters(in: .newlines)
} else if loadingQuickSets, !strLine.starts(with: "%quick") {
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
quickDefMap[strFirstCell, default: .init()].append(strSecondCell)
} else if loadingCharDefinitions, !loadingSymbolDefinitions,
!strLine.starts(with: "%chardef"), !strLine.starts(with: "%symboldef")
{
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
charDefMap[strFirstCell, default: []].append(strSecondCell)
if strFirstCell.count > 1 {
strFirstCell.map(\.description).forEach { keyChar in
keysUsedInCharDef.insert(keyChar.description)
}
}
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
var keyComps = strFirstCell.map(\.description)
while !keyComps.isEmpty {
keyComps.removeLast()
charDefWildcardMap[keyComps.joined() + wildcard, default: []].append(strSecondCell)
}
} else if loadingSymbolDefinitions, !strLine.starts(with: "%chardef"), !strLine.starts(with: "%symboldef") {
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
symbolDefMap[strFirstCell, default: []].append(strSecondCell)
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
} else if loadingOctagramData, !strLine.starts(with: "%octagram") {
guard let countValue = Int(cells[1]) else { continue }
switch cells.count {
case 2: octagramMap[strFirstCell] = countValue
case 3: octagramDividedMap[strFirstCell] = (countValue, cells[2].trimmingCharacters(in: .newlines))
default: break
}
norm += Self.fscale ** (Double(cells[0].count) / 3.0 - 1.0) * Double(countValue)
}
guard !loadingKeys, !loadingQuickSets, !loadingCharDefinitions, !loadingOctagramData else { continue }
if nameENG.isEmpty, strLine.starts(with: "%ename ") {
for neta in cells[1].components(separatedBy: ";") {
let subNetaGroup = neta.components(separatedBy: ":") let subNetaGroup = neta.components(separatedBy: ":")
if subNetaGroup.count == 2, subNetaGroup[1].contains("en") { guard subNetaGroup.count == 2, subNetaGroup[1].contains("en") else { continue }
nameENG = String(subNetaGroup[0]) nameENG = String(subNetaGroup[0])
break break parseSubCells
}
} }
if nameENG.isEmpty { nameENG = strSecondCell } guard nameENG.isEmpty else { break processTags }
} nameENG = strSecondCell
if nameIntl.isEmpty, strLine.starts(with: "%intlname ") { case "%intlname" where nameIntl.isEmpty: nameIntl = strSecondCell.replacingOccurrences(of: "_", with: " ")
nameIntl = strSecondCell.replacingOccurrences(of: "_", with: " ") case "%cname" where nameCJK.isEmpty: nameCJK = strSecondCell
} case "%sname" where nameShort.isEmpty: nameShort = strSecondCell
if nameCJK.isEmpty, strLine.starts(with: "%cname ") { nameCJK = strSecondCell } case "%nullcandidate" where nullCandidate.isEmpty: nullCandidate = strSecondCell
if nameShort.isEmpty, strLine.starts(with: "%sname ") { nameShort = strSecondCell } case "%selkey" where selectionKeys.isEmpty: selectionKeys = strSecondCell.map(\.description).deduplicated.joined()
if nullCandidate.isEmpty, strLine.starts(with: "%nullcandidate ") { nullCandidate = strSecondCell } case "%endkey" where endKeys.isEmpty: endKeys = strSecondCell.map(\.description).deduplicated
if selectionKeys.isEmpty, strLine.starts(with: "%selkey ") { case "%wildcardkey" where wildcardKey.isEmpty: wildcardKey = strSecondCell.first?.description ?? ""
selectionKeys = cells[1].map(\.description).deduplicated.joined() case "%keys_to_directly_commit" where keysToDirectlyCommit.isEmpty: keysToDirectlyCommit = strSecondCell
} default: break processTags
if endKeys.isEmpty, strLine.starts(with: "%endkey ") {
endKeys = cells[1].map(\.description).deduplicated
}
if wildcardKey.isEmpty, strLine.starts(with: "%wildcardkey ") {
wildcardKey = cells[1].first?.description ?? ""
}
if keysToDirectlyCommit.isEmpty, strLine.starts(with: "%keys_to_directly_commit ") {
keysToDirectlyCommit = strSecondCell
} }
continue
} }
// Post process.
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" } //
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty { guard let strSecondCell = strSecondCell else { continue }
areCandidateKeysShiftHeld = true if loadingKeys {
keyNameMap[strFirstCell] = strSecondCell.trimmingCharacters(in: .newlines)
} else if loadingQuickSets {
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
quickDefMap[strFirstCell, default: .init()].append(strSecondCell)
} else if loadingCharDefinitions, !loadingSymbolDefinitions {
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
charDefMap[strFirstCell, default: []].append(strSecondCell)
if strFirstCell.count > 1 {
strFirstCell.map(\.description).forEach { keyChar in
keysUsedInCharDef.insert(keyChar.description)
}
}
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
var keyComps = strFirstCell.map(\.description)
while !keyComps.isEmpty {
keyComps.removeLast()
charDefWildcardMap[keyComps.joined() + wildcard, default: []].append(strSecondCell)
}
} else if loadingSymbolDefinitions {
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
symbolDefMap[strFirstCell, default: []].append(strSecondCell)
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
} else if loadingOctagramData {
guard let countValue = Int(strSecondCell) else { continue }
switch cells.count {
case 2: octagramMap[strFirstCell] = countValue
case 3: octagramDividedMap[strFirstCell] = (countValue, cells[2].trimmingCharacters(in: .newlines))
default: break
}
norm += Self.fscale ** (Double(cells[0].count) / 3.0 - 1.0) * Double(countValue)
} }
maxKeyLength = theMaxKeyLength
keyNameMap[wildcardKey] = keyNameMap[wildcardKey] ?? ""
filePath = path
return true
} catch {
vCLog("CIN Loading Failed: File Access Error.")
} }
// Post process.
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
areCandidateKeysShiftHeld = true
}
maxKeyLength = theMaxKeyLength
keyNameMap[wildcardKey] = keyNameMap[wildcardKey] ?? ""
filePath = path
return true
} catch {
vCLog("CIN Loading Failed: File Access Error.")
}
} else {
vCLog("CIN Loading Failed: File Missing.")
}
filePath = oldPath
return false
}
mutating func clear() {
self = .init()
}
func quickSetsFor(key: String) -> String? {
guard !key.isEmpty else { return nil }
var result = [String]()
if let specifiedResult = quickDefMap[key], !specifiedResult.isEmpty {
result.append(contentsOf: specifiedResult.map(\.description))
}
if supplyQuickResults, result.isEmpty {
if supplyPartiallyMatchedResults {
let fetched = charDefMap.compactMap {
$0.key.starts(with: key) ? $0 : nil
}.stableSort {
$0.key.count < $1.key.count
}.flatMap(\.value).filter {
$0.count == 1
}
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
} else { } else {
vCLog("CIN Loading Failed: File Missing.") let fetched = (charDefMap[key] ?? [String]()).filter { $0.count == 1 }
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
} }
filePath = oldPath
return false
} }
return result.isEmpty ? nil : result.joined(separator: "\t")
}
public mutating func clear() { ///
filePath = nil /// - parameters:
nullCandidate.removeAll() /// - key:
keyNameMap.removeAll() func unigramsFor(key: String) -> [Megrez.Unigram] {
quickDefMap.removeAll() let arrRaw = charDefMap[key]?.deduplicated ?? []
charDefMap.removeAll() var arrRawWildcard: [String] = []
charDefWildcardMap.removeAll() if let arrRawWildcardValues = charDefWildcardMap[key]?.deduplicated,
nameShort.removeAll() key.contains(wildcard), key.first?.description != wildcard
nameENG.removeAll() {
nameCJK.removeAll() arrRawWildcard.append(contentsOf: arrRawWildcardValues)
selectionKeys.removeAll()
endKeys.removeAll()
reverseLookupMap.removeAll()
octagramMap.removeAll()
octagramDividedMap.removeAll()
wildcardKey.removeAll()
nameIntl.removeAll()
maxKeyLength = 1
norm = 0
} }
var arrResults = [Megrez.Unigram]()
public func quickSetsFor(key: String) -> String? { var lowestScore: Double = 0
guard !key.isEmpty else { return nil } for neta in arrRaw {
var result = [String]() let theScore: Double = {
if let specifiedResult = quickDefMap[key], !specifiedResult.isEmpty { if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
result.append(contentsOf: specifiedResult.map(\.description)) return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
} } else if let freqData = octagramMap[neta] {
if supplyQuickResults, result.isEmpty { return calculateWeight(count: freqData, phraseLength: neta.count)
if supplyPartiallyMatchedResults {
let fetched = charDefMap.compactMap {
$0.key.starts(with: key) ? $0 : nil
}.stableSort {
$0.key.count < $1.key.count
}.flatMap(\.value).filter {
$0.count == 1
}
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
} else {
let fetched = (charDefMap[key] ?? [String]()).filter { $0.count == 1 }
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
} }
} return Double(arrResults.count) * -0.001 - 9.5
return result.isEmpty ? nil : result.joined(separator: "\t") }()
lowestScore = min(theScore, lowestScore)
arrResults.append(.init(value: neta, score: theScore))
} }
lowestScore = min(-9.5, lowestScore)
/// if !arrRawWildcard.isEmpty {
/// - parameters: for neta in arrRawWildcard {
/// - key: var theScore: Double = {
public func unigramsFor(key: String) -> [Megrez.Unigram] {
let arrRaw = charDefMap[key]?.deduplicated ?? []
var arrRawWildcard: [String] = []
if let arrRawWildcardValues = charDefWildcardMap[key]?.deduplicated,
key.contains(wildcard), key.first?.description != wildcard
{
arrRawWildcard.append(contentsOf: arrRawWildcardValues)
}
var arrResults = [Megrez.Unigram]()
var lowestScore: Double = 0
for neta in arrRaw {
let theScore: Double = {
if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 { if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
return calculateWeight(count: freqDataPair.0, phraseLength: neta.count) return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
} else if let freqData = octagramMap[neta] { } else if let freqData = octagramMap[neta] {
return calculateWeight(count: freqData, phraseLength: neta.count) return calculateWeight(count: freqData, phraseLength: neta.count)
} }
return Double(arrResults.count) * -0.001 - 9.5 return Double(arrResults.count) * -0.001 - 9.7
}() }()
lowestScore = min(theScore, lowestScore) theScore += lowestScore
arrResults.append(.init(value: neta, score: theScore)) arrResults.append(.init(value: neta, score: theScore))
} }
lowestScore = min(-9.5, lowestScore)
if !arrRawWildcard.isEmpty {
for neta in arrRawWildcard {
var theScore: Double = {
if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
} else if let freqData = octagramMap[neta] {
return calculateWeight(count: freqData, phraseLength: neta.count)
}
return Double(arrResults.count) * -0.001 - 9.7
}()
theScore += lowestScore
arrResults.append(.init(value: neta, score: theScore))
}
}
return arrResults
} }
return arrResults
}
/// ///
/// - parameters: /// - parameters:
/// - key: /// - key:
public func hasUnigramsFor(key: String) -> Bool { func hasUnigramsFor(key: String) -> Bool {
charDefMap[key] != nil charDefMap[key] != nil
|| (charDefWildcardMap[key] != nil && key.contains(wildcard) && key.first?.description != wildcard) || (charDefWildcardMap[key] != nil && key.contains(wildcard) && key.first?.description != wildcard)
} }
// MARK: - Private Functions. // MARK: - Private Functions.
private func calculateWeight(count theCount: Int, phraseLength: Int) -> Double { private func calculateWeight(count theCount: Int, phraseLength: Int) -> Double {
var weight: Double = 0 var weight: Double = 0
switch theCount { switch theCount {
case -2: // case -2: //
weight = -13 weight = -13
case -1: // case -1: //
weight = -13 weight = -13
case 0: // case 0: //
weight = log10( weight = log10(
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0) * 0.25 / norm) Self.fscale ** (Double(phraseLength) / 3.0 - 1.0) * 0.25 / norm)
default: default:
weight = log10( weight = log10(
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0) Self.fscale ** (Double(phraseLength) / 3.0 - 1.0)
* Double(theCount) / norm * Double(theCount) / norm
) )
}
return weight
} }
return weight
} }
} }

View File

@ -47,7 +47,7 @@ final class LMCassetteTests: XCTestCase {
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)") NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
XCTAssertFalse(lmCassette.quickDefMap.isEmpty) XCTAssertFalse(lmCassette.quickDefMap.isEmpty)
print(lmCassette.quickSetsFor(key: ",.") ?? "") print(lmCassette.quickSetsFor(key: ",.") ?? "")
XCTAssertEqual(lmCassette.keyNameMap.count, 41) XCTAssertEqual(lmCassette.keyNameMap.count, 31)
XCTAssertEqual(lmCassette.charDefMap.count, 29491) XCTAssertEqual(lmCassette.charDefMap.count, 29491)
XCTAssertEqual(lmCassette.charDefWildcardMap.count, 11946) XCTAssertEqual(lmCassette.charDefWildcardMap.count, 11946)
XCTAssertEqual(lmCassette.octagramMap.count, 0) XCTAssertEqual(lmCassette.octagramMap.count, 0)

View File

@ -23,9 +23,9 @@ final class LMUserOverrideTests: XCTestCase {
func testUOM_1_BasicOps() throws { func testUOM_1_BasicOps() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL) let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)" let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄋㄧㄢˊ-ㄓㄨㄥ" let headReading = "ㄍㄡˇ"
let expectedSuggestion = "年終" let expectedSuggestion = ""
observe(who: uom, key: key, candidate: expectedSuggestion, timestamp: nowTimeStamp) observe(who: uom, key: key, candidate: expectedSuggestion, timestamp: nowTimeStamp)
var suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp, headReading: headReading) var suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp, headReading: headReading)
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", expectedSuggestion) XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", expectedSuggestion)
@ -46,10 +46,10 @@ final class LMUserOverrideTests: XCTestCase {
func testUOM_2_NewestAgainstRepeatedlyUsed() throws { func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL) let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)" let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄋㄧㄢˊ-ㄓㄨㄥ" let headReading = "ㄍㄡˇ"
let valRepeatedlyUsed = "年終" // let valRepeatedlyUsed = "" //
let valNewest = "年中" // let valNewest = "" //
let stamps: [Double] = [0, 0.5, 2, 2.5, 4, 4.5, 5.3].map { nowTimeStamp + halfLife * $0 } let stamps: [Double] = [0, 0.5, 2, 2.5, 4, 4.5, 5.3].map { nowTimeStamp + halfLife * $0 }
stamps.forEach { stamp in stamps.forEach { stamp in
observe(who: uom, key: key, candidate: valRepeatedlyUsed, timestamp: stamp) observe(who: uom, key: key, candidate: valRepeatedlyUsed, timestamp: stamp)
@ -62,8 +62,6 @@ final class LMUserOverrideTests: XCTestCase {
} }
// //
observe(who: uom, key: key, candidate: valNewest, timestamp: nowTimeStamp + halfLife * 23.4) observe(who: uom, key: key, candidate: valNewest, timestamp: nowTimeStamp + halfLife * 23.4)
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 23.6, headReading: headReading)
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", valNewest)
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 26, headReading: headReading) suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 26, headReading: headReading)
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", valNewest) XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", valNewest)
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 50, headReading: headReading) suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 50, headReading: headReading)
@ -72,9 +70,9 @@ final class LMUserOverrideTests: XCTestCase {
} }
func testUOM_3_LRUTable() throws { func testUOM_3_LRUTable() throws {
let a = (key: "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)", value: "年終", head: "ㄋㄧㄢˊ-ㄓㄨㄥ") let a = (key: "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)", value: "", head: "ㄍㄡˇ")
let b = (key: "((ㄑㄧˋ-ㄧㄝˋ,企業),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "獎金", head: "ㄐㄧㄤˇ-ㄐㄧㄣ") let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
let c = (key: "((ㄒㄩㄝˊ-ㄕㄥ,學生),(ㄉㄜ˙,的),ㄈㄨˊ-ㄌㄧˋ)", value: "福利", head: "ㄈㄨˊ-ㄌㄧˋ") let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ") let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ")
let uom = vChewingLM.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL) let uom = vChewingLM.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp) observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp)

View File

@ -16,47 +16,38 @@
%phase_auto_skip_endkey %phase_auto_skip_endkey
%flag_disp_full_match %flag_disp_full_match
%flag_disp_partial_match %flag_disp_partial_match
%keys_to_directly_commit !@#$%^&*()-_=+[{]}\|:'"<>?
%keyname begin %keyname begin
a 1- a 1-
b 5v b 5v
c 3v c 3v
d 3- d 3-
e 3^ e 3^
f 4- f 4-
g 5- g 5-
h 6- h 6-
i 8^ i 8^
j 7- j 7-
k 8- k 8-
l 9- l 9-
m 7v m 7v
n 6v n 6v
o 9^ o 9^
p 0^ p 0^
q 1^ q 1^
r 4^ r 4^
s 2- s 2-
t 5^ t 5^
u 7^ u 7^
v 4v v 4v
w 2^ w 2^
x 2v x 2v
y 6^ y 6^
z 1v z 1v
. 9v . 9v
/ 0v / 0v
; 0- ; 0-
, 8v , 8v
1
2
3
4
5
6
7
8
9
0
%keyname end %keyname end
%quick begin %quick begin
, ,火米精燈料鄰勞類營 , ,火米精燈料鄰勞類營