11import Foundation
22
3+ /// しきい値ごとのストリーム統計を管理するクラス
4+ class StreamCounter {
5+ /// 現在カウント中のストリーム長(文字数)
6+ var currentLength : Int = 0
7+ /// ストリーム長さのヒストグラム(要素数51、インデックス0..50)
8+ var histogram : [ Int ] = Array ( repeating: 0 , count: 51 )
9+
10+ /// ストリームに文字を追加する
11+ /// - Parameters:
12+ /// - count: 追加する文字数
13+ /// - subtract: 減算する文字数(ヨミ分の除外など)
14+ func addChars( _ count: Int , subtract: Int = 0 ) {
15+ let net = count - subtract
16+ if net > 0 {
17+ currentLength += net
18+ } else if net < 0 {
19+ // 理論的には発生しないが予防的に
20+ currentLength = max ( 0 , currentLength + net)
21+ }
22+ // net == 0 は何もしない
23+ }
24+
25+ /// 現在のストリームを終了してヒストグラムを更新する
26+ func endStream( ) {
27+ guard currentLength > 0 else { return }
28+ let idx = min ( currentLength, 50 )
29+ // Log.i("★Stream ended with length \(currentLength), updating histogram at index \(idx)")
30+ histogram [ idx] += 1
31+ currentLength = 0
32+ }
33+
34+ /// カウンタをリセットする
35+ func reset( ) {
36+ currentLength = 0
37+ histogram = Array ( repeating: 0 , count: 51 )
38+ }
39+ }
40+
341/// 入力統計情報を管理するシングルトンクラス
442class InputStats {
543 static let i = InputStats ( )
@@ -28,37 +66,55 @@ class InputStats {
2866 private var lastStrokeKey : Int ? = nil
2967 private var lastStrokePane : String ? = nil // "L" or "R"
3068
69+ // ストリーム統計
70+ // 最後に確定した時刻(全しきい値で共有)
71+ private var lastKakuteiDate : Date ? = nil
72+ // しきい値文字列 -> StreamCounter
73+ private var streamCounters : [ String : StreamCounter ] = [ : ]
74+
3175 private let queue = DispatchQueue ( label: " jp.mad-p.inputmethods.MacTcode.inputstats " , attributes: . concurrent)
3276
3377 private init ( ) {
3478 // 初期化時に保存済みの stroke-stats.json を読み込む
3579 loadStrokeStatsMaybe ( )
80+ // ストリームカウンタを初期化
81+ initStreamCounters ( )
82+ }
83+
84+ /// 設定のしきい値に基づいてストリームカウンタを初期化する
85+ private func initStreamCounters( ) {
86+ let thresholds = UserConfigs . i. system. streamThresholds
87+ for key in thresholds {
88+ if streamCounters [ key] == nil {
89+ streamCounters [ key] = StreamCounter ( )
90+ }
91+ }
3692 }
3793
3894 /// 基本文字入力のカウントを増やす
3995 func incrementBasicCount( ) {
4096 queue. async ( flags: . barrier) {
4197 self . basicCount += 1
4298 self . totalActionCount += 1
43- Log . i ( " basicCount = \( self . basicCount) " )
99+ // Log.i("basicCount = \(self.basicCount)")
44100 }
45101 }
46102
47103 /// 部首変換のカウントを増やす
48104 func incrementBushuCount( ) {
49105 queue. async ( flags: . barrier) {
50106 self . bushuCount += 1
51- // continuity break
52- self . recordNonStrokeEventInternal ( )
107+ // バイグラム連続性を断つ(ストリームはrecordKakuteiで管理)
108+ self . recordNonStrokeEvent ( )
53109 }
54110 }
55111
56112 /// 交ぜ書き変換のカウントを増やす
57113 func incrementMazegakiCount( ) {
58114 queue. async ( flags: . barrier) {
59115 self . mazegakiCount += 1
60- // continuity break
61- self . recordNonStrokeEventInternal ( )
116+ // バイグラム連続性を断つ(ストリームはrecordKakuteiで管理)
117+ self . recordNonStrokeEvent ( )
62118 }
63119 }
64120
@@ -67,8 +123,9 @@ class InputStats {
67123 queue. async ( flags: . barrier) {
68124 self . functionCount += 1
69125 self . totalActionCount += 1
70- // continuity break
71- self . recordNonStrokeEventInternal ( )
126+ // バイグラム・ストリーム両方の連続性を断つ
127+ self . recordNonStrokeEvent ( )
128+ self . recordStreamEndEvent ( )
72129 }
73130 }
74131
@@ -124,20 +181,66 @@ class InputStats {
124181 }
125182 }
126183
127- /// 非ストロークイベントを記録して連続性を断つ
184+ /// 非ストロークイベントを記録して連続性を断つ(バイグラム統計のみ)
128185 func recordNonStrokeEvent( ) {
186+ // Log.i("recordNonStrokeEvent called")
129187 guard UserConfigs . i. system. strokeStatsEnabled else { return }
130188 queue. async ( flags: . barrier) {
131189 self . recordNonStrokeEventInternal ( )
132190 }
133191 }
134192
135- // internal: assumes barrier queue
193+ // internal: assumes barrier queue(バイグラム統計のみ)
136194 private func recordNonStrokeEventInternal( ) {
137195 self . lastStrokeKey = nil
138196 self . lastStrokePane = nil
139197 }
140198
199+ /// ストリーム統計の不連続を記録する(ストリームを終了させる)
200+ func recordStreamEndEvent( ) {
201+ // Log.i("recordStreamEndEvent called")
202+ guard UserConfigs . i. system. streamStatsEnabled else { return }
203+ queue. async ( flags: . barrier) {
204+ self . recordStreamEndEventInternal ( )
205+ }
206+ }
207+
208+ // internal: assumes barrier queue
209+ private func recordStreamEndEventInternal( ) {
210+ for counter in self . streamCounters. values {
211+ counter. endStream ( )
212+ }
213+ self . lastKakuteiDate = nil
214+ }
215+
216+ /// 漢直入力の確定を記録する(ストリーム統計用)
217+ /// - Parameters:
218+ /// - charCount: 確定された文字数
219+ /// - subtract: 減算する文字数(ヨミ分など。Bushuでは2、Mazegakiではhit.length)
220+ func recordKakutei( charCount: Int , subtract: Int = 0 ) {
221+ // Log.i("recordKakutei called: charCount = \(charCount), subtract = \(subtract)")
222+ guard UserConfigs . i. system. streamStatsEnabled else { return }
223+ queue. async ( flags: . barrier) {
224+ let now = Date ( )
225+ if let last = self . lastKakuteiDate {
226+ let elapsed = now. timeIntervalSince ( last)
227+ for (key, counter) in self . streamCounters {
228+ guard let threshold = Double ( key) else { continue }
229+ if elapsed >= threshold {
230+ // Log.i("Stream threshold \(threshold) exceeded (elapsed = \(elapsed)), ending stream")
231+ counter. endStream ( )
232+ }
233+ counter. addChars ( charCount, subtract: subtract)
234+ }
235+ } else {
236+ for counter in self . streamCounters. values {
237+ counter. addChars ( charCount, subtract: subtract)
238+ }
239+ }
240+ self . lastKakuteiDate = now
241+ }
242+ }
243+
141244 /// ストローク統計をリセット(メモリ内)
142245 func resetStrokeStats( ) {
143246 queue. async ( flags: . barrier) {
@@ -148,6 +251,9 @@ class InputStats {
148251 self . alternation = [ " alternate " : 0 , " consecutive " : 0 , " first " : 0 ]
149252 self . lastStrokeKey = nil
150253 self . lastStrokePane = nil
254+ // ストリーム統計もリセット
255+ for counter in self . streamCounters. values { counter. reset ( ) }
256+ self . lastKakuteiDate = nil
151257 }
152258 }
153259
@@ -170,6 +276,16 @@ class InputStats {
170276 if let bg = obj [ " bigram " ] as? [ Int ] , bg. count == nKeys * nKeys { self . bigramCount = bg }
171277 if let p = obj [ " panes " ] as? [ String : Int ] { self . panes = p }
172278 if let a = obj [ " alternation " ] as? [ String : Int ] { self . alternation = a }
279+ // ストリーム統計を読み込む
280+ if let sc = obj [ " streamCount " ] as? [ String : [ Int ] ] {
281+ for (key, hist) in sc {
282+ if hist. count == 51 {
283+ let counter = self . streamCounters [ key] ?? StreamCounter ( )
284+ counter. histogram = hist
285+ self . streamCounters [ key] = counter
286+ }
287+ }
288+ }
173289 }
174290 } catch {
175291 Log . i ( " Failed to load stroke-stats: \( error) " )
@@ -188,6 +304,14 @@ class InputStats {
188304 obj [ " panes " ] = self . panes
189305 obj [ " alternation " ] = self . alternation
190306 obj [ " lastUpdated " ] = ISO8601DateFormatter ( ) . string ( from: Date ( ) )
307+ // ストリーム統計を書き出す(現在進行中のストリームは反映しない)
308+ if UserConfigs . i. system. streamStatsEnabled {
309+ var streamCount : [ String : [ Int ] ] = [ : ]
310+ for (key, counter) in self . streamCounters {
311+ streamCount [ key] = counter. histogram
312+ }
313+ obj [ " streamCount " ] = streamCount
314+ }
191315
192316 do {
193317 let data = try JSONSerialization . data ( withJSONObject: obj, options: [ . prettyPrinted, . sortedKeys] )
0 commit comments