Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Core/Sources/Core/InputUtils/NumericSeparatorCandidates.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
enum NumericSeparatorCandidates {
/// 数字の区切りと単独の中黒に、記号の別表記を追加する。
static func variants(for reading: String) -> [String] {
if reading == "・" {
return ["/", "/"]
}
let separator: Character
let replacement: String
if reading.contains("。") {
separator = "。"
replacement = "."
} else if reading.contains("・") {
separator = "・"
replacement = "/"
} else {
return []
}
let groups = reading.split(separator: separator, omittingEmptySubsequences: false)
var normalized: [String] = []
for group in groups {
guard !group.isEmpty else {
return []
}
var digits = ""
for scalar in group.unicodeScalars {
switch scalar.value {
case 0x30...0x39:
digits.unicodeScalars.append(scalar)
case 0xFF10...0xFF19:
digits.unicodeScalars.append(UnicodeScalar(scalar.value - 0xFEE0)!)
default:
return []
}
}
normalized.append(digits)
}
return [normalized.joined(separator: replacement)]
}
}
17 changes: 16 additions & 1 deletion Core/Sources/Core/InputUtils/SegmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ public final class SegmentsManager {

let leftSideContext = forcedLeftSideContext ?? self.getCleanLeftSideContext(maxCount: ContextLength.conversion)
let rightSideContext = forcedRightSideContext ?? self.getCleanRightSideContext(maxCount: ContextLength.conversion)
let result = self.kanaKanjiConverter.requestCandidates(
var result = self.kanaKanjiConverter.requestCandidates(
self.composingText,
options: options(
leftSideContext: leftSideContext,
Expand All @@ -574,6 +574,21 @@ public final class SegmentsManager {
requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled
)
)
let separatorInput = self.composingText.prefixToCursorPosition()
let separatorVariants = NumericSeparatorCandidates.variants(for: separatorInput.convertTarget)
if !separatorVariants.isEmpty {
let separatorTexts = Set(result.mainResults.map(\.text))
let separatorCandidates = separatorVariants.filter { !separatorTexts.contains($0) }.map { text in
Candidate(
text: text, value: -18,
composingCount: .inputCount(separatorInput.input.count),
lastMid: MIDData.一般.mid,
data: [.init(word: text, ruby: separatorInput.convertTarget.toKatakana(), cid: CIDData.記号.cid, mid: MIDData.一般.mid, value: -18)],
isLearningTarget: false
)
}
result.mainResults.insert(contentsOf: separatorCandidates, at: min(5, result.mainResults.count))
}
self.rawCandidates = result
}

Expand Down
23 changes: 23 additions & 0 deletions Core/Tests/CoreTests/InputUtilsTests/EditedVariantRangeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Core
import Foundation
import KanaKanjiConverterModuleWithDefaultDictionary
import Testing

@MainActor
@Test func editedVariantConsumesOnlySelectedPrefix() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let manager = SegmentsManager(kanaKanjiConverter: .withDefaultDictionary(), applicationDirectoryURL: directory,
containerURL: nil, context: .init(useZenzai: false))
manager.insertAtCursorPosition("10・10", inputStyle: .direct)
manager.editSegment(count: -1)
manager.requestSetCandidateWindowState(visible: true)
guard case .selecting(let choices, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else {
Issue.record("Expected candidates for edited segment")
return
}
let candidate = try #require(choices.first { $0.text == "10/1" })
manager.prefixCandidateCommited(candidate, leftSideContext: "")
#expect(manager.convertTarget == "0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@testable import Core
import Foundation
import KanaKanjiConverterModuleWithDefaultDictionary
import Testing

@MainActor
struct NumericSeparatorCandidatesTests {
@Test func numericGroupsAndStandaloneMiddleDot() {
#expect(NumericSeparatorCandidates.variants(for: "5。1") == ["5.1"])
#expect(NumericSeparatorCandidates.variants(for: "5。1") == ["5.1"])
#expect(NumericSeparatorCandidates.variants(for: "10・10") == ["10/10"])
#expect(NumericSeparatorCandidates.variants(for: "2026・9・05") == ["2026/9/05"])
#expect(NumericSeparatorCandidates.variants(for: "1。2。3") == ["1.2.3"])
#expect(NumericSeparatorCandidates.variants(for: "・") == ["/", "/"])
for input in ["", "。", "5。", "。1", "10・・10", "1。2・3", "文章。", "名前・名前", "10/10", "5.1", "10", "5 。1"] {
#expect(NumericSeparatorCandidates.variants(for: input).isEmpty)
}
}

@Test(arguments: [("5。1", "5.1"), ("10・10", "10/10"), ("・", "/"), ("・", "/"), ("5。1", "5.1")])
func candidatesPreserveInputAndCommitWholeValue(input: String, expected: String) throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let manager = SegmentsManager(kanaKanjiConverter: .withDefaultDictionary(), applicationDirectoryURL: directory,
containerURL: nil, context: .init(useZenzai: false))
manager.insertAtCursorPosition(input, inputStyle: .direct)
for rich in [false, true] {
manager.update(requestRichCandidates: rich)
manager.requestSetCandidateWindowState(visible: true)
guard case .selecting(let choices, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else {
Issue.record("Expected conversion candidates")
return
}
#expect(manager.convertTarget == input)
#expect(choices.contains { $0.text == input })
#expect(choices.filter { $0.text == expected }.count == 1)
if rich {
let row = try #require(choices.firstIndex { $0.text == expected })
manager.requestSelectingRow(row)
let candidate = try #require(manager.selectedCandidate)
manager.prefixCandidateCommited(candidate, leftSideContext: "")
#expect(manager.isEmpty)
}
}
}
}