[Experimental] InputGraphベースの辞書ルックアップ (#45)

This commit is contained in:
Miwa / Ensan
2024-02-23 02:37:59 +09:00
committed by GitHub
parent 5431bafb27
commit 784a80c46a
3 changed files with 418 additions and 48 deletions

View File

@ -181,7 +181,7 @@ public final class DicdataStore {
return louds.prefixNodeIndices(chars: charIDs, maxDepth: depth)
}
private func getDicdataFromLoudstxt3(identifier: String, indices: Set<Int>) -> [DicdataElement] {
func getDicdataFromLoudstxt3(identifier: String, indices: some Sequence<Int>) -> [DicdataElement] {
debug("getDicdataFromLoudstxt3", identifier, indices)
// split = 2048
let dict = [Int: [Int]].init(grouping: indices, by: {$0 >> 11})

View File

@ -12,29 +12,31 @@ import XCTest
// prefix tree
enum ReplacePrefixTree {
static var characterNodes: [InputGraph.InputStyle.ID: [Character: [Node]]] = [:]
final class Node {
init(_ children: [Character: Node] = [:], value: String? = nil) {
init(_ children: [Character: Node] = [:], character: Character = "\0", value: String? = nil, parent: Node? = nil) {
self.children = children
self.value = value
self.character = character
self.parent = parent
}
static func terminal(_ value: String) -> Node {
Node(value: value)
}
var parent: Node?
var children: [Character: Node] = [:]
var character: Character
var value: String?
func find(key: Character) -> Node? {
return children[key]
}
func insert(route: some Collection<Character>, value: consuming String) {
func insert(route: some Collection<Character>, value: consuming String, inputStyle: InputGraph.InputStyle.ID) {
if let first = route.first {
if let tree = self.children[first] {
tree.insert(route: route.dropFirst(), value: consume value)
tree.insert(route: route.dropFirst(), value: consume value, inputStyle: inputStyle)
} else {
let tree = Node()
tree.insert(route: route.dropFirst(), value: consume value)
let tree = Node(character: first, parent: self)
tree.insert(route: route.dropFirst(), value: consume value, inputStyle: inputStyle)
self.children[first] = tree
ReplacePrefixTree.characterNodes[inputStyle, default: [:]][first, default: []].append(tree)
}
} else {
self.value = consume value
@ -45,7 +47,15 @@ enum ReplacePrefixTree {
static let roman2kana: Node = {
var tree = Node()
for item in KanaKanjiConverterModule.Roman2Kana.hiraganaChanges {
tree.insert(route: item.key, value: String(item.value))
tree.insert(route: item.key, value: String(item.value), inputStyle: .systemRomanKana)
}
// additionals
for item in ["bb", "cc", "dd", "ff", "gg", "hh", "jj", "kk", "ll", "mm", "pp", "qq", "rr", "ss", "tt", "vv", "ww", "xx", "yy", "zz"] {
tree.insert(route: Array(item), value: "" + String(item.last!), inputStyle: .systemRomanKana)
}
// additionals
for item in ["nb", "nc", "nd", "nf", "ng", "nh", "nj", "nk", "nl", "nm", "np", "nq", "nr", "ns", "nt", "nv", "nw", "nx", "nz"] {
tree.insert(route: Array(item), value: "" + String(item.last!), inputStyle: .systemRomanKana)
}
return tree
}()
@ -125,7 +135,7 @@ struct InputGraph {
self.correctPrefixTree = correctPrefixTree
}
struct ID: Equatable, Hashable, Sendable {
struct ID: Equatable, Hashable, Sendable, CustomStringConvertible {
init(id: UInt8) {
self.id = id
}
@ -149,6 +159,9 @@ struct InputGraph {
self == id
}
}
var description: String {
"ID(\(id))"
}
}
static let all: Self = InputStyle(
id: .all,
@ -194,7 +207,7 @@ struct InputGraph {
}
}
enum Correction {
enum Correction: CustomStringConvertible {
///
case none
///
@ -203,6 +216,13 @@ struct InputGraph {
var isTypo: Bool {
self == .typo
}
var description: String {
switch self {
case .none: "none"
case .typo: "typo"
}
}
}
struct Node: Equatable, CustomStringConvertible {
@ -229,24 +249,78 @@ struct InputGraph {
var inputElementsStartIndexToNodeIndices: [IndexSet] = []
var displayedTextEndIndexToNodeIndices: [IndexSet] = [IndexSet(integer: 0)] // rootindex
var inputElementsEndIndexToNodeIndices: [IndexSet] = [IndexSet(integer: 0)] // rootindex
// 使
var deadNodeIndices: [Int] = []
var root: Node {
nodes[0]
}
func next(for node: Node) -> [Node] {
var indexSet = IndexSet()
switch node.displayedTextRange {
case .unknown, .startIndex: break
case .endIndex(let endIndex), .range(_, let endIndex):
indexSet.formUnion(self.displayedTextStartIndexToNodeIndices[endIndex])
if let endIndex = node.displayedTextRange.endIndex {
if endIndex < self.displayedTextStartIndexToNodeIndices.endIndex {
indexSet.formUnion(self.displayedTextStartIndexToNodeIndices[endIndex])
}
}
switch node.inputElementsRange {
case .unknown, .startIndex: break
case .endIndex(let endIndex), .range(_, let endIndex):
indexSet.formUnion(self.inputElementsStartIndexToNodeIndices[endIndex])
if let endIndex = node.inputElementsRange.endIndex {
if endIndex < self.inputElementsStartIndexToNodeIndices.endIndex {
indexSet.formUnion(self.inputElementsStartIndexToNodeIndices[endIndex])
}
}
return indexSet.map{ self.nodes[$0] }
}
func prevIndices(for node: Node) -> IndexSet {
var indexSet = IndexSet()
if let startIndex = node.displayedTextRange.startIndex {
if startIndex < self.displayedTextEndIndexToNodeIndices.endIndex {
indexSet.formUnion(self.displayedTextEndIndexToNodeIndices[startIndex])
}
}
if let startIndex = node.inputElementsRange.startIndex {
if startIndex < self.inputElementsEndIndexToNodeIndices.endIndex {
indexSet.formUnion(self.inputElementsEndIndexToNodeIndices[startIndex])
}
}
return indexSet
}
func prev(for node: Node) -> [Node] {
prevIndices(for: node).map{ self.nodes[$0] }
}
private mutating func _insert(_ node: Node) -> Int {
// deadNodeIndices
if let deadIndex = self.deadNodeIndices.popLast() {
self.nodes[deadIndex] = node
return deadIndex
} else {
self.nodes.append(node)
return self.nodes.count - 1
}
}
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.displayedTextStartIndexToNodeIndices.mutatingForeach {
$0.remove(index)
}
self.displayedTextEndIndexToNodeIndices.mutatingForeach {
$0.remove(index)
}
self.inputElementsStartIndexToNodeIndices.mutatingForeach {
$0.remove(index)
}
self.inputElementsEndIndexToNodeIndices.mutatingForeach {
$0.remove(index)
}
}
mutating func insert(_ node: Node) {
let index = self.nodes.count
let index = self._insert(node)
if let startIndex = node.displayedTextRange.startIndex {
if self.displayedTextStartIndexToNodeIndices.endIndex <= startIndex {
self.displayedTextStartIndexToNodeIndices.append(contentsOf: Array(repeating: IndexSet(), count: startIndex - self.displayedTextStartIndexToNodeIndices.endIndex + 1))
@ -271,7 +345,6 @@ struct InputGraph {
}
self.inputElementsEndIndexToNodeIndices[endIndex].insert(index)
}
self.nodes.append(node)
}
// EOS
@ -332,10 +405,75 @@ struct InputGraph {
}
// replaceRule
for (index, item) in zip(input.indices, input) {
guard let beforeNodeIndex = inputGraph.inputElementsEndIndexToNodeIndices[index].first,
let displayedTextStartIndex = inputGraph.nodes[beforeNodeIndex].displayedTextRange.endIndex else { continue }
// backward search
// InputGraphNode
// ittaitt[][][t]
// a[t][a][ta]
// TODO: typo
typealias Match = (displayedTextStartIndex: Int?, inputElementsStartIndex: Int?, inputElementsEndIndex: Int, value: String, correction: Correction)
typealias BackSearchMatch = (endNode: ReplacePrefixTree.Node, route: [Character], inputStyleId: InputStyle.ID, correction: Correction, longestMatch: Match)
var backSearchMatch: [BackSearchMatch] = []
do {
if let characterNodes = ReplacePrefixTree.characterNodes[.init(from: item.inputStyle)],
let nodes = characterNodes[item.character] {
// nodes
typealias SearchItem = (
endNode: ReplacePrefixTree.Node,
endValue: String?,
node: ReplacePrefixTree.Node,
route: [Int],
inputStyleId: InputStyle.ID,
correction: Correction
)
var stack: [SearchItem] = nodes.map {
($0, $0.value, $0, [], .init(from: item.inputStyle), .none)
}
while let (endNode, endValue, cNode, cRoute, cInputStyleId, cCorrection) = stack.popLast() {
// pNoderoot
if let pNode = cNode.parent, pNode.parent != nil {
// parentNodenodebeforeGraphNode
let indices = if let first = cRoute.first {
inputGraph.prevIndices(for: inputGraph.nodes[first])
} else {
index < inputGraph.inputElementsEndIndexToNodeIndices.endIndex ? inputGraph.inputElementsEndIndexToNodeIndices[index] : .init()
}
for prevGraphNodeIndex in indices {
guard inputGraph.nodes[prevGraphNodeIndex].character == pNode.character else {
continue
}
// TODO: InputGraph.NodeInputStyle.ID
stack.append(
(
endNode,
endValue,
pNode,
[prevGraphNodeIndex] + cRoute,
cInputStyleId,
cCorrection.isTypo ? .typo : inputGraph.nodes[prevGraphNodeIndex].correction
)
)
}
} else {
// parentNodematches
// matchesindex1
guard let pNode = endNode.parent else { continue }
let inputElementsStartIndex = if cRoute.isEmpty { index } else { inputGraph.nodes[cRoute.first!].inputElementsRange.startIndex }
let displayedTextStartIndex = cRoute.first.flatMap { inputGraph.nodes[$0].displayedTextRange.startIndex }
let characterRoute = cRoute.map{inputGraph.nodes[$0].character}
backSearchMatch.append(
(
pNode,
characterRoute,
cInputStyleId,
cCorrection,
(displayedTextStartIndex, inputElementsStartIndex, index, "", cCorrection)
)
)
}
}
}
}
let replacePrefixTree = InputStyle(from: item.inputStyle).replacePrefixTree
typealias Match = (route: [Character], value: String, correction: Correction)
typealias SearchItem = (
node: ReplacePrefixTree.Node,
nextIndex: Int,
@ -343,32 +481,35 @@ struct InputGraph {
inputStyleId: InputStyle.ID,
longestMatch: Match
)
var stack: [SearchItem] = [
(replacePrefixTree, index, [], .all, (route: [], value: "", correction: .none))
]
var stack: [SearchItem] = []
for match in backSearchMatch {
stack.append((match.endNode, index, match.route, match.inputStyleId, match.longestMatch))
}
if stack.isEmpty {
stack.append((replacePrefixTree, index, [], .all, (nil, index, index, value: "", correction: .none)))
}
var matches: [Match] = []
while let (cNode, cIndex, cRoute, cInputStyleId, cLongestMatch) = stack.popLast() {
let continuous = cIndex < input.endIndex && cInputStyleId.isCompatible(with: .init(from: input[cIndex].inputStyle))
if continuous, let nNode = cNode.find(key: input[cIndex].character) {
if let value = nNode.value {
// valuelongestMatch
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], .init(from: input[cIndex].inputStyle), (cRoute + [input[cIndex].character], value, cLongestMatch.correction)))
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], .init(from: input[cIndex].inputStyle), (cLongestMatch.displayedTextStartIndex, cLongestMatch.inputElementsStartIndex, cIndex + 1, value, cLongestMatch.correction)))
} else if cRoute.isEmpty {
// value1longestMatch
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], .init(from: input[cIndex].inputStyle), ([input[cIndex].character], String(input[cIndex].character), .none)))
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], .init(from: input[cIndex].inputStyle), (cLongestMatch.displayedTextStartIndex, cIndex, cIndex + 1, String(input[cIndex].character), .none)))
} else {
//
stack.append((nNode, cIndex + 1, cRoute + [input[cIndex].character], .init(from: input[cIndex].inputStyle), cLongestMatch))
}
} else {
if !cLongestMatch.route.isEmpty {
if cLongestMatch.inputElementsStartIndex != cLongestMatch.inputElementsEndIndex {
// longestMatchmatch
matches.append(cLongestMatch)
} else if cRoute.isEmpty {
// 1rootcharactermatch
// .->\1
matches.append((route: [input[cIndex].character], value: String(input[cIndex].character), correction: .none))
matches.append((nil, index, index + 1, value: String(input[cIndex].character), correction: .none))
}
}
//
@ -391,28 +532,70 @@ struct InputGraph {
}
}
} else {
stack.append((.init(), cIndex + item.inputCount, cRoute + Array(item.replace), .init(from: input[cIndex].inputStyle), (cRoute + Array(item.replace), item.replace, .typo)))
stack.append(
(
.init(),
cIndex + item.inputCount,
cRoute + Array(item.replace),
.init(from: input[cIndex].inputStyle),
(cLongestMatch.displayedTextStartIndex, cLongestMatch.inputElementsStartIndex, cIndex + item.inputCount, item.replace, .typo)
)
)
}
if let node {
// value
if let value = node.value {
stack.append((node, cIndex + item.inputCount, cRoute + Array(item.replace), .init(from: input[cIndex].inputStyle),(cRoute + Array(item.replace), value, .typo)))
stack.append(
(
node,
cIndex + item.inputCount,
cRoute + Array(item.replace),
.init(from: input[cIndex].inputStyle),
(cLongestMatch.displayedTextStartIndex, cLongestMatch.inputElementsStartIndex, cIndex + item.inputCount, value, .typo)
)
)
} else {
stack.append((node, cIndex + item.inputCount, cRoute + Array(item.replace), .init(from: input[cIndex].inputStyle),(cLongestMatch.route, cLongestMatch.value, .typo)))
stack.append(
(
node,
cIndex + item.inputCount,
cRoute + Array(item.replace),
.init(from: input[cIndex].inputStyle),
(cLongestMatch.displayedTextStartIndex, cLongestMatch.inputElementsStartIndex, cIndex + item.inputCount, cLongestMatch.value, .typo)
)
)
}
}
}
}
// matchinsert
for match in matches {
let displayedTextStartIndex = if let d = match.displayedTextStartIndex {
d
} else if let beforeNodeIndex = inputGraph.inputElementsEndIndexToNodeIndices[index].first,
let d = inputGraph.nodes[beforeNodeIndex].displayedTextRange.endIndex {
d
} else {
Int?.none
}
guard let displayedTextStartIndex else { continue }
let characters = Array(match.value)
for (i, c) in zip(characters.indices, characters) {
let inputElementRange: InputGraph.Range = if i == characters.startIndex && i+1 == characters.endIndex {
.range(index, index + match.route.count)
if let startIndex = match.inputElementsStartIndex {
.range(startIndex, match.inputElementsEndIndex)
} else {
.endIndex(match.inputElementsEndIndex)
}
} else if i == characters.startIndex {
.startIndex(index)
if let startIndex = match.inputElementsStartIndex {
.startIndex(startIndex)
} else {
.unknown
}
} else if i+1 == characters.endIndex {
.endIndex(i + match.route.count)
.endIndex(match.inputElementsEndIndex)
} else {
.unknown
}
@ -439,6 +622,7 @@ final class InputGraphTests: XCTestCase {
graph.insert(node1)
graph.insert(node2)
XCTAssertEqual(graph.next(for: node1), [node2])
XCTAssertEqual(graph.prev(for: node2), [node1])
}
func testBuild() throws {
@ -449,7 +633,6 @@ final class InputGraphTests: XCTestCase {
.init(character: "", inputStyle: .direct)
])
XCTAssertEqual(graph.nodes.count, 4) // Root nodes
print(graph.nodes)
}
do {
let graph = InputGraph.build(input: [
@ -458,7 +641,6 @@ final class InputGraphTests: XCTestCase {
.init(character: "", inputStyle: .direct)
])
XCTAssertEqual(graph.nodes.count, 5) // Root nodes
print(graph.nodes)
}
do {
let graph = InputGraph.build(input: [
@ -467,7 +649,6 @@ final class InputGraphTests: XCTestCase {
.init(character: "a", inputStyle: .roman2kana),
])
XCTAssertEqual(graph.nodes.count, 3) // Root nodes
print(graph.nodes)
}
do {
let graph = InputGraph.build(input: [
@ -527,9 +708,70 @@ final class InputGraphTests: XCTestCase {
)
XCTAssertFalse(graph.nodes.contains(.init(character: "", displayedTextRange: .range(0, 1), inputElementsRange: .range(0, 2), correction: .typo)))
}
}
func testLOUDSLookup() throws {
do {
// ttt
let graph = InputGraph.build(input: [
.init(character: "t", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
])
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), correction: .none)
)
XCTAssertEqual(
graph.nodes.first(where: {$0.character == "t"}),
.init(character: "t", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(2), correction: .none)
)
}
do {
// ttt
let graph = InputGraph.build(input: [
.init(character: "t", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
.init(character: "a", inputStyle: .roman2kana),
])
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), correction: .none)
)
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(3), correction: .none)
)
}
do {
// ntt
let graph = InputGraph.build(input: [
.init(character: "n", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
.init(character: "a", inputStyle: .roman2kana),
])
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(0, 1), inputElementsRange: .startIndex(0), correction: .none)
)
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(1, 2), inputElementsRange: .endIndex(3), correction: .none)
)
}
do {
// t
// ttt
// ts (
// FIXME:
/*
let graph = InputGraph.build(input: [
.init(character: "t", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
.init(character: "s", inputStyle: .roman2kana),
])
print(graph)
XCTAssertEqual(
graph.nodes.first(where: {$0.character == ""}),
.init(character: "", displayedTextRange: .range(2, 3), inputElementsRange: .endIndex(4), correction: .none)
)
*/
}
}
}

