From 3fb04264e577004d8a2d0451cbf4f406ced9b725 Mon Sep 17 00:00:00 2001 From: Yukihiro Shinoda Date: Sat, 5 Sep 2026 12:38:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(input):=20=E6=9D=A5=E9=80=B1=E3=81=AE?= =?UTF-8?q?=E6=9B=9C=E6=97=A5=E3=83=BB=E6=9C=88=E6=9C=AB=E3=83=BB=E6=97=A5?= =?UTF-8?q?=E6=95=B0=E5=BE=8C=E3=81=AE=E6=97=A5=E4=BB=98=E5=80=99=E8=A3=9C?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InputUtils/RelativeDateShortcuts.swift | 86 ++++++++++++ .../Core/InputUtils/SegmentsManager.swift | 17 ++- .../RelativeDateShortcutsTests.swift | 123 ++++++++++++++++++ README.md | 2 + 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift create mode 100644 Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift diff --git a/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift b/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift new file mode 100644 index 00000000..9ff952e2 --- /dev/null +++ b/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift @@ -0,0 +1,86 @@ +import Foundation +import KanaKanjiConverterModuleWithDefaultDictionary + +enum RelativeDateShortcuts { + // 暦日の加算は最大10年程度に限定し、桁あふれや意図しない巨大な日付を避ける。 + static let maximumDaysAhead = 3660 + + static func date(matching ruby: String, now: Date = Date(), calendar: Calendar = .current) -> Date? { + let today = calendar.startOfDay(for: now) + if ruby == "ゲツマツ" { + guard let month = calendar.dateInterval(of: .month, for: today) else { + return nil + } + return calendar.date(byAdding: .day, value: -1, to: month.end) + } + let prefix = "ライシュウノ" + if ruby.hasPrefix(prefix) { + let weekdayReading = String(ruby.dropFirst(prefix.count)) + let weekdays = ["ゲツヨウ", "カヨウ", "スイヨウ", "モクヨウ", "キンヨウ", "ドヨウ", "ニチヨウ"] + guard let weekday = weekdays.firstIndex(where: { weekdayReading == $0 || weekdayReading == $0 + "ビ" }) else { + return nil + } + // Calendar.firstWeekday(地域設定)によらず、月曜を週の始まりとする。 + let daysSinceMonday = (calendar.component(.weekday, from: today) + 5) % 7 + return calendar.date(byAdding: .day, value: 7 - daysSinceMonday + weekday, to: today) + } + guard let days = daysAhead(matching: ruby) else { + return nil + } + return calendar.date(byAdding: .day, value: days, to: today) + } + + private static func daysAhead(matching ruby: String) -> Int? { + let suffix = "ニチゴ" + guard ruby.hasSuffix(suffix) else { + return nil + } + let digits = ruby.dropLast(suffix.count).unicodeScalars + guard !digits.isEmpty, digits.count <= 4 else { + return nil + } + var days = 0 + for scalar in digits { + let value: UInt32 + switch scalar.value { + case 0x30...0x39: value = scalar.value - 0x30 + case 0xFF10...0xFF19: value = scalar.value - 0xFF10 + default: return nil + } + days = days * 10 + Int(value) + } + guard days <= maximumDaysAhead else { + return nil + } + return days + } + + private struct Format { + let pattern: String + let value: PValue + let calendar: Calendar.Identifier + } + + static func candidates(matching ruby: String, now: Date = Date(), calendar: Calendar = .current) -> [DicdataElement] { + guard let date = date(matching: ruby, now: now, calendar: calendar) else { + return [] + } + let formats: [Format] = [ + .init(pattern: "M/d", value: -18, calendar: .gregorian), + .init(pattern: "yyyy/MM/dd", value: -18.1, calendar: .gregorian), + .init(pattern: "yyyy-MM-dd", value: -18.2, calendar: .gregorian), + .init(pattern: "M月d日(E)", value: -18.3, calendar: .gregorian), + .init(pattern: "yyyy年M月d日", value: -18.4, calendar: .gregorian), + .init(pattern: "Gyyyy年M月d日", value: -18.5, calendar: .japanese), + .init(pattern: "E曜日", value: -18.6, calendar: .gregorian) + ] + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ja_JP") + formatter.timeZone = calendar.timeZone + return formats.map { format in + formatter.calendar = Calendar(identifier: format.calendar) + formatter.dateFormat = format.pattern + return DicdataElement(word: formatter.string(from: date), ruby: ruby, cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: format.value) + } + } +} diff --git a/Core/Sources/Core/InputUtils/SegmentsManager.swift b/Core/Sources/Core/InputUtils/SegmentsManager.swift index 3188ca36..822e1648 100644 --- a/Core/Sources/Core/InputUtils/SegmentsManager.swift +++ b/Core/Sources/Core/InputUtils/SegmentsManager.swift @@ -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, @@ -574,6 +574,21 @@ public final class SegmentsManager { requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled ) ) + let relativeDates = RelativeDateShortcuts.candidates(matching: composingText.convertTarget.toKatakana()) + if !relativeDates.isEmpty { + let existingTexts = Set(result.mainResults.map(\.text)) + let candidates = relativeDates.filter { !existingTexts.contains($0.word) }.map { element in + Candidate( + text: element.word, + value: element.value(), + composingCount: .surfaceCount(composingText.convertTarget.count), + lastMid: element.mid, + data: [element], + isLearningTarget: false + ) + } + result.mainResults.insert(contentsOf: candidates, at: min(5, result.mainResults.count)) + } self.rawCandidates = result } diff --git a/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift b/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift new file mode 100644 index 00000000..34f289c3 --- /dev/null +++ b/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift @@ -0,0 +1,123 @@ +@testable import Core +import Foundation +import KanaKanjiConverterModuleWithDefaultDictionary +import Testing + +@Suite("Relative date shortcuts") +struct RelativeDateShortcutsTests { + private func calendar(_ zone: String = "Asia/Tokyo") -> Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: zone)! + return calendar + } + + private func date(_ year: Int, _ month: Int, _ day: Int, hour: Int = 12, calendar: Calendar) -> Date { + calendar.date(from: DateComponents(year: year, month: month, day: day, hour: hour))! + } + + @Test func nextWeekStartsOnMondayForEveryCurrentWeekday() { + var calendar = calendar() + calendar.firstWeekday = 1 + let readings = ["ゲツヨウ", "カヨウ", "スイヨウ", "モクヨウ", "キンヨウ", "ドヨウ", "ニチヨウ"] + for currentDay in 7...13 { + let now = date(2026, 9, currentDay, calendar: calendar) + for (index, reading) in readings.enumerated() { + for suffix in ["", "ビ"] { + let actual = RelativeDateShortcuts.date(matching: "ライシュウノ" + reading + suffix, now: now, calendar: calendar) + #expect(actual == date(2026, 9, 14 + index, hour: 0, calendar: calendar)) + } + } + } + } + + @Test(arguments: [(2026, 2, 15, 28), (2028, 2, 15, 29), (2026, 4, 30, 30), (2026, 12, 31, 31)]) + func monthEnd(year: Int, month: Int, day: Int, lastDay: Int) { + let calendar = calendar() + let actual = RelativeDateShortcuts.date(matching: "ゲツマツ", now: date(year, month, day, calendar: calendar), calendar: calendar) + #expect(actual == date(year, month, lastDay, hour: 0, calendar: calendar)) + } + + @Test func dayOffsets() { + let cases = [ + (2026, 9, 30, "3ニチゴ", 2026, 10, 3), + (2026, 12, 31, "1ニチゴ", 2027, 1, 1), + (2028, 2, 28, "1ニチゴ", 2028, 2, 29), + (2026, 9, 5, "0ニチゴ", 2026, 9, 5), + (2026, 9, 5, "01ニチゴ", 2026, 9, 6), + (2026, 9, 5, "0003ニチゴ", 2026, 9, 8) + ] + let calendar = calendar() + for (year, month, day, reading, targetYear, targetMonth, targetDay) in cases { + let actual = RelativeDateShortcuts.date(matching: reading, now: date(year, month, day, calendar: calendar), calendar: calendar) + #expect(actual == date(targetYear, targetMonth, targetDay, hour: 0, calendar: calendar)) + } + } + + @Test func maximumOffsetAndYearBoundary() { + let calendar = calendar() + let now = date(2026, 12, 31, calendar: calendar) + #expect(RelativeDateShortcuts.date(matching: "3660ニチゴ", now: now, calendar: calendar) == calendar.date(byAdding: .day, value: 3660, to: calendar.startOfDay(for: now))) + #expect(RelativeDateShortcuts.date(matching: "ライシュウノゲツヨウ", now: now, calendar: calendar) == date(2027, 1, 4, hour: 0, calendar: calendar)) + } + + @Test(arguments: [(2026, 3, 7, 9), (2026, 10, 31, 2)]) + func daylightSaving(year: Int, month: Int, day: Int, targetDay: Int) { + let calendar = calendar("America/Los_Angeles") + let now = date(year, month, day, hour: 23, calendar: calendar) + let targetMonth = month == 10 ? 11 : month + let target = date(year, targetMonth, targetDay, hour: 0, calendar: calendar) + #expect(RelativeDateShortcuts.date(matching: "2ニチゴ", now: now, calendar: calendar) == target) + #expect(RelativeDateShortcuts.date(matching: "ライシュウノゲツヨウ", now: now, calendar: calendar) == target) + } + + @Test func sevenExistingFormats() { + let calendar = calendar() + let elements = RelativeDateShortcuts.candidates(matching: "3ニチゴ", now: date(2026, 9, 5, calendar: calendar), calendar: calendar) + #expect(elements.map(\.word) == ["9/8", "2026/09/08", "2026-09-08", "9月8日(火)", "2026年9月8日", "令和8年9月8日", "火曜日"]) + #expect(elements.allSatisfy { $0.ruby == "3ニチゴ" }) + } + + @Test(arguments: ["", "ゲツヨウ", "ゲツヨウビ", "キョウ", "ライシュウ", "ライシュウノゲツ", "ライシュウノゲツヨウニ", "ゲツマツニ", "ニチゴ", "-1ニチゴ", "+1ニチゴ", "1.5ニチゴ", "٣ニチゴ", "③ニチゴ", "三ニチゴ", "3661ニチゴ", "999999999999ニチゴ", "00001ニチゴ", "3ニチゴニ", "アト3ニチゴ"]) + func requiresWholeSupportedReading(_ reading: String) { + #expect(RelativeDateShortcuts.candidates(matching: reading).isEmpty) + } + + @MainActor + @Test(arguments: ["らいしゅうのげつよう", "らいしゅうのにちようび", "げつまつ", "3にちご", "3にちご"]) + func dateCanCommitWholeInput(_ reading: String) throws { + try verifyWholeInput(reading, inputStyle: .direct) + } + + @MainActor + @Test(arguments: ["raishuunogetsuyou", "getsumatsu", "3nichigo"]) + func romanDateCanCommitWholeInput(_ input: String) throws { + try verifyWholeInput(input, inputStyle: .roman2kana) + } + + @MainActor + private func verifyWholeInput(_ input: String, inputStyle: InputStyle) throws { + let directory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: directory) } + let manager = SegmentsManager( + kanaKanjiConverter: .withDefaultDictionary(), applicationDirectoryURL: directory, + containerURL: nil, context: .init(useZenzai: false) + ) + manager.insertAtCursorPosition(input, inputStyle: inputStyle) + for rich in [false, true] { + manager.update(requestRichCandidates: rich) + manager.requestSetCandidateWindowState(visible: true) + guard case .selecting(let candidates, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else { + Issue.record("Expected selecting state") + return + } + let candidate = try #require(candidates.first { $0.text.range(of: #"^\d{4}/\d{2}/\d{2}$"#, options: .regularExpression) != nil }) + #expect(!candidate.isLearningTarget) + #expect(candidates.contains { $0.text.range(of: "[年月日/曜日-]", options: .regularExpression) == nil }) + #expect(candidates.filter { $0.text == candidate.text }.count == 1) + if rich { + manager.prefixCandidateCommited(candidate, leftSideContext: "") + #expect(manager.isEmpty) + } + } + } +} diff --git a/README.md b/README.md index e710c7a7..f75830e4 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ GitHub Sponsorsをご利用ください。 ## 機能 +* 相対日付変換:「らいしゅうのげつよう(び)」など全曜日、「げつまつ」、「3にちご」を日付候補に追加します。来週は月曜始まりの次週、月末は今月末、日数は今日基準の0〜3660日(半角・全角数字)です。通常の変換候補も残ります。 + * ニューラルかな漢字変換システム「Zenzai」による高精度な変換 * プロフィールプロンプト機能 * 履歴学習機能 From c6d8d342e504f73b2bb38c21a4dedb7fbfbf6fbb Mon Sep 17 00:00:00 2001 From: Yukihiro Shinoda Date: Sat, 5 Sep 2026 13:03:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(input):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C=E3=81=97?= =?UTF-8?q?=E5=A4=89=E6=8F=9B=E7=AF=84=E5=9B=B2=E3=81=A8=E5=80=99=E8=A3=9C?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/InputUtils/DateShortcutFormats.swift | 20 ++++++++++++ .../InputUtils/RelativeDateShortcuts.swift | 19 ++---------- .../Core/InputUtils/SegmentsManager.swift | 20 ++++++------ .../RelativeDateShortcutsTests.swift | 31 +++++++++++++++++++ README.md | 2 +- 5 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 Core/Sources/Core/InputUtils/DateShortcutFormats.swift diff --git a/Core/Sources/Core/InputUtils/DateShortcutFormats.swift b/Core/Sources/Core/InputUtils/DateShortcutFormats.swift new file mode 100644 index 00000000..c5e96c55 --- /dev/null +++ b/Core/Sources/Core/InputUtils/DateShortcutFormats.swift @@ -0,0 +1,20 @@ +import KanaKanjiConverterModuleWithDefaultDictionary + +/// 日付ショートカットで共通の表記と候補スコア。 +enum DateShortcutFormats { + struct Format { + let pattern: String + let value: PValue + let calendar: DateTemplateLiteral.CalendarType + } + + static let all: [Format] = [ + .init(pattern: "M/d", value: -18, calendar: .western), + .init(pattern: "yyyy/MM/dd", value: -18.1, calendar: .western), + .init(pattern: "yyyy-MM-dd", value: -18.2, calendar: .western), + .init(pattern: "M月d日(E)", value: -18.3, calendar: .western), + .init(pattern: "yyyy年M月d日", value: -18.4, calendar: .western), + .init(pattern: "Gyyyy年M月d日", value: -18.5, calendar: .japanese), + .init(pattern: "E曜日", value: -18.6, calendar: .western) + ] +} diff --git a/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift b/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift index 9ff952e2..c3035b31 100644 --- a/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift +++ b/Core/Sources/Core/InputUtils/RelativeDateShortcuts.swift @@ -55,30 +55,15 @@ enum RelativeDateShortcuts { return days } - private struct Format { - let pattern: String - let value: PValue - let calendar: Calendar.Identifier - } - static func candidates(matching ruby: String, now: Date = Date(), calendar: Calendar = .current) -> [DicdataElement] { guard let date = date(matching: ruby, now: now, calendar: calendar) else { return [] } - let formats: [Format] = [ - .init(pattern: "M/d", value: -18, calendar: .gregorian), - .init(pattern: "yyyy/MM/dd", value: -18.1, calendar: .gregorian), - .init(pattern: "yyyy-MM-dd", value: -18.2, calendar: .gregorian), - .init(pattern: "M月d日(E)", value: -18.3, calendar: .gregorian), - .init(pattern: "yyyy年M月d日", value: -18.4, calendar: .gregorian), - .init(pattern: "Gyyyy年M月d日", value: -18.5, calendar: .japanese), - .init(pattern: "E曜日", value: -18.6, calendar: .gregorian) - ] let formatter = DateFormatter() formatter.locale = Locale(identifier: "ja_JP") formatter.timeZone = calendar.timeZone - return formats.map { format in - formatter.calendar = Calendar(identifier: format.calendar) + return DateShortcutFormats.all.map { format in + formatter.calendar = Calendar(identifier: format.calendar == .japanese ? .japanese : .gregorian) formatter.dateFormat = format.pattern return DicdataElement(word: formatter.string(from: date), ruby: ruby, cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: format.value) } diff --git a/Core/Sources/Core/InputUtils/SegmentsManager.swift b/Core/Sources/Core/InputUtils/SegmentsManager.swift index 822e1648..5645ea10 100644 --- a/Core/Sources/Core/InputUtils/SegmentsManager.swift +++ b/Core/Sources/Core/InputUtils/SegmentsManager.swift @@ -532,16 +532,11 @@ public final class SegmentsManager { } /// 日付・時刻変換を事前に入れておく let dynamicShortcuts: [DicdataElement] = - [ - ("M/d", -18, DateTemplateLiteral.CalendarType.western), - ("yyyy/MM/dd", -18.1, .western), - ("yyyy-MM-dd", -18.2, .western), - ("M月d日(E)", -18.3, .western), - ("yyyy年M月d日", -18.4, .western), - ("Gyyyy年M月d日", -18.5, .japanese), - ("E曜日", -18.6, .western) - ].flatMap { (format, value: PValue, type) in - [ + DateShortcutFormats.all.flatMap { dateFormat -> [DicdataElement] in + let format = dateFormat.pattern + let value = dateFormat.value + let type = dateFormat.calendar + return [ .init(word: DateTemplateLiteral(format: format, type: type, language: .japanese, delta: "-2", deltaUnit: 60 * 60 * 24).export(), ruby: "オトトイ", cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: value), .init(word: DateTemplateLiteral(format: format, type: type, language: .japanese, delta: "-1", deltaUnit: 60 * 60 * 24).export(), ruby: "キノウ", cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: value), .init(word: DateTemplateLiteral(format: format, type: type, language: .japanese, delta: "0", deltaUnit: 1).export(), ruby: "キョウ", cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: value), @@ -574,7 +569,10 @@ public final class SegmentsManager { requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled ) ) - let relativeDates = RelativeDateShortcuts.candidates(matching: composingText.convertTarget.toKatakana()) + // 全体入力に対する候補は文節を短くしている間には出さない。 + let relativeDates = composingText.isAtEndIndex + ? RelativeDateShortcuts.candidates(matching: composingText.convertTarget.toKatakana()) + : [] if !relativeDates.isEmpty { let existingTexts = Set(result.mainResults.map(\.text)) let candidates = relativeDates.filter { !existingTexts.contains($0.word) }.map { element in diff --git a/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift b/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift index 34f289c3..4006a041 100644 --- a/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift +++ b/Core/Tests/CoreTests/InputUtilsTests/RelativeDateShortcutsTests.swift @@ -94,6 +94,37 @@ struct RelativeDateShortcutsTests { try verifyWholeInput(input, inputStyle: .roman2kana) } + @MainActor + @Test(arguments: ["げつまつ", "らいしゅうのげつよう", "3にちご"]) + func shortenedSegmentDoesNotOfferWholeInputDate(_ reading: String) throws { + let directory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: directory) } + let manager = SegmentsManager( + kanaKanjiConverter: .withDefaultDictionary(), applicationDirectoryURL: directory, + containerURL: nil, context: .init(useZenzai: false) + ) + manager.insertAtCursorPosition(reading, inputStyle: .direct) + manager.editSegment(count: -1) + for rich in [false, true] { + manager.update(requestRichCandidates: rich) + manager.requestSetCandidateWindowState(visible: true) + guard case .selecting(let candidates, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else { + Issue.record("Expected selecting state") + return + } + #expect(!candidates.contains { $0.text.range(of: #"^\d{4}/\d{2}/\d{2}$"#, options: .regularExpression) != nil }) + } + manager.editSegment(count: reading.count) + manager.update(requestRichCandidates: true) + guard case .selecting(let candidates, _) = manager.getCurrentCandidateWindow(inputState: .selecting) else { + Issue.record("Expected selecting state") + return + } + let candidate = try #require(candidates.first { $0.text.range(of: #"^\d{4}/\d{2}/\d{2}$"#, options: .regularExpression) != nil }) + manager.prefixCandidateCommited(candidate, leftSideContext: "") + #expect(manager.isEmpty) + } + @MainActor private func verifyWholeInput(_ input: String, inputStyle: InputStyle) throws { let directory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) diff --git a/README.md b/README.md index f75830e4..2cea0083 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ GitHub Sponsorsをご利用ください。 ## 機能 -* 相対日付変換:「らいしゅうのげつよう(び)」など全曜日、「げつまつ」、「3にちご」を日付候補に追加します。来週は月曜始まりの次週、月末は今月末、日数は今日基準の0〜3660日(半角・全角数字)です。通常の変換候補も残ります。 +* 相対日付変換:「らいしゅうのげつよう(び)」など全曜日、「げつまつ」、「3にちご」を日付候補に追加します。来週は月曜始まりの次週、月末は今月末、日数は今日基準の0〜3660日(半角・全角数字、数字部分は最大4桁)です。通常の変換候補も残ります。 * ニューラルかな漢字変換システム「Zenzai」による高精度な変換 * プロフィールプロンプト機能