Skip to content

Commit

Permalink
Merge pull request #55 from mtgto/number-entry
Browse files Browse the repository at this point in the history
数値変換の簡易な対応
  • Loading branch information
mtgto authored Nov 5, 2023
2 parents 22d839c + be75a79 commit fb834e3
Showing 21 changed files with 547 additions and 121 deletions.
20 changes: 16 additions & 4 deletions macSKK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -10,11 +10,14 @@
CE06CA2B2AAC171B00E80E5E /* FileDictTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE06CA292AAC171B00E80E5E /* FileDictTests.swift */; };
CE06CA2D2AAC172F00E80E5E /* empty.txt in Resources */ = {isa = PBXBuildFile; fileRef = CE06CA2C2AAC172F00E80E5E /* empty.txt */; };
CE06CA342AAC199500E80E5E /* UserDict+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE06CA332AAC199500E80E5E /* UserDict+Utilities.swift */; };
CE313A1F2AF5213700A49142 /* Candidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE313A1E2AF5213700A49142 /* Candidate.swift */; };
CE39DB212A8DFD8F00BC619F /* MarkedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE39DB202A8DFD8F00BC619F /* MarkedText.swift */; };
CE40D9A12A6D0C2F00D44799 /* SystemDictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE40D9A02A6D0C2F00D44799 /* SystemDictView.swift */; };
CE40D9A32A6D0C3900D44799 /* SystemDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE40D9A22A6D0C3900D44799 /* SystemDict.swift */; };
CE485A882A8FA195008271EF /* Release+UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE485A872A8FA195008271EF /* Release+UNNotification.swift */; };
CE485A8A2A8FA5C6008271EF /* UserNotificationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE485A892A8FA5C6008271EF /* UserNotificationDelegate.swift */; };
CE4CB5CC2AD557D90046FA34 /* NumberEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4CB5CB2AD557D90046FA34 /* NumberEntry.swift */; };
CE4CB5CE2AD55DF90046FA34 /* NumberEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4CB5CD2AD55DF90046FA34 /* NumberEntryTests.swift */; };
CE5EB6AD2AAC0DE000389B98 /* FileDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5EB6AC2AAC0DE000389B98 /* FileDict.swift */; };
CE5EB6AF2AAC0DEB00389B98 /* MemoryDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5EB6AE2AAC0DEB00389B98 /* MemoryDict.swift */; };
CE5ECF362957034B00E7BE7D /* macSKKApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5ECF352957034B00E7BE7D /* macSKKApp.swift */; };
@@ -65,7 +68,7 @@
CED7CA5B2A83DE7F004EF988 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED7CA5A2A83DE7F004EF988 /* SettingsViewModel.swift */; };
CED7F51F2AB5F4A7007FC6BD /* Character+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED7F51E2AB5F4A7007FC6BD /* Character+Additions.swift */; };
CEE2D9772A99FE1B00A4CD76 /* Word.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE2D9762A99FE1B00A4CD76 /* Word.swift */; };
CEE2D9792A99FEC700A4CD76 /* WordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE2D9782A99FEC700A4CD76 /* WordTest.swift */; };
CEE2D9792A99FEC700A4CD76 /* CandidateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE2D9782A99FEC700A4CD76 /* CandidateTest.swift */; };
CEE3717529653112000DB2C3 /* SoftwareUpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE3717429653112000DB2C3 /* SoftwareUpdateView.swift */; };
CEF0823629685C0800646366 /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0823529685C0800646366 /* StateTests.swift */; };
CEF0823A296BCDB000646366 /* InputModePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF08239296BCDB000646366 /* InputModePanel.swift */; };
@@ -109,11 +112,14 @@
CE06CA292AAC171B00E80E5E /* FileDictTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileDictTests.swift; sourceTree = "<group>"; };
CE06CA2C2AAC172F00E80E5E /* empty.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = empty.txt; sourceTree = "<group>"; };
CE06CA332AAC199500E80E5E /* UserDict+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDict+Utilities.swift"; sourceTree = "<group>"; };
CE313A1E2AF5213700A49142 /* Candidate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Candidate.swift; sourceTree = "<group>"; };
CE39DB202A8DFD8F00BC619F /* MarkedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedText.swift; sourceTree = "<group>"; };
CE40D9A02A6D0C2F00D44799 /* SystemDictView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemDictView.swift; sourceTree = "<group>"; };
CE40D9A22A6D0C3900D44799 /* SystemDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemDict.swift; sourceTree = "<group>"; };
CE485A872A8FA195008271EF /* Release+UNNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Release+UNNotification.swift"; sourceTree = "<group>"; };
CE485A892A8FA5C6008271EF /* UserNotificationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationDelegate.swift; sourceTree = "<group>"; };
CE4CB5CB2AD557D90046FA34 /* NumberEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberEntry.swift; sourceTree = "<group>"; };
CE4CB5CD2AD55DF90046FA34 /* NumberEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberEntryTests.swift; sourceTree = "<group>"; };
CE5EB6AC2AAC0DE000389B98 /* FileDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDict.swift; sourceTree = "<group>"; };
CE5EB6AE2AAC0DEB00389B98 /* MemoryDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDict.swift; sourceTree = "<group>"; };
CE5ECF322957034B00E7BE7D /* macSKK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macSKK.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -172,7 +178,7 @@
CED7CA5A2A83DE7F004EF988 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
CED7F51E2AB5F4A7007FC6BD /* Character+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Character+Additions.swift"; sourceTree = "<group>"; };
CEE2D9762A99FE1B00A4CD76 /* Word.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Word.swift; sourceTree = "<group>"; };
CEE2D9782A99FEC700A4CD76 /* WordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordTest.swift; sourceTree = "<group>"; };
CEE2D9782A99FEC700A4CD76 /* CandidateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidateTest.swift; sourceTree = "<group>"; };
CEE3717429653112000DB2C3 /* SoftwareUpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateView.swift; sourceTree = "<group>"; };
CEF0823529685C0800646366 /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
CEF08239296BCDB000646366 /* InputModePanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputModePanel.swift; sourceTree = "<group>"; };
@@ -241,6 +247,7 @@
CE84A3DB2957174D009394C4 /* State.swift */,
CE84A3DF295717CB009394C4 /* StateMachine.swift */,
CEE2D9762A99FE1B00A4CD76 /* Word.swift */,
CE4CB5CB2AD557D90046FA34 /* NumberEntry.swift */,
CE84A3EA295DA715009394C4 /* Dict.swift */,
CE5EB6AC2AAC0DE000389B98 /* FileDict.swift */,
CE5EB6AE2AAC0DEB00389B98 /* MemoryDict.swift */,
@@ -267,6 +274,7 @@
CE6DBAC62A85BF3E00F5A227 /* Localizable.strings */,
CEB0888B2A7F342000EFD1E3 /* Credits.rtf */,
CE5ECF3B2957034D00E7BE7D /* Preview Content */,
CE313A1E2AF5213700A49142 /* Candidate.swift */,
);
path = macSKK;
sourceTree = "<group>";
@@ -284,7 +292,8 @@
isa = PBXGroup;
children = (
CE84A3E8295DA504009394C4 /* RomajiTests.swift */,
CEE2D9782A99FEC700A4CD76 /* WordTest.swift */,
CEE2D9782A99FEC700A4CD76 /* CandidateTest.swift */,
CE4CB5CD2AD55DF90046FA34 /* NumberEntryTests.swift */,
CE84A3EC295DA818009394C4 /* MemoryDictTests.swift */,
CE06CA292AAC171B00E80E5E /* FileDictTests.swift */,
CEA78FB129646CA100B67E25 /* UserDictTests.swift */,
@@ -540,12 +549,14 @@
CED7CA3C2A839603004EF988 /* UpdateChecker.swift in Sources */,
CE5ECF362957034B00E7BE7D /* macSKKApp.swift in Sources */,
CEC061C82ABB0A0100A11614 /* CompletionPanel.swift in Sources */,
CE4CB5CC2AD557D90046FA34 /* NumberEntry.swift in Sources */,
CE84A3DE29571797009394C4 /* Action.swift in Sources */,
CE485A882A8FA195008271EF /* Release+UNNotification.swift in Sources */,
CED7CA3A2A839505004EF988 /* FetchUpdateServiceProtocol.swift in Sources */,
CEA78FB02964209B00B67E25 /* UserDict.swift in Sources */,
CEF08257296D8CBD00646366 /* CandidatesPanel.swift in Sources */,
CE97887B2A9B93EB00F9B196 /* DirectModeView.swift in Sources */,
CE313A1F2AF5213700A49142 /* Candidate.swift in Sources */,
CE6DBA8F2A845E8B00F5A227 /* ReleaseVersion.swift in Sources */,
CEB088902A7F73B400EFD1E3 /* Pasteboard.swift in Sources */,
CE84A3DC2957174D009394C4 /* State.swift in Sources */,
@@ -569,8 +580,9 @@
CE84A3ED295DA818009394C4 /* MemoryDictTests.swift in Sources */,
CE06CA342AAC199500E80E5E /* UserDict+Utilities.swift in Sources */,
CE84A3E9295DA504009394C4 /* RomajiTests.swift in Sources */,
CE4CB5CE2AD55DF90046FA34 /* NumberEntryTests.swift in Sources */,
CEA78FAA295EBCAC00B67E25 /* StateMachineTests.swift in Sources */,
CEE2D9792A99FEC700A4CD76 /* WordTest.swift in Sources */,
CEE2D9792A99FEC700A4CD76 /* CandidateTest.swift in Sources */,
CEF0823629685C0800646366 /* StateTests.swift in Sources */,
CED7CA3E2A8397E4004EF988 /* UpdateCheckerTests.swift in Sources */,
CEA78FB229646CA100B67E25 /* UserDictTests.swift in Sources */,
74 changes: 74 additions & 0 deletions macSKK/Candidate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2023 mtgto <hogerappa@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/**
* 変換候補
*/
struct Candidate: Hashable {
/**
* 辞書上での表記
*/
struct Original: Hashable {
/**
* 辞書上の見出し語の表記。
* 数値変換の場合 "だい#" のような数値部分を "#" で表す表記がされている。
*/
let midashi: String
/**
* 辞書上の変換結果の表記。
* 数値変換の場合 "第#1" のような変換フォーマットを表す表記がされている。
*/
let word: Word.Word
}

/**
* 変換結果。数値変換の場合は例外あり。
* 数値変換の場合、辞書には "第#1" のように登録されているが "第5" のようにユーザー入力で置換されている。
*/
let word: Word.Word

/**
* 辞書上の表記。現在は数値変換時のみ設定される。
*/
let original: Original?

/**
* 注釈。複数の辞書によるものがあればまとめられている。
*/
private(set) var annotations: [Annotation]

/**
* 辞書に登録されている読み。
*/
func toMidashiString(yomi: String) -> String {
original?.midashi ?? yomi
}

/**
* 辞書に登録されている変換候補。
*/
var candidateString: String {
original?.word ?? word
}

init(_ word: Word.Word, annotations: [Annotation] = [], original: Original? = nil) {
self.word = word
self.annotations = annotations
self.original = original
}

func hash(into hasher: inout Hasher) {
hasher.combine(word)
}

/// 注釈を追加する。すでに同じテキストをもつ注釈があれば追加されない。
mutating func appendAnnotations(_ annotations: [Annotation]) {
for annotation in annotations {
if self.annotations.allSatisfy({ $0.text != annotation.text }) {
self.annotations.append(annotation)
}
}
}
}
4 changes: 4 additions & 0 deletions macSKK/Character+Additions.swift
Original file line number Diff line number Diff line change
@@ -10,4 +10,8 @@ extension Character {
var isAlphabet: Bool {
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(self)
}

var isNumber: Bool {
"0123456789".contains(self)
}
}
1 change: 1 addition & 0 deletions macSKK/Dict.swift
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ protocol DictProtocol {
* - prefixが空文字列ならnilを返す
* - ユーザー辞書の送りなしの読みのうち、最近変換したものから選択する。
* - prefixと読みが完全に一致する場合は補完候補とはしない
* - 数値変換用の読みは補完候補としない
*/
func findCompletion(prefix: String) -> String?
}
2 changes: 1 addition & 1 deletion macSKK/MemoryDict.swift
Original file line number Diff line number Diff line change
@@ -160,7 +160,7 @@ struct MemoryDict: DictProtocol {
func findCompletion(prefix: String) -> String? {
if !prefix.isEmpty {
for yomi in okuriNashiYomis.reversed() {
if yomi.count > prefix.count && yomi.hasPrefix(prefix) {
if yomi.count > prefix.count && yomi.hasPrefix(prefix) && !yomi.contains(where: { $0 == "#" }) {
return yomi
}
}
172 changes: 172 additions & 0 deletions macSKK/NumberEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-FileCopyrightText: 2023 mtgto <hogerappa@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

/**
* 数値変換用のユーザーが入力した読み部分。"だい1" のような入力を受け取り、整数部分と非整数部分に分ける。
*/
struct NumberYomi {
static let pattern = /#([01234589])/

// TODO: Stringを作るコストをなくしyomiのSubstringでもっておく。Substringだとユニットテストが書きにくくなるしこれでもいいかも。
enum Element: Equatable {
/// 正規表現だと /[0-9]+/ となる文字列。UInt64で収まらない数値はあきらめる
case number(UInt64)
/// ``number`` 以外
case other(String)
}

let elements: [Element]

/**
* ユーザーが入力した読み部分を受け取り初期化する。
*
* - Returns: 整数が一つも含まれない (整数が異常に巨大な場合を含む) ときは nil。
*/
init?(_ yomi: String) {
// 連続する整数と連続する非整数をパースする
var elements: [Element] = []
var current = yomi[...]
var containsNumber: Bool = false
while !current.isEmpty {
let numberString = current.prefix(while: { $0.isNumber })
if !numberString.isEmpty {
if let number = UInt64(numberString) {
elements.append(.number(number))
current = yomi.suffix(from: numberString.endIndex)
containsNumber = true
} else {
logger.log("巨大な数値が含まれているためパースできませんでした")
return nil
}
}
let other = current.prefix(while: { !$0.isNumber })
if !other.isEmpty {
elements.append(.other(String(other)))
current = yomi.suffix(from: other.endIndex)
}
}
self.elements = elements
if !containsNumber {
return nil
}
}

/**
* 辞書の見出し語となる文字列に変換して返す
*/
func toMidashiString() -> String {
return elements.map { element in
switch element {
case .number:
return "#"
case .other(let string):
return string
}
}.joined()
}
}

// 数値入りの変換候補
struct NumberCandidate {
static let pattern = /#([01234589])/
static let kanjiFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "ja-JP")
formatter.numberStyle = .spellOut
return formatter
}()

enum Element: Equatable {
/// 正規表現だと /#[01234589]/ となる文字列。引数は数値部分
case number(Int)
/// ``number`` 以外
case other(String)
}

let elements: [Element]

init(yomi: String) throws {
var result: [Element] = []
var current = yomi[...]
while !current.isEmpty {
if let match = try Self.pattern.firstMatch(in: current) {
let prefix = current.prefix(upTo: match.range.lowerBound)
if !prefix.isEmpty {
result.append(.other(String(prefix)))
}
if let type = Int(match.1) {
result.append(.number(type))
}
current = current.suffix(from: match.range.upperBound)
} else {
break
}
}
if !current.isEmpty {
result.append(.other(String(current)))
}
self.elements = result
}

/**
* 数値入りの変換候補を読みに含まれる数値の情報を使って文字列に変換します
*
* もし読みと変換候補で数値情報の数に差があったり文字列に変換できない数値を含む場合はnilを返します
*/
func toString(yomi: NumberYomi) -> String? {
var result: String = ""
if yomi.elements.count != elements.count {
return nil
}
for (i, yomiElement) in yomi.elements.enumerated() {
switch yomiElement {
case .number(let number):
if case .number(let type) = elements[i] {
switch type {
case 0:
result.append(String(number))
case 1: // 全角
result.append(String(number).toZenkaku())
case 2: // 漢数字(位取りあり)
result.append(toKanjiString(number: number))
case 3: // 漢数字(位取りなし)
result.append(Self.kanjiFormatter.string(from: NSNumber(value: number))!)
case 4: // 数字部分で辞書を引く
// TODO: あとで対応する
return nil
case 5: // 小切手や手形の金額記入の際用いられる表記
// TODO: あとで対応する
return nil
case 8: // 3桁ごとに区切る
result.append(number.formatted(.number))
case 9: // 将棋の棋譜入力用
if number < 10 || number > 99 || number % 10 == 0 {
return nil
}
result.append(String(number / 10).toZenkaku() + toKanjiString(number: number % 10))
default:
fatalError("未サポートの数値変換タイプ \(type) が指定されています")
}
} else {
return nil
}
case .other:
if case .other(let other) = elements[i] {
result.append(other)
} else {
return nil
}
}
}
return result
}

func toKanjiString(number: UInt64) -> String {
if number == 0 {
return ""
}
return toKanjiString(number: number / 10) + ["", "", "", "", "", "", "", "", "", ""][Int(number % 10)]
}
}
Loading

0 comments on commit fb834e3

Please sign in to comment.