calculator-on-ruby 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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +18 -0
  3. data/bin/calculator +7 -0
  4. data/lib/main.rb +220 -0
  5. data/lib/operators.rb +285 -0
  6. metadata +75 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2d709e1d63066165d3a0cf0131adeac870c52ad1992b74209db9f4b9889a687c
4
+ data.tar.gz: 38331de1844359a2aa86135888326e6e5afe049f2872b250a747d7a9433314f3
5
+ SHA512:
6
+ metadata.gz: c44172f1060712941a9f0127939b9f3c0fd7d643cefb8096643c2df866d1348142f88b3a173ba73309c926186cf6ad473bf1e2f3ee5d9e7d039ac2b4fdbcfe79
7
+ data.tar.gz: ea9807b4d2c09c7b6acb1e0735abb5a2b3ad84b5ff7715cbfc066e71921a2643b7b41a42a3ae93f7d8e45c5c548ecc0de4216cc90be043aba738e0f5cb168c40
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Calculator on Ruby
2
+
3
+ I started this project so I could learn how to code in ruby. Now I know! But was it worth 500(exact) lines?? Probably not. Oh well.
4
+
5
+ # Usage
6
+
7
+ - Install ruby
8
+ - Windows: `winget install -e --id RubyInstallerTeam.Ruby.4.0` or on ruby's website
9
+ - Fedora: `sudo dnf install ruby`
10
+ - Debian: `sudo apt install ruby-full`
11
+ - Mac: idk, either with brew or on their website
12
+ - Clone this project (`git clone https://gitlab.com/peanutplays61/calculator-on-ruby.git`)
13
+ - run `bundle config set --local without 'development'`
14
+ - run `bundle install`
15
+ - run `bundle exec ruby main.rb`
16
+ - Enter some calculations! There might be some errors at the beginning - this is beyond my control. It only happened on Windows for me.
17
+
18
+ Thanks for trying my project! This unfortunately took me 15 hours.
data/bin/calculator ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require 'main'
6
+
7
+ main
data/lib/main.rb ADDED
@@ -0,0 +1,220 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'colorize'
6
+
7
+ class Module
8
+ include T::Sig
9
+ include T::Helpers
10
+ end
11
+
12
+ require_relative 'operators'
13
+
14
+ extend T::Sig
15
+
16
+ class InvalidOperation < StandardError; end
17
+
18
+ sig do
19
+ params(operation: T.nilable(OperatorClass), number: T.nilable(Numeric), total_operation: T.nilable(Operator),
20
+ parentheses_index: Integer).returns(T.nilable(Operator))
21
+ end
22
+ def perform_operation(operation, number, total_operation, parentheses_index)
23
+ make_parentheses_setter = proc { |parentheses|
24
+ proc { |new_value|
25
+ T.let(parentheses, Parentheses).value = T.cast(new_value, Operator)
26
+ }
27
+ }
28
+ if total_operation.nil?
29
+ total_operation = if operation.nil?
30
+ Number.new(T.must(number)) unless number.nil?
31
+ elsif operation != Parentheses
32
+ raise InvalidOperation, 'Too many operators in a row'
33
+ else
34
+ Parentheses.new(number.nil? ? nil : Number.new(number))
35
+ end
36
+ else
37
+ raise StandardError, 'Operation is nil, but total operation is not! Impossible!' if operation.nil?
38
+
39
+ current_operation = T.let(total_operation, T.nilable(Operator))
40
+ set_current_operation = proc { |new_value| total_operation = T.cast(new_value, Operator) }
41
+ dive_operation = T.let(current_operation, T.nilable(Operator))
42
+ current_parentheses = (operation == Parentheses ? 1 : 0)
43
+ while current_parentheses < parentheses_index
44
+ if dive_operation.instance_of? Parentheses
45
+ set_current_operation = make_parentheses_setter.call(dive_operation)
46
+ current_operation = dive_operation.value
47
+
48
+ dive_operation = dive_operation.value
49
+ current_parentheses += 1
50
+ elsif dive_operation.instance_of?(Number)
51
+ # is a number, there is an issue
52
+ raise InvalidOperation
53
+ elsif dive_operation.is_a?(TwoSidedOperator)
54
+ dive_operation = dive_operation.right
55
+ end
56
+ end
57
+
58
+ if operation.include? TwoSidedOperator
59
+ operation = T.cast(operation, T.all(T::Class[TwoSidedOperator], Operator::ClassMethods))
60
+ raise InvalidOperation, 'Number must be first in parentheses!' if current_operation.nil?
61
+
62
+ if current_operation.priority >= operation.priority
63
+ set_current_operation.call(operation.new(current_operation, number ? Number.new(number) : nil))
64
+ else
65
+ dive_operation = T.cast(current_operation, TwoSidedOperator)
66
+
67
+ loop do
68
+ if T.must(dive_operation.right).priority > operation.priority
69
+ dive_operation.right = operation.new(dive_operation.right, number ? Number.new(number) : nil)
70
+ break
71
+ end
72
+ dive_operation = dive_operation.right
73
+ end
74
+ end
75
+ elsif operation == Parentheses
76
+ if current_operation.nil?
77
+ set_current_operation.call(Parentheses.new(number.nil? ? nil : Number.new(number)))
78
+ else
79
+
80
+ dive_operation = T.cast(current_operation, OneOrTwoOperator)
81
+
82
+ get_value = T.let(Kernel.proc do |operator|
83
+ case operator
84
+ when TwoSidedOperator
85
+ operator.right
86
+ when Number
87
+ raise 'Do not call this function with a number type.'
88
+ when Parentheses
89
+ operator.value
90
+ end
91
+ end, T.proc.params(operator: OneOrTwoOperator).returns(T.nilable(Operator)))
92
+
93
+ set_value = T.let(Kernel.proc do |operator, new_value|
94
+ case operator
95
+ when TwoSidedOperator
96
+ operator.right = new_value
97
+ when Number
98
+ raise 'Do not call this function with a number type.'
99
+ when Parentheses
100
+ operator.value = new_value
101
+ end
102
+ end, T.proc.params(operator: OneOrTwoOperator, new_value: OneOrTwoOperator).void)
103
+
104
+ loop do
105
+ if dive_operation.is_a?(Number)
106
+ raise 'This should not happen!'
107
+ elsif get_value.call(dive_operation).nil?
108
+ set_value.call(dive_operation, Parentheses.new(number.nil? ? nil : Number.new(number)))
109
+ break
110
+ elsif T.must(get_value.call(dive_operation)).priority >= Times.priority
111
+ new_dive = get_value.call(dive_operation)
112
+ loop do
113
+ break if new_dive.is_a? Number
114
+
115
+ next_value = get_value.call(T.cast(new_dive, OneOrTwoOperator))
116
+
117
+ if next_value.nil?
118
+ dive_operation = new_dive
119
+ break
120
+ end
121
+
122
+ new_dive = next_value
123
+ end
124
+ if get_value.call(T.cast(new_dive, OneOrTwoOperator)).nil?
125
+ # there is a nil deeper down
126
+ next
127
+ end
128
+
129
+ dive_operation = Times.new(get_value.call(T.cast(dive_operation, OneOrTwoOperator)),
130
+ Parentheses.new(number.nil? ? nil : Number.new(number)))
131
+ break
132
+ end
133
+
134
+ dive_operation = get_value.call(dive_operation)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ total_operation
141
+ end
142
+
143
+ def main
144
+ Signal.trap('INT') do
145
+ puts
146
+ return
147
+ end
148
+
149
+ print 'hi! This is a calculator. '
150
+ loop do
151
+ print 'What is your equation? '
152
+ STDOUT.flush # rubocop:disable Style/GlobalStdStream
153
+
154
+ equation = gets.chomp
155
+ current_number = T.let(nil, T.nilable(Numeric))
156
+ total_operation = T.let(nil, T.nilable(Operator))
157
+ current_operation = T.let(nil, T.nilable(OperatorClass))
158
+ number_is_negative = T.let(false, T::Boolean)
159
+ in_decimal_count = 0
160
+
161
+ parentheses_index = 0
162
+ next_parentheses_index = 0
163
+
164
+ begin
165
+ equation.each_char do |char|
166
+ if char.match?(/\d/)
167
+ current_number = if current_number.nil?
168
+ (char.to_i * (number_is_negative ? -1 : 1))
169
+ .fdiv(in_decimal_count.positive? ? T.cast(10.pow(in_decimal_count), Integer) : 1)
170
+ else
171
+ (current_number * (in_decimal_count.positive? ? 1 : 10)) +
172
+ (char.to_i * (number_is_negative ? -1 : 1))
173
+ .fdiv(in_decimal_count.positive? ? T.cast(10.pow(in_decimal_count), Integer) : 1)
174
+ end
175
+ in_decimal_count += 1 if in_decimal_count.positive?
176
+ elsif char == '-' && current_number.nil?
177
+ number_is_negative = true
178
+ elsif char == '.'
179
+ in_decimal_count = 1 if in_decimal_count.zero?
180
+ elsif CHARACTERS.include?(char)
181
+ in_decimal_count = 0
182
+ number_is_negative = false
183
+
184
+ total_operation = perform_operation(current_operation, current_number, total_operation, parentheses_index)
185
+ parentheses_index = next_parentheses_index
186
+ current_operation = CHARACTERS[char]
187
+ current_number = nil
188
+ elsif char == '('
189
+ next_parentheses_index += 1
190
+ total_operation = perform_operation(current_operation, current_number, total_operation, parentheses_index)
191
+ parentheses_index = next_parentheses_index
192
+ current_operation = Parentheses
193
+ elsif char == ')'
194
+ next_parentheses_index -= 1
195
+ end
196
+ end
197
+
198
+ raise InvalidOperation, 'No closing parenthesis!' if next_parentheses_index.positive?
199
+ raise InvalidOperation, 'Too many closing parentheses!' if next_parentheses_index.negative?
200
+
201
+ total_operation = perform_operation(current_operation, current_number, total_operation, parentheses_index)
202
+ raise InvalidOperation, 'No equation' if total_operation.nil?
203
+
204
+ raise InvalidOperation unless total_operation.can_evaluate?
205
+
206
+ puts total_operation.evaluate.round(8)
207
+ rescue InvalidOperation => e
208
+ puts 'That equation is invalid!'.colorize(:red)
209
+ puts "Reason: #{e.message}" if e.message != e.class.to_s
210
+ end
211
+ print 'Would you like to calculate another equation [Y/n]? '
212
+ STDOUT.flush # rubocop:disable Style/GlobalStdStream
213
+ answer = gets.strip.downcase
214
+ unless answer in ('y' | 'yes' | 'yeah' | 'ye' | 'sure' | 'yes.' | '')
215
+ break
216
+ end
217
+ end
218
+ end
219
+
220
+ main if T.let($PROGRAM_NAME, String) == __FILE__
data/lib/operators.rb ADDED
@@ -0,0 +1,285 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ extend T::Sig
5
+
6
+ class Module
7
+ include T::Sig
8
+ include T::Helpers
9
+ end
10
+
11
+ # OperatorClass is courtesy of deepseek
12
+ OperatorClass = T.type_alias { T.all(T::Class[Operator], Operator::ClassMethods) }
13
+ OneOrTwoOperator = T.type_alias { T.any(OneSidedOperator, TwoSidedOperator) }
14
+
15
+ module Operator
16
+ abstract!
17
+ include Kernel
18
+
19
+ module ClassMethods
20
+ interface!
21
+
22
+ sig { abstract.returns(Integer) }
23
+ def priority; end
24
+ end
25
+
26
+ mixes_in_class_methods(ClassMethods)
27
+
28
+ sig { returns(Integer) }
29
+ def priority
30
+ T.let(self.class, OperatorClass).priority
31
+ end
32
+
33
+ sig { abstract.returns(T::Boolean) }
34
+ def can_evaluate?; end
35
+
36
+ sig { abstract.returns(Numeric) }
37
+ def evaluate; end
38
+
39
+ sig { abstract.returns(String) }
40
+ def to_string; end
41
+ end
42
+
43
+ module TwoSidedOperator
44
+ include Operator
45
+ include Kernel
46
+
47
+ abstract!
48
+
49
+ mixes_in_class_methods Operator::ClassMethods
50
+
51
+ sig { returns(T.nilable(Operator)) }
52
+ def left = T.let(@left, T.nilable(Operator))
53
+
54
+ sig { params(left: T.nilable(Operator)).void }
55
+ def left=(left)
56
+ @left = left
57
+ end
58
+
59
+ sig { returns(T.nilable(Operator)) }
60
+ def right = T.let(@right, T.nilable(Operator))
61
+
62
+ sig { params(right: T.nilable(Operator)).void }
63
+ def right=(right)
64
+ @right = right
65
+ end
66
+
67
+ sig { params(left: T.nilable(Operator), right: T.nilable(Operator)).void }
68
+ def initialize(left, right)
69
+ @left = left
70
+ @right = right
71
+ end
72
+
73
+ sig { override.returns(String) }
74
+ def to_string
75
+ "(#{@left.nil? ? 'nil' : @left.to_string}" \
76
+ "#{" #{char}" if is_a?(CharacterOperator)} #{@right.nil? ? 'nil' : @right.to_string})"
77
+ end
78
+
79
+ sig { override.returns(T::Boolean) }
80
+ def can_evaluate?
81
+ !(@left.nil? || @right.nil?) && @left.can_evaluate? && @right.can_evaluate?
82
+ end
83
+ end
84
+
85
+ module OneSidedOperator
86
+ include Operator
87
+
88
+ abstract!
89
+
90
+ sig { returns(T.any(T.nilable(Operator), Numeric)) }
91
+ def value = T.let(@value, T.any(T.nilable(Operator), Numeric))
92
+
93
+ sig { params(value: T.any(T.nilable(Operator), Numeric)).void }
94
+ def value=(value)
95
+ @value = value
96
+ end
97
+
98
+ mixes_in_class_methods(Operator::ClassMethods)
99
+
100
+ sig { params(value: T.any(T.nilable(Operator), Numeric)).void }
101
+ def initialize(value); end
102
+ end
103
+
104
+ module CharacterOperator
105
+ abstract!
106
+ include Kernel
107
+
108
+ module ClassMethods
109
+ interface!
110
+ include Operator::ClassMethods
111
+
112
+ sig { abstract.returns(String) }
113
+ def char; end
114
+ end
115
+
116
+ sig { returns(String) }
117
+ def char
118
+ T.let(self.class, T.all(T::Class[CharacterOperator], ClassMethods)).char
119
+ end
120
+
121
+ mixes_in_class_methods ClassMethods
122
+ end
123
+
124
+ class Plus
125
+ include CharacterOperator
126
+ include TwoSidedOperator
127
+
128
+ sig { override.returns(String) }
129
+ def self.char = '+'
130
+ sig { override.returns(Integer) }
131
+ def self.priority
132
+ 0
133
+ end
134
+
135
+ sig { override.returns(Numeric) }
136
+ def evaluate
137
+ T.must_because(@left) { 'Must not be null when evaluating' }.evaluate + T.must_because(@right) do
138
+ 'Must not be null when evaluating'
139
+ end.evaluate
140
+ end
141
+ end
142
+
143
+ class Minus
144
+ include TwoSidedOperator
145
+ include CharacterOperator
146
+
147
+ sig { override.returns(String) }
148
+ def self.char = '-'
149
+ sig { override.returns(Integer) }
150
+ def self.priority = 0
151
+ sig { override.returns(Numeric) }
152
+ def evaluate
153
+ T.must_because(@left) { 'Must not be null when evaluating' }.evaluate - T.must_because(@right) {
154
+ 'Must not be null when evaluating'
155
+ }.evaluate
156
+ end
157
+ end
158
+
159
+ class Times
160
+ include TwoSidedOperator
161
+ include CharacterOperator
162
+
163
+ sig { override.returns(String) }
164
+ def self.char = '*'
165
+ sig { override.returns(Integer) }
166
+ def self.priority = 1
167
+ sig { override.returns(Numeric) }
168
+ def evaluate
169
+ T.must_because(@left) { 'Must not be null when evaluating' }.evaluate * T.must_because(@right) {
170
+ 'Must not be null when evaluating'
171
+ }.evaluate
172
+ end
173
+ end
174
+
175
+ class Divided
176
+ include TwoSidedOperator
177
+ include CharacterOperator
178
+
179
+ sig { override.returns(String) }
180
+ def self.char = '/'
181
+ sig { override.returns(Integer) }
182
+ def self.priority = 1
183
+ sig { override.returns(Numeric) }
184
+ def evaluate
185
+ T.must_because(@left) { 'Must not be null when evaluating' }.evaluate / T.must_because(@right) {
186
+ 'Must not be null when evaluating'
187
+ }.evaluate
188
+ end
189
+ end
190
+
191
+ class Power
192
+ include TwoSidedOperator
193
+ include CharacterOperator
194
+
195
+ sig { override.returns(String) }
196
+ def self.char = '^'
197
+ sig { override.returns(Integer) }
198
+ def self.priority = 2
199
+ sig { override.returns(Numeric) }
200
+ def evaluate
201
+ T.cast(T.must_because(@left) do
202
+ 'Must not be null when evaluating'
203
+ end.evaluate, T.any(Integer, Float))**T.must_because(@right) {
204
+ 'Must not be null when evaluating'
205
+ }.evaluate
206
+ end
207
+ end
208
+
209
+ class Parentheses
210
+ include OneSidedOperator
211
+
212
+ sig { returns T.nilable(Operator) }
213
+ attr_accessor :value
214
+
215
+ sig { override.returns(Integer) }
216
+ def self.priority = 100
217
+
218
+ sig { params(value: T.nilable(Operator)).void }
219
+ def initialize(value)
220
+ @value = value
221
+ end
222
+
223
+ sig { override.returns(Numeric) }
224
+ def evaluate
225
+ T.must_because(@value) { 'Must not be null when evaluating' }.evaluate
226
+ end
227
+
228
+ sig { override.returns(String) }
229
+ def to_string
230
+ "[#{@value&.to_string}]"
231
+ end
232
+ sig { override.returns(T::Boolean) }
233
+ def can_evaluate? = !@value.nil? && @value.can_evaluate?
234
+ end
235
+
236
+ class Number
237
+ include OneSidedOperator
238
+
239
+ sig { override.returns(Integer) }
240
+ def self.priority = 100
241
+
242
+ sig { params(value: Numeric).void }
243
+ def initialize(value)
244
+ @value = value
245
+ end
246
+
247
+ sig { override.returns(Numeric) }
248
+ def evaluate
249
+ @value
250
+ end
251
+
252
+ sig { override.returns(String) }
253
+ def to_string
254
+ "(#{@value})"
255
+ end
256
+ sig { override.returns(T::Boolean) }
257
+ def can_evaluate? = true
258
+ end
259
+
260
+ OPERATORS = T.let(
261
+ [Plus, Minus, Times, Divided, Power, Number].freeze,
262
+ T::Array[OperatorClass]
263
+ )
264
+
265
+ PRIORITIES = T.let(
266
+ OPERATORS.group_by(&:priority),
267
+ T::Hash[Integer, T::Array[OperatorClass]]
268
+ )
269
+
270
+ CHARACTERS = T.let(
271
+ begin
272
+ hash = T::Hash[String, OperatorClass].new
273
+ OPERATORS.each do |operator|
274
+ next unless operator.respond_to?(:char)
275
+
276
+ char_operator = T.cast(
277
+ operator,
278
+ T.all(T::Class[CharacterOperator], CharacterOperator::ClassMethods)
279
+ )
280
+ hash[char_operator.char] = operator
281
+ end
282
+ hash
283
+ end,
284
+ T::Hash[String, OperatorClass]
285
+ )
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calculator-on-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Isaiah
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: colorize
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sorbet-runtime
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.6.13572
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.6.13572
40
+ description: This is an executable script that can run any calculations in string
41
+ form.
42
+ email:
43
+ - peanutplays61@gmail.com
44
+ executables:
45
+ - calculator
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - bin/calculator
51
+ - lib/main.rb
52
+ - lib/operators.rb
53
+ homepage: https://gitlab.com/peanutplays61/calculator-on-ruby
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 3.4.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: A calculator.
75
+ test_files: []