keisanjaku 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.txt +21 -0
- data/README.md +126 -0
- data/exe/keisanjaku +6 -0
- data/lib/keisanjaku/ansi.rb +79 -0
- data/lib/keisanjaku/app.rb +144 -0
- data/lib/keisanjaku/drill.rb +118 -0
- data/lib/keisanjaku/engine.rb +98 -0
- data/lib/keisanjaku/input.rb +78 -0
- data/lib/keisanjaku/modes/demo.rb +142 -0
- data/lib/keisanjaku/modes/drill.rb +127 -0
- data/lib/keisanjaku/modes/sandbox.rb +159 -0
- data/lib/keisanjaku/parser.rb +185 -0
- data/lib/keisanjaku/planner.rb +306 -0
- data/lib/keisanjaku/procedure.rb +108 -0
- data/lib/keisanjaku/renderer.rb +142 -0
- data/lib/keisanjaku/rule_state.rb +98 -0
- data/lib/keisanjaku/scale.rb +205 -0
- data/lib/keisanjaku/terminal_viewport.rb +107 -0
- data/lib/keisanjaku/version.rb +3 -0
- data/lib/keisanjaku.rb +13 -0
- metadata +64 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
module Keisanjaku
|
|
2
|
+
class ScaleError < ArgumentError; end
|
|
3
|
+
|
|
4
|
+
class Scale
|
|
5
|
+
DEGREE = Math::PI / 180.0
|
|
6
|
+
S_MIN_DEGREES = Math.asin(0.1) / DEGREE
|
|
7
|
+
T_MIN_DEGREES = Math.atan(0.1) / DEGREE
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :kind, :decades
|
|
10
|
+
|
|
11
|
+
def initialize(name, kind:, decades: 1)
|
|
12
|
+
@name = name.to_s.upcase
|
|
13
|
+
@kind = kind
|
|
14
|
+
@decades = decades
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def position(value)
|
|
18
|
+
numeric = Float(value)
|
|
19
|
+
pos = case kind
|
|
20
|
+
when :log
|
|
21
|
+
require_range(numeric, 1.0, 10.0)
|
|
22
|
+
Math.log10(numeric)
|
|
23
|
+
when :inverse_log
|
|
24
|
+
require_range(numeric, 1.0, 10.0)
|
|
25
|
+
1.0 - Math.log10(numeric)
|
|
26
|
+
when :power
|
|
27
|
+
require_range(numeric, 1.0, 10.0**decades)
|
|
28
|
+
Math.log10(numeric) / decades
|
|
29
|
+
when :linear
|
|
30
|
+
require_range(numeric, 0.0, 1.0)
|
|
31
|
+
numeric
|
|
32
|
+
when :sin
|
|
33
|
+
require_range(numeric, S_MIN_DEGREES, 90.0)
|
|
34
|
+
Math.log10(10.0 * Math.sin(numeric * DEGREE))
|
|
35
|
+
when :tan
|
|
36
|
+
require_range(numeric, T_MIN_DEGREES, 45.0)
|
|
37
|
+
Math.log10(10.0 * Math.tan(numeric * DEGREE))
|
|
38
|
+
else
|
|
39
|
+
raise ScaleError, "unknown scale kind: #{kind}"
|
|
40
|
+
end
|
|
41
|
+
clamp_unit(pos)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def value_at(pos)
|
|
45
|
+
numeric = clamp_unit(Float(pos))
|
|
46
|
+
case kind
|
|
47
|
+
when :log
|
|
48
|
+
10.0**numeric
|
|
49
|
+
when :inverse_log
|
|
50
|
+
10.0**(1.0 - numeric)
|
|
51
|
+
when :power
|
|
52
|
+
10.0**(decades * numeric)
|
|
53
|
+
when :linear
|
|
54
|
+
numeric
|
|
55
|
+
when :sin
|
|
56
|
+
Math.asin((10.0**numeric) / 10.0) / DEGREE
|
|
57
|
+
when :tan
|
|
58
|
+
Math.atan((10.0**numeric) / 10.0) / DEGREE
|
|
59
|
+
else
|
|
60
|
+
raise ScaleError, "unknown scale kind: #{kind}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ticks(width)
|
|
65
|
+
case kind
|
|
66
|
+
when :log, :inverse_log
|
|
67
|
+
log_ticks(width)
|
|
68
|
+
when :power
|
|
69
|
+
power_ticks(width)
|
|
70
|
+
when :linear
|
|
71
|
+
linear_ticks(width)
|
|
72
|
+
when :sin
|
|
73
|
+
trigonometric_ticks(:sin, width)
|
|
74
|
+
when :tan
|
|
75
|
+
trigonometric_ticks(:tan, width)
|
|
76
|
+
else
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.fetch(name)
|
|
82
|
+
key = name.to_s.upcase
|
|
83
|
+
SCALES.fetch(key) { raise ScaleError, "unknown scale: #{name}" }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.names
|
|
87
|
+
SCALES.keys
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def require_range(value, min, max)
|
|
93
|
+
return if value >= min - 1e-12 && value <= max + 1e-12
|
|
94
|
+
|
|
95
|
+
raise ScaleError, "#{name} value #{value} is outside #{min}..#{max}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def clamp_unit(pos)
|
|
99
|
+
return 0.0 if pos.abs < 1e-12
|
|
100
|
+
return 1.0 if (pos - 1.0).abs < 1e-12
|
|
101
|
+
return pos if pos >= 0.0 && pos <= 1.0
|
|
102
|
+
|
|
103
|
+
raise ScaleError, "#{name} position #{pos} is outside 0..1"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_tick(ticks, width, pos, label, level)
|
|
107
|
+
col = (clamp_unit(pos) * (width - 1)).round
|
|
108
|
+
current = ticks[col]
|
|
109
|
+
if current.nil? || level < current[:level]
|
|
110
|
+
ticks[col] = { col: col, label: label, level: level }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def log_ticks(width)
|
|
115
|
+
ticks = {}
|
|
116
|
+
if width >= 90
|
|
117
|
+
(10..99).each do |n|
|
|
118
|
+
value = n / 10.0
|
|
119
|
+
level = (n % 10).zero? || n == 10 ? 0 : 2
|
|
120
|
+
label = level.zero? ? major_log_label(value) : nil
|
|
121
|
+
add_tick(ticks, width, position(value), label, level)
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
(2..18).each do |n|
|
|
125
|
+
value = n / 2.0
|
|
126
|
+
level = value == value.floor ? 0 : 1
|
|
127
|
+
label = level.zero? ? major_log_label(value) : nil
|
|
128
|
+
add_tick(ticks, width, position(value), label, level)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
add_tick(ticks, width, position(10.0), "1", 0)
|
|
132
|
+
ticks.values.sort_by { |tick| tick[:col] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def major_log_label(value)
|
|
136
|
+
rounded = value.round
|
|
137
|
+
rounded == 10 ? "1" : rounded.to_s
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def power_ticks(width)
|
|
141
|
+
ticks = {}
|
|
142
|
+
(0...decades).each do |decade|
|
|
143
|
+
base = 10**decade
|
|
144
|
+
(1..9).each do |n|
|
|
145
|
+
value = base * n
|
|
146
|
+
add_tick(ticks, width, position(value), n.to_s, 0)
|
|
147
|
+
next if width < 90 || n == 9
|
|
148
|
+
|
|
149
|
+
add_tick(ticks, width, position(value + base * 0.5), nil, 1)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
add_tick(ticks, width, position(10.0**decades), "1", 0)
|
|
153
|
+
ticks.values.sort_by { |tick| tick[:col] }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def linear_ticks(width)
|
|
157
|
+
ticks = {}
|
|
158
|
+
step = width >= 90 ? 0.05 : 0.1
|
|
159
|
+
count = (1.0 / step).round
|
|
160
|
+
(0..count).each do |n|
|
|
161
|
+
value = n * step
|
|
162
|
+
major = (value * 10).round == value * 10
|
|
163
|
+
label = if major
|
|
164
|
+
value.zero? ? "0" : (value == 1.0 ? "1" : format(".%d", (value * 10).round))
|
|
165
|
+
end
|
|
166
|
+
add_tick(ticks, width, position(value), label, major ? 0 : 1)
|
|
167
|
+
end
|
|
168
|
+
ticks.values.sort_by { |tick| tick[:col] }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def trigonometric_ticks(type, width)
|
|
172
|
+
values = if type == :sin
|
|
173
|
+
[S_MIN_DEGREES, 6, 10, 15, 20, 30, 45, 60, 90]
|
|
174
|
+
else
|
|
175
|
+
[T_MIN_DEGREES, 6, 10, 15, 20, 30, 45]
|
|
176
|
+
end
|
|
177
|
+
ticks = {}
|
|
178
|
+
values.each do |value|
|
|
179
|
+
label = value == values.first ? "5.7" : value.round.to_s
|
|
180
|
+
add_tick(ticks, width, position(value), label, 0)
|
|
181
|
+
end
|
|
182
|
+
if width >= 90
|
|
183
|
+
max = type == :sin ? 90 : 45
|
|
184
|
+
(6..max).each do |degree|
|
|
185
|
+
next if values.any? { |value| (value - degree).abs < 1e-9 }
|
|
186
|
+
|
|
187
|
+
add_tick(ticks, width, position(degree), nil, 2)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
ticks.values.sort_by { |tick| tick[:col] }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
Scale::SCALES = {
|
|
195
|
+
"K" => Scale.new("K", kind: :power, decades: 3),
|
|
196
|
+
"A" => Scale.new("A", kind: :power, decades: 2),
|
|
197
|
+
"B" => Scale.new("B", kind: :power, decades: 2),
|
|
198
|
+
"CI" => Scale.new("CI", kind: :inverse_log),
|
|
199
|
+
"C" => Scale.new("C", kind: :log),
|
|
200
|
+
"S" => Scale.new("S", kind: :sin),
|
|
201
|
+
"T" => Scale.new("T", kind: :tan),
|
|
202
|
+
"D" => Scale.new("D", kind: :log),
|
|
203
|
+
"L" => Scale.new("L", kind: :linear)
|
|
204
|
+
}.freeze
|
|
205
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
require_relative "renderer"
|
|
4
|
+
|
|
5
|
+
module Keisanjaku
|
|
6
|
+
class TerminalViewport
|
|
7
|
+
POLL_INTERVAL = 0.25
|
|
8
|
+
|
|
9
|
+
attr_reader :width
|
|
10
|
+
|
|
11
|
+
def initialize(width: nil, provider: nil, poll_interval: POLL_INTERVAL, platform: nil, right_margin: 0, clamp_fixed_width: false)
|
|
12
|
+
@requested_width = width
|
|
13
|
+
@fixed = !width.nil?
|
|
14
|
+
@provider = provider || -> { IO.console&.winsize&.last || Renderer::DEFAULT_WIDTH }
|
|
15
|
+
@poll_interval = poll_interval
|
|
16
|
+
@platform = platform || RbConfig::CONFIG["host_os"]
|
|
17
|
+
@right_margin = [right_margin.to_i, 0].max
|
|
18
|
+
@clamp_fixed_width = clamp_fixed_width
|
|
19
|
+
@dirty = false
|
|
20
|
+
@last_poll = Time.now
|
|
21
|
+
@previous_winch_handler = nil
|
|
22
|
+
@width = normalize_width(target_width)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def install
|
|
26
|
+
return self if fixed_width?
|
|
27
|
+
return self unless Signal.list.key?("WINCH")
|
|
28
|
+
return self if windows?
|
|
29
|
+
|
|
30
|
+
@previous_winch_handler = Signal.trap("WINCH") { @dirty = true }
|
|
31
|
+
self
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def restore
|
|
37
|
+
return unless @previous_winch_handler
|
|
38
|
+
|
|
39
|
+
Signal.trap("WINCH", @previous_winch_handler)
|
|
40
|
+
@previous_winch_handler = nil
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current_width
|
|
46
|
+
return width if fixed_width?
|
|
47
|
+
|
|
48
|
+
refresh if @dirty || poll_due?
|
|
49
|
+
width
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def refresh_needed?
|
|
53
|
+
return false if fixed_width?
|
|
54
|
+
|
|
55
|
+
signaled = @dirty
|
|
56
|
+
return false unless signaled || poll_due?
|
|
57
|
+
|
|
58
|
+
previous_width = @width
|
|
59
|
+
refresh
|
|
60
|
+
signaled || previous_width != @width
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def refresh
|
|
64
|
+
@width = normalize_width(target_width)
|
|
65
|
+
@dirty = false
|
|
66
|
+
@last_poll = Time.now
|
|
67
|
+
width
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fixed_width?
|
|
71
|
+
@fixed && !@clamp_fixed_width
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def target_width
|
|
77
|
+
return usable_width unless @fixed
|
|
78
|
+
return @requested_width.to_i unless @clamp_fixed_width
|
|
79
|
+
|
|
80
|
+
[@requested_width.to_i, usable_width].min
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def usable_width
|
|
84
|
+
read_width.to_i - @right_margin
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_width
|
|
88
|
+
@provider.call
|
|
89
|
+
rescue StandardError
|
|
90
|
+
Renderer::DEFAULT_WIDTH
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalize_width(value)
|
|
94
|
+
[value.to_i, Renderer::MIN_WIDTH].max
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def poll_due?
|
|
98
|
+
return false unless windows?
|
|
99
|
+
|
|
100
|
+
Time.now - @last_poll >= @poll_interval
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def windows?
|
|
104
|
+
@platform.match?(/mswin|mingw|cygwin/i)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/keisanjaku.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "keisanjaku/version"
|
|
2
|
+
require_relative "keisanjaku/scale"
|
|
3
|
+
require_relative "keisanjaku/rule_state"
|
|
4
|
+
require_relative "keisanjaku/ansi"
|
|
5
|
+
require_relative "keisanjaku/renderer"
|
|
6
|
+
require_relative "keisanjaku/terminal_viewport"
|
|
7
|
+
require_relative "keisanjaku/input"
|
|
8
|
+
require_relative "keisanjaku/parser"
|
|
9
|
+
require_relative "keisanjaku/planner"
|
|
10
|
+
require_relative "keisanjaku/procedure"
|
|
11
|
+
require_relative "keisanjaku/engine"
|
|
12
|
+
require_relative "keisanjaku/drill"
|
|
13
|
+
require_relative "keisanjaku/app"
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: keisanjaku
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: keisanjaku is a Ruby terminal slide rule simulator with sandbox, demo,
|
|
13
|
+
and drill modes.
|
|
14
|
+
email:
|
|
15
|
+
- t.yudai92@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- keisanjaku
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/keisanjaku
|
|
24
|
+
- lib/keisanjaku.rb
|
|
25
|
+
- lib/keisanjaku/ansi.rb
|
|
26
|
+
- lib/keisanjaku/app.rb
|
|
27
|
+
- lib/keisanjaku/drill.rb
|
|
28
|
+
- lib/keisanjaku/engine.rb
|
|
29
|
+
- lib/keisanjaku/input.rb
|
|
30
|
+
- lib/keisanjaku/modes/demo.rb
|
|
31
|
+
- lib/keisanjaku/modes/drill.rb
|
|
32
|
+
- lib/keisanjaku/modes/sandbox.rb
|
|
33
|
+
- lib/keisanjaku/parser.rb
|
|
34
|
+
- lib/keisanjaku/planner.rb
|
|
35
|
+
- lib/keisanjaku/procedure.rb
|
|
36
|
+
- lib/keisanjaku/renderer.rb
|
|
37
|
+
- lib/keisanjaku/rule_state.rb
|
|
38
|
+
- lib/keisanjaku/scale.rb
|
|
39
|
+
- lib/keisanjaku/terminal_viewport.rb
|
|
40
|
+
- lib/keisanjaku/version.rb
|
|
41
|
+
homepage: https://rubygems.org/gems/keisanjaku
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
homepage_uri: https://rubygems.org/gems/keisanjaku
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 4.0.6
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: A terminal slide rule simulator.
|
|
64
|
+
test_files: []
|