Merge pull request #227 from azooKey/feat/mapped_input_style

feat: `.mapped(id)`を新たな入力スタイルとして導入し、カスタムローマ字かな変換テーブルに対応
This commit is contained in:
Miwa
2025-07-23 23:35:36 +09:00
committed by GitHub
parent 6192a8817e
commit d8b06e9367
18 changed files with 1126 additions and 414 deletions

View File

@ -62,7 +62,12 @@ extension Kana2Kanji {
switch inputStyle {
case .direct:
dicdata = self.dicdataStore.getPredictionLOUDSDicdata(key: lastRuby)
case .roman2kana:
case .roman2kana, .mapped:
let table = if case let .mapped(id) = inputStyle {
InputStyleManager.shared.table(for: id)
} else {
InputStyleManager.shared.table(for: .defaultRomanToKana)
}
let roman = lastRuby.suffix(while: {String($0).onlyRomanAlphabet})
if !roman.isEmpty {
let ruby: Substring = lastRuby.dropLast(roman.count)
@ -70,7 +75,7 @@ extension Kana2Kanji {
dicdata = []
break
}
let possibleNexts: [Substring] = DicdataStore.possibleNexts[String(roman), default: []].map {ruby + $0}
let possibleNexts: [Substring] = table.possibleNexts[String(roman), default: []].map {ruby + $0}
debug(#function, lastRuby, ruby, roman, possibleNexts, prepart, lastRubyCount)
dicdata = possibleNexts.flatMap { self.dicdataStore.getPredictionLOUDSDicdata(key: $0) }
} else {

View File

@ -336,10 +336,8 @@ import EfficientNGram
///
private func getTopLevelAdditionalCandidate(_ inputData: ComposingText, options: ConvertRequestOptions) -> [Candidate] {
var candidates: [Candidate] = []
if inputData.input.allSatisfy({$0.inputStyle == .roman2kana}) {
if options.englishCandidateInRoman2KanaInput {
candidates.append(contentsOf: self.getForeignPredictionCandidate(inputData: inputData, language: "en-US", penalty: -10))
}
if options.englishCandidateInRoman2KanaInput, inputData.input.allSatisfy({$0.character.isASCII}) {
candidates.append(contentsOf: self.getForeignPredictionCandidate(inputData: inputData, language: "en-US", penalty: -10))
}
return candidates
}

View File

@ -1027,15 +1027,4 @@ public final class DicdataStore {
return true
}
static let possibleNexts: [String: [String]] = {
var results: [String: [String]] = [:]
for (key, value) in Roman2Kana.katakanaChanges {
for prefixCount in 0 ..< key.count where 0 < prefixCount {
let prefix = String(key.prefix(prefixCount))
results[prefix, default: []].append(value)
}
}
return results
}()
}

View File

@ -97,9 +97,14 @@ struct TypoCorrectionGenerator: Sendable {
switch item.inputStyle {
case .direct:
stablePrefix.append(contentsOf: item.string)
case .roman2kana:
case .roman2kana, .mapped:
let table = if case let .mapped(id) = item.inputStyle {
InputStyleManager.shared.table(for: id)
} else {
InputStyleManager.shared.table(for: .defaultRomanToKana)
}
var stableIndex = item.string.endIndex
for suffix in Roman2Kana.unstableSuffixes {
for suffix in table.unstableSuffixes {
if item.string.hasSuffix(suffix) {
stableIndex = min(stableIndex, item.string.endIndex - suffix.count)
}
@ -197,7 +202,7 @@ struct TypoCorrectionGenerator: Sendable {
return result
}
}
if (elements.allSatisfy {$0.inputStyle == .roman2kana}) {
if (elements.allSatisfy {$0.inputStyle == .roman2kana || $0.inputStyle == .mapped(id: .defaultRomanToKana)}) {
let dictionary: [String: [TypoCandidate]] = frozen ? [:] : Self.roman2KanaPossibleTypo
if key.count > 1 {
return dictionary[key, default: []]
@ -210,7 +215,14 @@ struct TypoCorrectionGenerator: Sendable {
return result
}
}
return []
// `.mapped`
return if elements.count == 1 {
[
TypoCandidate(inputElements: [elements.first!], weight: 0)
]
} else {
[]
}
}
fileprivate static let lengths = [0, 1]

View File

@ -185,10 +185,17 @@ public struct ComposingText: Sendable {
// covnertTarget|`[a, k, y, o]`prefix
// lastPrefix=11(suffix)
else if converted.hasPrefix(target) {
// lastPrefixIndex: 1
// count: 4
// replaceCount: 3
let replaceCount = count - lastPrefixIndex
// suffix:
let suffix = converted.suffix(converted.count - lastPrefix.count)
// lastPrefixIndexReplace
self.input.removeSubrange(count - replaceCount ..< count)
self.input.insert(contentsOf: suffix.map {InputElement(character: $0, inputStyle: CharacterUtils.isRomanLetter($0) ? .roman2kana : .direct)}, at: count - replaceCount)
// suffix1
// `frozen`
self.input.insert(contentsOf: suffix.map {InputElement(character: $0, inputStyle: .frozen)}, at: count - replaceCount)
count -= replaceCount
count += suffix.count
@ -436,7 +443,9 @@ extension ComposingText {
case .direct:
return current + [newCharacter]
case .roman2kana:
return Roman2Kana.toHiragana(currentText: current, added: newCharacter)
return InputStyleManager.shared.table(for: .defaultRomanToKana).toHiragana(currentText: current, added: newCharacter)
case .mapped(let id):
return InputStyleManager.shared.table(for: id).toHiragana(currentText: current, added: newCharacter)
}
}
@ -445,7 +454,9 @@ extension ComposingText {
case .direct:
convertTarget.append(newCharacter)
case .roman2kana:
convertTarget = Roman2Kana.toHiragana(currentText: convertTarget, added: newCharacter)
convertTarget = InputStyleManager.shared.table(for: .defaultRomanToKana).toHiragana(currentText: convertTarget, added: newCharacter)
case .mapped(let id):
convertTarget = InputStyleManager.shared.table(for: id).toHiragana(currentText: convertTarget, added: newCharacter)
}
}
@ -486,9 +497,11 @@ extension ComposingText.InputElement: CustomDebugStringConvertible {
public var debugDescription: String {
switch self.inputStyle {
case .direct:
return "direct(\(character))"
"direct(\(character))"
case .roman2kana:
return "roman2kana(\(character))"
"roman2kana(\(character))"
case .mapped(let id):
"mapped(\(id); \(character))"
}
}
}
@ -500,7 +513,14 @@ extension ComposingText.ConvertTargetElement: CustomDebugStringConvertible {
}
extension InputStyle: CustomDebugStringConvertible {
public var debugDescription: String {
"." + self.rawValue
switch self {
case .direct:
".direct"
case .roman2kana:
".roman2kana"
case .mapped(let id):
".mapped(\(id))"
}
}
}
#endif

View File

@ -0,0 +1,12 @@
public enum InputStyle: Sendable, Equatable, Hashable {
///
case direct
///
case roman2kana
///
case mapped(id: InputTableID)
static var frozen: Self {
.mapped(id: .empty)
}
}

View File

@ -0,0 +1,101 @@
import Foundation
import SwiftUtils
final class InputStyleManager {
nonisolated(unsafe) static let shared = InputStyleManager()
struct Table {
init(hiraganaChanges: [[Character] : [Character]]) {
self.hiraganaChanges = hiraganaChanges
self.unstableSuffixes = hiraganaChanges.keys.flatMapSet { characters in
characters.indices.map { i in
Array(characters[...i])
}
}
let katakanaChanges = Dictionary(uniqueKeysWithValues: hiraganaChanges.map { (String($0.key), String($0.value).toKatakana()) })
self.katakanaChanges = katakanaChanges
self.maxKeyCount = hiraganaChanges.lazy.map { $0.key.count }.max() ?? 0
self.possibleNexts = {
var results: [String: [String]] = [:]
for (key, value) in katakanaChanges {
for prefixCount in 0 ..< key.count where 0 < prefixCount {
let prefix = String(key.prefix(prefixCount))
results[prefix, default: []].append(value)
}
}
return results
}()
}
let unstableSuffixes: Set<[Character]>
let katakanaChanges: [String: String]
let hiraganaChanges: [[Character]: [Character]]
let maxKeyCount: Int
let possibleNexts: [String: [String]]
static let empty = Table(hiraganaChanges: [:])
func toHiragana(currentText: [Character], added: Character) -> [Character] {
for n in (0 ..< self.maxKeyCount).reversed() {
if n == 0 {
if let kana = self.hiraganaChanges[[added]] {
return currentText + kana
}
} else {
let last = currentText.suffix(n)
if let kana = self.hiraganaChanges[last + [added]] {
return currentText.prefix(currentText.count - last.count) + kana
}
}
}
return currentText + [added]
}
}
private var tables: [InputTableID: Table] = [:]
private init() {
//
let defaultRomanToKana = Table(hiraganaChanges: Roman2KanaMaps.defaultRomanToKanaMap)
let defaultAZIK = Table(hiraganaChanges: Roman2KanaMaps.defaultAzikMap)
self.tables = [
.empty: .empty,
.defaultRomanToKana: defaultRomanToKana,
.defaultAZIK: defaultAZIK
]
}
func table(for id: InputTableID) -> Table {
switch id {
case .defaultRomanToKana, .defaultAZIK, .empty:
return self.tables[id]!
case .custom(let url):
if let table = self.tables[id] {
return table
} else if let table = try? Self.loadTable(from: url) {
self.tables[id] = table
return table
} else {
return .empty
}
}
}
private static func loadTable(from url: URL) throws -> Table {
let content = try String(contentsOf: url, encoding: .utf8)
var map: [[Character]: [Character]] = [:]
for line in content.components(separatedBy: .newlines) {
//
guard !line.trimmingCharacters(in: .whitespaces).isEmpty else { continue }
// `# `
guard !line.hasPrefix("# ") else { continue }
let cols = line.split(separator: "\t")
//
guard cols.count >= 2 else { continue }
let key = Array(String(cols[0]))
let value = Array(String(cols[1]))
map[key] = value
}
return Table(hiraganaChanges: map)
}
}

View File

@ -0,0 +1,8 @@
public import struct Foundation.URL
public enum InputTableID: Sendable, Equatable, Hashable {
case defaultRomanToKana
case defaultAZIK
case empty
case custom(URL)
}

View File

@ -1,349 +0,0 @@
//
// Roman2Kana.swift
// Keyboard
//
// Created by ensan on 2020/09/24.
// Copyright © 2020 ensan. All rights reserved.
//
import Foundation
import SwiftUtils
enum Roman2Kana {
static let unstableSuffixes: Set<[Character]> = hiraganaChanges.keys.flatMapSet { characters in
characters.indices.map { i in
Array(characters[...i])
}
}
static let katakanaChanges: [String: String] = Dictionary(uniqueKeysWithValues: hiraganaChanges.map { (String($0.key), String($0.value).toKatakana()) })
static let hiraganaChanges: [[Character]: [Character]] = Dictionary(uniqueKeysWithValues: [
"a": "",
"xa": "",
"la": "",
"i": "",
"xi": "",
"li": "",
"u": "",
"wu": "",
"vu": "",
"xu": "",
"lu": "",
"e": "",
"xe": "",
"le": "",
"o": "",
"xo": "",
"lo": "",
"ka": "",
"ca": "",
"ga": "",
"xka": "",
"lka": "",
"ki": "",
"gi": "",
"ku": "",
"cu": "",
"gu": "",
"ke": "",
"ge": "",
"xke": "",
"lke": "",
"ko": "",
"co": "",
"go": "",
"sa": "",
"za": "",
"si": "",
"ci": "",
"shi": "",
"zi": "",
"ji": "",
"su": "",
"zu": "",
"se": "",
"ce": "",
"ze": "",
"so": "",
"zo": "",
"ta": "",
"da": "",
"ti": "",
"chi": "",
"di": "",
"tu": "",
"tsu": "",
"xtu": "",
"ltu": "",
"xtsu": "",
"ltsu": "",
"du": "",
"te": "",
"de": "",
"to": "",
"do": "",
"na": "",
"ni": "",
"nu": "",
"ne": "",
"no": "",
"ha": "",
"ba": "",
"pa": "",
"hi": "",
"bi": "",
"pi": "",
"hu": "",
"fu": "",
"bu": "",
"pu": "",
"he": "",
"be": "",
"pe": "",
"ho": "",
"bo": "",
"po": "",
"ma": "",
"mi": "",
"mu": "",
"me": "",
"mo": "",
"ya": "",
"xya": "",
"lya": "",
"yu": "",
"xyu": "",
"lyu": "",
"yo": "",
"xyo": "",
"lyo": "",
"ra": "",
"ri": "",
"ru": "",
"re": "",
"ro": "",
"wa": "",
"xwa": "",
"lwa": "",
"wyi": "",
"wye": "",
"wo": "",
"nn": "",
"ye": "いぇ",
"va": "ゔぁ",
"vi": "ゔぃ",
"ve": "ゔぇ",
"vo": "ゔぉ",
"kya": "きゃ",
"kyu": "きゅ",
"kye": "きぇ",
"kyo": "きょ",
"gya": "ぎゃ",
"gyu": "ぎゅ",
"gye": "ぎぇ",
"gyo": "ぎょ",
"qa": "くぁ",
"kwa": "くぁ",
"qwa": "くぁ",
"qi": "くぃ",
"kwi": "くぃ",
"qwi": "くぃ",
"qu": "くぅ",
"kwu": "くぅ",
"qwu": "くぅ",
"qe": "くぇ",
"kwe": "くぇ",
"qwe": "くぇ",
"qo": "くぉ",
"kwo": "くぉ",
"qwo": "くぉ",
"gwa": "ぐぁ",
"gwi": "ぐぃ",
"gwu": "ぐぅ",
"gwe": "ぐぇ",
"gwo": "ぐぉ",
"sha": "しゃ",
"sya": "しゃ",
"shu": "しゅ",
"syu": "しゅ",
"she": "しぇ",
"sye": "しぇ",
"sho": "しょ",
"syo": "しょ",
"ja": "じゃ",
"zya": "じゃ",
"jya": "じゃ",
"jyi": "じぃ",
"ju": "じゅ",
"zyu": "じゅ",
"jyu": "じゅ",
"je": "じぇ",
"zye": "じぇ",
"jye": "じぇ",
"jo": "じょ",
"zyo": "じょ",
"jyo": "じょ",
"swa": "すぁ",
"swi": "すぃ",
"swu": "すぅ",
"swe": "すぇ",
"swo": "すぉ",
"cha": "ちゃ",
"cya": "ちゃ",
"tya": "ちゃ",
"tyi": "ちぃ",
"cyi": "ちぃ",
"chu": "ちゅ",
"cyu": "ちゅ",
"tyu": "ちゅ",
"che": "ちぇ",
"cye": "ちぇ",
"tye": "ちぇ",
"cho": "ちょ",
"cyo": "ちょ",
"tyo": "ちょ",
"tsa": "つぁ",
"tsi": "つぃ",
"tse": "つぇ",
"tso": "つぉ",
"tha": "てゃ",
"thi": "てぃ",
"thu": "てゅ",
"the": "てぇ",
"tho": "てょ",
"twa": "とぁ",
"twi": "とぃ",
"twu": "とぅ",
"twe": "とぇ",
"two": "とぉ",
"dya": "ぢゃ",
"dyi": "ぢぃ",
"dyu": "ぢゅ",
"dye": "ぢぇ",
"dyo": "ぢょ",
"dha": "でゃ",
"dhi": "でぃ",
"dhu": "でゅ",
"dhe": "でぇ",
"dho": "でょ",
"dwa": "どぁ",
"dwi": "どぃ",
"dwu": "どぅ",
"dwe": "どぇ",
"dwo": "どぉ",
"nya": "にゃ",
"nyi": "にぃ",
"nyu": "にゅ",
"nye": "にぇ",
"nyo": "にょ",
"hya": "ひゃ",
"hyi": "ひぃ",
"hyu": "ひゅ",
"hye": "ひぇ",
"hyo": "ひょ",
"bya": "びゃ",
"byi": "びぃ",
"byu": "びゅ",
"bye": "びぇ",
"byo": "びょ",
"pya": "ぴゃ",
"pyi": "ぴぃ",
"pyu": "ぴゅ",
"pye": "ぴぇ",
"pyo": "ぴょ",
"fa": "ふぁ",
"hwa": "ふぁ",
"fwa": "ふぁ",
"fi": "ふぃ",
"hwi": "ふぃ",
"fwi": "ふぃ",
"fwu": "ふぅ",
"fe": "ふぇ",
"hwe": "ふぇ",
"fwe": "ふぇ",
"fo": "ふぉ",
"hwo": "ふぉ",
"fwo": "ふぉ",
"fya": "ふゃ",
"fyu": "ふゅ",
"fyo": "ふょ",
"mya": "みゃ",
"myi": "みぃ",
"myu": "みゅ",
"mye": "みぇ",
"myo": "みょ",
"rya": "りゃ",
"ryi": "りぃ",
"ryu": "りゅ",
"rye": "りぇ",
"ryo": "りょ",
"wi": "うぃ",
"we": "うぇ",
"wha": "うぁ",
"whi": "うぃ",
"whu": "",
"whe": "うぇ",
"who": "うぉ",
"bb": "っb",
"cc": "っc",
"dd": "っd",
"ff": "っf",
"gg": "っg",
"hh": "っh",
"jj": "っj",
"kk": "っk",
"ll": "っl",
"mm": "っm",
"pp": "っp",
"qq": "っq",
"rr": "っr",
"ss": "っs",
"tt": "っt",
"vv": "っv",
"ww": "っw",
"xx": "っx",
"yy": "っy",
"zz": "っz",
"nb": "んb",
"nc": "んc",
"nd": "んd",
"nf": "んf",
"ng": "んg",
"nh": "んh",
"nj": "んj",
"nk": "んk",
"nl": "んl",
"nm": "んm",
"np": "んp",
"nq": "んq",
"nr": "んr",
"ns": "んs",
"nt": "んt",
"nv": "んv",
"nw": "んw",
"nx": "んx",
"nz": "んz",
"xn": "",
"zh": "",
"zj": "",
"zk": "",
"zl": ""
].map {(Array($0.key), Array($0.value))})
static let maxKeyCount = hiraganaChanges.lazy.map { $0.key.count }.max() ?? 0
static func toHiragana(currentText: [Character], added: Character) -> [Character] {
for n in (0 ..< maxKeyCount).reversed() {
if n == 0 {
if let kana = Roman2Kana.hiraganaChanges[[added]] {
return currentText + kana
}
} else {
let last = currentText.suffix(n)
if let kana = Roman2Kana.hiraganaChanges[last + [added]] {
return currentText.prefix(currentText.count - last.count) + kana
}
}
}
return currentText + [added]
}
}

View File

@ -0,0 +1,888 @@
import Foundation
enum Roman2KanaMaps {
static let defaultRomanToKanaMap: [[Character]: [Character]] = Dictionary(uniqueKeysWithValues: [
"a": "",
"xa": "",
"la": "",
"i": "",
"xi": "",
"li": "",
"u": "",
"wu": "",
"vu": "",
"xu": "",
"lu": "",
"e": "",
"xe": "",
"le": "",
"o": "",
"xo": "",
"lo": "",
"ka": "",
"ca": "",
"ga": "",
"xka": "",
"lka": "",
"ki": "",
"gi": "",
"ku": "",
"cu": "",
"gu": "",
"ke": "",
"ge": "",
"xke": "",
"lke": "",
"ko": "",
"co": "",
"go": "",
"sa": "",
"za": "",
"si": "",
"ci": "",
"shi": "",
"zi": "",
"ji": "",
"su": "",
"zu": "",
"se": "",
"ce": "",
"ze": "",
"so": "",
"zo": "",
"ta": "",
"da": "",
"ti": "",
"chi": "",
"di": "",
"tu": "",
"tsu": "",
"xtu": "",
"ltu": "",
"xtsu": "",
"ltsu": "",
"du": "",
"te": "",
"de": "",
"to": "",
"do": "",
"na": "",
"ni": "",
"nu": "",
"ne": "",
"no": "",
"ha": "",
"ba": "",
"pa": "",
"hi": "",
"bi": "",
"pi": "",
"hu": "",
"fu": "",
"bu": "",
"pu": "",
"he": "",
"be": "",
"pe": "",
"ho": "",
"bo": "",
"po": "",
"ma": "",
"mi": "",
"mu": "",
"me": "",
"mo": "",
"ya": "",
"xya": "",
"lya": "",
"yu": "",
"xyu": "",
"lyu": "",
"yo": "",
"xyo": "",
"lyo": "",
"ra": "",
"ri": "",
"ru": "",
"re": "",
"ro": "",
"wa": "",
"xwa": "",
"lwa": "",
"wyi": "",
"wye": "",
"wo": "",
"nn": "",
"ye": "いぇ",
"va": "ゔぁ",
"vi": "ゔぃ",
"ve": "ゔぇ",
"vo": "ゔぉ",
"kya": "きゃ",
"kyu": "きゅ",
"kye": "きぇ",
"kyo": "きょ",
"gya": "ぎゃ",
"gyu": "ぎゅ",
"gye": "ぎぇ",
"gyo": "ぎょ",
"qa": "くぁ",
"kwa": "くぁ",
"qwa": "くぁ",
"qi": "くぃ",
"kwi": "くぃ",
"qwi": "くぃ",
"qu": "くぅ",
"kwu": "くぅ",
"qwu": "くぅ",
"qe": "くぇ",
"kwe": "くぇ",
"qwe": "くぇ",
"qo": "くぉ",
"kwo": "くぉ",
"qwo": "くぉ",
"gwa": "ぐぁ",
"gwi": "ぐぃ",
"gwu": "ぐぅ",
"gwe": "ぐぇ",
"gwo": "ぐぉ",
"sha": "しゃ",
"sya": "しゃ",
"shu": "しゅ",
"syu": "しゅ",
"she": "しぇ",
"sye": "しぇ",
"sho": "しょ",
"syo": "しょ",
"ja": "じゃ",
"zya": "じゃ",
"jya": "じゃ",
"jyi": "じぃ",
"ju": "じゅ",
"zyu": "じゅ",
"jyu": "じゅ",
"je": "じぇ",
"zye": "じぇ",
"jye": "じぇ",
"jo": "じょ",
"zyo": "じょ",
"jyo": "じょ",
"swa": "すぁ",
"swi": "すぃ",
"swu": "すぅ",
"swe": "すぇ",
"swo": "すぉ",
"cha": "ちゃ",
"cya": "ちゃ",
"tya": "ちゃ",
"tyi": "ちぃ",
"cyi": "ちぃ",
"chu": "ちゅ",
"cyu": "ちゅ",
"tyu": "ちゅ",
"che": "ちぇ",
"cye": "ちぇ",
"tye": "ちぇ",
"cho": "ちょ",
"cyo": "ちょ",
"tyo": "ちょ",
"tsa": "つぁ",
"tsi": "つぃ",
"tse": "つぇ",
"tso": "つぉ",
"tha": "てゃ",
"thi": "てぃ",
"thu": "てゅ",
"the": "てぇ",
"tho": "てょ",
"twa": "とぁ",
"twi": "とぃ",
"twu": "とぅ",
"twe": "とぇ",
"two": "とぉ",
"dya": "ぢゃ",
"dyi": "ぢぃ",
"dyu": "ぢゅ",
"dye": "ぢぇ",
"dyo": "ぢょ",
"dha": "でゃ",
"dhi": "でぃ",
"dhu": "でゅ",
"dhe": "でぇ",
"dho": "でょ",
"dwa": "どぁ",
"dwi": "どぃ",
"dwu": "どぅ",
"dwe": "どぇ",
"dwo": "どぉ",
"nya": "にゃ",
"nyi": "にぃ",
"nyu": "にゅ",
"nye": "にぇ",
"nyo": "にょ",
"hya": "ひゃ",
"hyi": "ひぃ",
"hyu": "ひゅ",
"hye": "ひぇ",
"hyo": "ひょ",
"bya": "びゃ",
"byi": "びぃ",
"byu": "びゅ",
"bye": "びぇ",
"byo": "びょ",
"pya": "ぴゃ",
"pyi": "ぴぃ",
"pyu": "ぴゅ",
"pye": "ぴぇ",
"pyo": "ぴょ",
"fa": "ふぁ",
"hwa": "ふぁ",
"fwa": "ふぁ",
"fi": "ふぃ",
"hwi": "ふぃ",
"fwi": "ふぃ",
"fwu": "ふぅ",
"fe": "ふぇ",
"hwe": "ふぇ",
"fwe": "ふぇ",
"fo": "ふぉ",
"hwo": "ふぉ",
"fwo": "ふぉ",
"fya": "ふゃ",
"fyu": "ふゅ",
"fyo": "ふょ",
"mya": "みゃ",
"myi": "みぃ",
"myu": "みゅ",
"mye": "みぇ",
"myo": "みょ",
"rya": "りゃ",
"ryi": "りぃ",
"ryu": "りゅ",
"rye": "りぇ",
"ryo": "りょ",
"wi": "うぃ",
"we": "うぇ",
"wha": "うぁ",
"whi": "うぃ",
"whu": "",
"whe": "うぇ",
"who": "うぉ",
"bb": "っb",
"cc": "っc",
"dd": "っd",
"ff": "っf",
"gg": "っg",
"hh": "っh",
"jj": "っj",
"kk": "っk",
"ll": "っl",
"mm": "っm",
"pp": "っp",
"qq": "っq",
"rr": "っr",
"ss": "っs",
"tt": "っt",
"vv": "っv",
"ww": "っw",
"xx": "っx",
"yy": "っy",
"zz": "っz",
"nb": "んb",
"nc": "んc",
"nd": "んd",
"nf": "んf",
"ng": "んg",
"nh": "んh",
"nj": "んj",
"nk": "んk",
"nl": "んl",
"nm": "んm",
"np": "んp",
"nq": "んq",
"nr": "んr",
"ns": "んs",
"nt": "んt",
"nv": "んv",
"nw": "んw",
"nx": "んx",
"nz": "んz",
"xn": "",
"zh": "",
"zj": "",
"zk": "",
"zl": ""
].map {(Array($0.key), Array($0.value))})
static let defaultAzikMap: [[Character]: [Character]] = Dictionary(uniqueKeysWithValues: [
"a": "",
"i": "",
"u": "",
"e": "",
"o": "",
"ka": "",
"ki": "",
"ku": "",
"ke": "",
"ko": "",
"sa": "",
"si": "",
"su": "",
"se": "",
"so": "",
"ta": "",
"ti": "",
"tu": "",
"te": "",
"to": "",
"na": "",
"ni": "",
"nu": "",
"ne": "",
"no": "",
"ha": "",
"hi": "",
"hu": "",
"he": "",
"ho": "",
"ma": "",
"mi": "",
"mu": "",
"me": "",
"mo": "",
"ya": "",
"yu": "",
"yo": "",
"ra": "",
"ri": "",
"ru": "",
"re": "",
"ro": "",
"wa": "",
"wi": "うぃ",
"we": "うぇ",
"wo": "",
"ga": "",
"gi": "",
"gu": "",
"ge": "",
"go": "",
"za": "",
"zi": "",
"zu": "",
"ze": "",
"zo": "",
"da": "",
"di": "",
"du": "",
"de": "",
"do": "",
"ba": "",
"bi": "",
"bu": "",
"be": "",
"bo": "",
"pa": "",
"pi": "",
"pu": "",
"pe": "",
"po": "",
"kya": "きゃ",
"kyu": "きゅ",
"kye": "きぇ",
"kyo": "きょ",
"kga": "きゃ",
"kgu": "きゅ",
"kge": "きぇ",
"kgo": "きょ",
"sya": "しゃ",
"syu": "しゅ",
"sye": "しぇ",
"syo": "しょ",
"xa": "しゃ",
"xu": "しゅ",
"xe": "しぇ",
"xo": "しょ",
"tya": "ちゃ",
"tyu": "ちゅ",
"tye": "ちぇ",
"tyo": "ちょ",
"ca": "ちゃ",
"cu": "ちゅ",
"ce": "ちぇ",
"co": "ちょ",
"nya": "にゃ",
"nyu": "にゅ",
"nye": "にぇ",
"nyo": "にょ",
"nga": "にゃ",
"ngu": "にゅ",
"nge": "にぇ",
"ngo": "にょ",
"hya": "ひゃ",
"hyu": "ひゅ",
"hye": "ひぇ",
"hyo": "ひょ",
"hga": "ひゃ",
"hgu": "ひゅ",
"hge": "ひぇ",
"hgo": "ひょ",
"mya": "みゃ",
"myu": "みゅ",
"mye": "みぇ",
"myo": "みょ",
"mga": "みゃ",
"mgu": "みゅ",
"mge": "みぇ",
"mgo": "みょ",
"rya": "りゃ",
"ryu": "りゅ",
"rye": "りぇ",
"ryo": "りょ",
"gya": "ぎゃ",
"gyu": "ぎゅ",
"gye": "ぎぇ",
"gyo": "ぎょ",
"zya": "じゃ",
"zyu": "じゅ",
"zye": "じぇ",
"zyo": "じょ",
"ja": "じゃ",
"ju": "じゅ",
"je": "じぇ",
"jo": "じょ",
"bya": "びゃ",
"byu": "びゅ",
"bye": "びぇ",
"byo": "びょ",
"pya": "ぴゃ",
"pyu": "ぴゅ",
"pye": "ぴぇ",
"pyo": "ぴょ",
"pga": "ぴゃ",
"pgu": "ぴゅ",
"pge": "ぴぇ",
"pgo": "ぴょ",
"fa": "ふぁ",
"fi": "ふぃ",
"fu": "",
"fe": "ふぇ",
"fo": "ふぉ",
"va": "ヴぁ",
"vi": "ヴぃ",
"vu": "",
"ve": "ヴぇ",
"vo": "ヴぉ",
"tgi": "てぃ",
"tgu": "とぅ",
"dci": "でぃ",
"dcu": "どぅ",
"wso": "うぉ",
"la": "",
"li": "",
"lu": "",
"le": "",
"lo": "",
"lya": "",
"lyu": "",
"lyo": "",
"": "",
"q": "",
"nn": "",
"": "",
"z。": "",
"z、": "",
"zー": "",
"z「": "",
"z」": "",
"kz": "かん",
"kn": "かん",
"kk": "きん",
"kj": "くん",
"kd": "けん",
"kl": "こん",
"sz": "さん",
"sn": "さん",
"sk": "しん",
"sj": "すん",
"sd": "せん",
"sl": "そん",
"tz": "たん",
"tn": "たん",
"tk": "ちん",
"tj": "つん",
"td": "てん",
"tl": "とん",
"nz": "なん",
"nk": "にん",
"nj": "ぬん",
"nd": "ねん",
"nl": "のん",
"hz": "はん",
"hn": "はん",
"hk": "ひん",
"hj": "ふん",
"hd": "へん",
"hl": "ほん",
"mz": "まん",
"mk": "みん",
"mj": "むん",
"md": "めん",
"ml": "もん",
"yz": "やん",
"yn": "やん",
"yj": "ゆん",
"yl": "よん",
"rz": "らん",
"rn": "らん",
"rk": "りん",
"rj": "るん",
"rd": "れん",
"rl": "ろん",
"wz": "わん",
"wn": "わん",
"wk": "うぃん",
"wd": "うぇん",
"wl": "うぉん",
"gz": "がん",
"gn": "がん",
"gk": "ぎん",
"gj": "ぐん",
"gd": "げん",
"gl": "ごん",
"zz": "ざん",
"zn": "ざん",
"zk": "じん",
"zj": "ずん",
"zd": "ぜん",
"zl": "ぞん",
"dz": "だん",
"dn": "だん",
"dk": "ぢん",
"dj": "づん",
"dd": "でん",
"dl": "どん",
"bz": "ばん",
"bn": "ばん",
"bk": "びん",
"bj": "ぶん",
"bd": "べん",
"bl": "ぼん",
"pz": "ぱん",
"pn": "ぱん",
"pk": "ぴん",
"pj": "ぷん",
"pd": "ぺん",
"pl": "ぽん",
"kyz": "きゃん",
"kyn": "きゃん",
"kyj": "きゅん",
"kyd": "きぇん",
"kyl": "きょん",
"kgz": "きゃん",
"kgn": "きゃん",
"kgj": "きゅん",
"kgd": "きぇん",
"kgl": "きょん",
"syz": "しゃん",
"syn": "しゃん",
"syj": "しゅん",
"syd": "しぇん",
"syl": "しょん",
"xz": "しゃん",
"xn": "しゃん",
"xj": "しゅん",
"xd": "しぇん",
"xl": "しょん",
"tyz": "ちゃん",
"tyn": "ちゃん",
"tyj": "ちゅん",
"tyd": "ちぇん",
"tyl": "ちょん",
"cz": "ちゃん",
"cn": "ちゃん",
"cj": "ちゅん",
"cd": "ちぇん",
"cl": "ちょん",
"nyz": "にゃん",
"nyn": "にゃん",
"nyj": "にゅん",
"nyd": "にぇん",
"nyl": "にょん",
"ngz": "にゃん",
"ngn": "にゃん",
"ngj": "にゅん",
"ngd": "にぇん",
"ngl": "にょん",
"hyz": "ひゃん",
"hyn": "ひゃん",
"hyj": "ひゅん",
"hyd": "ひぇん",
"hyl": "ひょん",
"hgz": "ひゃん",
"hgn": "ひゃん",
"hgj": "ひゅん",
"hgd": "ひぇん",
"hgl": "ひょん",
"myz": "みゃん",
"myn": "みゃん",
"myj": "みゅん",
"myd": "みぇん",
"myl": "みょん",
"mgz": "みゃん",
"mgn": "みゃん",
"mgj": "みゅん",
"mgd": "みぇん",
"mgl": "みょん",
"ryz": "りゃん",
"ryn": "りゃん",
"ryj": "りゅん",
"ryd": "りぇん",
"ryl": "りょん",
"gyz": "ぎゃん",
"gyn": "ぎゃん",
"gyj": "ぎゅん",
"gyd": "ぎぇん",
"gyl": "ぎょん",
"zyz": "じゃん",
"zyn": "じゃん",
"zyj": "じゅん",
"zyd": "じぇん",
"zyl": "じょん",
"jz": "じゃん",
"jn": "じゃん",
"jj": "じゅん",
"jd": "じぇん",
"jl": "じょん",
"byz": "びゃん",
"byn": "びゃん",
"byj": "びゅん",
"byd": "びぇん",
"byl": "びょん",
"pyz": "ぴゃん",
"pyn": "ぴゃん",
"pyj": "ぴゅん",
"pyd": "ぴぇん",
"pyl": "ぴょん",
"pgz": "ぴゃん",
"pgn": "ぴゃん",
"pgj": "ぴゅん",
"pgd": "ぴぇん",
"pgl": "ぴょん",
"fz": "ふぁん",
"fn": "ふぁん",
"fk": "ふぃん",
"fj": "ふん",
"fd": "ふぇん",
"fl": "ふぉん",
"vz": "ゔぁん",
"vn": "ゔぁん",
"vk": "ゔぃん",
"vj": "ゔん",
"vd": "ゔぇん",
"vl": "ゔぉん",
"tgk": "てぃん",
"tgj": "とぅん",
"dck": "でぃん",
"dcj": "どぅん",
"lz": "ぁん",
"ln": "ぁん",
"lk": "ぃん",
"ld": "ぇん",
"ll": "ぉん",
"lyz": "ゃん",
"lyn": "ゃん",
"lyj": "ゅん",
"lyl": "ょん",
"kq": "かい",
"kh": "くう",
"kw": "けい",
"kp": "こう",
"sq": "さい",
"sh": "すう",
"sw": "せい",
"sp": "そう",
"tq": "たい",
"th": "つう",
"tw": "てい",
"tp": "とう",
"nq": "ない",
"nh": "ぬう",
"nw": "ねい",
"np": "のう",
"hq": "はい",
"hh": "ふう",
"hw": "へい",
"hp": "ほう",
"mq": "まい",
"mh": "むう",
"mw": "めい",
"mp": "もう",
"yq": "やい",
"yh": "ゆう",
"yp": "よう",
"rq": "らい",
"rh": "るう",
"rw": "れい",
"rp": "ろう",
"gq": "がい",
"gh": "ぐう",
"gw": "げい",
"gp": "ごう",
"zq": "ざい",
"zh": "ずう",
"zw": "ぜい",
"zp": "ぞう",
"dq": "だい",
"dh": "づう",
"dw": "でい",
"dp": "どう",
"bq": "ばい",
"bh": "ぶう",
"bw": "べい",
"bp": "ぼう",
"pq": "ぱい",
"ph": "ぷう",
"pw": "ぺい",
"pp": "ぽう",
"kyq": "きゃい",
"kyh": "きゅう",
"kyw": "きぇい",
"kyp": "きょう",
"kgq": "きゃい",
"kgh": "きゅう",
"kgw": "きぇい",
"kgp": "きょう",
"syq": "しゃい",
"syh": "しゅう",
"syw": "しぇい",
"syp": "しょう",
"xq": "しゃい",
"xh": "しゅう",
"xw": "しぇい",
"xp": "しょう",
"tyq": "ちゃい",
"tyh": "ちゅう",
"tyw": "ちぇい",
"typ": "ちょう",
"cq": "ちゃい",
"ch": "ちゅう",
"cw": "ちぇい",
"cp": "ちょう",
"nyq": "にゃい",
"nyh": "にゅう",
"nyw": "にぇい",
"nyp": "にょう",
"ngq": "にゃい",
"ngh": "にゅう",
"ngw": "にぇい",
"ngp": "にょう",
"hyq": "ひゃい",
"hyh": "ひゅう",
"hyw": "ひぇい",
"hyp": "ひょう",
"hgq": "ひゃい",
"hgh": "ひゅう",
"hgw": "ひぇい",
"hgp": "ひょう",
"myq": "みゃい",
"myh": "みゅう",
"myw": "みぇい",
"myp": "みょう",
"mgq": "みゃい",
"mgh": "みゅう",
"mgw": "みぇい",
"mgp": "みょう",
"ryq": "りゃい",
"ryh": "りゅう",
"ryw": "りぇい",
"ryp": "りょう",
"gyq": "ぎゃい",
"gyh": "ぎゅう",
"gyw": "ぎぇい",
"gyp": "ぎょう",
"zyq": "じゃい",
"zyh": "じゅう",
"zyw": "じぇい",
"zyp": "じょう",
"jq": "じゃい",
"jh": "じゅう",
"jw": "じぇい",
"jp": "じょう",
"byq": "びゃい",
"byh": "びゅう",
"byw": "びぇい",
"byp": "びょう",
"pyq": "ぴゃい",
"pyh": "ぴゅう",
"pyw": "ぴぇい",
"pyp": "ぴょう",
"pgq": "ぴゃい",
"pgh": "ぴゅう",
"pgw": "ぴぇい",
"pgp": "ぴょう",
"fq": "ふぁい",
"fh": "ふう",
"fw": "ふぇい",
"fp": "ふぉー",
"vq": "ゔぁい",
"vh": "ゔー",
"vw": "ゔぇい",
"vp": "ゔぉー",
"tgh": "とぅー",
"dch": "どぅー",
"wq": "わい",
"ww": "うぇい",
"wp": "うぉー",
"lq": "ぁい",
"lh": "ぅう",
"lw": "ぇい",
"lp": "ぉう",
"lyq": "ゃい",
"lyh": "ゅう",
"lyp": "ょう",
"kf": "",
"jf": "じゅ",
"hf": "",
"yf": "",
"mf": "",
"nf": "",
"df": "",
"cf": "ちぇ",
"pf": "ぽん",
"wf": "わい",
"sf": "さい",
"ss": "せい",
"zc": "",
"zv": "ざい",
"zf": "",
"zx": "ぜい",
"kt": "こと",
"wt": "わた",
"km": "かも",
"sr": "する",
"rr": "られ",
"nb": "ねば",
"nt": "にち",
"st": "した",
"mn": "もの",
"tm": "ため",
"tr": "たら",
"zr": "ざる",
"bt": "びと",
"dt": "だち",
"tt": "たち",
"ms": "ます",
"dm": "でも",
"nr": "なる",
"mt": "また",
"gr": "がら",
"wr": "われ",
"ht": "ひと",
"ds": "です",
"kr": "から",
"yr": "よる",
"tb": "たび",
"gt": "ごと",
].map {(Array($0.key), Array($0.value))})
}

View File

@ -5,13 +5,6 @@
// Created by ensan on 2023/04/30.
//
public enum InputStyle: String, Sendable {
///
case direct = "direct"
///
case roman2kana = "roman"
}
public enum KeyboardLanguage: String, Codable, Equatable, Sendable {
case en_US
case ja_JP

View File

@ -25,11 +25,6 @@ public enum CharacterUtils {
kogakiKana.contains(character)
}
/// (a-z, A-Z)
@inlinable public static func isRomanLetter(_ character: Character) -> Bool {
character.isASCII && character.isCased
}
///
public static func kogaki(_ character: Character) -> Character {
switch character {

View File

@ -140,7 +140,7 @@ final class ComposingTextTests: XCTestCase {
ComposingText.InputElement(character: "a", inputStyle: .roman2kana),
ComposingText.InputElement(character: "k", inputStyle: .roman2kana),
ComposingText.InputElement(character: "a", inputStyle: .roman2kana),
ComposingText.InputElement(character: "", inputStyle: .direct)
ComposingText.InputElement(character: "", inputStyle: .frozen)
])
XCTAssertEqual(c.convertTarget, "あかふ")
XCTAssertEqual(c.convertTargetCursorPosition, 3)

View File

@ -0,0 +1,28 @@
@testable import KanaKanjiConverterModule
import XCTest
final class InputStyleManagerTests: XCTestCase {
func testCustomTableLoading() throws {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("custom.tsv")
try "a\t\nka\t\n".write(to: url, atomically: true, encoding: .utf8)
let table = InputStyleManager.shared.table(for: .custom(url))
XCTAssertEqual(table.toHiragana(currentText: [], added: "a"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: ["k"], added: "a"), Array(""))
}
func testCustomTableLoadingWithBlankLines() throws {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("custom.tsv")
try "a\t\n\n\nka\t\n".write(to: url, atomically: true, encoding: .utf8)
let table = InputStyleManager.shared.table(for: .custom(url))
XCTAssertEqual(table.toHiragana(currentText: [], added: "a"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: ["k"], added: "a"), Array(""))
}
func testCustomTableLoadingWithCommentLines() throws {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("custom.tsv")
try "a\t\n# here is comment\nka\t\n".write(to: url, atomically: true, encoding: .utf8)
let table = InputStyleManager.shared.table(for: .custom(url))
XCTAssertEqual(table.toHiragana(currentText: [], added: "a"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: ["k"], added: "a"), Array(""))
}
}

View File

@ -3,24 +3,25 @@ import XCTest
final class Roman2KanaTests: XCTestCase {
func testToHiragana() throws {
let table = InputStyleManager.shared.table(for: .defaultRomanToKana)
// xtsu ->
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array(""), added: "x"), Array("x"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("x"), added: "t"), Array("xt"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("xt"), added: "s"), Array("xts"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("xts"), added: "u"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: Array(""), added: "x"), Array("x"))
XCTAssertEqual(table.toHiragana(currentText: Array("x"), added: "t"), Array("xt"))
XCTAssertEqual(table.toHiragana(currentText: Array("xt"), added: "s"), Array("xts"))
XCTAssertEqual(table.toHiragana(currentText: Array("xts"), added: "u"), Array(""))
// kanto ->
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array(""), added: "k"), Array("k"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("k"), added: "a"), Array(""))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array(""), added: "n"), Array("かn"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("かn"), added: "t"), Array("かんt"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("かんt"), added: "o"), Array("かんと"))
XCTAssertEqual(table.toHiragana(currentText: Array(""), added: "k"), Array("k"))
XCTAssertEqual(table.toHiragana(currentText: Array("k"), added: "a"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: Array(""), added: "n"), Array("かn"))
XCTAssertEqual(table.toHiragana(currentText: Array("かn"), added: "t"), Array("かんt"))
XCTAssertEqual(table.toHiragana(currentText: Array("かんt"), added: "o"), Array("かんと"))
// zl ->
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array(""), added: "z"), Array("z"))
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("z"), added: "l"), Array(""))
XCTAssertEqual(table.toHiragana(currentText: Array(""), added: "z"), Array("z"))
XCTAssertEqual(table.toHiragana(currentText: Array("z"), added: "l"), Array(""))
// TT -> TT
XCTAssertEqual(Roman2Kana.toHiragana(currentText: Array("T"), added: "T"), Array("TT"))
XCTAssertEqual(table.toHiragana(currentText: Array("T"), added: "T"), Array("TT"))
}
}