View File

@ -0,0 +1,128 @@
//
// extension LOUDS+InputGraph.swift
//
//
// Created by miwa on 2024/02/22.
//
import XCTest
import Foundation
@testable import KanaKanjiConverterModule
extension LOUDS {
func byfixNodeIndices(_ inputGraph: InputGraph, char2id: (Character) -> UInt8?) -> IndexSet {
var indexSet = IndexSet(integer: 1)
typealias SearchItem = (
node: InputGraph.Node,
lastNodeIndex: Int
)
var stack: [SearchItem] = inputGraph.next(for: inputGraph.root).map { ($0, 1) }
while let (cNode, cNodeIndex) = stack.popLast() {
// nextNodes
if let charId = char2id(cNode.character), let nodeIndex = self.searchCharNodeIndex(from: cNodeIndex, char: charId) {
indexSet.insert(nodeIndex)
stack.append(contentsOf: inputGraph.next(for: cNode).map { ($0, nodeIndex) })
} else {
continue
}
}
return indexSet
}
}
final class InputGraphBasedLOUDSTests: XCTestCase {
static var resourceURL = Bundle.module.resourceURL!.standardizedFileURL.appendingPathComponent("DictionaryMock", isDirectory: true)
func requestOptions() -> ConvertRequestOptions {
var options: ConvertRequestOptions = .default
options.dictionaryResourceURL = Self.resourceURL
return options
}
func loadCharIDs() -> [Character: UInt8] {
do {
let string = try String(contentsOf: Self.resourceURL.appendingPathComponent("louds/charID.chid", isDirectory: false), encoding: String.Encoding.utf8)
return [Character: UInt8](uniqueKeysWithValues: string.enumerated().map {($0.element, UInt8($0.offset))})
} catch {
print("ファイルが見つかりませんでした")
return [:]
}
}
func testByfixNodeIndices() throws {
let dicdataStore = DicdataStore(requestOptions: requestOptions())
let charIDs = loadCharIDs()
let louds = LOUDS.load("", option: requestOptions())
XCTAssertNotNil(louds)
guard let louds else { return }
do {
let inputGraph = InputGraph.build(input: [
.init(character: "", inputStyle: .direct),
.init(character: "", inputStyle: .direct),
.init(character: "", inputStyle: .direct),
])
let nodeIndices = louds.byfixNodeIndices(inputGraph, char2id: {charIDs[$0.toKatakana()]})
let dicdata: [DicdataElement] = dicdataStore.getDicdataFromLoudstxt3(identifier: "", indices: nodeIndices)
//
XCTAssertTrue(dicdata.contains {$0.word == ""})
//
XCTAssertTrue(dicdata.contains {$0.word == "鹿"})
XCTAssertTrue(dicdata.contains {$0.word == "歯科"})
//
XCTAssertTrue(dicdata.contains {$0.word == "滋賀"})
//
XCTAssertTrue(dicdata.contains {$0.word == "司会"})
XCTAssertTrue(dicdata.contains {$0.word == "視界"})
XCTAssertTrue(dicdata.contains {$0.word == "死界"})
//
XCTAssertTrue(dicdata.contains {$0.word == "市外"})
XCTAssertTrue(dicdata.contains {$0.word == "市街"})
XCTAssertTrue(dicdata.contains {$0.word == "死骸"})
}
do {
// ts -> ta
let inputGraph = InputGraph.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 nodeIndices = louds.byfixNodeIndices(inputGraph, char2id: {charIDs[$0.toKatakana()]})
let dicdata: [DicdataElement] = dicdataStore.getDicdataFromLoudstxt3(identifier: "", indices: nodeIndices)
//
XCTAssertTrue(dicdata.contains {$0.word == ""})
// []
XCTAssertTrue(dicdata.contains {$0.word == ""})
XCTAssertTrue(dicdata.contains {$0.word == ""})
//
XCTAssertTrue(dicdata.contains {$0.word == ""})
XCTAssertTrue(dicdata.contains {$0.word == ""})
//
XCTAssertTrue(dicdata.contains {$0.word == "死体"})
XCTAssertTrue(dicdata.contains {$0.word == "肢体"})
}
do {
//
let inputGraph = InputGraph.build(input: [
.init(character: "s", inputStyle: .roman2kana),
.init(character: "i", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
.init(character: "t", inputStyle: .roman2kana),
.init(character: "a", inputStyle: .roman2kana),
.init(character: "i", inputStyle: .roman2kana),
])
print(inputGraph)
let nodeIndices = louds.byfixNodeIndices(inputGraph, char2id: {return charIDs[$0.toKatakana()]})
let dicdata: [DicdataElement] = dicdataStore.getDicdataFromLoudstxt3(identifier: "", indices: nodeIndices)
//
XCTAssertTrue(dicdata.contains {$0.word == ""})
//
XCTAssertTrue(dicdata.contains {$0.word == "知っ"})
XCTAssertTrue(dicdata.contains {$0.word == "しっ"})
//
XCTAssertTrue(dicdata.contains {$0.word == "叱咤"})
//
XCTAssertTrue(dicdata.contains {$0.word == "失態"})
}
}
}