Files
AzooKeyKanaKanjiConverter/Sources/KanaKanjiConverterModule/ConverterAPI/Candidate.swift
2025-07-21 22:09:16 +09:00

243 lines
8.1 KiB
Swift

//
// Candidate.swift
// Keyboard
//
// Created by ensan on 2020/10/26.
// Copyright © 2020 ensan. All rights reserved.
//
import Foundation
/// Data of clause.
final class ClauseDataUnit {
/// The MID of the clause.
var mid: Int = MIDData.EOS.mid
/// The LCID in the next clause.
var nextLcid = CIDData.EOS.cid
/// The text of the unit.
var text: String = ""
/// The range of the unit in input text.
var ranges: [Lattice.LatticeRange] = []
/// Merge the given unit to this unit.
/// - Parameter:
/// - unit: The unit to merge.
func merge(with unit: ClauseDataUnit) {
self.text.append(unit.text)
self.ranges.append(contentsOf: unit.ranges)
self.nextLcid = unit.nextLcid
}
}
extension ClauseDataUnit: Equatable {
static func == (lhs: ClauseDataUnit, rhs: ClauseDataUnit) -> Bool {
lhs.mid == rhs.mid && lhs.nextLcid == rhs.nextLcid && lhs.text == rhs.text && lhs.ranges == rhs.ranges
}
}
#if DEBUG
extension ClauseDataUnit: CustomDebugStringConvertible {
var debugDescription: String {
"ClauseDataUnit(mid: \(mid), nextLcid: \(nextLcid), text: \(text), ranges: \(ranges))"
}
}
#endif
struct CandidateData {
typealias ClausesUnit = (clause: ClauseDataUnit, value: PValue)
var clauses: [ClausesUnit]
var data: [DicdataElement]
init(clauses: [ClausesUnit], data: [DicdataElement]) {
self.clauses = clauses
self.data = data
}
var lastClause: ClauseDataUnit? {
self.clauses.last?.clause
}
var isEmpty: Bool {
clauses.isEmpty
}
}
public enum CompleteAction: Equatable, Sendable {
/// 調
case moveCursor(Int)
}
public enum ComposingCount: Sendable, Equatable {
/// composingText.input
case inputCount(Int)
/// composingText.convertTarge
case surfaceCount(Int)
///
indirect case composite(lhs: Self, rhs: Self)
static func composite(_ lhs: Self, _ rhs: Self) -> Self {
switch (lhs, rhs) {
case (.inputCount(let l), .inputCount(let r)):
.inputCount(l + r)
case (.surfaceCount(let l), .surfaceCount(let r)):
.surfaceCount(l + r)
default:
.composite(lhs: lhs, rhs: rhs)
}
}
private struct FlatComposingCount: Equatable {
enum Kind {
case inputCount
case surfaceCount
}
var kind: Kind
var value: Int
static func inputCount(_ value: Int) -> Self {
.init(kind: .inputCount, value: value)
}
static func surfaceCount(_ value: Int) -> Self {
.init(kind: .surfaceCount, value: value)
}
}
private var flatten: [FlatComposingCount] {
switch self {
case .inputCount(let value):
if value == 0 {
[]
} else {
[.inputCount(value)]
}
case .surfaceCount(let value):
if value == 0 {
[]
} else {
[.surfaceCount(value)]
}
case .composite(let lhs, let rhs):
{
let lFlatten = lhs.flatten
let rFlatten = rhs.flatten
return switch (lFlatten.last?.kind, rFlatten.first?.kind) {
case (.inputCount, .inputCount):
lFlatten.dropLast() + [.inputCount(lFlatten.last!.value + rFlatten.first!.value)] + rFlatten.dropFirst()
case (.surfaceCount, .surfaceCount):
lFlatten.dropLast() + [.surfaceCount(lFlatten.last!.value + rFlatten.first!.value)] + rFlatten.dropFirst()
default:
lFlatten + rFlatten
}
}()
}
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.flatten == rhs.flatten
}
}
///
public struct Candidate: Sendable {
///
public var text: String
///
public var value: PValue
public var composingCount: ComposingCount
/// mid()
public var lastMid: Int
/// DicdataElement
public var data: [DicdataElement]
/// `action`
/// - note:
public var actions: [CompleteAction]
///
/// - note:
public let inputable: Bool
///
public let rubyCount: Int
public init(text: String, value: PValue, composingCount: ComposingCount, lastMid: Int, data: [DicdataElement], actions: [CompleteAction] = [], inputable: Bool = true) {
self.text = text
self.value = value
self.composingCount = composingCount
self.lastMid = lastMid
self.data = data
self.actions = actions
self.inputable = inputable
self.rubyCount = self.data.reduce(into: 0) { $0 += $1.ruby.count }
}
/// `action`
/// - parameters:
/// - actions: `action`
@inlinable public mutating func withActions(_ actions: [CompleteAction]) {
self.actions = actions
}
private static let dateExpression = "<date format=\".*?\" type=\".*?\" language=\".*?\" delta=\".*?\" deltaunit=\".*?\">"
private static let randomExpression = "<random type=\".*?\" value=\".*?\">"
///
public static func parseTemplate(_ text: String) -> String {
// MARK: Coarse Filtering: 調
guard text.contains("<") else {
return text
}
// MARK: Fine Filtering:
var newText = text
while let range = newText.range(of: Self.dateExpression, options: .regularExpression) {
let templateString = String(newText[range])
let template = DateTemplateLiteral.import(from: templateString)
let value = template.previewString()
newText.replaceSubrange(range, with: value)
}
while let range = newText.range(of: Self.randomExpression, options: .regularExpression) {
let templateString = String(newText[range])
let template = RandomTemplateLiteral.import(from: templateString)
let value = template.previewString()
newText.replaceSubrange(range, with: value)
}
return newText
}
///
@inlinable public mutating func parseTemplate() {
// Candidate.textdata.map(\.word).join("")
// data
self.text = Self.parseTemplate(text)
}
/// prefixCandidate
public static func makePrefixClauseCandidate(data: some Collection<DicdataElement>) -> Candidate {
var text = ""
var composingCount = 0
var lastRcid = CIDData.BOS.cid
var lastMid = 501
var candidateData: [DicdataElement] = []
for item in data {
//
if DicdataStore.isClause(lastRcid, item.lcid) {
break
}
text.append(item.word)
composingCount += item.ruby.count
lastRcid = item.rcid
//
if item.mid != 500 && DicdataStore.includeMMValueCalculation(item) {
lastMid = item.mid
}
candidateData.append(item)
}
return Candidate(
text: text,
value: -5,
composingCount: .surfaceCount(composingCount),
lastMid: lastMid,
data: candidateData
)
}
}