View File

@ -75,6 +75,29 @@ final class ConverterTests: XCTestCase {
}
}
func testAzikFullConversion() async throws {
for needTypoCorrection in [true, false] {
do {
let converter = await KanaKanjiConverter()
var c = ComposingText()
// -> , sk -> , dq , kf -> , ds:
c.insertAtCursorPosition("azukihaskzidqnokfbodoapurids", inputStyle: .mapped(id: .defaultAZIK))
XCTAssertEqual(c.convertTarget, "あずーきーはしんじだいのきーぼーどあぷりです")
let results = await converter.requestCandidates(c, options: requestOptions(needTypoCorrection: needTypoCorrection))
XCTAssertEqual(results.mainResults.first?.text, "azooKeyは新時代のキーボードアプリです")
}
do {
let converter = await KanaKanjiConverter()
var c = ComposingText()
// yp -> , xp -> , kf -> , kr -> , kyh -> , rk -> , kd -> , pp -> , -> , kw -> , gr -> , -> , kp -> , dq -> , sz -> , kk -> , tq -> , zq -> , tw ->
c.insertAtCursorPosition("ypxpkfkrtenisusuieiyakyhxprkzikdppnadosamazamanasupotuwokwkdsinagrsodatixpgakpzidqharoszzerusukkkpnitqzqsiteorigoruhuyatenisuwonaratwta", inputStyle: .mapped(id: .defaultAZIK))
XCTAssertEqual(c.convertTarget, "ようしょうきからてにすすいえいやきゅうしょうりんじけんぽうなどさまざまなすぽーつをけいけんしながらそだちしょうがっこうじだいはろさんぜるすきんこうにたいざいしておりごるふやてにすをならっていた")
let results = await converter.requestCandidates(c, options: requestOptions(needTypoCorrection: needTypoCorrection))
XCTAssertEqual(results.mainResults.first?.text, "幼少期からテニス水泳野球少林寺拳法など様々なスポーツを経験しながら育ち小学校時代はロサンゼルス近郊に滞在しておりゴルフやテニスを習っていた")
}
}
}
// 1
// memo:
func testGradualConversion() async throws {

View File

@ -331,7 +331,7 @@ final class DicdataStoreTests: XCTestCase {
}
func testPossibleNexts() throws {
let possibleNexts = DicdataStore.possibleNexts
let possibleNexts = InputStyleManager.shared.table(for: .defaultRomanToKana).possibleNexts
XCTAssertEqual(Set(possibleNexts["f", default: []]).symmetricDifference(["ファ", "フィ", "", "フェ", "フォ", "フャ", "フュ", "フョ", "フゥ", "ッf"]), [])
XCTAssertEqual(Set(possibleNexts["xy", default: []]).symmetricDifference(["", "", ""]), [])
XCTAssertEqual(possibleNexts["", default: []], [])

View File

@ -24,18 +24,6 @@ final class CharacterUtilsTests: XCTestCase {
XCTAssertFalse(CharacterUtils.isKogana("!"))
}
func testIsRomanLetter() throws {
XCTAssertTrue(CharacterUtils.isRomanLetter("a"))
XCTAssertTrue(CharacterUtils.isRomanLetter("A"))
XCTAssertTrue(CharacterUtils.isRomanLetter("b"))
XCTAssertFalse(CharacterUtils.isRomanLetter(""))
XCTAssertFalse(CharacterUtils.isRomanLetter("'"))
XCTAssertFalse(CharacterUtils.isRomanLetter(""))
XCTAssertFalse(CharacterUtils.isRomanLetter(""))
XCTAssertFalse(CharacterUtils.isRomanLetter("!"))
}
func testIsDakuten() throws {
XCTAssertTrue(CharacterUtils.isDakuten(""))
XCTAssertTrue(CharacterUtils.isDakuten(""))