mirror of
https://github.com/mii443/AzooKeyKanaKanjiConverter.git
synced 2025-08-22 15:05:26 +00:00
WIP: switching to new architecture
This commit is contained in:
@ -9,50 +9,53 @@ import XCTest
|
||||
import Foundation
|
||||
@testable import KanaKanjiConverterModule
|
||||
|
||||
struct ConvertGraph: InputGraphProtocol {
|
||||
struct Node: InputGraphNodeProtocol {
|
||||
struct ConvertGraph {
|
||||
struct Node {
|
||||
var latticeNodes: [LatticeNode]
|
||||
var displayedTextRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var correction: CorrectGraph.Correction = .none
|
||||
var inputElementsRange: InputGraphRange
|
||||
var correction: CorrectGraph2.Correction = .none
|
||||
}
|
||||
|
||||
var nodes: [Node] = [
|
||||
// root node
|
||||
Node(latticeNodes: [], displayedTextRange: .endIndex(0), inputElementsRange: .endIndex(0))
|
||||
Node(latticeNodes: [], inputElementsRange: .endIndex(0))
|
||||
]
|
||||
|
||||
var structure: InputGraphStructure = InputGraphStructure()
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: IndexSet] = [:]
|
||||
/// 許可されたprevIndex
|
||||
var allowedPrevIndex: [Int: IndexSet] = [:]
|
||||
|
||||
static func build(input: LookupGraph, nodeIndex2LatticeNode: [Int: [LatticeNode]]) -> Self {
|
||||
let nodes = input.nodes.enumerated().map { (index, node) in
|
||||
Node(latticeNodes: nodeIndex2LatticeNode[index, default: []], displayedTextRange: node.displayedTextRange, inputElementsRange: node.inputElementsRange, correction: node.correction)
|
||||
Node(latticeNodes: nodeIndex2LatticeNode[index, default: []], inputElementsRange: node.inputElementsRange, correction: node.correction)
|
||||
}
|
||||
return Self(nodes: nodes, structure: input.structure)
|
||||
return Self(nodes: nodes, allowedNextIndex: input.allowedNextIndex, allowedPrevIndex: input.allowedPrevIndex)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConvertGraph {
|
||||
/// ラティスのノード。これを用いて計算する。
|
||||
final class LatticeNode: CustomStringConvertible {
|
||||
/// このノードが保持する辞書データ
|
||||
public let data: DicdataElement
|
||||
/// このノードが保持するデータの次に続くノードのConvertGraph上のindex
|
||||
var nextConvertNodeIndices: IndexSet = []
|
||||
/// このノードの前に来ているノード。`N_best`の分だけ保存する
|
||||
var prevs: [RegisteredNode] = []
|
||||
/// `prevs`の各要素に対応するスコアのデータ
|
||||
var values: [PValue] = []
|
||||
/// inputData.input内のrange
|
||||
var displayedTextRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphRange
|
||||
|
||||
/// `EOS`に対応するノード。
|
||||
static var EOSNode: LatticeNode {
|
||||
LatticeNode(data: DicdataElement.EOSData, displayedTextRange: .unknown, inputElementsRange: .unknown)
|
||||
LatticeNode(data: DicdataElement.EOSData, nextConvertNodeIndices: [], inputElementsRange: .unknown)
|
||||
}
|
||||
|
||||
init(data: DicdataElement, displayedTextRange: InputGraphStructure.Range, inputElementsRange: InputGraphStructure.Range, prevs: [RegisteredNode] = []) {
|
||||
init(data: DicdataElement, nextConvertNodeIndices: IndexSet, inputElementsRange: InputGraphRange, prevs: [RegisteredNode] = []) {
|
||||
self.data = data
|
||||
self.values = [data.value()]
|
||||
self.displayedTextRange = displayedTextRange
|
||||
self.nextConvertNodeIndices = nextConvertNodeIndices
|
||||
self.inputElementsRange = inputElementsRange
|
||||
self.prevs = prevs
|
||||
}
|
||||
@ -65,7 +68,6 @@ extension ConvertGraph {
|
||||
data: self.data,
|
||||
registered: self.prevs[index],
|
||||
totalValue: value,
|
||||
displayedTextRange: self.displayedTextRange,
|
||||
inputElementsRange: self.inputElementsRange
|
||||
)
|
||||
}
|
||||
@ -82,21 +84,19 @@ extension ConvertGraph {
|
||||
/// 始点からこのノードまでのコスト
|
||||
let totalValue: PValue
|
||||
/// inputData.input内のrange
|
||||
var displayedTextRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphRange
|
||||
|
||||
init(data: DicdataElement, registered: RegisteredNode?, totalValue: PValue, displayedTextRange: InputGraphStructure.Range, inputElementsRange: InputGraphStructure.Range) {
|
||||
init(data: DicdataElement, registered: RegisteredNode?, totalValue: PValue, inputElementsRange: InputGraphRange) {
|
||||
self.data = data
|
||||
self.prev = registered
|
||||
self.totalValue = totalValue
|
||||
self.displayedTextRange = displayedTextRange
|
||||
self.inputElementsRange = inputElementsRange
|
||||
}
|
||||
|
||||
/// 始点ノードを生成する関数
|
||||
/// - Returns: 始点ノードのデータ
|
||||
static func BOSNode() -> RegisteredNode {
|
||||
RegisteredNode(data: DicdataElement.BOSData, registered: nil, totalValue: 0, displayedTextRange: .endIndex(0), inputElementsRange: .endIndex(0))
|
||||
RegisteredNode(data: DicdataElement.BOSData, registered: nil, totalValue: 0, inputElementsRange: .endIndex(0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,31 +108,29 @@ protocol RegisteredNodeProtocol {
|
||||
var data: DicdataElement {get}
|
||||
var prev: (any RegisteredNodeProtocol)? {get}
|
||||
var totalValue: PValue {get}
|
||||
/// inputData.input内のrange
|
||||
var displayedTextRange: InputGraphStructure.Range {get}
|
||||
var inputElementsRange: InputGraphStructure.Range {get}
|
||||
var inputElementsRange: InputGraphRange {get}
|
||||
}
|
||||
|
||||
extension ConvertGraph {
|
||||
func convertAll(option: borrowing ConvertRequestOptions, dicdataStore: DicdataStore) -> LatticeNode {
|
||||
let result: LatticeNode = LatticeNode.EOSNode
|
||||
result.displayedTextRange = .startIndex(self.structure.displayedTextEndIndexToNodeIndices.endIndex)
|
||||
result.inputElementsRange = .startIndex(self.structure.inputElementsEndIndexToNodeIndices.endIndex)
|
||||
result.inputElementsRange = .init(startIndex: self.nodes.compactMap {$0.inputElementsRange.endIndex}.max(), endIndex: nil)
|
||||
var processStack = Array(self.nodes.enumerated().reversed())
|
||||
var processedIndices: IndexSet = [0] // root
|
||||
var invalidIndices: IndexSet = []
|
||||
// 「i文字目から始まるnodes」に対して
|
||||
while let (i, graphNode) = processStack.popLast() {
|
||||
// 処理済みなら無視する
|
||||
guard !processedIndices.contains(i), !invalidIndices.contains(i) else {
|
||||
continue
|
||||
}
|
||||
// 全てのprevNodeが処理済みか確かめる
|
||||
let prevIndices = self.structure.prevIndices(displayedTextStartIndex: graphNode.displayedTextRange.startIndex, inputElementsStartIndex: graphNode.inputElementsRange.startIndex)
|
||||
let prevIndices = self.allowedPrevIndex[i, default: []]
|
||||
guard !prevIndices.isEmpty else {
|
||||
// 空の場合は無視して次へ
|
||||
invalidIndices.insert(i)
|
||||
continue
|
||||
}
|
||||
|
||||
var unprocessedPrevs: [(Int, Node)] = []
|
||||
for prevIndex in prevIndices {
|
||||
if !processedIndices.contains(prevIndex) && !invalidIndices.contains(prevIndex) {
|
||||
@ -145,7 +143,7 @@ extension ConvertGraph {
|
||||
processStack.append(contentsOf: unprocessedPrevs)
|
||||
continue
|
||||
}
|
||||
print(i, graphNode.displayedTextRange, graphNode.inputElementsRange)
|
||||
print(i, graphNode.inputElementsRange)
|
||||
processedIndices.insert(i)
|
||||
// 処理を実施する
|
||||
for node in graphNode.latticeNodes {
|
||||
@ -164,19 +162,14 @@ extension ConvertGraph {
|
||||
// valuesを更新する
|
||||
node.values = node.prevs.map {$0.totalValue + wValue}
|
||||
}
|
||||
// このLatticeNodeに後続するグラフのノードを検索
|
||||
let nextIndices = self.structure.nextIndices(
|
||||
displayedTextEndIndex: node.displayedTextRange.endIndex,
|
||||
inputElementsEndIndex: node.inputElementsRange.endIndex
|
||||
)
|
||||
// 文字数がcountと等しい場合登録する
|
||||
if nextIndices.isEmpty || self.structure.inputElementsStartIndexToNodeIndices.endIndex == node.inputElementsRange.endIndex {
|
||||
// 終端の場合は終了
|
||||
if node.nextConvertNodeIndices.isEmpty || result.inputElementsRange.startIndex == node.inputElementsRange.endIndex {
|
||||
for index in node.prevs.indices {
|
||||
let newnode: RegisteredNode = node.getRegisteredNode(index, value: node.values[index])
|
||||
result.prevs.append(newnode)
|
||||
}
|
||||
} else {
|
||||
for nextIndex in nextIndices {
|
||||
for nextIndex in node.nextConvertNodeIndices {
|
||||
// nodeの繋がる次にあり得る全てのnextnodeに対して
|
||||
for nextnode in self.nodes[nextIndex].latticeNodes {
|
||||
// この関数はこの時点で呼び出して、後のnode.registered.isEmptyで最終的に弾くのが良い。
|
||||
|
@ -10,6 +10,136 @@ import Foundation
|
||||
import XCTest
|
||||
|
||||
struct CorrectGraph {
|
||||
var nodes: [Node] = [
|
||||
// BOSノードは最初から追加
|
||||
.init(inputElementsRange: .endIndex(0), inputStyle: .all, correction: .none, value: "\0")
|
||||
]
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: IndexSet] = [:]
|
||||
/// 許可されたprevIndex
|
||||
var allowedPrevIndex: [Int: IndexSet] = [:]
|
||||
|
||||
struct Node: Equatable, Sendable {
|
||||
var inputElementsRange: InputGraphRange
|
||||
var inputStyle: InputGraphInputStyle.ID
|
||||
var correction: CorrectGraph2.Correction
|
||||
var value: Character
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func insert(_ node: consuming Node, nextTo prevNodeIndexSet: IndexSet) -> Int {
|
||||
let index = nodes.count
|
||||
for prevNodeIndex in prevNodeIndexSet {
|
||||
self.allowedNextIndex[prevNodeIndex, default: IndexSet()].insert(index)
|
||||
}
|
||||
self.allowedPrevIndex[index, default: IndexSet()].formUnion(prevNodeIndexSet)
|
||||
self.nodes.append(consume node)
|
||||
return index
|
||||
}
|
||||
|
||||
mutating func insertConnectedTypoNodes(values: [Character], startIndex: Int, endIndex: Int, inputStyle: InputGraphInputStyle.ID, lastIndexSet: IndexSet) -> Int {
|
||||
guard !values.isEmpty else {
|
||||
fatalError("values must not be empty")
|
||||
}
|
||||
var lastIndexSet = lastIndexSet
|
||||
for (i, c) in zip(values.indices, values) {
|
||||
let inputElementRange: InputGraphRange = if i == values.startIndex && i+1 == values.endIndex {
|
||||
.range(startIndex, endIndex)
|
||||
} else if i == values.startIndex {
|
||||
.init(startIndex: startIndex, endIndex: nil)
|
||||
} else if i+1 == values.endIndex {
|
||||
.init(startIndex: nil, endIndex: endIndex)
|
||||
} else {
|
||||
.unknown
|
||||
}
|
||||
let node = Node(
|
||||
inputElementsRange: inputElementRange,
|
||||
inputStyle: inputStyle,
|
||||
correction: .typo,
|
||||
value: c
|
||||
)
|
||||
lastIndexSet = IndexSet(integer: self.insert(node, nextTo: lastIndexSet))
|
||||
}
|
||||
return lastIndexSet.first!
|
||||
}
|
||||
|
||||
static func build(input: [ComposingText.InputElement]) -> Self {
|
||||
var correctGraph = Self()
|
||||
var inputIndexToEndNodeIndices: [Int: IndexSet] = [0: IndexSet(integer: 0)]
|
||||
for (index, item) in zip(input.indices, input) {
|
||||
// 訂正のない候補を追加
|
||||
do {
|
||||
let nodeIndex = correctGraph.insert(
|
||||
Node(
|
||||
inputElementsRange: .range(index, index + 1),
|
||||
inputStyle: InputGraphInputStyle(from: input[index].inputStyle).id,
|
||||
correction: .none,
|
||||
value: item.character
|
||||
),
|
||||
nextTo: inputIndexToEndNodeIndices[index, default: IndexSet()]
|
||||
)
|
||||
inputIndexToEndNodeIndices[index + 1, default: IndexSet()].insert(nodeIndex)
|
||||
}
|
||||
|
||||
// 訂正候補を追加
|
||||
let correctPrefixTree = switch item.inputStyle {
|
||||
case .roman2kana: CorrectPrefixTree.roman2kana
|
||||
case .direct: CorrectPrefixTree.direct
|
||||
}
|
||||
typealias Match = (replace: String, inputCount: Int)
|
||||
typealias SearchItem = (
|
||||
node: CorrectPrefixTree.Node,
|
||||
nextIndex: Int,
|
||||
route: [Character],
|
||||
inputStyleId: InputGraphInputStyle.ID
|
||||
)
|
||||
var stack: [SearchItem] = [
|
||||
(correctPrefixTree, index, [], .all),
|
||||
]
|
||||
while let (cNode, cIndex, cRoute, cInputStyleId) = stack.popLast() {
|
||||
guard cIndex < input.endIndex else {
|
||||
continue
|
||||
}
|
||||
let inputStyleId = InputGraphInputStyle(from: input[cIndex].inputStyle).id
|
||||
guard cInputStyleId.isCompatible(with: inputStyleId) else {
|
||||
continue
|
||||
}
|
||||
if let nNode = cNode.find(key: input[cIndex].character) {
|
||||
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], inputStyleId))
|
||||
for value in nNode.value {
|
||||
if value.isEmpty {
|
||||
continue
|
||||
} else if value.count > 1 {
|
||||
let nodeIndex = correctGraph.insertConnectedTypoNodes(
|
||||
values: Array(value),
|
||||
startIndex: index,
|
||||
endIndex: index + cRoute.count + 1,
|
||||
inputStyle: inputStyleId,
|
||||
lastIndexSet: inputIndexToEndNodeIndices[index, default: IndexSet()]
|
||||
)
|
||||
inputIndexToEndNodeIndices[index + cRoute.count + 1, default: IndexSet()].insert(nodeIndex)
|
||||
} else {
|
||||
let nodeIndex = correctGraph.insert(
|
||||
Node(
|
||||
inputElementsRange: .range(index, index + cRoute.count + 1),
|
||||
inputStyle: inputStyleId,
|
||||
correction: .typo,
|
||||
value: value.first!
|
||||
),
|
||||
nextTo: inputIndexToEndNodeIndices[index, default: IndexSet()]
|
||||
)
|
||||
inputIndexToEndNodeIndices[index + cRoute.count + 1, default: IndexSet()].insert(nodeIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return correctGraph
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct CorrectGraph2 {
|
||||
var nodes: [Node] = []
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: Int] = [:]
|
||||
@ -44,8 +174,8 @@ struct CorrectGraph {
|
||||
}
|
||||
|
||||
struct Node: Equatable, Sendable {
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var inputStyle: InputGraph.InputStyle.ID
|
||||
var inputElementsRange: InputGraphRange
|
||||
var inputStyle: InputGraphInputStyle.ID
|
||||
var correction: Correction
|
||||
var value: Character
|
||||
var groupId: Int?
|
||||
@ -88,11 +218,11 @@ struct CorrectGraph {
|
||||
return index
|
||||
}
|
||||
|
||||
mutating func insertConnectedTypoNodes(values: [Character], startIndex: Int, endIndex: Int, inputStyle: InputGraph.InputStyle.ID) {
|
||||
mutating func insertConnectedTypoNodes(values: [Character], startIndex: Int, endIndex: Int, inputStyle: InputGraphInputStyle.ID) {
|
||||
var indices: [Int] = []
|
||||
let id = self.groupIdIota.new()
|
||||
for (i, c) in zip(values.indices, values) {
|
||||
let inputElementRange: InputGraphStructure.Range = if i == values.startIndex && i+1 == values.endIndex {
|
||||
let inputElementRange: InputGraphRange = if i == values.startIndex && i+1 == values.endIndex {
|
||||
.range(startIndex, endIndex)
|
||||
} else if i == values.startIndex {
|
||||
.init(startIndex: startIndex, endIndex: nil)
|
||||
@ -124,7 +254,7 @@ struct CorrectGraph {
|
||||
correctGraph.insert(
|
||||
Node(
|
||||
inputElementsRange: .range(index, index + 1),
|
||||
inputStyle: InputGraph.InputStyle(from: input[index].inputStyle).id,
|
||||
inputStyle: InputGraphInputStyle(from: input[index].inputStyle).id,
|
||||
correction: .none,
|
||||
value: item.character
|
||||
)
|
||||
@ -138,7 +268,7 @@ struct CorrectGraph {
|
||||
node: CorrectPrefixTree.Node,
|
||||
nextIndex: Int,
|
||||
route: [Character],
|
||||
inputStyleId: InputGraph.InputStyle.ID
|
||||
inputStyleId: InputGraphInputStyle.ID
|
||||
)
|
||||
var stack: [SearchItem] = [
|
||||
(correctPrefixTree, index, [], .all),
|
||||
@ -147,7 +277,7 @@ struct CorrectGraph {
|
||||
guard cIndex < input.endIndex else {
|
||||
continue
|
||||
}
|
||||
let inputStyleId = InputGraph.InputStyle(from: input[cIndex].inputStyle).id
|
||||
let inputStyleId = InputGraphInputStyle(from: input[cIndex].inputStyle).id
|
||||
guard cInputStyleId.isCompatible(with: inputStyleId) else {
|
||||
continue
|
||||
}
|
||||
@ -223,7 +353,7 @@ final class CorrectGraphTests: XCTestCase {
|
||||
.init(inputElementsRange: .range(2, 3), inputStyle: .systemFlickDirect, correction: .none, value: "う")
|
||||
)
|
||||
if let index = graph.nodes.firstIndex(where: {$0.value == "う"}) {
|
||||
XCTAssertEqual(graph.prevIndices(for: index).count, 2)
|
||||
XCTAssertEqual(graph.allowedPrevIndex[index, default: .init()].count, 2)
|
||||
} else {
|
||||
XCTAssertThrowsError("Should not be nil")
|
||||
}
|
||||
@ -257,14 +387,14 @@ final class CorrectGraphTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(
|
||||
graph.nodes.first(where: {$0.value == "t" && $0.inputElementsRange == .startIndex(0)}),
|
||||
.init(inputElementsRange: .startIndex(0), inputStyle: .systemRomanKana, correction: .typo, value: "t", groupId: 0)
|
||||
.init(inputElementsRange: .startIndex(0), inputStyle: .systemRomanKana, correction: .typo, value: "t")
|
||||
)
|
||||
XCTAssertEqual(
|
||||
graph.nodes.first(where: {$0.value == "a"}),
|
||||
.init(inputElementsRange: .endIndex(2), inputStyle: .systemRomanKana, correction: .typo, value: "a", groupId: 0)
|
||||
.init(inputElementsRange: .endIndex(2), inputStyle: .systemRomanKana, correction: .typo, value: "a")
|
||||
)
|
||||
if let index = graph.nodes.firstIndex(where: {$0.value == "a"}) {
|
||||
let indices = graph.prevIndices(for: index)
|
||||
let indices = graph.allowedPrevIndex[index, default: .init()]
|
||||
XCTAssertEqual(indices.count, 1)
|
||||
XCTAssertEqual(
|
||||
indices.first,
|
||||
|
@ -11,504 +11,208 @@ import DequeModule
|
||||
@testable import KanaKanjiConverterModule
|
||||
import XCTest
|
||||
|
||||
struct InputGraphStructure {
|
||||
enum Range: Equatable, Sendable {
|
||||
case unknown
|
||||
case startIndex(Int)
|
||||
case endIndex(Int)
|
||||
case range(Int, Int)
|
||||
|
||||
init(startIndex: Int?, endIndex: Int?) {
|
||||
self = switch (startIndex, endIndex) {
|
||||
case let (s?, e?): .range(s, e)
|
||||
case (let s?, nil): .startIndex(s)
|
||||
case (nil, let e?): .endIndex(e)
|
||||
case (nil, nil): .unknown
|
||||
}
|
||||
}
|
||||
|
||||
var startIndex: Int? {
|
||||
switch self {
|
||||
case .unknown, .endIndex: nil
|
||||
case .startIndex(let index), .range(let index, _): index
|
||||
}
|
||||
}
|
||||
|
||||
var endIndex: Int? {
|
||||
switch self {
|
||||
case .unknown, .startIndex: nil
|
||||
case .endIndex(let index), .range(_, let index): index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `displayedTextStartIndexToNodeIndices[0]`は`displayedTextRange==.startIndex(0)`または`displayedTextRange==.range(0, k)`であるようなノードのindexのセットを返す
|
||||
var displayedTextStartIndexToNodeIndices: [IndexSet] = []
|
||||
var inputElementsStartIndexToNodeIndices: [IndexSet] = []
|
||||
var displayedTextEndIndexToNodeIndices: [IndexSet] = [IndexSet(integer: 0)] // rootノードのindexで初期化
|
||||
var inputElementsEndIndexToNodeIndices: [IndexSet] = [IndexSet(integer: 0)] // rootノードのindexで初期化
|
||||
/// 使用されなくなったインデックスの集合
|
||||
var deadNodeIndices: [Int] = []
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: [Int]] = [:]
|
||||
/// 許可されたprevIndex
|
||||
var allowedPrevIndex: [Int: [Int]] = [:]
|
||||
/// id生成用
|
||||
var groupIdIota: Iota = Iota()
|
||||
|
||||
func nextIndices(displayedTextEndIndex: Int?, inputElementsEndIndex: Int?) -> IndexSet {
|
||||
var indexSet = IndexSet()
|
||||
if let displayedTextEndIndex {
|
||||
if displayedTextEndIndex < self.displayedTextStartIndexToNodeIndices.endIndex {
|
||||
indexSet.formUnion(self.displayedTextStartIndexToNodeIndices[displayedTextEndIndex])
|
||||
}
|
||||
}
|
||||
if let inputElementsEndIndex {
|
||||
if inputElementsEndIndex < self.inputElementsStartIndexToNodeIndices.endIndex {
|
||||
indexSet.formUnion(self.inputElementsStartIndexToNodeIndices[inputElementsEndIndex])
|
||||
}
|
||||
}
|
||||
return indexSet
|
||||
}
|
||||
|
||||
func prevIndices(displayedTextStartIndex: Int?, inputElementsStartIndex: Int?) -> IndexSet {
|
||||
var indexSet = IndexSet()
|
||||
if let displayedTextStartIndex {
|
||||
if displayedTextStartIndex < self.displayedTextEndIndexToNodeIndices.endIndex {
|
||||
indexSet.formUnion(self.displayedTextEndIndexToNodeIndices[displayedTextStartIndex])
|
||||
}
|
||||
}
|
||||
if let inputElementsStartIndex {
|
||||
if inputElementsStartIndex < self.inputElementsEndIndexToNodeIndices.endIndex {
|
||||
indexSet.formUnion(self.inputElementsEndIndexToNodeIndices[inputElementsStartIndex])
|
||||
}
|
||||
}
|
||||
return indexSet
|
||||
}
|
||||
|
||||
enum Connection {
|
||||
case none
|
||||
case nextRestriction(Int)
|
||||
case restriction(prev: Int, next: Int)
|
||||
case prevRestriction(Int)
|
||||
}
|
||||
/// 戻り値は`index`
|
||||
mutating func insert<T>(_ node: T, nodes: inout [T], displayedTextRange: Range, inputElementsRange: Range, connection: Connection = .none) -> Int {
|
||||
// 可能ならdeadNodeIndicesを再利用する
|
||||
let index: Int
|
||||
if let deadIndex = self.deadNodeIndices.popLast() {
|
||||
nodes[deadIndex] = node
|
||||
index = deadIndex
|
||||
} else {
|
||||
nodes.append(node)
|
||||
index = nodes.count - 1
|
||||
}
|
||||
// このケースではここにだけ追加する
|
||||
if case let .restriction(prev, next) = connection {
|
||||
self.allowedPrevIndex[prev, default: []].append(index)
|
||||
self.allowedPrevIndex[index, default: []].append(prev)
|
||||
self.allowedNextIndex[next, default: []].append(index)
|
||||
self.allowedNextIndex[index, default: []].append(next)
|
||||
return index
|
||||
}
|
||||
if case let .nextRestriction(next) = connection {
|
||||
self.allowedNextIndex[index, default: []].append(next)
|
||||
self.allowedNextIndex[next, default: []].append(index)
|
||||
} else {
|
||||
// 出ているノードに特に制限はないので、endIndexは登録できる
|
||||
if let endIndex = displayedTextRange.endIndex {
|
||||
if self.displayedTextEndIndexToNodeIndices.endIndex <= endIndex {
|
||||
self.displayedTextEndIndexToNodeIndices.append(contentsOf: Array(repeating: IndexSet(), count: endIndex - self.displayedTextEndIndexToNodeIndices.endIndex + 1))
|
||||
}
|
||||
self.displayedTextEndIndexToNodeIndices[endIndex].insert(index)
|
||||
}
|
||||
if let endIndex = inputElementsRange.endIndex {
|
||||
if self.inputElementsEndIndexToNodeIndices.endIndex <= endIndex {
|
||||
self.inputElementsEndIndexToNodeIndices.append(contentsOf: Array(repeating: IndexSet(), count: endIndex - self.inputElementsEndIndexToNodeIndices.endIndex + 1))
|
||||
}
|
||||
self.inputElementsEndIndexToNodeIndices[endIndex].insert(index)
|
||||
}
|
||||
}
|
||||
if case let .prevRestriction(prev) = connection {
|
||||
self.allowedPrevIndex[index, default: []].append(prev)
|
||||
self.allowedPrevIndex[prev, default: []].append(index)
|
||||
} else {
|
||||
// 入ってくるノードに特に制限はないので、startIndexは登録できる
|
||||
// それ以外の場合は通常の通り追加する
|
||||
if let startIndex = displayedTextRange.startIndex {
|
||||
if self.displayedTextStartIndexToNodeIndices.endIndex <= startIndex {
|
||||
self.displayedTextStartIndexToNodeIndices.append(contentsOf: Array(repeating: IndexSet(), count: startIndex - self.displayedTextStartIndexToNodeIndices.endIndex + 1))
|
||||
}
|
||||
self.displayedTextStartIndexToNodeIndices[startIndex].insert(index)
|
||||
}
|
||||
if let startIndex = inputElementsRange.startIndex {
|
||||
if self.inputElementsStartIndexToNodeIndices.endIndex <= startIndex {
|
||||
self.inputElementsStartIndexToNodeIndices.append(contentsOf: Array(repeating: IndexSet(), count: startIndex - self.inputElementsStartIndexToNodeIndices.endIndex + 1))
|
||||
}
|
||||
self.inputElementsStartIndexToNodeIndices[startIndex].insert(index)
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
mutating func remove(at index: Int) {
|
||||
assert(index != 0, "Node at index 0 is root and must not be removed.")
|
||||
self.deadNodeIndices.append(index)
|
||||
// FIXME: 多分nodeの情報を使えばもっと効率的にremoveできる
|
||||
self.allowedPrevIndex.values.mutatingForeach {
|
||||
$0.removeAll(where: {$0 == index})
|
||||
}
|
||||
self.allowedPrevIndex.removeValue(forKey: index)
|
||||
self.allowedNextIndex.values.mutatingForeach {
|
||||
$0.removeAll(where: {$0 == index})
|
||||
}
|
||||
self.allowedNextIndex.removeValue(forKey: index)
|
||||
self.displayedTextStartIndexToNodeIndices.mutatingForeach {
|
||||
$0.remove(index)
|
||||
}
|
||||
self.displayedTextEndIndexToNodeIndices.mutatingForeach {
|
||||
$0.remove(index)
|
||||
}
|
||||
self.inputElementsStartIndexToNodeIndices.mutatingForeach {
|
||||
$0.remove(index)
|
||||
}
|
||||
self.inputElementsEndIndexToNodeIndices.mutatingForeach {
|
||||
$0.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InputGraph: InputGraphProtocol {
|
||||
struct InputStyle: Identifiable {
|
||||
init(from deprecatedInputStyle: KanaKanjiConverterModule.InputStyle) {
|
||||
switch deprecatedInputStyle {
|
||||
case .direct:
|
||||
self = .systemFlickDirect
|
||||
case .roman2kana:
|
||||
self = .systemRomanKana
|
||||
}
|
||||
}
|
||||
|
||||
init(id: InputGraph.InputStyle.ID, replacePrefixTree: ReplacePrefixTree.Node, correctPrefixTree: CorrectPrefixTree.Node) {
|
||||
self.id = id
|
||||
self.replacePrefixTree = replacePrefixTree
|
||||
self.correctPrefixTree = correctPrefixTree
|
||||
}
|
||||
|
||||
struct ID: Equatable, Hashable, Sendable, CustomStringConvertible {
|
||||
init(id: UInt8) {
|
||||
self.id = id
|
||||
}
|
||||
init(from deprecatedInputStyle: KanaKanjiConverterModule.InputStyle) {
|
||||
switch deprecatedInputStyle {
|
||||
case .direct:
|
||||
self = .systemFlickDirect
|
||||
case .roman2kana:
|
||||
self = .systemRomanKana
|
||||
}
|
||||
}
|
||||
static let all = Self(id: 0x00)
|
||||
static let systemFlickDirect = Self(id: 0x01)
|
||||
static let systemRomanKana = Self(id: 0x02)
|
||||
var id: UInt8
|
||||
|
||||
func isCompatible(with id: ID) -> Bool {
|
||||
if self == .all {
|
||||
true
|
||||
} else {
|
||||
self == id
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
"ID(\(id))"
|
||||
}
|
||||
}
|
||||
static let all: Self = InputStyle(
|
||||
id: .all,
|
||||
replacePrefixTree: ReplacePrefixTree.Node(),
|
||||
correctPrefixTree: CorrectPrefixTree.Node()
|
||||
)
|
||||
static let systemFlickDirect: Self = InputStyle(
|
||||
id: .systemFlickDirect,
|
||||
replacePrefixTree: ReplacePrefixTree.direct,
|
||||
correctPrefixTree: CorrectPrefixTree.direct
|
||||
)
|
||||
static let systemRomanKana: Self = InputStyle(
|
||||
id: .systemRomanKana,
|
||||
replacePrefixTree: ReplacePrefixTree.roman2kana,
|
||||
correctPrefixTree: CorrectPrefixTree.roman2kana
|
||||
)
|
||||
|
||||
/// `id` for the input style.
|
||||
/// - warning: value `0x00-0x7F` is reserved for system space.
|
||||
var id: ID
|
||||
var replacePrefixTree: ReplacePrefixTree.Node
|
||||
var correctPrefixTree: CorrectPrefixTree.Node
|
||||
}
|
||||
|
||||
struct Node: InputGraphNodeProtocol, Equatable, CustomStringConvertible {
|
||||
struct InputGraph {
|
||||
struct Node: Equatable, CustomStringConvertible {
|
||||
var character: Character
|
||||
var displayedTextRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var groupId: Int? = nil
|
||||
var correction: CorrectGraph.Correction = .none
|
||||
/// すでにreplaceされてしまったノードであるかどうか?
|
||||
var isReplaced: Bool = false
|
||||
var inputElementsRange: InputGraphRange
|
||||
var correction: CorrectGraph2.Correction = .none
|
||||
|
||||
var description: String {
|
||||
let ds = displayedTextRange.startIndex?.description ?? "?"
|
||||
let de = displayedTextRange.endIndex?.description ?? "?"
|
||||
let `is` = inputElementsRange.startIndex?.description ?? "?"
|
||||
let ie = inputElementsRange.endIndex?.description ?? "?"
|
||||
return "Node(\"\(character)\", d(\(ds)..<\(de)), i(\(`is`)..<\(ie)), isTypo: \(correction.isTypo), id: \(groupId))"
|
||||
return "Node(\"\(character)\", i(\(`is`)..<\(ie)), isTypo: \(correction.isTypo))"
|
||||
}
|
||||
}
|
||||
|
||||
var nodes: [Node] = [
|
||||
// root node
|
||||
Node(character: "\0", displayedTextRange: .endIndex(0), inputElementsRange: .endIndex(0))
|
||||
Node(character: "\0", inputElementsRange: .endIndex(0), correction: .none)
|
||||
]
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: IndexSet] = [:]
|
||||
/// 許可されたprevIndex
|
||||
var allowedPrevIndex: [Int: IndexSet] = [:]
|
||||
/// correctGraphのノード情報
|
||||
var nextCorrectNodeIndices: [Int: IndexSet] = [:]
|
||||
|
||||
var structure: InputGraphStructure = InputGraphStructure()
|
||||
|
||||
mutating func backwardMatches(_ correctGraph: CorrectGraph, nodeIndex: Int) {
|
||||
let correctGraphNode = correctGraph.nodes[nodeIndex]
|
||||
mutating func update(_ correctGraph: CorrectGraph, nodeIndex: Int) {
|
||||
let cgNode = correctGraph.nodes[nodeIndex]
|
||||
// アルゴリズム
|
||||
// 1. nodeIndexをnextCorrectNodeIndicesに持っているノードを列挙する
|
||||
// 2. それぞれのノードにcgNodes[nodeIndex]を追加し、末尾置換が可能であれば実施する
|
||||
// 3. 可能でない場合、そのまま追加する
|
||||
// まず、cgNodeをinsertする
|
||||
let prevNodeIndices: [Int] = self.nextCorrectNodeIndices.lazy.filter {
|
||||
$0.value.contains(nodeIndex)
|
||||
}.map {
|
||||
$0.key
|
||||
}
|
||||
let newIndex = self.nodes.endIndex
|
||||
self.nodes.append(Node(character: cgNode.value, inputElementsRange: cgNode.inputElementsRange, correction: cgNode.correction))
|
||||
// 構造の情報を更新
|
||||
self.allowedPrevIndex[newIndex] = IndexSet(prevNodeIndices)
|
||||
for prevNodeIndex in prevNodeIndices {
|
||||
self.allowedNextIndex[prevNodeIndex, default: IndexSet()].insert(newIndex)
|
||||
}
|
||||
// correct graphにおけるnext nodeの情報
|
||||
self.nextCorrectNodeIndices[newIndex] = correctGraph.allowedNextIndex[nodeIndex]
|
||||
|
||||
let startNode = switch correctGraphNode.inputStyle {
|
||||
// 次に置換を動かす
|
||||
let startNode = switch cgNode.inputStyle {
|
||||
case .systemFlickDirect:
|
||||
ReplaceSuffixTree.direct
|
||||
case .systemRomanKana:
|
||||
ReplaceSuffixTree.roman2kana
|
||||
default: fatalError("implement it")
|
||||
}
|
||||
print(nodeIndex, startNode.children.count, correctGraphNode)
|
||||
// nodesをそれぞれ遡っていく必要がある
|
||||
typealias SearchItem = (
|
||||
suffixTreeNode: ReplaceSuffixTree.Node,
|
||||
startNodeIndex: Int,
|
||||
// 辿ってきたインデックス
|
||||
route: [Int],
|
||||
correction: CorrectGraph.Correction
|
||||
// 発見された置換
|
||||
foundValue: Replacement?,
|
||||
correction: CorrectGraph2.Correction
|
||||
)
|
||||
typealias Match = (
|
||||
displayedTextStartIndex: Int?,
|
||||
inputElementsStartIndex: Int?,
|
||||
inputElementsEndIndex: Int?,
|
||||
backwardRoute: [Int],
|
||||
value: String,
|
||||
/// このマッチを認可するノードの`index`
|
||||
licenserNodeIndex: Int?,
|
||||
/// groupId
|
||||
groupId: Int?,
|
||||
correction: CorrectGraph.Correction
|
||||
// 置換
|
||||
replacement: Replacement,
|
||||
// 置換を含むroute
|
||||
route: [Int]
|
||||
)
|
||||
struct Replacement: Hashable {
|
||||
var route: [Int]
|
||||
var value: String
|
||||
}
|
||||
var backSearchMatch: [Match] = []
|
||||
var stack: [SearchItem] = [(startNode, nodeIndex, [nodeIndex], correctGraphNode.correction.isTypo ? .typo : .none)]
|
||||
while let (cSuffixTreeNode, cNodeIndex, cRoute, cCorrection) = stack.popLast() {
|
||||
let isUnInsertedNode = cNodeIndex == nodeIndex && cRoute.count == 1
|
||||
let bNode = if isUnInsertedNode {
|
||||
cSuffixTreeNode.find(key: correctGraphNode.value)
|
||||
} else {
|
||||
cSuffixTreeNode.find(key: self.nodes[cNodeIndex].character)
|
||||
}
|
||||
print(nodeIndex, cRoute, isUnInsertedNode ? correctGraphNode.value : self.nodes[cNodeIndex].character, bNode?.character, cSuffixTreeNode.children.count, cCorrection)
|
||||
if let bNode {
|
||||
// cNodeIndexのprevをリスト
|
||||
let indices = if isUnInsertedNode {
|
||||
if let groupId = correctGraphNode.groupId,
|
||||
let lastNodeIndex = self.nodes.lastIndex(where: {$0.groupId == groupId}) {
|
||||
self.structure.prevIndices(displayedTextStartIndex: nil, inputElementsStartIndex: correctGraphNode.inputElementsRange.startIndex)
|
||||
.union(IndexSet(integer: lastNodeIndex))
|
||||
} else {
|
||||
self.structure.prevIndices(displayedTextStartIndex: nil, inputElementsStartIndex: correctGraphNode.inputElementsRange.startIndex)
|
||||
}
|
||||
} else {
|
||||
self.prevIndices(for: self.nodes[cNodeIndex]).union(IndexSet(self.structure.allowedPrevIndex[cNodeIndex, default: []]))
|
||||
.filteredIndexSet {
|
||||
if let pEndIndex = self.nodes[$0].inputElementsRange.endIndex,
|
||||
let cStartIndex = self.nodes[cNodeIndex].inputElementsRange.startIndex {
|
||||
return pEndIndex == cStartIndex
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let nonReplacedIndices = indices.filteredIndexSet {!self.nodes[$0].isReplaced}
|
||||
|
||||
print(nodeIndex, cRoute, bNode.character, Array(indices), indices.map{(self.nodes[$0].character, self.nodes[$0].isReplaced)})
|
||||
// bNode: 1つ前のノード
|
||||
// bNodeが値を持っているか?
|
||||
if let value = bNode.value {
|
||||
// MARK: 条件A: bNodeがchildrenを持たない→longestMatchで確定なので追加して良い
|
||||
if bNode.children.isEmpty && !cCorrection.isTypo {
|
||||
let lastNode = nonReplacedIndices.first {!self.nodes[$0].correction.isTypo}.map{self.nodes[$0]}
|
||||
let inputElementsStartIndex = lastNode?.inputElementsRange.endIndex
|
||||
let displayedTextStartIndex = lastNode?.displayedTextRange.endIndex
|
||||
backSearchMatch.append(
|
||||
(displayedTextStartIndex, inputElementsStartIndex, correctGraphNode.inputElementsRange.endIndex, cRoute, value, nil, nil, cCorrection)
|
||||
)
|
||||
} else {
|
||||
// MARK: 条件B: findできないprevノードが存在する
|
||||
for prevGraphNodeIndex in nonReplacedIndices {
|
||||
if bNode.find(key: self.nodes[prevGraphNodeIndex].character) == nil {
|
||||
let inputElementsStartIndex = self.nodes[prevGraphNodeIndex].inputElementsRange.endIndex
|
||||
let displayedTextStartIndex = self.nodes[prevGraphNodeIndex].displayedTextRange.endIndex
|
||||
let licenser: Int? = if self.nodes[prevGraphNodeIndex].correction.isTypo {
|
||||
prevGraphNodeIndex
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
backSearchMatch.append(
|
||||
(displayedTextStartIndex, inputElementsStartIndex, correctGraphNode.inputElementsRange.endIndex, cRoute, value, licenser, nil, cCorrection)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if isUnInsertedNode {
|
||||
let lastNode = nonReplacedIndices.first {!self.nodes[$0].correction.isTypo}.map{self.nodes[$0]}
|
||||
let displayedTextStartIndex = lastNode?.displayedTextRange.endIndex
|
||||
backSearchMatch.append(
|
||||
(
|
||||
displayedTextStartIndex,
|
||||
correctGraphNode.inputElementsRange.startIndex,
|
||||
correctGraphNode.inputElementsRange.endIndex,
|
||||
cRoute,
|
||||
String(correctGraphNode.value),
|
||||
nil,
|
||||
groupId: correctGraphNode.groupId,
|
||||
cCorrection
|
||||
)
|
||||
)
|
||||
}
|
||||
for prevGraphNodeIndex in indices {
|
||||
var stack: [SearchItem] = [(startNode, [newIndex], foundValue: nil, correction: cgNode.correction)]
|
||||
while let (cSuffixTreeNode, cRoute, cFoundValue, cCorrection) = stack.popLast() {
|
||||
// must not be empty
|
||||
let cNodeIndex = cRoute[0]
|
||||
if let bNode = cSuffixTreeNode.find(key: self.nodes[cNodeIndex].character) {
|
||||
for prevGraphNodeIndex in self.allowedPrevIndex[cNodeIndex, default: IndexSet()] {
|
||||
// TODO: InputGraph.NodeにもInputStyle.IDを持たせてここで比較する
|
||||
stack.append(
|
||||
(
|
||||
bNode,
|
||||
prevGraphNodeIndex,
|
||||
// FIXME: 配列を生成し直しており、よくない
|
||||
[prevGraphNodeIndex] + cRoute,
|
||||
// bNodeがvalueを持っていればそれで置き換え、持っていなければ現在のものを用いる
|
||||
foundValue: bNode.value.map {Replacement(route: cRoute, value: $0)} ?? cFoundValue,
|
||||
cCorrection.isTypo ? .typo : self.nodes[prevGraphNodeIndex].correction
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 最初である場合
|
||||
if isUnInsertedNode {
|
||||
let displayedTextStartIndex: Int? = if cCorrection.isTypo {
|
||||
nil
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
backSearchMatch.append(
|
||||
(
|
||||
displayedTextStartIndex,
|
||||
correctGraphNode.inputElementsRange.startIndex,
|
||||
correctGraphNode.inputElementsRange.endIndex,
|
||||
cRoute,
|
||||
String(correctGraphNode.value),
|
||||
nil,
|
||||
correctGraphNode.groupId,
|
||||
cCorrection
|
||||
)
|
||||
)
|
||||
// bNodeが見つからない場合、発見された置換をbackSearcMatchに追加する
|
||||
if let cFoundValue {
|
||||
backSearchMatch.append((cFoundValue, cRoute))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print(backSearchMatch)
|
||||
var removeTargetIndices: IndexSet = IndexSet()
|
||||
for match in backSearchMatch {
|
||||
// licenserが存在するケースではlicenserと同じgroupIdを振る
|
||||
let licenser = match.licenserNodeIndex.map{self.nodes[$0]}
|
||||
// そうでなければ一塊で同じgroupとして追加。新規groupIdを発行
|
||||
if match.value.count > 1 {
|
||||
self.insertConnectedNodes(
|
||||
values: Array(match.value),
|
||||
inputElementsRange: .init(startIndex: match.inputElementsStartIndex, endIndex: match.inputElementsEndIndex),
|
||||
displayedTextStartIndex: match.displayedTextStartIndex,
|
||||
correction: match.correction,
|
||||
inputStyle: correctGraphNode.inputStyle
|
||||
)
|
||||
} else if match.value.count == 1 {
|
||||
let index = self.insert(
|
||||
Node(
|
||||
character: match.value.first!,
|
||||
displayedTextRange: match.displayedTextStartIndex.map{.range($0, $0 + match.value.count)} ?? .unknown,
|
||||
inputElementsRange: .init(startIndex: match.inputElementsStartIndex, endIndex: match.inputElementsEndIndex),
|
||||
groupId: match.groupId ?? licenser?.groupId,
|
||||
correction: match.correction
|
||||
)
|
||||
)
|
||||
if licenser?.groupId == nil, let licenserNodeIndex = match.licenserNodeIndex {
|
||||
self.createNewConnection(from: licenserNodeIndex, to: index)
|
||||
}
|
||||
}
|
||||
if match.correction == .none {
|
||||
for nodeIndex in match.backwardRoute.dropLast() {
|
||||
self.nodes[nodeIndex].isReplaced = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// backSearchMatchを統合する
|
||||
let replacementToTarget = Dictionary(grouping: backSearchMatch, by: \.replacement)
|
||||
for (replacement, matches) in replacementToTarget {
|
||||
// MARK: replaceを実行する
|
||||
// 1. valueをnodeとして追加する
|
||||
// 2. routeに含まれるnodeをinvalidateする
|
||||
|
||||
mutating func createNewConnection(from fromNodeIndex: Int, to toNodeIndex: Int) {
|
||||
assert(self.nodes[fromNodeIndex].groupId == nil)
|
||||
let newId = self.structure.groupIdIota.new()
|
||||
self.nodes[fromNodeIndex].groupId = newId
|
||||
self.nodes[toNodeIndex].groupId = newId
|
||||
self.structure.inputElementsStartIndexToNodeIndices.mutatingForeach { indexSet in
|
||||
indexSet.remove(toNodeIndex)
|
||||
}
|
||||
self.structure.displayedTextStartIndexToNodeIndices.mutatingForeach { indexSet in
|
||||
indexSet.remove(toNodeIndex)
|
||||
}
|
||||
self.structure.inputElementsEndIndexToNodeIndices.mutatingForeach { indexSet in
|
||||
indexSet.remove(fromNodeIndex)
|
||||
}
|
||||
self.structure.displayedTextEndIndexToNodeIndices.mutatingForeach { indexSet in
|
||||
indexSet.remove(fromNodeIndex)
|
||||
}
|
||||
self.structure.allowedNextIndex[fromNodeIndex, default: []].append(toNodeIndex)
|
||||
self.structure.allowedPrevIndex[toNodeIndex, default: []].append(fromNodeIndex)
|
||||
}
|
||||
// MARK: 新規ノードを追加
|
||||
let startIndex = self.nodes[replacement.route[0]].inputElementsRange.startIndex
|
||||
let endIndex = self.nodes[replacement.route[replacement.route.endIndex - 1]].inputElementsRange.endIndex
|
||||
|
||||
mutating func insertConnectedNodes(values: [Character], inputElementsRange: InputGraphStructure.Range, displayedTextStartIndex: Int?, correction: CorrectGraph.Correction, inputStyle: InputGraph.InputStyle.ID) {
|
||||
let id = self.structure.groupIdIota.new()
|
||||
var lastNodeIndex: Int? = nil
|
||||
for (i, c) in zip(values.indices, values) {
|
||||
let inputElementRange: InputGraphStructure.Range = if i == values.startIndex && i+1 == values.endIndex {
|
||||
.init(startIndex: inputElementsRange.startIndex, endIndex: inputElementsRange.endIndex)
|
||||
} else if i == values.startIndex {
|
||||
.init(startIndex: inputElementsRange.startIndex, endIndex: nil)
|
||||
} else if i+1 == values.endIndex {
|
||||
.init(startIndex: nil, endIndex: inputElementsRange.endIndex)
|
||||
let characters = Array(replacement.value)
|
||||
let correction: CorrectGraph2.Correction = if replacement.route.allSatisfy({!self.nodes[$0].correction.isTypo}) {
|
||||
.none
|
||||
} else {
|
||||
.unknown
|
||||
.typo
|
||||
}
|
||||
let node = Node(
|
||||
character: c,
|
||||
displayedTextRange: displayedTextStartIndex.map{.range($0+i, $0 + i+1)} ?? .unknown,
|
||||
inputElementsRange: inputElementRange,
|
||||
groupId: id,
|
||||
correction: correction
|
||||
)
|
||||
lastNodeIndex = self.insert(node, connection: lastNodeIndex.map {.prevRestriction($0)} ?? .none)
|
||||
let newNodes = characters.indices.map { index in
|
||||
let range: InputGraphRange = if index == characters.startIndex && index == characters.endIndex - 1 {
|
||||
.init(startIndex: startIndex, endIndex: endIndex)
|
||||
} else if index == characters.startIndex {
|
||||
.init(startIndex: startIndex, endIndex: nil)
|
||||
} else if index == characters.endIndex - 1 {
|
||||
.init(startIndex: nil, endIndex: endIndex)
|
||||
} else {
|
||||
.unknown
|
||||
}
|
||||
return Node(character: characters[index], inputElementsRange: range, correction: correction)
|
||||
}
|
||||
let firstIndex = self.nodes.endIndex
|
||||
let lastIndex = self.nodes.endIndex + newNodes.count - 1
|
||||
self.nodes.append(contentsOf: newNodes)
|
||||
// MARK: next/prevを調整
|
||||
// firstIndexの処理: 直前ノードとのつながりをコピーする
|
||||
// routeからreplaceされる部分を落とし、置換の直前のindexを得る
|
||||
let prevIndices = matches.compactMap { match in
|
||||
assert(match.route.hasSuffix(replacement.route))
|
||||
return match.route.dropLast(replacement.route.count).last
|
||||
}
|
||||
self.allowedPrevIndex[firstIndex] = IndexSet(prevIndices)
|
||||
for i in prevIndices {
|
||||
// firstIndexを追加してreplacementの最初を削除する
|
||||
self.allowedNextIndex[i, default: IndexSet()].insert(firstIndex)
|
||||
self.allowedNextIndex[i, default: IndexSet()].remove(replacement.route[0])
|
||||
}
|
||||
// 中央部の処理
|
||||
for i in firstIndex ..< lastIndex {
|
||||
self.allowedNextIndex[i, default: IndexSet()].insert(i + 1)
|
||||
self.allowedPrevIndex[i + 1, default: IndexSet()].insert(i)
|
||||
}
|
||||
// lastIndexの処理: correctGraphの情報を修正する
|
||||
self.nextCorrectNodeIndices[lastIndex] = correctGraph.allowedNextIndex[nodeIndex]
|
||||
}
|
||||
// 上のforループを出てからこの処理を実行する
|
||||
for replacement in replacementToTarget.keys {
|
||||
// 置換済みのノードに後ろ向きに迷い込むことを防ぐ
|
||||
self.nextCorrectNodeIndices[replacement.route.last!] = IndexSet()
|
||||
self.allowedPrevIndex[replacement.route.last!] = IndexSet()
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func clean() {
|
||||
var newGraph = Self(nodes: [])
|
||||
var indices: [(nodeIndex: Int, fromIndex: Int?)] = [(0, nil)]
|
||||
var processedNodeIndices: [Int: Int] = [:]
|
||||
while let (nodeIndex, fromIndex) = indices.popLast() {
|
||||
let newIndex = if let newIndex = processedNodeIndices[nodeIndex] {
|
||||
newIndex
|
||||
} else {
|
||||
{
|
||||
let newIndex = newGraph.nodes.endIndex
|
||||
newGraph.nodes.append(self.nodes[nodeIndex])
|
||||
newGraph.nextCorrectNodeIndices[newIndex] = self.nextCorrectNodeIndices[nodeIndex]
|
||||
return newIndex
|
||||
}()
|
||||
}
|
||||
if let fromIndex {
|
||||
newGraph.allowedNextIndex[fromIndex, default: IndexSet()].insert(newIndex)
|
||||
newGraph.allowedPrevIndex[newIndex, default: IndexSet()].insert(fromIndex)
|
||||
}
|
||||
for nextNodeIndex in self.allowedNextIndex[nodeIndex, default: IndexSet()] {
|
||||
indices.append((nextNodeIndex, newIndex))
|
||||
}
|
||||
processedNodeIndices[nodeIndex] = newIndex
|
||||
}
|
||||
self = newGraph
|
||||
}
|
||||
|
||||
static func build(input: CorrectGraph) -> Self {
|
||||
var inputGraph = Self()
|
||||
inputGraph.structure.groupIdIota = input.groupIdIota
|
||||
// 必ず、ノードより前のすべてのノードが処理済みであることを保証しながら、insertCorrectGraphNodeを実行する
|
||||
var nodeIndices = Array(input.inputElementsStartIndexToNodeIndices.first ?? .init())
|
||||
// 必ず、ノードより前のすべてのノードが処理済みであることを保証しながら、updateを実行する
|
||||
var nodeIndices = Array([0])
|
||||
var processedIndices = IndexSet()
|
||||
while let nodeIndex = nodeIndices.popLast() {
|
||||
print("build", input.nodes[nodeIndex].value)
|
||||
if processedIndices.contains(nodeIndex) {
|
||||
continue
|
||||
}
|
||||
var prevIndices = input.prevIndices(for: nodeIndex)
|
||||
if let prevIndex = input.allowedPrevIndex[nodeIndex] {
|
||||
prevIndices.insert(prevIndex)
|
||||
}
|
||||
let prevIndices = input.allowedPrevIndex[nodeIndex, default: IndexSet()]
|
||||
// 差がある場合
|
||||
let diff = prevIndices.subtracting(processedIndices)
|
||||
guard diff.isEmpty else {
|
||||
@ -517,12 +221,18 @@ struct InputGraph: InputGraphProtocol {
|
||||
continue
|
||||
}
|
||||
processedIndices.insert(nodeIndex)
|
||||
inputGraph.backwardMatches(input, nodeIndex: nodeIndex)
|
||||
nodeIndices.append(contentsOf: input.nextIndices(for: nodeIndex))
|
||||
if let nextIndex = input.allowedNextIndex[nodeIndex] {
|
||||
nodeIndices.append(nextIndex)
|
||||
// root以外
|
||||
if nodeIndex != 0 {
|
||||
inputGraph.update(input, nodeIndex: nodeIndex)
|
||||
} else {
|
||||
// nextCorrectNodeIndicesを更新しておく
|
||||
inputGraph.nextCorrectNodeIndices[0] = input.allowedNextIndex[0]
|
||||
}
|
||||
nodeIndices.append(contentsOf: input.allowedNextIndex[nodeIndex, default: IndexSet()])
|
||||
}
|
||||
|
||||
// invalidateしたnodeを削除する
|
||||
inputGraph.clean()
|
||||
return inputGraph
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,110 @@
|
||||
//
|
||||
// InputGraph.swift
|
||||
//
|
||||
//
|
||||
// Created by miwa on 2024/02/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import DequeModule
|
||||
|
||||
@testable import KanaKanjiConverterModule
|
||||
import XCTest
|
||||
|
||||
enum InputGraphRange: Equatable, Sendable {
|
||||
case unknown
|
||||
case startIndex(Int)
|
||||
case endIndex(Int)
|
||||
case range(Int, Int)
|
||||
|
||||
init(startIndex: Int?, endIndex: Int?) {
|
||||
self = switch (startIndex, endIndex) {
|
||||
case let (s?, e?): .range(s, e)
|
||||
case (let s?, nil): .startIndex(s)
|
||||
case (nil, let e?): .endIndex(e)
|
||||
case (nil, nil): .unknown
|
||||
}
|
||||
}
|
||||
|
||||
var startIndex: Int? {
|
||||
switch self {
|
||||
case .unknown, .endIndex: nil
|
||||
case .startIndex(let index), .range(let index, _): index
|
||||
}
|
||||
}
|
||||
|
||||
var endIndex: Int? {
|
||||
switch self {
|
||||
case .unknown, .startIndex: nil
|
||||
case .endIndex(let index), .range(_, let index): index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct InputGraphInputStyle: Identifiable {
|
||||
init(from deprecatedInputStyle: KanaKanjiConverterModule.InputStyle) {
|
||||
switch deprecatedInputStyle {
|
||||
case .direct:
|
||||
self = .systemFlickDirect
|
||||
case .roman2kana:
|
||||
self = .systemRomanKana
|
||||
}
|
||||
}
|
||||
|
||||
init(id: InputGraphInputStyle.ID, replacePrefixTree: ReplacePrefixTree.Node, correctPrefixTree: CorrectPrefixTree.Node) {
|
||||
self.id = id
|
||||
self.replacePrefixTree = replacePrefixTree
|
||||
self.correctPrefixTree = correctPrefixTree
|
||||
}
|
||||
|
||||
struct ID: Equatable, Hashable, Sendable, CustomStringConvertible {
|
||||
init(id: UInt8) {
|
||||
self.id = id
|
||||
}
|
||||
init(from deprecatedInputStyle: KanaKanjiConverterModule.InputStyle) {
|
||||
switch deprecatedInputStyle {
|
||||
case .direct:
|
||||
self = .systemFlickDirect
|
||||
case .roman2kana:
|
||||
self = .systemRomanKana
|
||||
}
|
||||
}
|
||||
static let all = Self(id: 0x00)
|
||||
static let systemFlickDirect = Self(id: 0x01)
|
||||
static let systemRomanKana = Self(id: 0x02)
|
||||
var id: UInt8
|
||||
|
||||
func isCompatible(with id: ID) -> Bool {
|
||||
if self == .all {
|
||||
true
|
||||
} else {
|
||||
self == id
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
"ID(\(id))"
|
||||
}
|
||||
}
|
||||
static let all: Self = Self(
|
||||
id: .all,
|
||||
replacePrefixTree: ReplacePrefixTree.Node(),
|
||||
correctPrefixTree: CorrectPrefixTree.Node()
|
||||
)
|
||||
static let systemFlickDirect: Self = Self(
|
||||
id: .systemFlickDirect,
|
||||
replacePrefixTree: ReplacePrefixTree.direct,
|
||||
correctPrefixTree: CorrectPrefixTree.direct
|
||||
)
|
||||
static let systemRomanKana: Self = Self(
|
||||
id: .systemRomanKana,
|
||||
replacePrefixTree: ReplacePrefixTree.roman2kana,
|
||||
correctPrefixTree: CorrectPrefixTree.roman2kana
|
||||
)
|
||||
|
||||
/// `id` for the input style.
|
||||
/// - warning: value `0x00-0x7F` is reserved for system space.
|
||||
var id: ID
|
||||
var replacePrefixTree: ReplacePrefixTree.Node
|
||||
var correctPrefixTree: CorrectPrefixTree.Node
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
//
|
||||
// InputGraphProtocol.swift
|
||||
//
|
||||
//
|
||||
// Created by miwa on 2024/02/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol InputGraphNodeProtocol {
|
||||
var displayedTextRange: InputGraphStructure.Range { get set }
|
||||
var inputElementsRange: InputGraphStructure.Range { get set }
|
||||
}
|
||||
|
||||
protocol InputGraphProtocol {
|
||||
associatedtype Node: InputGraphNodeProtocol
|
||||
var nodes: [Node] { get set }
|
||||
|
||||
var structure: InputGraphStructure { get set }
|
||||
}
|
||||
|
||||
extension InputGraphProtocol {
|
||||
var root: Node {
|
||||
nodes[0]
|
||||
}
|
||||
|
||||
func nextIndices(for node: Node) -> IndexSet {
|
||||
self.structure.nextIndices(
|
||||
displayedTextEndIndex: node.displayedTextRange.endIndex,
|
||||
inputElementsEndIndex: node.inputElementsRange.endIndex
|
||||
)
|
||||
}
|
||||
|
||||
func next(for node: Node) -> [Node] {
|
||||
nextIndices(for: node).map{ self.nodes[$0] }
|
||||
}
|
||||
|
||||
func prevIndices(for node: Node) -> IndexSet {
|
||||
self.structure.prevIndices(
|
||||
displayedTextStartIndex: node.displayedTextRange.startIndex,
|
||||
inputElementsStartIndex: node.inputElementsRange.startIndex
|
||||
)
|
||||
}
|
||||
|
||||
func prev(for node: Node) -> [Node] {
|
||||
prevIndices(for: node).map{ self.nodes[$0] }
|
||||
}
|
||||
|
||||
mutating func remove(at index: Int) {
|
||||
assert(index != 0, "Node at index 0 is root and must not be removed.")
|
||||
self.structure.remove(at: index)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func insert(_ node: Node, connection: InputGraphStructure.Connection = .none) -> Int {
|
||||
var nodes = self.nodes
|
||||
let index = self.structure.insert(node, nodes: &nodes, displayedTextRange: node.displayedTextRange, inputElementsRange: node.inputElementsRange, connection: connection)
|
||||
self.nodes = nodes
|
||||
return index
|
||||
}
|
||||
}
|
@ -12,16 +12,6 @@ import XCTest
|
||||
|
||||
|
||||
final class InputGraphTests: XCTestCase {
|
||||
func testInsert() throws {
|
||||
var graph = InputGraph()
|
||||
let node1 = InputGraph.Node(character: "a", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1))
|
||||
let node2 = InputGraph.Node(character: "b", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 2))
|
||||
graph.insert(node1)
|
||||
graph.insert(node2)
|
||||
XCTAssertEqual(graph.next(for: node1), [node2])
|
||||
XCTAssertEqual(graph.prev(for: node2), [node1])
|
||||
}
|
||||
|
||||
func testBuildSimpleDirectInput() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "あ", inputStyle: .direct),
|
||||
@ -31,7 +21,7 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(inputGraph.nodes.count, 4) // Root nodes
|
||||
}
|
||||
func testBuildSimpleDirectInput_typoあり() throws {
|
||||
func testBuildSimpleDirectInput_あかう() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "あ", inputStyle: .direct),
|
||||
.init(character: "か", inputStyle: .direct),
|
||||
@ -40,6 +30,17 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(inputGraph.nodes.count, 5) // Root nodes
|
||||
}
|
||||
|
||||
func testBuildSimpleDirectInput_たいか() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "た", inputStyle: .direct),
|
||||
.init(character: "い", inputStyle: .direct),
|
||||
.init(character: "か", inputStyle: .direct)
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(inputGraph.nodes.count, 5) // Root nodes
|
||||
}
|
||||
|
||||
func testBuildSimpleRoman2KanaInput_1文字だけ() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
@ -47,12 +48,8 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "i"})
|
||||
)
|
||||
XCTAssertEqual(inputGraph.nodes.count, 2) // Root nodes
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_2文字_it() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
@ -62,13 +59,12 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t"}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 2), correction: .none)
|
||||
.init(character: "t", inputElementsRange: .range(1, 2), correction: .none)
|
||||
)
|
||||
print(inputGraph)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_3文字_ita() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
@ -79,17 +75,12 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t"}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 2), correction: .none, isReplaced: true)
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 3), correction: .none)
|
||||
.init(character: "た", inputElementsRange: .range(1, 3), correction: .none)
|
||||
)
|
||||
print(inputGraph)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_4文字_sits() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
@ -100,19 +91,17 @@ final class InputGraphTests: XCTestCase {
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "し" && $0.displayedTextRange == .range(0, 1)}),
|
||||
.init(character: "し", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 2), correction: .none)
|
||||
inputGraph.nodes.first(where: {$0.character == "し"}),
|
||||
.init(character: "し", inputElementsRange: .range(0, 2), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "s"}),
|
||||
.init(character: "s", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none, isReplaced: true)
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && !$0.correction.isTypo}),
|
||||
.init(character: "t", inputElementsRange: .range(2, 3), correction: .none)
|
||||
)
|
||||
// [s]のノードを消していないため、displayedTextIndex側で拾ってしまってエラー
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(2, 4), correction: .typo)
|
||||
.init(character: "た", inputElementsRange: .range(2, 4), correction: .typo)
|
||||
)
|
||||
print(inputGraph)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_3文字_its() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
@ -123,25 +112,24 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .range(1, 2)}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 2), correction: .none)
|
||||
.init(character: "t", inputElementsRange: .range(1, 2), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "s"}),
|
||||
.init(character: "s", displayedTextRange: .range(2, 3), inputElementsRange: .range(2, 3), correction: .none)
|
||||
.init(character: "s", inputElementsRange: .range(2, 3), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .startIndex(1)}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .startIndex(1), groupId: 0, correction: .typo)
|
||||
// 消える
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .startIndex(1)})
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 3), correction: .typo)
|
||||
.init(character: "た", inputElementsRange: .range(1, 3), correction: .typo)
|
||||
)
|
||||
print(inputGraph)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_4文字_itsa() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
@ -153,36 +141,33 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .range(1, 2)}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 2), correction: .none, isReplaced: true)
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .range(1, 2)})
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "s"}),
|
||||
.init(character: "s", displayedTextRange: .range(2, 3), inputElementsRange: .range(2, 3), correction: .none, isReplaced: true)
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "s"})
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .startIndex(1)}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .startIndex(1), groupId: 0, correction: .typo)
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .startIndex(1)})
|
||||
)
|
||||
// groupIdの制約により、「た→あ」のみが許される遷移になる
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 3), groupId: 1, correction: .typo)
|
||||
.init(character: "た", inputElementsRange: .range(1, 3), correction: .typo)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "あ"}),
|
||||
.init(character: "あ", displayedTextRange: .range(2, 3), inputElementsRange: .range(3, 4), groupId: 1, correction: .none)
|
||||
.init(character: "あ", inputElementsRange: .range(3, 4), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "つ"}),
|
||||
.init(character: "つ", displayedTextRange: .range(1, 2), inputElementsRange: .startIndex(1), groupId: 2, correction: .none)
|
||||
.init(character: "つ", inputElementsRange: .startIndex(1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "ぁ"}),
|
||||
.init(character: "ぁ", displayedTextRange: .range(2, 3), inputElementsRange: .endIndex(4), groupId: 2, correction: .none)
|
||||
.init(character: "ぁ", inputElementsRange: .endIndex(4), correction: .none)
|
||||
)
|
||||
// 「さ」の生成は許されない
|
||||
XCTAssertNil(inputGraph.nodes.first(where: {$0.character == "さ"}))
|
||||
@ -201,28 +186,149 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "よ"}),
|
||||
.init(character: "よ", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 2), correction: .none)
|
||||
.init(character: "よ", inputElementsRange: .range(0, 2), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "う" && $0.displayedTextRange.startIndex == 1}),
|
||||
.init(character: "う", displayedTextRange: .range(1, 2), inputElementsRange: .range(2, 3), correction: .none)
|
||||
inputGraph.nodes.first(where: {$0.character == "う" && $0.inputElementsRange == .range(2, 3)}),
|
||||
.init(character: "う", inputElementsRange: .range(2, 3), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "し"}),
|
||||
.init(character: "し", displayedTextRange: .range(2, 3), inputElementsRange: .startIndex(3), groupId: 0, correction: .none)
|
||||
.init(character: "し", inputElementsRange: .startIndex(3), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "ょ"}),
|
||||
.init(character: "ょ", displayedTextRange: .range(3, 4), inputElementsRange: .endIndex(6), groupId: 0,
|
||||
correction: .none)
|
||||
.init(character: "ょ", inputElementsRange: .endIndex(6), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "う" && $0.displayedTextRange.startIndex == 4}),
|
||||
.init(character: "う", displayedTextRange: .range(4, 5), inputElementsRange: .range(6, 7), correction: .none)
|
||||
inputGraph.nodes.first(where: {$0.character == "う" && $0.inputElementsRange == .range(6, 7)}),
|
||||
.init(character: "う", inputElementsRange: .range(6, 7), correction: .none)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func testBuildSimpleRoman2KanaInput_2文字_tt() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", inputElementsRange: .startIndex(0), correction: .none)
|
||||
)
|
||||
XCTAssertNil(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .range(0, 1)})
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.inputElementsRange == .endIndex(2)}),
|
||||
.init(character: "t", inputElementsRange: .endIndex(2), correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_3文字_tta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", inputElementsRange: .startIndex(0), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", inputElementsRange: .endIndex(3), correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_3文字_nta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "n", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "ん"}),
|
||||
.init(character: "ん", inputElementsRange: .startIndex(0), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", inputElementsRange: .endIndex(3), correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildSimpleRoman2KanaInput_4文字_itta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", inputElementsRange: .startIndex(1), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", inputElementsRange: .endIndex(4), correction: .none)
|
||||
)
|
||||
}
|
||||
|
||||
func testBuildSimpleRoman2KanaInput_5文字_sitsi() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "し"}),
|
||||
.init(character: "し", inputElementsRange: .range(0, 2), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", inputElementsRange: .range(2, 4), correction: .typo)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", inputElementsRange: .range(4, 5), correction: .none)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func testBuildSimpleRoman2KanaInput_3文字_tts() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ" && $0.correction == .none}),
|
||||
.init(character: "っ", inputElementsRange: .startIndex(0), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ" && $0.correction == .typo}),
|
||||
.init(character: "っ", inputElementsRange: .startIndex(0), correction: .typo)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "s"}),
|
||||
.init(character: "s", inputElementsRange: .range(2, 3), correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", inputElementsRange: .endIndex(3), correction: .typo)
|
||||
)
|
||||
print(inputGraph)
|
||||
}
|
||||
|
||||
func testBuildMixedInput_2文字_ts() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
@ -231,121 +337,8 @@ final class InputGraphTests: XCTestCase {
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t"}),
|
||||
.init(character: "t", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), correction: .none)
|
||||
.init(character: "t", inputElementsRange: .range(0, 1), correction: .none)
|
||||
)
|
||||
XCTAssertFalse(inputGraph.nodes.contains(.init(character: "た", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 2), correction: .typo)))
|
||||
XCTAssertFalse(inputGraph.nodes.contains(.init(character: "た", inputElementsRange: .range(0, 2), correction: .typo)))
|
||||
}
|
||||
func testBuildMixedInput_2文字_tt() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), groupId: 0, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "t" && $0.groupId != nil}),
|
||||
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(2), groupId: 0, correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildMixedInput_3文字_tta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), groupId: 0, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
// FIXME: 「た」のgroupIdは0だと嬉しい
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(3), groupId: nil, correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildMixedInput_3文字_nta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "n", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "ん"}),
|
||||
.init(character: "ん", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), groupId: 0, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(3), groupId: nil, correction: .none)
|
||||
)
|
||||
}
|
||||
func testBuildMixedInput_4文字_itta() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "a", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 1), groupId: nil, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ"}),
|
||||
.init(character: "っ", displayedTextRange: .range(1, 2), inputElementsRange: .startIndex(1), groupId: 0, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
// FIXME: 「た」のgroupIdは0だと嬉しい
|
||||
.init(character: "た", displayedTextRange: .range(2, 3), inputElementsRange: .endIndex(4), groupId: nil, correction: .none)
|
||||
)
|
||||
}
|
||||
|
||||
func testBuildMixedInput_5文字_sitsi() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
.init(character: "i", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "し"}),
|
||||
.init(character: "し", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 2), groupId: nil, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(2, 4), groupId: 1, correction: .typo)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "い"}),
|
||||
.init(character: "い", displayedTextRange: .range(2, 3), inputElementsRange: .range(4, 5), groupId: 1, correction: .none)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func testBuildMixedInput_3文字_tts() throws {
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "t", inputStyle: .roman2kana),
|
||||
.init(character: "s", inputStyle: .roman2kana),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "っ" && $0.correction == .none}),
|
||||
.init(character: "っ", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), groupId: 2, correction: .none)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inputGraph.nodes.first(where: {$0.character == "た"}),
|
||||
// FIXME: 「た」のgroupIdは0だと嬉しい
|
||||
.init(character: "た", displayedTextRange: .range(1, 2), inputElementsRange: .range(1, 3), groupId: nil, correction: .typo)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,37 +9,38 @@ import XCTest
|
||||
import Foundation
|
||||
@testable import KanaKanjiConverterModule
|
||||
|
||||
struct LookupGraph: InputGraphProtocol {
|
||||
struct Node: Equatable, InputGraphNodeProtocol {
|
||||
struct LookupGraph {
|
||||
struct Node: Equatable {
|
||||
var character: Character
|
||||
var charId: UInt8
|
||||
var loudsNodeIndices: Set<Int> = []
|
||||
var displayedTextRange: InputGraphStructure.Range
|
||||
var inputElementsRange: InputGraphStructure.Range
|
||||
var correction: CorrectGraph.Correction = .none
|
||||
var inputElementsRange: InputGraphRange
|
||||
var correction: CorrectGraph2.Correction = .none
|
||||
}
|
||||
|
||||
var nodes: [Node] = [
|
||||
// root node
|
||||
Node(character: "\0", charId: 0x00, displayedTextRange: .endIndex(0), inputElementsRange: .endIndex(0))
|
||||
Node(character: "\0", charId: 0x00, inputElementsRange: .endIndex(0))
|
||||
]
|
||||
|
||||
var structure: InputGraphStructure = InputGraphStructure()
|
||||
/// 許可されたNextIndex
|
||||
var allowedNextIndex: [Int: IndexSet] = [:]
|
||||
/// 許可されたprevIndex
|
||||
var allowedPrevIndex: [Int: IndexSet] = [:]
|
||||
|
||||
static func build(input: InputGraph, character2CharId: (Character) -> UInt8) -> Self {
|
||||
let nodes = input.nodes.map {
|
||||
Node(character: $0.character, charId: character2CharId($0.character), displayedTextRange: $0.displayedTextRange, inputElementsRange: $0.inputElementsRange, correction: $0.correction)
|
||||
Node(character: $0.character, charId: character2CharId($0.character), inputElementsRange: $0.inputElementsRange, correction: $0.correction)
|
||||
}
|
||||
return Self(nodes: nodes, structure: input.structure)
|
||||
return Self(nodes: nodes, allowedNextIndex: input.allowedNextIndex, allowedPrevIndex: input.allowedPrevIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension LOUDS {
|
||||
func byfixNodeIndices(_ lookupGraph: LookupGraph, startGraphNodeIndex: Int = 0) -> (IndexSet, [Int: [(displayedTextEndIndex: Int?, inputElementsEndIndex: Int?)]]) {
|
||||
func byfixNodeIndices(_ lookupGraph: LookupGraph, startGraphNodeIndex: Int = 0) -> (IndexSet, [Int: [Int?]]) {
|
||||
var indexSet = IndexSet(integer: 1)
|
||||
// loudsのノードとLookupGraphのノードの対応を取るための辞書
|
||||
var loudsNodeIndex2GraphNodeEndIndices: [Int: [(displayedTextEndIndex: Int?, inputElementsEndIndex: Int?)]] = [:]
|
||||
var loudsNodeIndex2GraphNodeEndIndices: [Int: [Int?]] = [:]
|
||||
typealias SearchItem = (
|
||||
nodeIndex: Int,
|
||||
lastLoudsNodeIndex: Int
|
||||
@ -49,20 +50,13 @@ extension LOUDS {
|
||||
let cNode = lookupGraph.nodes[cNodeIndex]
|
||||
// nextNodesを探索
|
||||
if let loudsNodeIndex = self.searchCharNodeIndex(from: cLastLoudsNodeIndex, char: cNode.charId) {
|
||||
loudsNodeIndex2GraphNodeEndIndices[loudsNodeIndex, default: []].append((cNode.displayedTextRange.endIndex, cNode.inputElementsRange.endIndex))
|
||||
loudsNodeIndex2GraphNodeEndIndices[loudsNodeIndex, default: []].append(cNode.inputElementsRange.endIndex)
|
||||
indexSet.insert(loudsNodeIndex)
|
||||
var nextIndices = lookupGraph.nextIndices(for: cNode)
|
||||
nextIndices.formUnion(IndexSet(lookupGraph.structure.allowedNextIndex[cNodeIndex, default: []]))
|
||||
let nextIndices = lookupGraph.allowedNextIndex[cNodeIndex, default: IndexSet()]
|
||||
stack.append(contentsOf: nextIndices.compactMap { index in
|
||||
let node = lookupGraph.nodes[index]
|
||||
// endIndexをチェックする
|
||||
// endIndexは単調増加である必要がある
|
||||
if let cDisplayedTextEndIndex = cNode.displayedTextRange.endIndex,
|
||||
let nDisplayedTextEndIndex = node.displayedTextRange.endIndex {
|
||||
guard cDisplayedTextEndIndex < nDisplayedTextEndIndex else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if let cInputElementsEndIndex = cNode.inputElementsRange.endIndex,
|
||||
let nInputElementsEndIndex = node.inputElementsRange.endIndex {
|
||||
guard cInputElementsEndIndex < nInputElementsEndIndex else {
|
||||
@ -77,12 +71,13 @@ extension LOUDS {
|
||||
}
|
||||
return (indexSet, loudsNodeIndex2GraphNodeEndIndices)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DicdataStore {
|
||||
func buildConvertGraph(inputGraph: consuming InputGraph, option: ConvertRequestOptions) -> ConvertGraph {
|
||||
let lookupGraph = LookupGraph.build(input: consume inputGraph, character2CharId: { self.character2charId($0.toKatakana()) } )
|
||||
var stack: [Int] = Array(lookupGraph.nextIndices(for: lookupGraph.root) + lookupGraph.structure.allowedNextIndex[0, default: []])
|
||||
var stack = Array(lookupGraph.allowedNextIndex[0, default: []])
|
||||
var graphNodeIndex2LatticeNodes: [Int: [ConvertGraph.LatticeNode]] = [:]
|
||||
while let graphNodeIndex = stack.popLast() {
|
||||
let graphNode = lookupGraph.nodes[graphNodeIndex]
|
||||
@ -94,22 +89,20 @@ extension DicdataStore {
|
||||
var latticeNodes: [ConvertGraph.LatticeNode] = []
|
||||
for (loudsNodeIndex, dicdata) in dicdataWithIndex {
|
||||
for endIndex in loudsNodeIndex2GraphEndIndices[loudsNodeIndex, default: []] {
|
||||
let displayedTextRange = InputGraphStructure.Range(startIndex: graphNode.displayedTextRange.startIndex, endIndex: endIndex.displayedTextEndIndex)
|
||||
let inputElementsRange = InputGraphStructure.Range(startIndex: graphNode.inputElementsRange.startIndex, endIndex: endIndex.inputElementsEndIndex)
|
||||
if graphNode.displayedTextRange.startIndex == 0 || graphNode.inputElementsRange.startIndex == 0 {
|
||||
let inputElementsRange = InputGraphRange(startIndex: graphNode.inputElementsRange.startIndex, endIndex: endIndex)
|
||||
if graphNode.inputElementsRange.startIndex == 0 {
|
||||
latticeNodes.append(contentsOf: dicdata.map {
|
||||
.init(data: $0, displayedTextRange: displayedTextRange, inputElementsRange: inputElementsRange, prevs: [.BOSNode()])
|
||||
.init(data: $0, nextConvertNodeIndices: lookupGraph.allowedNextIndex[graphNodeIndex, default: []], inputElementsRange: inputElementsRange, prevs: [.BOSNode()])
|
||||
})
|
||||
} else {
|
||||
latticeNodes.append(contentsOf: dicdata.map {
|
||||
.init(data: $0, displayedTextRange: displayedTextRange, inputElementsRange: inputElementsRange)
|
||||
.init(data: $0, nextConvertNodeIndices: lookupGraph.allowedNextIndex[graphNodeIndex, default: []], inputElementsRange: inputElementsRange)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
graphNodeIndex2LatticeNodes[graphNodeIndex] = latticeNodes
|
||||
stack.append(contentsOf: lookupGraph.nextIndices(for: graphNode))
|
||||
stack.append(contentsOf: lookupGraph.structure.allowedNextIndex[graphNodeIndex, default: []])
|
||||
stack.append(contentsOf: lookupGraph.allowedNextIndex[graphNodeIndex, default: []])
|
||||
}
|
||||
return ConvertGraph.build(input: consume lookupGraph, nodeIndex2LatticeNode: graphNodeIndex2LatticeNodes)
|
||||
}
|
||||
@ -151,7 +144,7 @@ final class LookupGraphTests: XCTestCase {
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
let lookupGraph = LookupGraph.build(input: inputGraph, character2CharId: values.character2CharId)
|
||||
let startNodeIndex = lookupGraph.nextIndices(for: lookupGraph.root).first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
let startNodeIndex = lookupGraph.allowedNextIndex[0, default: IndexSet()].first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
XCTAssertNotNil(startNodeIndex)
|
||||
let (loudsNodeIndices, _) = louds.byfixNodeIndices(lookupGraph, startGraphNodeIndex: startNodeIndex ?? 0)
|
||||
let dicdataWithIndex = values.dicdataStore.getDicdataFromLoudstxt3(identifier: "シ", indices: loudsNodeIndices, option: requestOptions())
|
||||
@ -179,6 +172,38 @@ final class LookupGraphTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testByfixNodeIndices_みらい() throws {
|
||||
let values = setup()
|
||||
guard let louds = LOUDS.load("ミ", option: requestOptions()) else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
let correctGraph = CorrectGraph.build(input: [
|
||||
.init(character: "み", inputStyle: .direct),
|
||||
.init(character: "ら", inputStyle: .direct),
|
||||
.init(character: "い", inputStyle: .direct),
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
let lookupGraph = LookupGraph.build(input: inputGraph, character2CharId: values.character2CharId)
|
||||
let startNodeIndex = lookupGraph.allowedNextIndex[0, default: IndexSet()].first(where: { lookupGraph.nodes[$0].character == "み" })
|
||||
XCTAssertNotNil(startNodeIndex)
|
||||
let (loudsNodeIndices, _) = louds.byfixNodeIndices(lookupGraph, startGraphNodeIndex: startNodeIndex ?? 0)
|
||||
let dicdataWithIndex = values.dicdataStore.getDicdataFromLoudstxt3(identifier: "ミ", indices: loudsNodeIndices, option: requestOptions())
|
||||
let dicdata = dicdataWithIndex.flatMapSet { $0.dicdata }
|
||||
// ミ
|
||||
XCTAssertTrue(dicdata.contains {$0.word == "見"})
|
||||
// ミラ
|
||||
XCTAssertTrue(dicdata.contains {$0.word == "ミラ"})
|
||||
// ミライ
|
||||
XCTAssertTrue(dicdata.contains {$0.word == "未来"})
|
||||
|
||||
// all keys
|
||||
XCTAssertEqual(
|
||||
dicdata.mapSet {$0.ruby}.symmetricDifference(["ミ", "ミラ", "ミライ"]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testByfixNodeIndices_たいかく() throws {
|
||||
let values = setup()
|
||||
guard let louds = LOUDS.load("タ", option: requestOptions()) else {
|
||||
@ -193,7 +218,7 @@ final class LookupGraphTests: XCTestCase {
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
let lookupGraph = LookupGraph.build(input: inputGraph, character2CharId: values.character2CharId)
|
||||
let startNodeIndex = lookupGraph.nextIndices(for: lookupGraph.root).first(where: { lookupGraph.nodes[$0].character == "た" })
|
||||
let startNodeIndex = lookupGraph.allowedNextIndex[0, default: IndexSet()].first(where: { lookupGraph.nodes[$0].character == "た" })
|
||||
XCTAssertNotNil(startNodeIndex)
|
||||
let (loudsNodeIndices, _) = louds.byfixNodeIndices(lookupGraph, startGraphNodeIndex: startNodeIndex ?? 0)
|
||||
let dicdataWithIndex = values.dicdataStore.getDicdataFromLoudstxt3(identifier: "タ", indices: loudsNodeIndices, option: requestOptions())
|
||||
@ -235,7 +260,7 @@ final class LookupGraphTests: XCTestCase {
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
let lookupGraph = LookupGraph.build(input: inputGraph, character2CharId: values.character2CharId)
|
||||
let startNodeIndex = lookupGraph.nextIndices(for: lookupGraph.root).first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
let startNodeIndex = lookupGraph.allowedNextIndex[0, default: IndexSet()].first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
XCTAssertNotNil(startNodeIndex)
|
||||
let (loudsNodeIndices, _) = louds.byfixNodeIndices(lookupGraph, startGraphNodeIndex: startNodeIndex ?? 0)
|
||||
let dicdataWithIndex = values.dicdataStore.getDicdataFromLoudstxt3(identifier: "シ", indices: loudsNodeIndices, option: requestOptions())
|
||||
@ -272,7 +297,7 @@ final class LookupGraphTests: XCTestCase {
|
||||
])
|
||||
let inputGraph = InputGraph.build(input: correctGraph)
|
||||
let lookupGraph = LookupGraph.build(input: inputGraph, character2CharId: values.character2CharId)
|
||||
let startNodeIndex = lookupGraph.nextIndices(for: lookupGraph.root).first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
let startNodeIndex = lookupGraph.allowedNextIndex[0, default: IndexSet()].first(where: { lookupGraph.nodes[$0].character == "し" })
|
||||
XCTAssertNotNil(startNodeIndex)
|
||||
let (loudsNodeIndices, _) = louds.byfixNodeIndices(lookupGraph, startGraphNodeIndex: startNodeIndex ?? 0)
|
||||
let dicdataWithIndex = values.dicdataStore.getDicdataFromLoudstxt3(identifier: "シ", indices: loudsNodeIndices, option: requestOptions())
|
||||
|
@ -12,7 +12,7 @@ import XCTest
|
||||
|
||||
// 置換のためのprefix tree
|
||||
enum ReplacePrefixTree {
|
||||
static var characterNodes: [InputGraph.InputStyle.ID: [Character: [Node]]] = [:]
|
||||
static var characterNodes: [InputGraphInputStyle.ID: [Character: [Node]]] = [:]
|
||||
|
||||
final class Node {
|
||||
init(_ children: [Character: Node] = [:], character: Character = "\0", value: String? = nil, parent: Node? = nil) {
|
||||
@ -28,7 +28,7 @@ enum ReplacePrefixTree {
|
||||
func find(key: Character) -> Node? {
|
||||
return children[key]
|
||||
}
|
||||
func insert(route: some Collection<Character>, value: consuming String, inputStyle: InputGraph.InputStyle.ID) {
|
||||
func insert(route: some Collection<Character>, value: consuming String, inputStyle: InputGraphInputStyle.ID) {
|
||||
if let first = route.first {
|
||||
if let tree = self.children[first] {
|
||||
tree.insert(route: route.dropFirst(), value: consume value, inputStyle: inputStyle)
|
||||
@ -77,7 +77,7 @@ enum ReplaceSuffixTree {
|
||||
func find(key: Character) -> Node? {
|
||||
return children[key]
|
||||
}
|
||||
func insert(route: some Collection<Character>, value: consuming String, inputStyle: InputGraph.InputStyle.ID) {
|
||||
func insert(route: some Collection<Character>, value: consuming String, inputStyle: InputGraphInputStyle.ID) {
|
||||
if let first = route.first {
|
||||
if let tree = self.children[first] {
|
||||
tree.insert(route: route.dropFirst(), value: consume value, inputStyle: inputStyle)
|
||||
|
@ -62,6 +62,39 @@ final class ExperimentalConversionTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testConversion_たい() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
var c = ComposingText()
|
||||
c.insertAtCursorPosition("たい", inputStyle: .direct)
|
||||
let result = kana2kanji._experimental_all(c, option: requestOptions())
|
||||
XCTAssertTrue(result.joinedPrevs().contains("タイ")) // たい
|
||||
XCTAssertTrue(result.joinedPrevs().contains("台")) // たい
|
||||
}
|
||||
|
||||
func testConversion_いか() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
var c = ComposingText()
|
||||
c.insertAtCursorPosition("いか", inputStyle: .direct)
|
||||
let result = kana2kanji._experimental_all(c, option: requestOptions())
|
||||
XCTAssertTrue(result.joinedPrevs().contains("以下")) // いか
|
||||
XCTAssertTrue(result.joinedPrevs().contains("伊賀")) // いが
|
||||
print(result.joinedPrevs())
|
||||
}
|
||||
|
||||
func testConversion_たいか() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
var c = ComposingText()
|
||||
c.insertAtCursorPosition("たいか", inputStyle: .direct)
|
||||
let result = kana2kanji._experimental_all(c, option: requestOptions())
|
||||
XCTAssertTrue(result.joinedPrevs().contains("対価")) // たいか
|
||||
XCTAssertTrue(result.joinedPrevs().contains("大河")) // たいが
|
||||
// FIXME: 「たいいか」が入っている
|
||||
print(result.joinedPrevs())
|
||||
}
|
||||
|
||||
func testConversion_たいかく() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
@ -106,7 +139,7 @@ final class ExperimentalConversionTests: XCTestCase {
|
||||
XCTAssertTrue(result.joinedPrevs().contains("幼少期")) // ようしょうき
|
||||
}
|
||||
|
||||
func testConversion() throws {
|
||||
func testConversion_みらいえいが() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
do {
|
||||
@ -121,6 +154,11 @@ final class ExperimentalConversionTests: XCTestCase {
|
||||
let result = kana2kanji._experimental_all(c, option: requestOptions())
|
||||
XCTAssertTrue(result.joinedPrevs().contains("未来映画"))
|
||||
}
|
||||
}
|
||||
|
||||
func testConversion() throws {
|
||||
let dicdataStore = DicdataStore(requestOptions: requestOptions())
|
||||
let kana2kanji = Kana2Kanji(dicdataStore: dicdataStore)
|
||||
do {
|
||||
var c = ComposingText()
|
||||
c.insertAtCursorPosition("sitta", inputStyle: .roman2kana)
|
||||
|
Reference in New Issue
Block a user