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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ffacfdf861aea59dabc76828c2dd031aca04c085a44879483e97065b213c3a5
|
|
4
|
+
data.tar.gz: c20326becf52746e40dde658664098e74f7542d0e890e6f1971525793f95bcb5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c3a73cf7c5c9a9ab9cc37969ebe1aaa23015bc3ab75f20574aae81c63291f6d441fe01a4755594b994ac91ef0217fad5f88fe2dcb48a933c758e6145591585bc
|
|
7
|
+
data.tar.gz: 8691de8b1f57efdf538f4d84f874e01599278c3e121210faf513b98cecf160a9e54a9c8c237f43fb2cc3adeb2341504127c3fcd8ef4ce47b59cb0fc621fee41d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 算木
|
|
2
|
+
|
|
3
|
+
此器者,象算木之法,以西字之畫陳整數加減之程者也。其器以紅玉之術作之,行於命令之列。甲行、乙行存本數以爲觀,作行則置棒一枚,去棒一枚,令算術進退之迹,皆見於目。
|
|
4
|
+
|
|
5
|
+
## 置法
|
|
6
|
+
|
|
7
|
+
此庫不假外玉。
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ruby -Ilib exe/sangi --version
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
欲鑄爲玉,則行下命。
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
gem build sangi.gemspec
|
|
17
|
+
gem install ./sangi-0.1.0.gem
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 用法
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
exe/sangi
|
|
24
|
+
exe/sangi "128+47"
|
|
25
|
+
exe/sangi "100-7" --all
|
|
26
|
+
exe/sangi "-12+7" --learn
|
|
27
|
+
exe/sangi "3--5" --mode hist --all
|
|
28
|
+
exe/sangi "128+47" --export steps.txt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
無引數而起之,則入讀評印之場,待人書式。
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
sangi> 7+8
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 所受之式
|
|
38
|
+
|
|
39
|
+
今之初版,唯受二項有符整數之加減。
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
1+2
|
|
43
|
+
1 - 2
|
|
44
|
+
-1+2
|
|
45
|
+
-1 - 2
|
|
46
|
+
3--5
|
|
47
|
+
3 - -5
|
|
48
|
+
0+0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 鍵法
|
|
52
|
+
|
|
53
|
+
觀段之時,用鍵如下。
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
n / Enter 進一段
|
|
57
|
+
p 退一段
|
|
58
|
+
r 還始段
|
|
59
|
+
e 至終段
|
|
60
|
+
a 自行進止
|
|
61
|
+
q 去觀段
|
|
62
|
+
? 示鍵法
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 選項
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
--mode edu|hist
|
|
69
|
+
--zero blank|digit|circle
|
|
70
|
+
--sign modern|slash|color|dual
|
|
71
|
+
--explain none|brief|learn
|
|
72
|
+
--learn
|
|
73
|
+
--all
|
|
74
|
+
--export PATH
|
|
75
|
+
--max-digits N
|
|
76
|
+
--max-steps N
|
|
77
|
+
--no-color
|
|
78
|
+
--version
|
|
79
|
+
--help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
教育之式,以 `0` 顯零,以今符 `-` 顯負數。史風之式,零則空之,負數則加斜記以識之。然史風者,取算木之風而濃之耳,非謂盡合古籍舊法,毫釐不差也。
|
|
83
|
+
|
|
84
|
+
## 例
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
exe/sangi "7+8" --all
|
|
88
|
+
exe/sangi "99+1" --all
|
|
89
|
+
exe/sangi "100-7" --all
|
|
90
|
+
exe/sangi "-12+7" --all
|
|
91
|
+
exe/sangi "3--5" --all
|
|
92
|
+
exe/sangi "128+47" --mode edu --learn --all
|
|
93
|
+
exe/sangi "128+47" --mode hist --all
|
|
94
|
+
exe/sangi "128+47" --zero blank --sign slash --all
|
|
95
|
+
exe/sangi "128+47" --export steps.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
其所得當如下。
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
7+8 -> 15
|
|
102
|
+
99+1 -> 100
|
|
103
|
+
100-7 -> 93
|
|
104
|
+
-12+7 -> -5
|
|
105
|
+
3--5 -> 8
|
|
106
|
+
128+47 -> 175
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 未及
|
|
110
|
+
|
|
111
|
+
乘除、小數、分數、萬國符號之算木字、萬國符號之罫線、圖形窓、網版,今皆未及。數大則段亦多,畫幅亦廣。若端末座之幅不足,則出警辭;然終端狹隘,畫或折行。
|
data/exe/sangi
ADDED
data/lib/sangi/board.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Board
|
|
3
|
+
attr_accessor :rows, :highlight
|
|
4
|
+
|
|
5
|
+
def self.from_numbers(a:, b:, work:, place_count:, rod_factory:)
|
|
6
|
+
new(
|
|
7
|
+
rows: {
|
|
8
|
+
a: Row.from_integer(name: :a, value: a, place_count: place_count, rod_factory: rod_factory),
|
|
9
|
+
b: Row.from_integer(name: :b, value: b, place_count: place_count, rod_factory: rod_factory),
|
|
10
|
+
work: Row.from_integer(name: :work, value: work, place_count: place_count, rod_factory: rod_factory)
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(rows:, highlight: nil)
|
|
16
|
+
@rows = rows
|
|
17
|
+
@highlight = highlight
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def row(name)
|
|
21
|
+
rows.fetch(name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def place_count
|
|
25
|
+
rows.values.map { |row| row.cells.size }.max || 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clone_deep
|
|
29
|
+
Board.new(
|
|
30
|
+
rows: rows.transform_values(&:clone_deep),
|
|
31
|
+
highlight: highlight&.dup
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/sangi/cell.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Cell
|
|
3
|
+
attr_reader :place_index
|
|
4
|
+
attr_accessor :unit_rods, :five_rods
|
|
5
|
+
|
|
6
|
+
def self.from_digit(place_index:, digit:, rod_factory:)
|
|
7
|
+
raise ValidationError, "digit must be between 0 and 9" unless (0..9).cover?(digit)
|
|
8
|
+
|
|
9
|
+
five_count = digit / 5
|
|
10
|
+
unit_count = digit % 5
|
|
11
|
+
new(
|
|
12
|
+
place_index: place_index,
|
|
13
|
+
unit_rods: unit_count.times.map { rod_factory.build(:unit) },
|
|
14
|
+
five_rods: five_count.times.map { rod_factory.build(:five) }
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.empty(place_index:)
|
|
19
|
+
new(place_index: place_index)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(place_index:, unit_rods: [], five_rods: [])
|
|
23
|
+
@place_index = place_index
|
|
24
|
+
@unit_rods = unit_rods
|
|
25
|
+
@five_rods = five_rods
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def orientation
|
|
29
|
+
place_index.even? ? :vertical : :horizontal
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def value
|
|
33
|
+
unit_rods.size + five_rods.size * 5
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_rod(rod)
|
|
37
|
+
case rod.kind
|
|
38
|
+
when :unit then unit_rods << rod
|
|
39
|
+
when :five then five_rods << rod
|
|
40
|
+
else raise ValidationError, "invalid rod kind"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remove_rod(kind)
|
|
45
|
+
case kind
|
|
46
|
+
when :unit then unit_rods.pop
|
|
47
|
+
when :five then five_rods.pop
|
|
48
|
+
else raise ValidationError, "invalid rod kind"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def clone_deep
|
|
53
|
+
Cell.new(
|
|
54
|
+
place_index: place_index,
|
|
55
|
+
unit_rods: unit_rods.dup,
|
|
56
|
+
five_rods: five_rods.dup
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/sangi/cli.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module Sangi
|
|
4
|
+
class CLI
|
|
5
|
+
SEPARATOR = "=" * 60
|
|
6
|
+
|
|
7
|
+
def run(argv)
|
|
8
|
+
args = argv.dup
|
|
9
|
+
expression_source = extract_expression!(args)
|
|
10
|
+
options, parser = parse_options(args)
|
|
11
|
+
if options[:help]
|
|
12
|
+
puts parser
|
|
13
|
+
return 0
|
|
14
|
+
end
|
|
15
|
+
if options[:version]
|
|
16
|
+
puts VERSION
|
|
17
|
+
return 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
expression_source ||= args.shift
|
|
21
|
+
raise ParseError, "式は1つだけ指定してください。" unless args.empty?
|
|
22
|
+
|
|
23
|
+
config = build_config(options)
|
|
24
|
+
return REPL.new(config: config).start if expression_source.nil?
|
|
25
|
+
|
|
26
|
+
run_expression(expression_source, config)
|
|
27
|
+
0
|
|
28
|
+
rescue OptionParser::ParseError => e
|
|
29
|
+
warn e.message
|
|
30
|
+
1
|
|
31
|
+
rescue Sangi::Error => e
|
|
32
|
+
warn e.message
|
|
33
|
+
warn e.full_message if ENV["SANGI_DEBUG"] == "1"
|
|
34
|
+
1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def extract_expression!(args)
|
|
40
|
+
expression_index = args.index { |arg| expression_like?(arg) }
|
|
41
|
+
return nil if expression_index.nil?
|
|
42
|
+
|
|
43
|
+
args.delete_at(expression_index)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def expression_like?(arg)
|
|
47
|
+
/\A\s*[+-]?\d+\s*[+-]\s*[+-]?\d+\s*\z/.match?(arg.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_options(args)
|
|
51
|
+
options = {
|
|
52
|
+
mode: :edu,
|
|
53
|
+
zero_mode: nil,
|
|
54
|
+
sign_mode: nil,
|
|
55
|
+
explain_mode: :brief,
|
|
56
|
+
all: false,
|
|
57
|
+
export_path: nil,
|
|
58
|
+
max_digits: 8,
|
|
59
|
+
max_steps: 10_000,
|
|
60
|
+
color: true,
|
|
61
|
+
version: false,
|
|
62
|
+
help: false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parser = OptionParser.new do |opts|
|
|
66
|
+
opts.banner = "Usage: sangi [EXPR] [options]"
|
|
67
|
+
opts.separator ""
|
|
68
|
+
opts.separator "Examples:"
|
|
69
|
+
opts.separator " sangi"
|
|
70
|
+
opts.separator " sangi \"128+47\""
|
|
71
|
+
opts.separator " sangi \"100-7\" --all"
|
|
72
|
+
opts.separator " sangi \"-12+7\" --learn"
|
|
73
|
+
opts.separator " sangi \"3--5\" --mode hist --all"
|
|
74
|
+
opts.separator " sangi \"128+47\" --export steps.txt"
|
|
75
|
+
opts.separator ""
|
|
76
|
+
opts.separator "Options:"
|
|
77
|
+
|
|
78
|
+
opts.on("--mode MODE", "edu or hist") { |value| options[:mode] = value.to_sym }
|
|
79
|
+
opts.on("--zero MODE", "blank, digit, circle") { |value| options[:zero_mode] = value.to_sym }
|
|
80
|
+
opts.on("--sign MODE", "modern, slash, color, dual") { |value| options[:sign_mode] = value.to_sym }
|
|
81
|
+
opts.on("--explain MODE", "none, brief, learn") { |value| options[:explain_mode] = value.to_sym }
|
|
82
|
+
opts.on("--learn", "same as --explain learn") { options[:explain_mode] = :learn }
|
|
83
|
+
opts.on("--all", "print all steps") { options[:all] = true }
|
|
84
|
+
opts.on("--export PATH", "write all steps to PATH") { |value| options[:export_path] = value }
|
|
85
|
+
opts.on("--max-digits N", Integer, "maximum input digits") { |value| options[:max_digits] = value }
|
|
86
|
+
opts.on("--max-steps N", Integer, "maximum generated steps") { |value| options[:max_steps] = value }
|
|
87
|
+
opts.on("--no-color", "disable ANSI color") { options[:color] = false }
|
|
88
|
+
opts.on("--version", "print version") { options[:version] = true }
|
|
89
|
+
opts.on("-h", "--help", "print help") { options[:help] = true }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
parser.parse!(args)
|
|
93
|
+
[options, parser]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_config(options)
|
|
97
|
+
Config.new(
|
|
98
|
+
mode: options[:mode],
|
|
99
|
+
zero_mode: options[:zero_mode],
|
|
100
|
+
sign_mode: options[:sign_mode],
|
|
101
|
+
explain_mode: options[:explain_mode],
|
|
102
|
+
all: options[:all],
|
|
103
|
+
export_path: options[:export_path],
|
|
104
|
+
max_digits: options[:max_digits],
|
|
105
|
+
max_steps: options[:max_steps],
|
|
106
|
+
color: color_output_enabled?(options)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def color_output_enabled?(options)
|
|
111
|
+
options[:color] && $stdout.respond_to?(:tty?) && $stdout.tty?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def run_expression(source, config)
|
|
115
|
+
expression = Parser.new(config).parse(source)
|
|
116
|
+
steps = Engine.new(config).build_steps(expression)
|
|
117
|
+
renderer = Renderer::AsciiRenderer.new(config)
|
|
118
|
+
|
|
119
|
+
export_steps(config, steps) if config.export_path
|
|
120
|
+
print_all_steps(renderer, steps) if config.all
|
|
121
|
+
return if config.all || config.export_path
|
|
122
|
+
|
|
123
|
+
StepViewer.new(steps: steps, renderer: renderer).start
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def print_all_steps(renderer, steps)
|
|
127
|
+
TerminalWidthWarning.new(output: $stdout).warn_once(steps, error_output: $stderr)
|
|
128
|
+
steps.each do |step|
|
|
129
|
+
puts SEPARATOR
|
|
130
|
+
puts "step #{step.index}/#{steps.size}"
|
|
131
|
+
print renderer.render(step, total_steps: steps.size)
|
|
132
|
+
end
|
|
133
|
+
puts SEPARATOR
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def export_steps(config, steps)
|
|
137
|
+
export_config = config.with(color: false)
|
|
138
|
+
export_renderer = Renderer::AsciiRenderer.new(export_config)
|
|
139
|
+
Exporter::TextExporter.new(export_config, export_renderer).export(config.export_path, steps)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/sangi/config.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Config
|
|
3
|
+
VALID_MODES = %i[edu hist].freeze
|
|
4
|
+
VALID_ZERO_MODES = %i[blank digit circle].freeze
|
|
5
|
+
VALID_SIGN_MODES = %i[modern slash color dual].freeze
|
|
6
|
+
VALID_EXPLAIN_MODES = %i[none brief learn].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :mode, :zero_mode, :sign_mode, :explain_mode
|
|
9
|
+
attr_reader :all, :export_path, :max_digits, :max_steps, :color
|
|
10
|
+
|
|
11
|
+
def initialize(
|
|
12
|
+
mode: :edu,
|
|
13
|
+
zero_mode: nil,
|
|
14
|
+
sign_mode: nil,
|
|
15
|
+
explain_mode: :brief,
|
|
16
|
+
all: false,
|
|
17
|
+
export_path: nil,
|
|
18
|
+
max_digits: 8,
|
|
19
|
+
max_steps: 10_000,
|
|
20
|
+
color: true
|
|
21
|
+
)
|
|
22
|
+
@mode = normalize_symbol(mode)
|
|
23
|
+
@zero_mode = normalize_symbol(zero_mode) || default_zero_mode(@mode)
|
|
24
|
+
@sign_mode = normalize_symbol(sign_mode) || default_sign_mode(@mode)
|
|
25
|
+
@explain_mode = normalize_symbol(explain_mode)
|
|
26
|
+
@all = all
|
|
27
|
+
@export_path = export_path
|
|
28
|
+
@max_digits = Integer(max_digits)
|
|
29
|
+
@max_steps = Integer(max_steps)
|
|
30
|
+
@color = color ? true : false
|
|
31
|
+
validate!
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
raise ValidationError, "数値オプションには整数を指定してください。"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with(**overrides)
|
|
37
|
+
Config.new(
|
|
38
|
+
mode: overrides.fetch(:mode, mode),
|
|
39
|
+
zero_mode: overrides.fetch(:zero_mode, zero_mode),
|
|
40
|
+
sign_mode: overrides.fetch(:sign_mode, sign_mode),
|
|
41
|
+
explain_mode: overrides.fetch(:explain_mode, explain_mode),
|
|
42
|
+
all: overrides.fetch(:all, all),
|
|
43
|
+
export_path: overrides.fetch(:export_path, export_path),
|
|
44
|
+
max_digits: overrides.fetch(:max_digits, max_digits),
|
|
45
|
+
max_steps: overrides.fetch(:max_steps, max_steps),
|
|
46
|
+
color: overrides.fetch(:color, color)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def normalize_symbol(value)
|
|
53
|
+
return nil if value.nil?
|
|
54
|
+
|
|
55
|
+
value.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_zero_mode(mode)
|
|
59
|
+
mode == :hist ? :blank : :digit
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def default_sign_mode(mode)
|
|
63
|
+
mode == :hist ? :slash : :modern
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate!
|
|
67
|
+
raise ValidationError, "invalid mode" unless VALID_MODES.include?(mode)
|
|
68
|
+
raise ValidationError, "invalid zero mode" unless VALID_ZERO_MODES.include?(zero_mode)
|
|
69
|
+
raise ValidationError, "invalid sign mode" unless VALID_SIGN_MODES.include?(sign_mode)
|
|
70
|
+
raise ValidationError, "invalid explain mode" unless VALID_EXPLAIN_MODES.include?(explain_mode)
|
|
71
|
+
raise ValidationError, "max_digits must be positive" unless max_digits.positive?
|
|
72
|
+
raise ValidationError, "max_steps must be positive" unless max_steps.positive?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/sangi/engine.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Engine
|
|
3
|
+
def initialize(config)
|
|
4
|
+
@config = config
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def build_steps(expression)
|
|
8
|
+
left = expression.left
|
|
9
|
+
right = expression.effective_right
|
|
10
|
+
|
|
11
|
+
steps = build_steps_for_normalized(left, right, expression)
|
|
12
|
+
validate_step_count!(steps)
|
|
13
|
+
steps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def build_steps_for_normalized(left, right, expression)
|
|
19
|
+
return copy_steps(left, right, 0, expression) if left.zero? && right.zero?
|
|
20
|
+
return copy_steps(left, right, left, expression) if right.zero?
|
|
21
|
+
return copy_steps(left, right, right, expression) if left.zero?
|
|
22
|
+
|
|
23
|
+
if same_sign?(left, right)
|
|
24
|
+
result_sign = left.negative? ? -1 : 1
|
|
25
|
+
return AdditionEngine.new(@config).build(
|
|
26
|
+
abs_a: left.abs,
|
|
27
|
+
abs_b: right.abs,
|
|
28
|
+
result_sign: result_sign,
|
|
29
|
+
display_a: left,
|
|
30
|
+
display_b: right,
|
|
31
|
+
expression: expression
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
comparison = left.abs <=> right.abs
|
|
36
|
+
return copy_steps(left, right, 0, expression) if comparison.zero?
|
|
37
|
+
|
|
38
|
+
if comparison.positive?
|
|
39
|
+
SubtractionEngine.new(@config).build(
|
|
40
|
+
minuend_abs: left.abs,
|
|
41
|
+
subtrahend_abs: right.abs,
|
|
42
|
+
result_sign: left.negative? ? -1 : 1,
|
|
43
|
+
expression: expression
|
|
44
|
+
)
|
|
45
|
+
else
|
|
46
|
+
SubtractionEngine.new(@config).build(
|
|
47
|
+
minuend_abs: right.abs,
|
|
48
|
+
subtrahend_abs: left.abs,
|
|
49
|
+
result_sign: right.negative? ? -1 : 1,
|
|
50
|
+
expression: expression
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def same_sign?(left, right)
|
|
56
|
+
(left.negative? && right.negative?) || (left.positive? && right.positive?)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def copy_steps(left, right, result, expression)
|
|
60
|
+
CopyEngine.new(@config).build(
|
|
61
|
+
display_a: left,
|
|
62
|
+
display_b: right,
|
|
63
|
+
result: result,
|
|
64
|
+
expression: expression
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_step_count!(steps)
|
|
69
|
+
return if steps.size <= @config.max_steps
|
|
70
|
+
|
|
71
|
+
raise StepLimitError,
|
|
72
|
+
"ステップ数が上限を超えました。現在の上限: #{@config.max_steps}。上限を変更するには --max-steps N を指定してください。"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|