From 5e5845889517616962d69e00b60283ac85e2bd40 Mon Sep 17 00:00:00 2001 From: Miwa <63481257+ensan-hcl@users.noreply.github.com> Date: Sun, 25 May 2025 18:40:18 +0900 Subject: [PATCH] Add comma separated number special candidate --- .../Converter/CommaSeparatedNumber.swift | 46 +++++++++++++++++++ .../Converter/ConvertRequestOptions.swift | 1 + .../Converter/KanaKanjiConverter.swift | 3 +- .../Converter/SpecialCandidateProvider.swift | 11 +++++ .../CommaSeparatedNumberTests.swift | 41 +++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 Sources/KanaKanjiConverterModule/Converter/CommaSeparatedNumber.swift create mode 100644 Tests/KanaKanjiConverterModuleTests/ConverterTests/CommaSeparatedNumberTests.swift diff --git a/Sources/KanaKanjiConverterModule/Converter/CommaSeparatedNumber.swift b/Sources/KanaKanjiConverterModule/Converter/CommaSeparatedNumber.swift new file mode 100644 index 0000000..9e238d4 --- /dev/null +++ b/Sources/KanaKanjiConverterModule/Converter/CommaSeparatedNumber.swift @@ -0,0 +1,46 @@ +import Foundation + +extension KanaKanjiConverter { + func commaSeparatedNumberCandidates(_ inputData: ComposingText) -> [Candidate] { + var text = inputData.convertTarget + guard !text.isEmpty else { return [] } + + var negative = false + if text.first == "-" { + negative = true + text.removeFirst() + } + let parts = text.split(separator: ".", omittingEmptySubsequences: false) + guard parts.count <= 2, + parts.allSatisfy({ !$0.isEmpty && $0.allSatisfy({ $0.isNumber && $0.isASCII }) }) else { + return [] + } + let integerPart = parts[0] + guard integerPart.count > 3 else { return [] } + + var reversed = Array(integerPart.reversed()) + var formatted = "" + for (i, ch) in reversed.enumerated() { + if i > 0 && i % 3 == 0 { + formatted.append(",") + } + formatted.append(ch) + } + let integerString = String(formatted.reversed()) + var result = (negative ? "-" : "") + integerString + if parts.count == 2 { + let fractional = parts[1] + result += "." + fractional + } + + let ruby = inputData.convertTarget.toKatakana() + let candidate = Candidate( + text: result, + value: -10, + correspondingCount: inputData.input.count, + lastMid: MIDData.一般.mid, + data: [DicdataElement(word: result, ruby: ruby, cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: -10)] + ) + return [candidate] + } +} diff --git a/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift b/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift index 4723609..a3e3d96 100644 --- a/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift +++ b/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift @@ -83,6 +83,7 @@ public struct ConvertRequestOptions: Sendable { specialCandidateProviders.append(.timeExpression) specialCandidateProviders.append(.calendar) specialCandidateProviders.append(.version) + specialCandidateProviders.append(.commaSeparatedNumber) self.N_best = N_best self.requireJapanesePrediction = requireJapanesePrediction diff --git a/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift b/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift index b04ad9c..1ec8498 100644 --- a/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift +++ b/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift @@ -23,7 +23,8 @@ import EfficientNGram EmailAddressSpecialCandidateProvider(), UnicodeSpecialCandidateProvider(), VersionSpecialCandidateProvider(), - TimeExpressionSpecialCandidateProvider() + TimeExpressionSpecialCandidateProvider(), + CommaSeparatedNumberSpecialCandidateProvider() ] @MainActor private var checker = SpellChecker() private var checkerInitialized: [KeyboardLanguage: Bool] = [.none: true, .ja_JP: true] diff --git a/Sources/KanaKanjiConverterModule/Converter/SpecialCandidateProvider.swift b/Sources/KanaKanjiConverterModule/Converter/SpecialCandidateProvider.swift index f7dd403..54d81c1 100644 --- a/Sources/KanaKanjiConverterModule/Converter/SpecialCandidateProvider.swift +++ b/Sources/KanaKanjiConverterModule/Converter/SpecialCandidateProvider.swift @@ -45,6 +45,13 @@ public struct TimeExpressionSpecialCandidateProvider: SpecialCandidateProvider { } } +public struct CommaSeparatedNumberSpecialCandidateProvider: SpecialCandidateProvider { + public init() {} + @MainActor public func provideCandidates(converter: KanaKanjiConverter, inputData: ComposingText, options _: ConvertRequestOptions) -> [Candidate] { + converter.commaSeparatedNumberCandidates(inputData) + } +} + public extension SpecialCandidateProvider where Self == CalendarSpecialCandidateProvider { static var calendar: Self { .init() } } @@ -68,3 +75,7 @@ public extension SpecialCandidateProvider where Self == VersionSpecialCandidateP public extension SpecialCandidateProvider where Self == TimeExpressionSpecialCandidateProvider { static var timeExpression: Self { .init() } } + +public extension SpecialCandidateProvider where Self == CommaSeparatedNumberSpecialCandidateProvider { + static var commaSeparatedNumber: Self { .init() } +} diff --git a/Tests/KanaKanjiConverterModuleTests/ConverterTests/CommaSeparatedNumberTests.swift b/Tests/KanaKanjiConverterModuleTests/ConverterTests/CommaSeparatedNumberTests.swift new file mode 100644 index 0000000..c08b36e --- /dev/null +++ b/Tests/KanaKanjiConverterModuleTests/ConverterTests/CommaSeparatedNumberTests.swift @@ -0,0 +1,41 @@ +import XCTest +@testable import KanaKanjiConverterModule + +final class CommaSeparatedNumberTests: XCTestCase { + private func makeDirectInput(direct input: String) -> ComposingText { + ComposingText( + convertTargetCursorPosition: input.count, + input: input.map { .init(character: $0, inputStyle: .direct) }, + convertTarget: input + ) + } + + func testCommaSeparatedNumberCandidates() async throws { + let converter = await KanaKanjiConverter() + + func result(_ text: String) async -> [Candidate] { + await converter.commaSeparatedNumberCandidates(makeDirectInput(direct: text)) + } + + let r1 = await result("49000") + XCTAssertEqual(r1.first?.text, "49,000") + + let r2 = await result("109428081") + XCTAssertEqual(r2.first?.text, "109,428,081") + + let r3 = await result("2129.49") + XCTAssertEqual(r3.first?.text, "2,129.49") + + let r4 = await result("-13932") + XCTAssertEqual(r4.first?.text, "-13,932") + + let r5 = await result("12") + XCTAssertTrue(r5.isEmpty) + + let r6 = await result("1A9B") + XCTAssertTrue(r6.isEmpty) + + let r7 = await result("123") + XCTAssertTrue(r7.isEmpty) + } +}