hachiwari 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +36 -1
- data/CHANGELOG.md +39 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +41 -20
- data/LICENSE +34 -0
- data/README.de.md +103 -0
- data/README.en.md +103 -0
- data/README.es.md +103 -0
- data/README.fr.md +103 -0
- data/README.md +53 -65
- data/RELEASE_NOTES_v1.0.0.md +37 -0
- data/Rakefile +3 -0
- data/bin/hachiwari +9 -0
- data/config/locales/de.yml +70 -0
- data/config/locales/en.yml +70 -0
- data/config/locales/es.yml +70 -0
- data/config/locales/fr.yml +70 -0
- data/config/locales/ja.yml +70 -0
- data/hachiwari.gemspec +11 -16
- data/lib/hachiwari/cli.rb +250 -49
- data/lib/hachiwari/locales.rb +59 -0
- data/lib/hachiwari/results.rb +11 -0
- data/lib/hachiwari/status_calculator.rb +61 -0
- data/lib/hachiwari/status_presenter.rb +27 -0
- data/lib/hachiwari/status_runner.rb +82 -0
- data/lib/hachiwari/storage.rb +205 -0
- data/lib/hachiwari/version.rb +2 -1
- data/lib/hachiwari.rb +8 -1
- data/lib/rubygems_plugin.rb +47 -0
- metadata +41 -16
- data/CODE_OF_CONDUCT.md +0 -163
- data/LICENSE.txt +0 -21
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/exe/hachiwari +0 -5
- data/lib/results.yml +0 -5
data/lib/hachiwari/cli.rb
CHANGED
@@ -1,76 +1,277 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "thor"
|
3
|
-
|
4
|
+
require_relative "status_runner"
|
5
|
+
require_relative "storage"
|
6
|
+
require_relative "locales"
|
4
7
|
|
5
8
|
module Hachiwari
|
6
9
|
class CLI < Thor
|
7
|
-
|
8
|
-
|
9
|
-
@@db = YAML::Store.new("#{Dir.home}/.hachiwari")
|
10
|
-
@@results = @@db.transaction { @@db[:results] } if @@db
|
11
|
-
@@results ||= Results.new(0, 0, 80, :ja)
|
10
|
+
HIDDEN_GENERAL_HELP_COMMANDS = %w[info i calculate s].freeze
|
11
|
+
DEFAULT_THOR_LOCALE = :ja
|
12
12
|
|
13
13
|
class << self
|
14
|
-
|
15
|
-
|
14
|
+
# CLI 起動時に旧フォーマットの保存データをマイグレートし、ヘルプやバージョン表示を先に処理する
|
15
|
+
def start(given_args = ARGV, config = {}, &)
|
16
|
+
migrate_legacy_store
|
17
|
+
|
18
|
+
args = Array(given_args).dup
|
19
|
+
return display_help(nil) if args.empty?
|
20
|
+
return if handle_direct_option?(args)
|
21
|
+
|
22
|
+
args = ["status", *args] if default_to_status?(args)
|
23
|
+
super(args, config, &)
|
16
24
|
end
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
25
|
+
|
26
|
+
# ヘルプ用の文字列をユーザー設定ロケール優先で取得する
|
27
|
+
def thor_string(command, key)
|
28
|
+
fetch_locale_value(help_language, command, key) || fetch_locale_value(DEFAULT_THOR_LOCALE, command, key) || ""
|
29
|
+
end
|
30
|
+
|
31
|
+
# ヘルプ詳細文をロケール順に取得して配列化する
|
32
|
+
def thor_details(command)
|
33
|
+
value = fetch_locale_value(help_language, command, :details)
|
34
|
+
value = fetch_locale_value(DEFAULT_THOR_LOCALE, command, :details) if value.nil?
|
35
|
+
array_wrap(value)
|
36
|
+
end
|
37
|
+
|
38
|
+
# 長文説明をメイン文と詳細の組み合わせで構築する
|
39
|
+
def thor_long_description(command)
|
40
|
+
build_long_description(thor_string(command, :long), thor_details(command))
|
41
|
+
end
|
42
|
+
|
43
|
+
# デフォルトロケールから短い説明を取得する
|
44
|
+
def default_thor_string(command, key)
|
45
|
+
fetch_locale_value(DEFAULT_THOR_LOCALE, command, key) || ""
|
46
|
+
end
|
47
|
+
|
48
|
+
# デフォルトロケールの詳細情報を配列で取得する
|
49
|
+
def default_thor_details(command)
|
50
|
+
array_wrap(fetch_locale_value(DEFAULT_THOR_LOCALE, command, :details))
|
51
|
+
end
|
52
|
+
|
53
|
+
# デフォルトロケールで長文説明を生成する
|
54
|
+
def default_thor_long_description(command)
|
55
|
+
build_long_description(default_thor_string(command, :long), default_thor_details(command))
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# 引数に含まれるバージョン/ヘルプ系の即時オプションを処理する
|
61
|
+
def handle_direct_option?(args)
|
62
|
+
case args.first
|
63
|
+
when "--version", "-v"
|
64
|
+
puts Hachiwari::VERSION
|
65
|
+
return true
|
66
|
+
when "--help", "-h", "help"
|
67
|
+
display_help(args[1])
|
68
|
+
return true
|
69
|
+
end
|
70
|
+
|
71
|
+
help_index = args.index("--help") || args.index("-h")
|
72
|
+
return false unless help_index
|
73
|
+
|
74
|
+
display_help(extract_command_for_help(args, help_index))
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
# 旧ストレージ形式が存在する場合にマイグレーションを実行する
|
79
|
+
def migrate_legacy_store
|
80
|
+
Storage.new.send(:migrate_legacy_if_needed)
|
81
|
+
rescue StandardError
|
82
|
+
# 続行
|
83
|
+
end
|
84
|
+
|
85
|
+
# ヘルプ引数の位置からコマンド名候補を取得する
|
86
|
+
def extract_command_for_help(args, help_index)
|
87
|
+
before = help_index.positive? ? args[help_index - 1] : nil
|
88
|
+
after = args[help_index + 1]
|
89
|
+
[before, after].compact.find { |candidate| candidate && !candidate.start_with?("-") }
|
90
|
+
end
|
91
|
+
|
92
|
+
# 指定されたコマンドのヘルプを出力する
|
93
|
+
def display_help(command)
|
94
|
+
command = command&.to_s&.strip
|
95
|
+
command = nil if command.nil? || command.empty?
|
96
|
+
command ? print_command_help(command) : print_general_help
|
97
|
+
end
|
98
|
+
|
99
|
+
# 全体ヘルプの各コマンド情報を出力する
|
100
|
+
def print_general_help
|
101
|
+
data = help_content
|
102
|
+
puts data[:general_intro]
|
103
|
+
data[:commands].each do |name, info|
|
104
|
+
next if HIDDEN_GENERAL_HELP_COMMANDS.include?(name.to_s)
|
105
|
+
|
106
|
+
puts " #{info[:usage]}"
|
107
|
+
puts " #{info[:description]}"
|
108
|
+
Array(info[:details]).each { |detail| puts " #{detail}" }
|
109
|
+
end
|
110
|
+
Array(data[:general_footer]).each { |line| puts line }
|
111
|
+
end
|
112
|
+
|
113
|
+
# 個別コマンドの詳細ヘルプを出力する
|
114
|
+
def print_command_help(command)
|
115
|
+
data = help_content
|
116
|
+
info = data[:commands][command.to_sym] || data[:commands][command.to_s]
|
117
|
+
|
118
|
+
unless info
|
119
|
+
puts format(data[:unknown_command], command: command)
|
120
|
+
puts
|
121
|
+
print_general_help
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
puts format(data[:command_heading], usage: info[:usage])
|
126
|
+
puts format(data[:description_heading], description: info[:description])
|
127
|
+
Array(info[:details]).each { |detail| puts " #{detail}" }
|
128
|
+
end
|
129
|
+
|
130
|
+
# ロケールごとのヘルプデータを取得する
|
131
|
+
def help_content
|
132
|
+
Hachiwari::Locales.t(help_language, :help)
|
133
|
+
rescue KeyError
|
134
|
+
Hachiwari::Locales.t(:ja, :help)
|
135
|
+
end
|
136
|
+
|
137
|
+
# 保存ロケールが利用可能か確認し、ヘルプ用ロケールを返す
|
138
|
+
def help_language
|
139
|
+
locale = Storage.new.load.language
|
140
|
+
available = Hachiwari::Locales.available_locales
|
141
|
+
available.include?(locale) ? locale : :ja
|
142
|
+
rescue StandardError
|
143
|
+
:ja
|
144
|
+
end
|
145
|
+
|
146
|
+
# 指定コマンドが登録済みか判定し、未登録なら status へフォールバックする
|
147
|
+
def default_to_status?(args)
|
148
|
+
return false if args.empty?
|
149
|
+
|
150
|
+
first = args.first
|
151
|
+
return true if first.start_with?("-")
|
152
|
+
|
153
|
+
!all_commands.key?(first)
|
154
|
+
end
|
155
|
+
|
156
|
+
# ロケール付きの Thor 文言を取得する
|
157
|
+
def fetch_locale_value(locale, command, key)
|
158
|
+
Hachiwari::Locales.t(locale, :thor, command, key)
|
159
|
+
rescue KeyError
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# メイン文と詳細配列から長文を組み立てる
|
164
|
+
def build_long_description(primary, extra_lines)
|
165
|
+
lines = []
|
166
|
+
lines << primary if primary && !primary.empty?
|
167
|
+
lines.concat(array_wrap(extra_lines))
|
168
|
+
lines.compact.join("\n")
|
169
|
+
end
|
170
|
+
|
171
|
+
# 値を配列にラップして扱いやすくする
|
172
|
+
def array_wrap(value)
|
173
|
+
case value
|
174
|
+
when nil
|
175
|
+
[]
|
176
|
+
when Array
|
177
|
+
value
|
178
|
+
else
|
179
|
+
[value]
|
180
|
+
end
|
42
181
|
end
|
43
182
|
end
|
44
183
|
|
45
|
-
|
46
|
-
|
47
|
-
|
184
|
+
attr_reader :runner, :storage
|
185
|
+
|
186
|
+
# コマンド実行時に利用するストレージとランナーを初期化する
|
187
|
+
def initialize(*args, **kwargs)
|
188
|
+
super
|
189
|
+
@storage = Storage.new
|
190
|
+
@runner = StatusRunner.new(storage: storage)
|
191
|
+
end
|
192
|
+
|
193
|
+
option :trial, type: :boolean, aliases: "-t", desc: CLI.default_thor_string(:status, :option_trial)
|
194
|
+
desc "status [wins] [losses] [target] [language]", CLI.default_thor_long_description(:status)
|
195
|
+
|
196
|
+
# 勝敗・目標勝率・言語を受け取り、状態を保存しながら結果を表示
|
197
|
+
def status(*args)
|
198
|
+
run_status_with_trial_flag(trial: options[:trial], **parse_status_arguments(args))
|
48
199
|
end
|
49
200
|
|
50
|
-
|
51
|
-
|
52
|
-
|
201
|
+
option :trial, type: :boolean, aliases: "-t", desc: CLI.default_thor_string(:info, :option_trial)
|
202
|
+
desc "info [wins] [losses] [target] [language]", CLI.default_thor_long_description(:info)
|
203
|
+
# 状態を保存せず試算だけ行うコマンド
|
204
|
+
def info(*args)
|
205
|
+
warn_deprecation("info", locale_key: :trial)
|
206
|
+
run_status_with_trial_flag(trial: true, **parse_status_arguments(args))
|
53
207
|
end
|
54
208
|
|
55
|
-
desc "i [wins] [losses] [target] [language]",
|
56
|
-
|
57
|
-
|
209
|
+
desc "i [wins] [losses] [target] [language]", CLI.default_thor_long_description(:alias_i)
|
210
|
+
# `info` と同等の動作を行う短縮エイリアス
|
211
|
+
def i(*args)
|
212
|
+
warn_deprecation("i", locale_key: :alias)
|
213
|
+
run_status_with_trial_flag(trial: true, **parse_status_arguments(args))
|
58
214
|
end
|
59
215
|
|
60
|
-
desc "version",
|
216
|
+
desc "version", CLI.default_thor_string(:version, :short)
|
217
|
+
# CLI のバージョン情報を表示する
|
61
218
|
def version
|
62
219
|
puts Hachiwari::VERSION
|
63
220
|
end
|
64
221
|
|
222
|
+
desc "clear", CLI.default_thor_string(:clear, :short)
|
223
|
+
# 保存済みの状態データを削除する
|
224
|
+
def clear
|
225
|
+
if storage.clear
|
226
|
+
say("Saved status data has been cleared.", :green)
|
227
|
+
else
|
228
|
+
say("No saved status data found.", :yellow)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
desc "calculate [wins] [losses]", CLI.default_thor_long_description(:calculate)
|
233
|
+
option :target, type: :numeric, desc: CLI.default_thor_string(:calculate, :option_target)
|
234
|
+
option :language, type: :string, desc: CLI.default_thor_string(:calculate, :option_language)
|
235
|
+
# `status`/`info` とは別に、オプション経由でパラメータを受け取って計算のみ行う
|
236
|
+
def calculate(wins, losses)
|
237
|
+
warn_deprecation("calculate")
|
238
|
+
run_status_with_trial_flag(
|
239
|
+
trial: true,
|
240
|
+
wins: wins, losses: losses, target: options[:target], language: options[:language]
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
65
244
|
private
|
66
245
|
|
67
|
-
|
68
|
-
|
246
|
+
# トライアル指定の有無に応じて保存フラグを切り替える
|
247
|
+
def run_status_with_trial_flag(trial:, **params)
|
248
|
+
run_status(save: !trial, **params)
|
249
|
+
end
|
250
|
+
|
251
|
+
# 位置引数から勝敗・目標・言語を取り出して整形する
|
252
|
+
def parse_status_arguments(args)
|
253
|
+
%i[wins losses target language].zip(args).to_h
|
254
|
+
end
|
255
|
+
|
256
|
+
# 非推奨コマンド利用時の警告をユーザーへ通知する
|
257
|
+
def warn_deprecation(command, message = nil, locale_key: :trial)
|
258
|
+
message ||= localized_deprecation(locale_key)
|
259
|
+
say("`hachiwari #{command}`: #{message}", :yellow)
|
260
|
+
end
|
261
|
+
|
262
|
+
# 保存された言語設定で非推奨メッセージを取得する
|
263
|
+
def localized_deprecation(key)
|
264
|
+
locale = storage.load.language
|
265
|
+
available = Hachiwari::Locales.available_locales
|
266
|
+
locale = :ja unless available.include?(locale)
|
267
|
+
Hachiwari::Locales.t(locale, :deprecations, key)
|
268
|
+
rescue StandardError
|
269
|
+
Hachiwari::Locales.t(:ja, :deprecations, key)
|
69
270
|
end
|
70
271
|
|
71
|
-
|
72
|
-
|
73
|
-
(
|
272
|
+
# `StatusRunner` へ依頼し、不要な nil を除いて保存フラグ付きで実行
|
273
|
+
def run_status(save:, **params)
|
274
|
+
runner.call(params.compact, save: save)
|
74
275
|
end
|
75
276
|
end
|
76
277
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Hachiwari
|
6
|
+
# 自前の軽量ローカライゼーションローダー
|
7
|
+
module Locales
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# 指定ロケールの翻訳データを YAML から読み込みキャッシュする
|
11
|
+
def load(locale)
|
12
|
+
cache[locale.to_sym] ||= begin
|
13
|
+
path = locale_path(locale)
|
14
|
+
raise KeyError, "Unknown locale: #{locale}" unless path
|
15
|
+
|
16
|
+
data = YAML.safe_load_file(path, aliases: true, symbolize_names: true)
|
17
|
+
data.fetch(locale.to_sym)
|
18
|
+
rescue Errno::ENOENT
|
19
|
+
raise KeyError, "Missing locale file: #{locale}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# ネストしたキーを辿って翻訳文字列を取得する
|
24
|
+
def t(locale, *keys)
|
25
|
+
keys.reduce(load(locale)) do |current, key|
|
26
|
+
current.fetch(key.to_sym)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# 利用可能なロケール一覧を返す
|
31
|
+
def available_locales
|
32
|
+
locale_files.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
# 翻訳データのキャッシュをクリアする
|
36
|
+
def clear_cache
|
37
|
+
cache.clear
|
38
|
+
end
|
39
|
+
|
40
|
+
# ロケールごとのキャッシュを初期化または返却する
|
41
|
+
def cache
|
42
|
+
@cache ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
# ロケールシンボルからファイルパスを取得する
|
46
|
+
def locale_path(locale)
|
47
|
+
locale_files[locale.to_sym]
|
48
|
+
end
|
49
|
+
|
50
|
+
# ロケールファイルを走査し、シンボルとパスの対応表を構築する
|
51
|
+
def locale_files
|
52
|
+
@locale_files ||= Dir[File.expand_path("../../config/locales/*.yml",
|
53
|
+
__dir__)].each_with_object({}) do |path, hash|
|
54
|
+
name = File.basename(path, ".yml").to_sym
|
55
|
+
hash[name] = path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hachiwari
|
4
|
+
# 勝敗や目標勝率、表示言語など CLI の状態を保持するデータオブジェクト
|
5
|
+
Results = Struct.new(:wins, :losses, :target, :language) do
|
6
|
+
# YAML への保存や表示時に利用しやすい Hash へ変換
|
7
|
+
def to_h
|
8
|
+
{ wins: wins, losses: losses, target: target, language: language }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hachiwari
|
4
|
+
# 勝率や必要勝数などの数値計算を担当するユーティリティ
|
5
|
+
class StatusCalculator
|
6
|
+
# 勝率(%)を小数第4位まで算出
|
7
|
+
def winning_percentage(results)
|
8
|
+
games = total_games(results)
|
9
|
+
return 0.0 if games.zero?
|
10
|
+
|
11
|
+
(results.wins / games.to_f * 100).round(4)
|
12
|
+
end
|
13
|
+
|
14
|
+
# 目標勝率を満たすまでに必要な追加勝利数を算出
|
15
|
+
# 既に目標を上回っている、もしくは目標が無効な場合は 0 を返す
|
16
|
+
def required_wins(results)
|
17
|
+
target = results.target.to_i
|
18
|
+
return 0 if target <= 0
|
19
|
+
|
20
|
+
wins = results.wins.to_i
|
21
|
+
losses = results.losses.to_i
|
22
|
+
total = wins + losses
|
23
|
+
|
24
|
+
return 0 if total.positive? && wins * 100 >= target * total
|
25
|
+
|
26
|
+
denominator = 100 - target
|
27
|
+
return 0 if denominator <= 0
|
28
|
+
|
29
|
+
numerator = (target * total) - (wins * 100)
|
30
|
+
return 0 if numerator <= 0
|
31
|
+
|
32
|
+
(numerator + denominator - 1) / denominator
|
33
|
+
end
|
34
|
+
|
35
|
+
# 勝率が目標を上回っている場合に、目標を下回るまでに許容される敗北数を算出
|
36
|
+
# それ以外の状況では 0 を返す
|
37
|
+
def losses_until_below_target(results)
|
38
|
+
target = results.target.to_i
|
39
|
+
return 0 if target <= 0
|
40
|
+
|
41
|
+
wins = results.wins.to_i
|
42
|
+
losses = results.losses.to_i
|
43
|
+
total = wins + losses
|
44
|
+
return 0 if total.zero?
|
45
|
+
|
46
|
+
numerator = (wins * 100) - (target * total)
|
47
|
+
return 0 if numerator.negative?
|
48
|
+
|
49
|
+
return 0 if numerator.zero? && target >= 100 && losses.positive?
|
50
|
+
|
51
|
+
return 1 if numerator.zero? && target >= 100
|
52
|
+
|
53
|
+
(numerator / target) + 1
|
54
|
+
end
|
55
|
+
|
56
|
+
# 総対局数(勝ち数 + 負け数)を返却
|
57
|
+
def total_games(results)
|
58
|
+
results.wins + results.losses
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hachiwari
|
4
|
+
# 計算済みの値をユーザー向けメッセージに整形して出力する責務を担う
|
5
|
+
class StatusPresenter
|
6
|
+
# 言語に応じたテンプレートへ値を埋め込み標準出力へ表示
|
7
|
+
def render(results, data)
|
8
|
+
template = status_template(results.language)
|
9
|
+
puts format(template[:summary], **data)
|
10
|
+
|
11
|
+
if data[:needed].positive?
|
12
|
+
puts format(template[:needed], **data)
|
13
|
+
elsif data[:losses_to_fall].positive?
|
14
|
+
puts format(template[:cushion], **data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# 指定ロケールのテンプレートを取得し、失敗時は日本語へフォールバック
|
21
|
+
def status_template(locale)
|
22
|
+
Hachiwari::Locales.t(locale, :status)
|
23
|
+
rescue KeyError
|
24
|
+
Hachiwari::Locales.t(:ja, :status)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "results"
|
4
|
+
require_relative "status_calculator"
|
5
|
+
require_relative "status_presenter"
|
6
|
+
require_relative "storage"
|
7
|
+
|
8
|
+
# 状態の読み書き・計算・表示を束ねる調停役
|
9
|
+
module Hachiwari
|
10
|
+
class StatusRunner
|
11
|
+
# 依存するコンポーネントを DI で受け取り、テストしやすくする
|
12
|
+
def initialize(storage: Storage.new, calculator: StatusCalculator.new, presenter: StatusPresenter.new)
|
13
|
+
@storage = storage
|
14
|
+
@calculator = calculator
|
15
|
+
@presenter = presenter
|
16
|
+
end
|
17
|
+
|
18
|
+
# 引数から `Results` を生成し、保存フラグに応じて永続化と表示を行う
|
19
|
+
def call(input, save:)
|
20
|
+
# レガシーデータが残っている環境でも、試算モード含め常に最新形式へ移行
|
21
|
+
storage.send(:migrate_legacy_if_needed) if storage.respond_to?(:send)
|
22
|
+
data = normalize_input(input)
|
23
|
+
base = storage.load
|
24
|
+
results = merge_results(base, data)
|
25
|
+
storage.save(results) if save
|
26
|
+
presenter.render(results, formatted_data(results))
|
27
|
+
results
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :storage, :calculator, :presenter
|
33
|
+
|
34
|
+
# 受け取った Hash のキーをシンボル化して扱いやすくする
|
35
|
+
def normalize_input(input)
|
36
|
+
input.transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier
|
37
|
+
end
|
38
|
+
|
39
|
+
# 既存結果と入力値をマージして新しい `Results` を生成
|
40
|
+
def merge_results(base, input)
|
41
|
+
Results.new(
|
42
|
+
resolve_integer(input[:wins], base.wins),
|
43
|
+
resolve_integer(input[:losses], base.losses),
|
44
|
+
resolve_integer(input[:target], base.target),
|
45
|
+
resolve_language(input[:language], base.language)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
# 数値系入力の正規化(空入力は既存値を維持)
|
50
|
+
def resolve_integer(value, fallback)
|
51
|
+
return fallback if value.nil?
|
52
|
+
|
53
|
+
string = value.to_s.strip
|
54
|
+
return fallback if string.empty?
|
55
|
+
|
56
|
+
string.to_i
|
57
|
+
end
|
58
|
+
|
59
|
+
# 言語指定の正規化(空入力は既存値を維持)
|
60
|
+
def resolve_language(value, fallback)
|
61
|
+
return fallback if value.nil?
|
62
|
+
|
63
|
+
string = value.to_s.strip
|
64
|
+
return fallback if string.empty?
|
65
|
+
|
66
|
+
string.to_sym
|
67
|
+
end
|
68
|
+
|
69
|
+
# 表示用の Hash を生成し、プレゼンターへ渡す
|
70
|
+
def formatted_data(results)
|
71
|
+
{
|
72
|
+
total: calculator.total_games(results),
|
73
|
+
wins: results.wins,
|
74
|
+
losses: results.losses,
|
75
|
+
percentage: calculator.winning_percentage(results),
|
76
|
+
needed: calculator.required_wins(results),
|
77
|
+
losses_to_fall: calculator.losses_until_below_target(results),
|
78
|
+
target: results.target.to_i
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|