sangi 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +111 -0
- data/exe/sangi +5 -0
- data/lib/sangi/board.rb +35 -0
- data/lib/sangi/cell.rb +60 -0
- data/lib/sangi/cli.rb +142 -0
- data/lib/sangi/config.rb +75 -0
- data/lib/sangi/engine.rb +75 -0
- data/lib/sangi/engines/addition_engine.rb +184 -0
- data/lib/sangi/engines/copy_engine.rb +85 -0
- data/lib/sangi/engines/subtraction_borrowing.rb +124 -0
- data/lib/sangi/engines/subtraction_engine.rb +134 -0
- data/lib/sangi/errors.rb +8 -0
- data/lib/sangi/event.rb +14 -0
- data/lib/sangi/exporter/text_exporter.rb +41 -0
- data/lib/sangi/expression.rb +20 -0
- data/lib/sangi/parser.rb +49 -0
- data/lib/sangi/renderer/ascii_renderer.rb +151 -0
- data/lib/sangi/renderer/cell_renderer.rb +97 -0
- data/lib/sangi/repl.rb +45 -0
- data/lib/sangi/rod.rb +16 -0
- data/lib/sangi/rod_factory.rb +13 -0
- data/lib/sangi/row.rb +51 -0
- data/lib/sangi/signed_number.rb +23 -0
- data/lib/sangi/step.rb +16 -0
- data/lib/sangi/step_builder.rb +87 -0
- data/lib/sangi/step_viewer.rb +124 -0
- data/lib/sangi/terminal_width_warning.rb +56 -0
- data/lib/sangi/version.rb +3 -0
- data/lib/sangi.rb +26 -0
- data/sangi.gemspec +31 -0
- metadata +77 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class AdditionEngine
|
|
3
|
+
def initialize(config)
|
|
4
|
+
@config = config
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def build(abs_a:, abs_b:, result_sign:, display_a:, display_b:, expression:)
|
|
8
|
+
@rod_factory = RodFactory.new
|
|
9
|
+
@result_value = result_sign * (abs_a + abs_b)
|
|
10
|
+
place_count = [
|
|
11
|
+
digit_count(abs_a),
|
|
12
|
+
digit_count(abs_b),
|
|
13
|
+
digit_count(@result_value.abs),
|
|
14
|
+
1
|
|
15
|
+
].max
|
|
16
|
+
@board = Board.from_numbers(
|
|
17
|
+
a: display_a,
|
|
18
|
+
b: display_b,
|
|
19
|
+
work: 0,
|
|
20
|
+
place_count: place_count,
|
|
21
|
+
rod_factory: @rod_factory
|
|
22
|
+
)
|
|
23
|
+
@board.row(:work).sign = result_sign
|
|
24
|
+
@builder = StepBuilder.new(
|
|
25
|
+
config: @config,
|
|
26
|
+
expression: expression,
|
|
27
|
+
board: @board,
|
|
28
|
+
result_value: @result_value,
|
|
29
|
+
rod_factory: @rod_factory
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
push_setup_step
|
|
33
|
+
max_operand_places(abs_a, abs_b).times do |place|
|
|
34
|
+
add_digit_to_work(source: :a, place: place, digit: digit_of(abs_a, place))
|
|
35
|
+
add_digit_to_work(source: :b, place: place, digit: digit_of(abs_b, place))
|
|
36
|
+
end
|
|
37
|
+
normalize_all_places
|
|
38
|
+
push_finish_step
|
|
39
|
+
@builder.steps
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def push_setup_step
|
|
45
|
+
@builder.push(
|
|
46
|
+
title: "準備",
|
|
47
|
+
brief_message: "A行とB行を表示し、Work行を空にします。",
|
|
48
|
+
learn_message: "A行とB行は参照用に残し、棒をWork行へ1本ずつ置いて合計を作ります。",
|
|
49
|
+
event: Event.new(type: :setup)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def push_finish_step
|
|
54
|
+
@board.row(:work).normalize_zero_sign!
|
|
55
|
+
@builder.push(
|
|
56
|
+
title: "完了",
|
|
57
|
+
brief_message: "計算結果は#{@result_value}です。",
|
|
58
|
+
learn_message: "Work行の棒を正規化したので、各位が読みやすい算木風表示になりました。結果は#{@result_value}です。",
|
|
59
|
+
event: Event.new(type: :finish)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_digit_to_work(source:, place:, digit:)
|
|
64
|
+
return if digit.zero?
|
|
65
|
+
|
|
66
|
+
source_label = source == :a ? "A" : "B"
|
|
67
|
+
(digit / 5).times do
|
|
68
|
+
@builder.place_rod(
|
|
69
|
+
row: :work,
|
|
70
|
+
place: place,
|
|
71
|
+
kind: :five,
|
|
72
|
+
title: "#{source_label}を加える",
|
|
73
|
+
brief_message: "#{source_label}行を参照し、Workの#{@builder.place_name(place)}にfive棒を1本置きます。",
|
|
74
|
+
learn_message: "#{source_label}行の#{@builder.place_name(place)}にある5を表す棒を、Work行へ1本追加します。"
|
|
75
|
+
)
|
|
76
|
+
normalize_from_place(place)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
(digit % 5).times do
|
|
80
|
+
@builder.place_rod(
|
|
81
|
+
row: :work,
|
|
82
|
+
place: place,
|
|
83
|
+
kind: :unit,
|
|
84
|
+
title: "#{source_label}を加える",
|
|
85
|
+
brief_message: "#{source_label}行を参照し、Workの#{@builder.place_name(place)}にunit棒を1本置きます。",
|
|
86
|
+
learn_message: "#{source_label}行の#{@builder.place_name(place)}にある1を表す棒を、Work行へ1本追加します。"
|
|
87
|
+
)
|
|
88
|
+
normalize_from_place(place)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize_all_places
|
|
93
|
+
@board.place_count.times { |place| normalize_from_place(place) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_from_place(start_place)
|
|
97
|
+
current = start_place
|
|
98
|
+
while current < @board.place_count
|
|
99
|
+
changed = false
|
|
100
|
+
cell = work_cell(current)
|
|
101
|
+
|
|
102
|
+
while cell.unit_rods.size >= 5
|
|
103
|
+
normalize_units_to_five(current)
|
|
104
|
+
changed = true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
while cell.five_rods.size >= 2
|
|
108
|
+
carry_five_pair(current)
|
|
109
|
+
changed = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
break unless changed
|
|
113
|
+
|
|
114
|
+
current += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def normalize_units_to_five(place)
|
|
119
|
+
5.times do
|
|
120
|
+
@builder.remove_rod(
|
|
121
|
+
row: :work,
|
|
122
|
+
place: place,
|
|
123
|
+
kind: :unit,
|
|
124
|
+
title: "5本をまとめる",
|
|
125
|
+
brief_message: "Workの#{@builder.place_name(place)}でunit棒5本をfive棒1本にまとめるため、unit棒を1本取り除きます。",
|
|
126
|
+
learn_message: "同じ位の1の棒が5本になったので、5を表す棒へ変換します。",
|
|
127
|
+
event_type: :convert_unit_to_five
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
@builder.place_rod(
|
|
131
|
+
row: :work,
|
|
132
|
+
place: place,
|
|
133
|
+
kind: :five,
|
|
134
|
+
title: "5本をまとめる",
|
|
135
|
+
brief_message: "Workの#{@builder.place_name(place)}にfive棒を1本置きます。",
|
|
136
|
+
learn_message: "unit棒5本を取り除いた代わりに、同じ位へ5を表す棒を1本置きます。",
|
|
137
|
+
event_type: :convert_unit_to_five
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def carry_five_pair(place)
|
|
142
|
+
2.times do
|
|
143
|
+
@builder.remove_rod(
|
|
144
|
+
row: :work,
|
|
145
|
+
place: place,
|
|
146
|
+
kind: :five,
|
|
147
|
+
title: "繰り上げ",
|
|
148
|
+
brief_message: "Workの#{@builder.place_name(place)}でfive棒2本を次の位へ繰り上げるため、five棒を1本取り除きます。",
|
|
149
|
+
learn_message: "同じ位の5の棒が2本で10になるため、次の位の1本へ置き換えます。",
|
|
150
|
+
event_type: :carry_to_next_place
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
@builder.place_rod(
|
|
154
|
+
row: :work,
|
|
155
|
+
place: place + 1,
|
|
156
|
+
kind: :unit,
|
|
157
|
+
title: "繰り上げ",
|
|
158
|
+
brief_message: "Workの#{@builder.place_name(place + 1)}にunit棒を1本置きます。",
|
|
159
|
+
learn_message: "下の位の10は、1つ上の位では1として表します。",
|
|
160
|
+
event_type: :carry_to_next_place
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def work_cell(place)
|
|
165
|
+
@board.row(:work).cells.fetch(place)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def digit_of(value, place)
|
|
169
|
+
digits_of(value).fetch(place, 0)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def digits_of(value)
|
|
173
|
+
SignedNumber.from_integer(value).digits
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def digit_count(value)
|
|
177
|
+
value.to_s.length
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def max_operand_places(abs_a, abs_b)
|
|
181
|
+
[digit_count(abs_a), digit_count(abs_b)].max
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class CopyEngine
|
|
3
|
+
def initialize(config)
|
|
4
|
+
@config = config
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def build(display_a:, display_b:, result:, expression:)
|
|
8
|
+
rod_factory = RodFactory.new
|
|
9
|
+
place_count = [
|
|
10
|
+
digit_count(display_a.abs),
|
|
11
|
+
digit_count(display_b.abs),
|
|
12
|
+
digit_count(result.abs),
|
|
13
|
+
1
|
|
14
|
+
].max
|
|
15
|
+
board = Board.from_numbers(
|
|
16
|
+
a: display_a,
|
|
17
|
+
b: display_b,
|
|
18
|
+
work: 0,
|
|
19
|
+
place_count: place_count,
|
|
20
|
+
rod_factory: rod_factory
|
|
21
|
+
)
|
|
22
|
+
board.row(:work).sign = result <=> 0
|
|
23
|
+
builder = StepBuilder.new(
|
|
24
|
+
config: @config,
|
|
25
|
+
expression: expression,
|
|
26
|
+
board: board,
|
|
27
|
+
result_value: result,
|
|
28
|
+
rod_factory: rod_factory
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
builder.push(
|
|
32
|
+
title: "準備",
|
|
33
|
+
brief_message: "A行とB行を表示し、Work行を空にします。",
|
|
34
|
+
learn_message: "0を含む計算なので、必要な棒だけをWork行へ写して結果を確認します。",
|
|
35
|
+
event: Event.new(type: :setup)
|
|
36
|
+
)
|
|
37
|
+
copy_result_to_work(builder, result.abs)
|
|
38
|
+
board.row(:work).normalize_zero_sign!
|
|
39
|
+
builder.push(
|
|
40
|
+
title: "完了",
|
|
41
|
+
brief_message: "計算結果は#{result}です。",
|
|
42
|
+
learn_message: "Work行の棒が表す値が計算結果です。結果は#{result}です。",
|
|
43
|
+
event: Event.new(type: :finish)
|
|
44
|
+
)
|
|
45
|
+
builder.steps
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def copy_result_to_work(builder, value)
|
|
51
|
+
digits_of(value).each_with_index do |digit, place|
|
|
52
|
+
add_digit_rods(builder, place, digit)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_digit_rods(builder, place, digit)
|
|
57
|
+
(digit / 5).times do
|
|
58
|
+
builder.place_rod(
|
|
59
|
+
row: :work,
|
|
60
|
+
place: place,
|
|
61
|
+
kind: :five,
|
|
62
|
+
title: "結果を写す",
|
|
63
|
+
brief_message: "結果を表すため、Workの#{builder.place_name(place)}にfive棒を1本置きます。"
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
(digit % 5).times do
|
|
67
|
+
builder.place_rod(
|
|
68
|
+
row: :work,
|
|
69
|
+
place: place,
|
|
70
|
+
kind: :unit,
|
|
71
|
+
title: "結果を写す",
|
|
72
|
+
brief_message: "結果を表すため、Workの#{builder.place_name(place)}にunit棒を1本置きます。"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def digits_of(value)
|
|
78
|
+
SignedNumber.from_integer(value).digits
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def digit_count(value)
|
|
82
|
+
value.to_s.length
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
module SubtractionBorrowing
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def ensure_unit_available(place)
|
|
6
|
+
cell = work_cell(place)
|
|
7
|
+
return if cell.unit_rods.any?
|
|
8
|
+
|
|
9
|
+
if cell.five_rods.any?
|
|
10
|
+
split_five_to_units(place)
|
|
11
|
+
else
|
|
12
|
+
borrow_to_place(place)
|
|
13
|
+
ensure_unit_available(place)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ensure_five_available(place)
|
|
18
|
+
cell = work_cell(place)
|
|
19
|
+
return if cell.five_rods.any?
|
|
20
|
+
|
|
21
|
+
if cell.unit_rods.size >= 5
|
|
22
|
+
normalize_units_to_five(place)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
borrow_to_place(place)
|
|
27
|
+
normalize_units_to_five(place) if work_cell(place).unit_rods.size >= 5
|
|
28
|
+
return if work_cell(place).five_rods.any?
|
|
29
|
+
|
|
30
|
+
raise InternalError, "#{@builder.place_name(place)}でfive棒を用意できません。"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def split_five_to_units(place)
|
|
34
|
+
@builder.remove_rod(
|
|
35
|
+
row: :work,
|
|
36
|
+
place: place,
|
|
37
|
+
kind: :five,
|
|
38
|
+
title: "fiveを分解",
|
|
39
|
+
brief_message: "Workの#{@builder.place_name(place)}でfive棒をunit棒5本へ分解するため、five棒を1本取り除きます。",
|
|
40
|
+
learn_message: "5を表す棒を、1を表す棒5本へ戻します。",
|
|
41
|
+
event_type: :convert_five_to_units
|
|
42
|
+
)
|
|
43
|
+
5.times do
|
|
44
|
+
@builder.place_rod(
|
|
45
|
+
row: :work,
|
|
46
|
+
place: place,
|
|
47
|
+
kind: :unit,
|
|
48
|
+
title: "fiveを分解",
|
|
49
|
+
brief_message: "Workの#{@builder.place_name(place)}にunit棒を1本置きます。",
|
|
50
|
+
learn_message: "5を表す棒の代わりに、1を表す棒を1本ずつ置きます。",
|
|
51
|
+
event_type: :convert_five_to_units
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize_units_to_five(place)
|
|
57
|
+
5.times do
|
|
58
|
+
@builder.remove_rod(
|
|
59
|
+
row: :work,
|
|
60
|
+
place: place,
|
|
61
|
+
kind: :unit,
|
|
62
|
+
title: "5本をまとめる",
|
|
63
|
+
brief_message: "Workの#{@builder.place_name(place)}でunit棒5本をfive棒1本にまとめるため、unit棒を1本取り除きます。",
|
|
64
|
+
event_type: :convert_unit_to_five
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
@builder.place_rod(
|
|
68
|
+
row: :work,
|
|
69
|
+
place: place,
|
|
70
|
+
kind: :five,
|
|
71
|
+
title: "5本をまとめる",
|
|
72
|
+
brief_message: "Workの#{@builder.place_name(place)}にfive棒を1本置きます。",
|
|
73
|
+
event_type: :convert_unit_to_five
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def borrow_to_place(target_place)
|
|
78
|
+
source_place = find_upper_nonzero_place(target_place)
|
|
79
|
+
raise InternalError, "繰り下げ元の位が見つかりません。" if source_place.nil?
|
|
80
|
+
|
|
81
|
+
source_place.downto(target_place + 1) do |place|
|
|
82
|
+
decrement_one_at_place(place)
|
|
83
|
+
add_ten_to_lower_place(place - 1)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find_upper_nonzero_place(target_place)
|
|
88
|
+
(target_place + 1...@board.place_count).find do |place|
|
|
89
|
+
work_cell(place).value.positive?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def decrement_one_at_place(place)
|
|
94
|
+
ensure_unit_available(place)
|
|
95
|
+
@builder.remove_rod(
|
|
96
|
+
row: :work,
|
|
97
|
+
place: place,
|
|
98
|
+
kind: :unit,
|
|
99
|
+
title: "繰り下げ",
|
|
100
|
+
brief_message: "Workの#{@builder.place_name(place)}から1を借りるため、unit棒を1本取り除きます。",
|
|
101
|
+
learn_message: "上の位から1を借りると、1つ下の位では10として扱えます。",
|
|
102
|
+
event_type: :borrow_from_upper_place
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_ten_to_lower_place(lower_place)
|
|
107
|
+
2.times do
|
|
108
|
+
@builder.place_rod(
|
|
109
|
+
row: :work,
|
|
110
|
+
place: lower_place,
|
|
111
|
+
kind: :five,
|
|
112
|
+
title: "繰り下げ",
|
|
113
|
+
brief_message: "Workの#{@builder.place_name(lower_place)}に10を作るため、five棒を1本置きます。",
|
|
114
|
+
learn_message: "上の位から借りた1は、下の位では10です。five棒2本で10を表します。",
|
|
115
|
+
event_type: :borrow_from_upper_place
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def work_cell(place)
|
|
121
|
+
@board.row(:work).cells.fetch(place)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class SubtractionEngine
|
|
3
|
+
include SubtractionBorrowing
|
|
4
|
+
|
|
5
|
+
def initialize(config)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def build(minuend_abs:, subtrahend_abs:, result_sign:, expression:)
|
|
10
|
+
@rod_factory = RodFactory.new
|
|
11
|
+
@result_value = result_sign * (minuend_abs - subtrahend_abs)
|
|
12
|
+
place_count = [
|
|
13
|
+
digit_count(minuend_abs),
|
|
14
|
+
digit_count(subtrahend_abs),
|
|
15
|
+
digit_count(@result_value.abs),
|
|
16
|
+
1
|
|
17
|
+
].max
|
|
18
|
+
@board = Board.from_numbers(
|
|
19
|
+
a: minuend_abs,
|
|
20
|
+
b: subtrahend_abs,
|
|
21
|
+
work: 0,
|
|
22
|
+
place_count: place_count,
|
|
23
|
+
rod_factory: @rod_factory
|
|
24
|
+
)
|
|
25
|
+
@board.row(:work).sign = result_sign
|
|
26
|
+
@builder = StepBuilder.new(
|
|
27
|
+
config: @config,
|
|
28
|
+
expression: expression,
|
|
29
|
+
board: @board,
|
|
30
|
+
result_value: @result_value,
|
|
31
|
+
rod_factory: @rod_factory
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
push_setup_step
|
|
35
|
+
copy_minuend_to_work(minuend_abs)
|
|
36
|
+
digit_count(subtrahend_abs).times do |place|
|
|
37
|
+
subtract_digit_from_work(place, digit_of(subtrahend_abs, place))
|
|
38
|
+
end
|
|
39
|
+
push_finish_step
|
|
40
|
+
@builder.steps
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def push_setup_step
|
|
46
|
+
@builder.push(
|
|
47
|
+
title: "準備",
|
|
48
|
+
brief_message: "絶対値の大きい数をA行、小さい数をB行に置き、Work行で減算します。",
|
|
49
|
+
learn_message: "異符号の加算や結果が負になる減算は、絶対値の大きい方から小さい方を引き、最後に符号を付けます。",
|
|
50
|
+
event: Event.new(type: :setup)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def push_finish_step
|
|
55
|
+
@board.row(:work).normalize_zero_sign!
|
|
56
|
+
@builder.push(
|
|
57
|
+
title: "完了",
|
|
58
|
+
brief_message: "計算結果は#{@result_value}です。",
|
|
59
|
+
learn_message: "Work行に残った棒が差を表します。結果の符号を反映すると#{@result_value}です。",
|
|
60
|
+
event: Event.new(type: :finish)
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def copy_minuend_to_work(value)
|
|
65
|
+
digits_of(value).each_with_index do |digit, place|
|
|
66
|
+
add_digit_rods_to_work(place, digit)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_digit_rods_to_work(place, digit)
|
|
71
|
+
(digit / 5).times do
|
|
72
|
+
@builder.place_rod(
|
|
73
|
+
row: :work,
|
|
74
|
+
place: place,
|
|
75
|
+
kind: :five,
|
|
76
|
+
title: "被減数を写す",
|
|
77
|
+
brief_message: "被減数を表すため、Workの#{@builder.place_name(place)}にfive棒を1本置きます。"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
(digit % 5).times do
|
|
81
|
+
@builder.place_rod(
|
|
82
|
+
row: :work,
|
|
83
|
+
place: place,
|
|
84
|
+
kind: :unit,
|
|
85
|
+
title: "被減数を写す",
|
|
86
|
+
brief_message: "被減数を表すため、Workの#{@builder.place_name(place)}にunit棒を1本置きます。"
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def subtract_digit_from_work(place, digit)
|
|
92
|
+
return if digit.zero?
|
|
93
|
+
|
|
94
|
+
unit_count = digit % 5
|
|
95
|
+
five_count = digit / 5
|
|
96
|
+
|
|
97
|
+
unit_count.times do
|
|
98
|
+
ensure_unit_available(place)
|
|
99
|
+
@builder.remove_rod(
|
|
100
|
+
row: :work,
|
|
101
|
+
place: place,
|
|
102
|
+
kind: :unit,
|
|
103
|
+
title: "減算",
|
|
104
|
+
brief_message: "Workの#{@builder.place_name(place)}からunit棒を1本取り除きます。",
|
|
105
|
+
learn_message: "減数の1に対応する棒を、Work行から1本取り除きます。"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
five_count.times do
|
|
110
|
+
ensure_five_available(place)
|
|
111
|
+
@builder.remove_rod(
|
|
112
|
+
row: :work,
|
|
113
|
+
place: place,
|
|
114
|
+
kind: :five,
|
|
115
|
+
title: "減算",
|
|
116
|
+
brief_message: "Workの#{@builder.place_name(place)}からfive棒を1本取り除きます。",
|
|
117
|
+
learn_message: "減数の5に対応する棒を、Work行から1本取り除きます。"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def digit_of(value, place)
|
|
123
|
+
digits_of(value).fetch(place, 0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def digits_of(value)
|
|
127
|
+
SignedNumber.from_integer(value).digits
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def digit_count(value)
|
|
131
|
+
value.to_s.length
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
data/lib/sangi/errors.rb
ADDED
data/lib/sangi/event.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Event
|
|
3
|
+
attr_reader :type, :row, :place_index, :kind, :from, :to
|
|
4
|
+
|
|
5
|
+
def initialize(type:, row: nil, place_index: nil, kind: nil, from: nil, to: nil)
|
|
6
|
+
@type = type
|
|
7
|
+
@row = row
|
|
8
|
+
@place_index = place_index
|
|
9
|
+
@kind = kind
|
|
10
|
+
@from = from
|
|
11
|
+
@to = to
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
module Exporter
|
|
3
|
+
class TextExporter
|
|
4
|
+
SEPARATOR = "=" * 60
|
|
5
|
+
|
|
6
|
+
def initialize(config, renderer)
|
|
7
|
+
@config = config
|
|
8
|
+
@renderer = renderer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def export(path, steps)
|
|
12
|
+
File.write(path, build_text(steps), mode: "w:UTF-8")
|
|
13
|
+
rescue SystemCallError
|
|
14
|
+
raise ExportError, "ログファイルを書き込めません: #{path}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_text(steps)
|
|
18
|
+
first = steps.first
|
|
19
|
+
state = first&.numeric_state || {}
|
|
20
|
+
lines = [
|
|
21
|
+
"sangi calculation log",
|
|
22
|
+
"version: #{VERSION}",
|
|
23
|
+
"expr: #{state[:source]}",
|
|
24
|
+
"normalized: #{state[:normalized]}",
|
|
25
|
+
"mode: #{@config.mode}",
|
|
26
|
+
"zero: #{@config.zero_mode}",
|
|
27
|
+
"sign: #{@config.sign_mode}",
|
|
28
|
+
"explain: #{@config.explain_mode}",
|
|
29
|
+
""
|
|
30
|
+
]
|
|
31
|
+
steps.each do |step|
|
|
32
|
+
lines << SEPARATOR
|
|
33
|
+
lines << "step #{step.index}/#{steps.size}"
|
|
34
|
+
lines << @renderer.render(step, total_steps: steps.size).chomp
|
|
35
|
+
end
|
|
36
|
+
lines << SEPARATOR unless steps.empty?
|
|
37
|
+
lines.join("\n") + "\n"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Expression
|
|
3
|
+
attr_reader :left, :operator, :right, :source
|
|
4
|
+
|
|
5
|
+
def initialize(left:, operator:, right:, source:)
|
|
6
|
+
@left = left
|
|
7
|
+
@operator = operator
|
|
8
|
+
@right = right
|
|
9
|
+
@source = source
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def effective_right
|
|
13
|
+
operator == :- ? -right : right
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def normalized_source
|
|
17
|
+
"#{left} + #{effective_right}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/sangi/parser.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Parser
|
|
3
|
+
PATTERN = /\A\s*([+-]?\d+)\s*([+-])\s*([+-]?\d+)\s*\z/
|
|
4
|
+
|
|
5
|
+
def initialize(config)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parse(source)
|
|
10
|
+
match = PATTERN.match(source.to_s)
|
|
11
|
+
raise ParseError, parse_error_message unless match
|
|
12
|
+
|
|
13
|
+
operator_text = match[2]
|
|
14
|
+
right_text = match[3]
|
|
15
|
+
raise ParseError, parse_error_message if operator_text == "+" && right_text.start_with?("+")
|
|
16
|
+
|
|
17
|
+
left = normalize_zero(match[1].to_i)
|
|
18
|
+
operator = operator_text.to_sym
|
|
19
|
+
right = normalize_zero(right_text.to_i)
|
|
20
|
+
|
|
21
|
+
validate_digits!(left)
|
|
22
|
+
validate_digits!(right)
|
|
23
|
+
|
|
24
|
+
Expression.new(
|
|
25
|
+
left: left,
|
|
26
|
+
operator: operator,
|
|
27
|
+
right: right,
|
|
28
|
+
source: source.to_s
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def normalize_zero(value)
|
|
35
|
+
value.zero? ? 0 : value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_digits!(value)
|
|
39
|
+
return if value.abs.to_s.length <= @config.max_digits
|
|
40
|
+
|
|
41
|
+
raise ValidationError,
|
|
42
|
+
"桁数が上限を超えています。現在の上限: #{@config.max_digits}桁。上限を変更するには --max-digits N を指定してください。"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_error_message
|
|
46
|
+
"入力式を解釈できません。例: 123+45, 100-7, -12+7, 3--5"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|