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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Keisanjaku
2
+ VERSION = "0.1.0"
3
+ 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: []