calculo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 647451e4c1e42739a7571ef0deb0db5bd4af3555
4
+ data.tar.gz: e27e02406f3fa7446749996881a986f81694dc49
5
+ SHA512:
6
+ metadata.gz: ca50356dc05252792acf85230babbbd64fb34e11bd1d6e486133722d46f9a0c8d6c50554155164e2081464ceff7e0db62ed9984b4217b0d871245d10b067a76a
7
+ data.tar.gz: 4e45b33979518f68c2a67769999299e8741a67cfa1ba9294ee96761b2c75b6eb8c25b8563f018e2c1aa1c0370745385822564080691d0831445d9664e6809cde
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Calculo
2
+ A calculator specifically designed to support any kind of mathematical syntax
3
+ you throw at it.
4
+
5
+ Consider it The AD to [BC](https://www.gnu.org/software/bc/), the AC to [DC](https://packages.debian.org/jessie/dc)
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rake/testtask'
2
+ require 'rake/dmg'
3
+
4
+ Rake::DmgTask.new 'calculo', '1.0.0' do |dmg|
5
+ dmg.source_files.include('bin/*')
6
+ dmg.source_files.include('calculo/*')
7
+ dmg.source_files.include('test/*')
8
+ dmg.source_files.include('README.md')
9
+ dmg.strip = 'build'
10
+ end
11
+
12
+ Rake::TestTask.new do |t|
13
+ t.libs << 'test'
14
+ end
15
+
16
+ desc "Run tests"
17
+ task :default => :test
data/bin/calculo ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'calculo'
4
+
5
+ options = {}
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: calculo.rb [options]"
8
+ opts.on("-t","--type TYPE",String,"The type of expression to be computed") do |type|
9
+ options[:type] = type
10
+ end
11
+
12
+ opts.on("-e","--expr EXPR","The expression to be computed") do |expr|
13
+ options[:expr] = expr
14
+ end
15
+ opts.on("-f","--file FILE",String,"Reads expressions from a file") do |file|
16
+ options[:file] = file
17
+ end
18
+ opts.on("-h","--help","Prints this help") do
19
+ puts opts
20
+ end
21
+ end.parse!
22
+
23
+ def cli_eval(string,type)
24
+ if type == 'lisp' or type == 'reverse-lisp' or type == 'reverse'
25
+ cli = Lisp.new(string,type)
26
+ output = "#{cli.string} : #{cli.result}"
27
+ return output
28
+ elsif type == 'rpn' or type == 'postfix' or type == 'pn' or type == 'prefix' or type == 'infix'
29
+ cli = MathNotation.new(string,type)
30
+ output = "#{cli.string} : #{cli.result}"
31
+ return output
32
+ end
33
+ end
34
+
35
+ if not options.empty?
36
+ if options.has_key?(:file)
37
+ File.open(options[:file]).each_line do |line|
38
+ puts(cli_eval(line,options[:type]))
39
+ end
40
+
41
+ elsif options.has_key?(:expr)
42
+ puts(cli_eval(options[:expr],options[:type]))
43
+
44
+ elsif options.has_key?(:type)
45
+ Repl.new("=>",options[:type])
46
+ end
47
+ else
48
+ Repl.new("=>","infix")
49
+ end
@@ -0,0 +1,58 @@
1
+ require 'parse'
2
+ class Lisp
3
+ attr_accessor :string
4
+ attr_accessor :type
5
+ attr_accessor :array
6
+ attr_accessor :result
7
+
8
+ def initialize(string, type)
9
+ @string = string
10
+ @type = type
11
+ @array = parse(@string)
12
+ @result = eval_lisp(@array)
13
+ end
14
+
15
+ @private
16
+ def execute(array,type)
17
+ case type
18
+ when 'lisp'
19
+ array[2..-2].reduce(array[1].to_sym)
20
+ when 'reverse-lisp' || 'reverse'
21
+ array[1..-3].reverse.reduce(array[-2].to_sym)
22
+ end
23
+ end
24
+
25
+ def innermost_pair(array)
26
+ l_parens = []
27
+ r_parens = []
28
+
29
+ array.each_with_index do |item, index|
30
+
31
+ if item == "("
32
+ l_parens.push(index)
33
+
34
+ elsif item == ")"
35
+ r_parens.push(index)
36
+ end
37
+ end
38
+
39
+ total_length = l_parens.length + r_parens.length
40
+ return [total_length, l_parens[-1], r_parens[0]]
41
+ end
42
+
43
+ def eval_lisp(array)
44
+ length, l, r = innermost_pair(array)
45
+ result = execute(array[l..r],type)
46
+
47
+ if length > 2
48
+ # Inserts the result in between the portions
49
+ # of the array not including the portion just
50
+ # used to calculate the result
51
+ new_array = array[0..(l-1)] + [result] + array[(r+1)..-1]
52
+ eval_lisp(new_array)
53
+
54
+ elsif length == 2
55
+ return result
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ require 'parse'
2
+ require 'shunting_yard'
3
+ class MathNotation
4
+ attr_accessor :type
5
+ attr_accessor :string
6
+ attr_accessor :array
7
+ attr_accessor :result
8
+
9
+ def initialize(string,type)
10
+ @string = string
11
+ if type == 'infix'
12
+ @type = 'rpn'
13
+ @array = shunting_yard(parse(@string))
14
+ @result = eval_rpn(@array)
15
+ else
16
+ @array = parse(@string)
17
+ @type = type
18
+ if @type == 'prefix' or @type == 'pn'
19
+ @result = eval_pn(@array)
20
+ elsif @type == 'postfix' or @type == 'rpn'
21
+ @result = eval_rpn(@array)
22
+ end
23
+ end
24
+ end
25
+
26
+ @private
27
+ def eval_pn(array)
28
+ operators = ['+','-','^','%','/','**','*']
29
+ stack = []
30
+ array.reverse.each{|item|
31
+ if operators.include?(item)
32
+ num2,num1 = stack.pop(2)
33
+ result = num1.method(item).call(num2)
34
+ stack.push(result)
35
+ else
36
+ stack.push(item)
37
+ end
38
+ }
39
+ return stack.pop
40
+ end
41
+
42
+ def eval_rpn(array)
43
+ operators = ['+','-','^','%','/','**','*']
44
+ stack = []
45
+ array.each{|item|
46
+ if operators.include?(item)
47
+ num1,num2 = stack.pop(2)
48
+ result = num1.method(item).call(num2)
49
+ stack.push(result)
50
+ else
51
+ stack.push(item)
52
+ end
53
+ }
54
+ return stack.pop
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ def parse(string)
2
+ # First regex puts a space after a parenthesis/math operation,
3
+ # Second regex a space between a digit and a parenthesis/math
4
+ # operation(s)
5
+ # Third regex converts any double spaces into single space
6
+ # Fourth regex converts to the exponent operator used in ruby
7
+ # split seperates the characters based on a space
8
+
9
+ op_re = Regexp.new(/([^\d\w\s\-])/)
10
+ dig_op_re = Regexp.new(/\d[^\d\w\s]+/)
11
+ dbl_space_re = Regexp.new(/\s{2,}/)
12
+ exp_re = Regexp.new(/\^/)
13
+ dig_re = Regexp.new(/\d+/)
14
+ array = string.gsub(op_re,'\1 ').gsub(dig_op_re){ |s| s.chars.join(' ')}.gsub(dbl_space_re,' ').gsub(exp_re,'**').split(' ')
15
+
16
+ # Regex finds if array element matches a digit, and converts it
17
+ # to a float.
18
+ array = array.map{|x| dig_re.match(x) != nil ? x.to_f : x}
19
+ return array
20
+ end
@@ -0,0 +1,110 @@
1
+ require 'IO/console'
2
+ require 'os'
3
+ require 'pry'
4
+
5
+ class Repl
6
+ attr_accessor :history
7
+ attr_accessor :prompt
8
+ attr_accessor :type
9
+ def initialize(prompt,type)
10
+ @history = []
11
+ @og_prompt = prompt
12
+ @prompt = "[#{@history.length}]" + @og_prompt + " "
13
+ @type = type
14
+ clear_screen()
15
+ calculo_loop()
16
+ end
17
+
18
+ def clear_screen
19
+ if OS.linux? or OS.mac? or OS.bsd?
20
+ system('clear')
21
+ elsif OS.windows?
22
+ system('cls')
23
+ end
24
+ end
25
+
26
+ def calculo_print(history)
27
+ history.each_with_index.map{|x,i| puts("#{i} : #{x}\n")}
28
+ end
29
+
30
+ def repl_eval(input)
31
+ # syntax for changing prompt/type is:
32
+ # set prompt/type = 'foo'
33
+ # or set prompt/type 'foo'
34
+ if input.start_with?("set","change")
35
+ @history.push(input)
36
+ input_array = input.split(' ')
37
+
38
+ if input_array[1] == "prompt" and input_array.last != "prompt"
39
+ @og_prompt = input_array.last
40
+ @prompt = "[#{@history.length}]" + @og_prompt + " "
41
+ return "prompt is now #{@prompt}"
42
+
43
+ elsif input_array[1] == "type" and input_array.last != "type"
44
+ @type = input_array.last
45
+ return "type is now #{@type}"
46
+ else
47
+ return "Error: variable can not be changed"
48
+ end
49
+
50
+ # If input is simply history, print all of the history
51
+ # can also use ruby range syntax, STARTING AT 0
52
+ # for example: history 0..-2 will print all but the
53
+ # last entry of history
54
+ elsif input.start_with?("history")
55
+ @history.push(input)
56
+ if input == "history"
57
+ return calculo_print(@history)
58
+ else
59
+ input_array = input.split(' ')
60
+ input_range = input_array[-1]
61
+ if input_range.length == 1
62
+ return @history[input_range.to_i]
63
+ elsif input_range.length == 4
64
+ return calculo_print(@history[(input_range.chars.first.to_i)..(input_range.chars.last.to_i)])
65
+ elsif input_range.length == 5
66
+ return calculo_print(@history[(input_range.chars.first.to_i)..(input_range.chars.drop(2).to_i)])
67
+ end
68
+ end
69
+
70
+ elsif input.start_with?("clear")
71
+ @history.push(input)
72
+ if input == "clear"
73
+ return clear_screen()
74
+ elsif input == "clear history"
75
+ @history = []
76
+ return "History is now blank"
77
+ end
78
+
79
+ elsif input == "exit" or input == "quit"
80
+ return "Exiting calculo!"
81
+
82
+ else
83
+ if @type == 'lisp' or @type == 'reverse-lisp' or @type == 'reverse'
84
+ repl_class = Lisp.new(input,@type)
85
+ calculo_output = "#{repl_class.string} : #{repl_class.result}"
86
+ @history.push(calculo_output)
87
+ return calculo_output
88
+
89
+ elsif @type == 'postfix' or @type == 'infix' or @type == 'prefix' or @type == 'pn' or @type == 'rpn'
90
+ repl_class = MathNotation.new(input,@type)
91
+ calculo_output = "#{repl_class.string} : #{repl_class.result}"
92
+ @history.push(calculo_output)
93
+ return calculo_output
94
+ end
95
+ end
96
+ end
97
+
98
+ def calculo_loop(output="Welcome to calculo!")
99
+ input = nil
100
+ while output != "Exiting calculo!"
101
+ puts(output)
102
+ @prompt = "[#{@history.length}]" + @og_prompt + " "
103
+ print(@prompt)
104
+ input = gets.chomp
105
+ output = repl_eval(input)
106
+ end
107
+ puts(output)
108
+ exit(1)
109
+ end
110
+ end
@@ -0,0 +1,32 @@
1
+ def shunting_yard(infix_array)
2
+ operators = {'+' => 2, '-' => 2, '*' => 3, '/' => 3, '>' => 4, '<' => 4, '=' => 4, '%' => 4, '**' => 4}
3
+ rpn_expr = []
4
+ op_stack = []
5
+
6
+ infix_array.each do |item|
7
+ if operators.has_key?(item)
8
+ op2 = op_stack.last
9
+ if operators.has_key?(op2) and ((op2 == "**" and operators[item] < operators[op2]) or (op2 != "**" and operators[item] <= operators[op2]))
10
+ rpn_expr.push(op_stack.pop)
11
+ end
12
+ op_stack.push(item)
13
+
14
+ elsif item == "("
15
+ op_stack.push(item)
16
+
17
+ elsif item == ")"
18
+ until op_stack.last == "("
19
+ rpn_expr.push(op_stack.pop)
20
+ end
21
+ op_stack.pop
22
+ else
23
+ rpn_expr.push(item)
24
+ end
25
+ end
26
+
27
+ until op_stack.empty?
28
+ rpn_expr.push(op_stack.pop)
29
+ end
30
+
31
+ return rpn_expr
32
+ end
data/lib/calculo.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'calculo/math'
2
+ require 'calculo/lisp'
3
+ require 'calculo/repl'
data/test/test.rb ADDED
@@ -0,0 +1,129 @@
1
+ require_relative "../lib/calculo/lisp"
2
+ require_relative "../lib/calculo/math"
3
+ require "minitest/autorun"
4
+
5
+ class TestLisp < MiniTest::Test
6
+ def setup
7
+ @type = "lisp"
8
+ @string = "( - ( + ( / 18 ( ^ 3 2 ) ) 12 ) 13 )"
9
+ @array = ["(", "-", "(", "+", "(", "/", 18.0, "(", "**", 3.0, 2.0, ")", ")", 12.0, ")", 13.0, ")"]
10
+ @result = 1
11
+ @Lisp = Lisp.new(@string, @type)
12
+ end
13
+
14
+ def test_type_access
15
+ assert_equal(@type, @Lisp.type)
16
+ end
17
+
18
+ def test_string_access
19
+ assert_equal(@string, @Lisp.string)
20
+ end
21
+
22
+ def test_array_access
23
+ assert_equal(@array, @Lisp.array)
24
+ end
25
+
26
+ def test_results
27
+ assert_equal(@result, @Lisp.result)
28
+ end
29
+ end
30
+
31
+ class TestReverseLisp < MiniTest::Test
32
+ def setup
33
+ @type = "reverse-lisp"
34
+ @string = "( 13 ( 12 ( ( 2 3 ^ ) 18 / ) + ) - )"
35
+ @array = ["(", 13.0, "(", 12.0, "(", "(", 2.0, 3.0, "**", ")", 18.0, "/", ")", "+", ")", "-", ")"]
36
+ @result = 1
37
+ @ReverseLisp = Lisp.new(@string, @type)
38
+ end
39
+
40
+ def test_type_access
41
+ assert_equal(@type, @ReverseLisp.type)
42
+ end
43
+
44
+ def test_string_access
45
+ assert_equal(@string, @ReverseLisp.string)
46
+ end
47
+
48
+ def test_array_access
49
+ assert_equal(@array, @ReverseLisp.array)
50
+ end
51
+
52
+ def test_results
53
+ assert_equal(@result, @ReverseLisp.result)
54
+ end
55
+ end
56
+
57
+ class TestPN < MiniTest::Test
58
+ def setup
59
+ @type = "pn"
60
+ @string = "- + / 18 ^ 3 2 12 13"
61
+ @array = ['-', '+', '/', 18.0, '**', 3.0, 2.0, 12.0, 13.0]
62
+ @result = 1
63
+ @PN = MathNotation.new(@string, @type)
64
+ end
65
+
66
+ def test_type_access
67
+ assert_equal(@type, @PN.type)
68
+ end
69
+
70
+ def test_string_access
71
+ assert_equal(@string, @PN.string)
72
+ end
73
+
74
+ def test_array_access
75
+ assert_equal(@array, @PN.array)
76
+ end
77
+
78
+ def test_result_access
79
+ assert_equal(@result, @PN.result)
80
+ end
81
+ end
82
+
83
+ class TestRPN < MiniTest::Test
84
+ def setup
85
+ @type = "rpn"
86
+ @string = "12 18 3 2 ^ / 13 - +"
87
+ @array = [12.0, 18.0, 3.0, 2.0, "**", "/", 13.0, "-", "+"]
88
+ @result = 1
89
+ @RPN = MathNotation.new(@string, @type)
90
+ end
91
+
92
+ def test_string_access
93
+ assert_equal(@string, @RPN.string)
94
+ end
95
+
96
+ def test_array_access
97
+ assert_equal(@array, @RPN.array)
98
+ end
99
+
100
+ def test_result_access
101
+ assert_equal(@result, @RPN.result)
102
+ end
103
+ end
104
+
105
+ class TestInfix < MiniTest::Test
106
+ def setup
107
+ @type = "rpn"
108
+ @string = "12 + 18 / ( 3 ^ 2 ) - 13"
109
+ @rpn_array = [12.0, 18.0, 3.0, 2.0, "**", "/", 13.0, "-", "+"]
110
+ @result = 1
111
+ @Infix = MathNotation.new(@string, "infix")
112
+ end
113
+
114
+ def test_type
115
+ assert_equal(@type, @Infix.type)
116
+ end
117
+
118
+ def test_string_access
119
+ assert_equal(@string, @Infix.string)
120
+ end
121
+
122
+ def test_shunting_yard
123
+ assert_equal(@rpn_array, @Infix.array)
124
+ end
125
+
126
+ def test_result_access
127
+ assert_equal(@result, @Infix.result)
128
+ end
129
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calculo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Noah Holt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: os
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake_dmg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ocra
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A calculator for the command line supporting postfix, prefix, infix,
56
+ and lisp notation (plus a REPL)
57
+ email: noahryanholt@gmail.com
58
+ executables:
59
+ - calculo
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - Rakefile
64
+ - README.md
65
+ - test/test.rb
66
+ - lib/calculo.rb
67
+ - lib/calculo/lisp.rb
68
+ - lib/calculo/parse.rb
69
+ - lib/calculo/math.rb
70
+ - lib/calculo/shunting_yard.rb
71
+ - lib/calculo/repl.rb
72
+ - bin/calculo
73
+ homepage: https://github.com/noaoh/calculo
74
+ licenses:
75
+ - GPLv3
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ - bin
82
+ - test
83
+ - lib/calculo
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.0.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.0.14.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: A calculator for the command line
100
+ test_files: []