hesabu 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abe943d484f4a7c14677bbd0edaa584a9705718b76dbf6584e827c61d804b290
4
- data.tar.gz: feb5c1e48b9bd534e9e780b282e4dc0c246b77c6c9897b7d355acb23efa19967
3
+ metadata.gz: a7919ff5f5986fbf22b42ccbc75d9b782619a6a037219d4e379528777e86168c
4
+ data.tar.gz: 7b33587c21aaf91e74c2d64d835d54b923a729e20dcd1419597b53411ca2972a
5
5
  SHA512:
6
- metadata.gz: d85f9343ef7f93028552e8ccf73be54c5eac7d4210a9172666f06b72bf941660e9bf21f528c5bc3a53236566963bb3b832b17456349f3959f5047e4d9831e5c1
7
- data.tar.gz: 696f4a816991e4fcf72cf1291c3c392215d2d7f13f1ebc167db32ae80cfdfa8d27ad38a802eae91d58e416f063a7bd744e6f4dfa56fc5a83333b92cd1956fd0e
6
+ metadata.gz: 93953561238b518ae4287e12a72725c55668402ebf0e7cac31f9559b12eabd45bde96af2b5a256d3d0ea2157a82a77eb8d217907d53a17afdffc94e87903b3a0
7
+ data.tar.gz: 1950edd4e3a4c757d8e97d8fde8b6d9013b470474545ce590925bf50314d2339cfea816732b1dc86da444c541763d7b9498f203a71d6e457b15543ea669dffe4
@@ -0,0 +1,11 @@
1
+ ## Self proof reading checklist
2
+
3
+ - [ ] Did I run Pronto and fixed all warnings
4
+ - [ ] Make sure all naming and code will remain understandable in 6 month by someone new to the project or by you.
5
+ - [ ] Did I test the right thing?
6
+ - [ ] Did I test corner cases, non happy path (defensive testing)?
7
+ - [ ] Check that I used the ad-hoc patterns and created files accordingly
8
+ - [ ] Check code efficiency with record intensive project
9
+ - [ ] Update documentation,readme accordingly
10
+
11
+ Thanks!
data/.gitignore CHANGED
@@ -10,4 +10,5 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
 
13
- *.gem
13
+ *.gem
14
+ .byebug_history
data/.rubocop.yml CHANGED
@@ -1,9 +1,7 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3
2
+ TargetRubyVersion: 2.5
3
3
  Exclude:
4
4
  - db/schema.rb
5
- Rails:
6
- Enabled: true
7
5
  Layout/AlignHash:
8
6
  # Alignment of entries using hash rocket as separator. Valid values are:
9
7
  #
@@ -52,9 +50,8 @@ Style/FormatStringToken:
52
50
  EnforcedStyle: template
53
51
  Metrics/LineLength:
54
52
  Exclude:
55
- - 'spec/**/*.rb'
53
+ - 'spec/**/*.rb'
56
54
  Metrics/BlockLength:
57
55
  Exclude:
58
56
  - 'spec/**/*.rb'
59
57
 
60
-
data/.travis.yml CHANGED
@@ -2,7 +2,7 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - 2.4.2
5
+ - 2.5.1
6
6
  before_install: gem install bundler -v 1.16.1
7
7
  before_script:
8
8
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at mestachs. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in hesabu.gemspec
6
6
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hesabu (0.1.1)
4
+ hesabu (0.1.2)
5
5
  parslet
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -7,6 +7,47 @@
7
7
 
8
8
  Hesabu : equation solver based on parslet.
9
9
 
10
+ ## sample usage
11
+ ```ruby
12
+ solver = Hesabu::Solver.new
13
+ solver.add("c", "a + b")
14
+ solver.add("a", "10")
15
+ solver.add("b", "10 + a")
16
+
17
+ solution = solver.solve!
18
+
19
+ expect(solution).to eq("a" => 10, "b" => 20, "c" => 30)
20
+ ```
21
+
22
+ The solver will deduce the correct order and find the values of a,b and c.
23
+
24
+ The expressions can be more complex (excel like), see the supported functions [here](https://github.com/BLSQ/hesabu/blob/master/lib/hesabu/types/fun_call.rb#L87)
25
+
26
+ Currently the solver is case sensitive (except function names)
27
+
28
+ Nb: Hesabu is swahili word for arithemtic.
29
+
30
+ ## Technical background
31
+
32
+ * https://tomassetti.me/guide-parsing-algorithms-terminology/
33
+ * https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics
34
+ * https://github.com/kschiess/parslet
35
+
36
+ ## Alternatives
37
+
38
+ * https://github.com/rubysolo/dentaku more complete, currently less performant.
39
+
40
+
41
+ # Development
42
+
43
+ ## Running the tests
44
+
45
+ ```
46
+ # only the fast
47
+ bundle exec rspec --tag ~slow
48
+ # all with integration test, expect around 30-40 seconds depending on your machine
49
+ bundle exec rspec
50
+ ```
10
51
 
11
52
  ## deployment to rubygems.org
12
53
 
data/Rakefile CHANGED
@@ -3,4 +3,4 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
data/hesabu.gemspec CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- lib = File.expand_path("../lib", __FILE__)
2
+ lib = File.expand_path("lib", __dir__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require "hesabu/version"
5
5
 
@@ -9,15 +9,14 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Stéphan Mestach"]
10
10
  spec.email = ["smestach@bluesquarehub.com"]
11
11
 
12
- spec.summary = %q{arithmetic equation solver.}
13
- spec.description = %q{arithmetic equation solver.}
12
+ spec.summary = "arithmetic equation solver."
13
+ spec.description = "arithmetic equation solver."
14
14
  spec.homepage = "https://github.com/BLSQ/hesabu"
15
15
  spec.license = "MIT"
16
16
 
17
-
18
17
  # Specify which files should be added to the gem when it is released.
19
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
21
  end
23
22
  spec.bindir = "exe"
data/lib/hesabu.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require "hesabu/version"
2
2
  require "parslet"
3
+ require_relative "./hesabu/errors"
3
4
  require_relative "./hesabu/parser"
5
+ require_relative "./hesabu/types/numeric"
4
6
  require_relative "./hesabu/types/float_lit"
5
7
  require_relative "./hesabu/types/fun_call"
6
8
  require_relative "./hesabu/types/indentifier_lit"
7
9
  require_relative "./hesabu/types/int_lit"
10
+ require_relative "./hesabu/types/string_lit"
8
11
  require_relative "./hesabu/types/operation"
9
12
 
10
13
  require_relative "./hesabu/interpreter"
@@ -0,0 +1,15 @@
1
+
2
+ module Hesabu
3
+ class Error < StandardError
4
+ end
5
+ class ParseError < Error
6
+ end
7
+ class CalculationError < Error
8
+ end
9
+ class DivideByZeroError < Error
10
+ end
11
+ class CyclicError < Error
12
+ end
13
+ class UnboundVariableError < Error
14
+ end
15
+ end
@@ -1,20 +1,23 @@
1
1
  module Hesabu
2
2
  class Interpreter < Parslet::Transform
3
- rule(left: simple(:left),
4
- right: simple(:right),
5
- op: simple(:op)) do
6
- Hesabu::Types::Operation.new(left, op, right)
7
- end
8
3
  rule(plist: sequence(:arr)) { arr }
9
4
  rule(plist: "()") { [] }
10
- rule(fcall: { name: simple(:name), varlist: sequence(:vars) }) do
11
- Hesabu::Types::FunCall.new(name, vars)
5
+ rule(l: simple(:left),
6
+ r: simple(:right),
7
+ o: simple(:op)) do
8
+ Hesabu::Types::Operation.new(left, op, right)
12
9
  end
13
10
  rule(identifier: simple(:id)) { id.to_s }
14
11
  rule(variable: simple(:variable)) do |d|
15
12
  d[:var_identifiers]&.add(d[:variable])
16
13
  Hesabu::Types::IdentifierLit.new(d[:variable], d[:doc])
17
14
  end
15
+ rule(fcall: { name: simple(:name), varlist: sequence(:vars) }) do
16
+ Hesabu::Types::FunCall.new(name, vars)
17
+ end
18
+ rule(str: subtree(:str)) do
19
+ Hesabu::Types::StringLit.new(str.map { |char| char.values.first.str }.join)
20
+ end
18
21
 
19
22
  rule(integer: simple(:integer)) { Hesabu::Types::IntLit.new(integer) }
20
23
  rule(float: simple(:float)) { Hesabu::Types::FloatLit.new(float) }
data/lib/hesabu/parser.rb CHANGED
@@ -5,51 +5,56 @@ module Hesabu
5
5
  end
6
6
 
7
7
  # simple things
8
- rule(:lparen) { str('(') >> space? }
9
- rule(:rparen) { str(')') >> space? }
10
- rule(:comma) { str(',') >> space? }
8
+ rule(:lparen) { str("(") >> space? }
9
+ rule(:rparen) { str(")") >> space? }
10
+ rule(:comma) { str(",") >> space? }
11
11
  rule(:space) { match["\s"] | match["\t"] | match["\n"] }
12
12
  rule(:spaces) { space.repeat }
13
13
  rule(:space?) { spaces.maybe }
14
14
 
15
+ rule(:nonquote) { str("'").absnt? >> any }
16
+ rule(:quote) { str("'") }
17
+ rule(:string) { quote >> nonquote.as(:char).repeat(1).as(:str) >> quote >> space? }
18
+
15
19
  rule(:identifier) do
16
- cts((match['a-zA-Z'] >> match['a-zA-Z0-9_'].repeat).as(:identifier))
20
+ cts((match["a-zA-Z"] >> match["a-zA-Z0-9_"].repeat).as(:identifier))
17
21
  end
18
22
 
19
- rule(:separator) { str(';') }
23
+ rule(:separator) { str(";") }
20
24
 
21
- rule(:digit) { match['0-9'] }
25
+ rule(:digit) { match["0-9"] }
22
26
 
23
27
  rule(:integer) do
24
- cts((str('-').maybe >> match['1-9'] >> digit.repeat).as(:integer) | str('0').as(:integer))
28
+ cts((str("-").maybe >> match["1-9"] >> digit.repeat).as(:integer) | str("0").as(:integer))
25
29
  end
26
30
 
27
31
  rule(:float) do
28
- cts((str('-').maybe >> digit.repeat(1) >> str('.') >> digit.repeat(1)).as(:float))
32
+ cts((str("-").maybe >> digit.repeat(1) >> str(".") >> digit.repeat(1)).as(:float))
29
33
  end
30
34
 
31
35
  # arithmetic
32
36
 
33
- rule(:expression) { sum | comparison | variable | pexpression }
37
+ rule(:expression) { iexpression | variable | pexpression }
34
38
  rule(:pexpression) { lparen >> expression >> rparen }
35
39
 
36
- rule(:variable) { identifier.as(:variable) } # gets simplified into a value, an "identifier" does not
37
- rule(:sum_op) { match('[+-]') >> space? }
38
- rule(:mul_op) { match('[*/]') >> space? }
39
- rule(:comparison_op) { (str('<=') | str('>=') | str('==') | str('<') | str('=') | str('>')) >> space? }
40
-
41
- rule(:atom) { pexpression | float | integer | fcall.as(:fcall) | variable }
42
-
43
- rule(:comparison) do
44
- atom.as(:left) >> comparison_op.as(:op) >> atom.as(:right)
40
+ rule(:variable) { identifier.as(:variable) }
41
+ rule(:sum_op) { match("[+-]") >> space? }
42
+ rule(:mul_op) { match("[*/]") >> space? }
43
+ rule(:comparison_op) do
44
+ (
45
+ str("<=") | str(">=") | str("==") |
46
+ str("!=") | str("<") | str("=") |
47
+ str(">") | str("AND")
48
+ ) >> space?
45
49
  end
46
50
 
47
- rule(:sum) do
48
- mul.as(:left) >> sum_op.as(:op) >> sum.as(:right) | mul | comparison
49
- end
51
+ rule(:atom) { string | pexpression | float | integer | fcall.as(:fcall) | variable }
50
52
 
51
- rule(:mul) do
52
- atom.as(:left) >> mul_op.as(:op) >> mul.as(:right) | comparison | atom
53
+ rule(:iexpression) do
54
+ infix_expression(atom,
55
+ [mul_op, 3, :left],
56
+ [sum_op, 2, :left],
57
+ [comparison_op, 1, :left])
53
58
  end
54
59
 
55
60
  # lists
@@ -57,13 +62,11 @@ module Hesabu
57
62
  rule(:pvarlist) { (lparen >> varlist.repeat >> rparen).as(:plist) }
58
63
 
59
64
  # functions
60
- rule(:fdef_keyword) { str('def ') >> space? }
61
- rule(:fend_keyword) { str('endf') >> space? }
62
65
  rule(:fcall) { identifier.as(:name) >> pvarlist.as(:varlist) }
63
66
 
64
67
  # root
65
68
  rule(:command) do
66
- sum
69
+ iexpression | expression | atom
67
70
  end
68
71
  rule(:commands) { commands.repeat }
69
72
  root :command
data/lib/hesabu/solver.rb CHANGED
@@ -2,7 +2,7 @@ module Hesabu
2
2
  class Solver
3
3
  include TSort
4
4
 
5
- Equation = Struct.new(:name, :evaluable, :dependencies)
5
+ Equation = Struct.new(:name, :evaluable, :dependencies, :raw_expression)
6
6
  EMPTY_DEPENDENCIES = [].freeze
7
7
  FakeEvaluable = Struct.new(:eval)
8
8
 
@@ -13,50 +13,29 @@ module Hesabu
13
13
  @bindings = {}
14
14
  end
15
15
 
16
- alias solving_order tsort
17
-
18
16
  def add(name, raw_expression)
19
- expression = raw_expression
20
- raw_expression_as_i = raw_expression.to_i
21
- raw_expression_as_f = raw_expression.to_f
22
-
23
- if raw_expression == raw_expression_as_i.to_s
24
- @equations[name] = Equation.new(
25
- name,
26
- FakeEvaluable.new(raw_expression_as_i),
27
- EMPTY_DEPENDENCIES
28
- )
29
- elsif raw_expression == raw_expression_as_f.to_s
30
- @equations[name] = Equation.new(
31
- name,
32
- FakeEvaluable.new(raw_expression_as_f),
33
- EMPTY_DEPENDENCIES
34
- )
17
+ if ::Hesabu::Types.as_numeric(raw_expression)
18
+ add_numeric(name, raw_expression)
35
19
  else
36
- expression = raw_expression.gsub(/\r\n?/, "")
37
- ast_tree = begin
38
- @parser.parse(expression)
39
- rescue Parslet::ParseFailed => e
40
- raise "failed to parse #{name} := #{expression} : #{e.message}"
41
- end
42
- var_identifiers = Set.new
43
- interpretation = @interpreter.apply(
44
- ast_tree,
45
- doc: @bindings,
46
- var_identifiers: var_identifiers
47
- )
48
- @equations[name] = Equation.new(name, interpretation, var_identifiers)
20
+ add_equation(name, raw_expression)
49
21
  end
50
22
  end
51
23
 
52
24
  def solve!
53
25
  solving_order.each do |name|
54
- equation = @equations[name]
55
- @bindings[equation.name] = equation.evaluable.eval
26
+ evaluate_equation(@equations[name])
56
27
  end
57
28
  solution = @bindings.dup
58
29
  @bindings.clear
59
- solution
30
+ to_numerics(solution)
31
+ rescue StandardError => e
32
+ log_and_raise(e)
33
+ end
34
+
35
+ def solving_order
36
+ tsort
37
+ rescue TSort::Cyclic => e
38
+ raise Hesabu::CyclicError, "There's a cycle between the variables : " + e.message[25..-1]
60
39
  end
61
40
 
62
41
  def tsort_each_node(&block)
@@ -64,7 +43,75 @@ module Hesabu
64
43
  end
65
44
 
66
45
  def tsort_each_child(node, &block)
67
- @equations[node].dependencies.each(&block)
46
+ equation = @equations[node]
47
+ raise UnboundVariableError, unbound_message(node) unless equation
48
+ equation.dependencies.each(&block)
49
+ end
50
+
51
+ private
52
+
53
+ def to_numerics(solution)
54
+ solution.each_with_object({}) do |kv, hash|
55
+ hash[kv.first] = Hesabu::Types.as_numeric(kv.last) || kv.last
56
+ end
57
+ end
58
+
59
+ def log_and_raise(e)
60
+ log "Error during processing: #{$ERROR_INFO}"
61
+ log "Error : #{e.class} #{e.message}"
62
+ log "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
63
+ raise e
64
+ end
65
+
66
+ def evaluate_equation(equation)
67
+ raise "not evaluable #{equation.evaluable} #{equation}" unless equation.evaluable.respond_to?(:eval, false)
68
+ begin
69
+ @bindings[equation.name] = equation.evaluable.eval
70
+ rescue StandardError => e
71
+ raise CalculationError, "Failed to evaluate #{equation.name} due to #{e.message} in formula #{equation.raw_expression}"
72
+ end
73
+ end
74
+
75
+ def log(message)
76
+ puts message
77
+ end
78
+
79
+ def add_numeric(name, raw_expression)
80
+ @equations[name] = Equation.new(
81
+ name,
82
+ FakeEvaluable.new(::Hesabu::Types.as_bigdecimal(raw_expression)),
83
+ EMPTY_DEPENDENCIES,
84
+ raw_expression
85
+ )
86
+ end
87
+
88
+ def add_equation(name, raw_expression)
89
+ expression = raw_expression.gsub(/\r\n?/, "")
90
+ ast_tree = begin
91
+ @parser.parse(expression)
92
+ rescue Parslet::ParseFailed => e
93
+ raise ParseError, "failed to parse #{name} := #{expression} : #{e.message}"
94
+ end
95
+ var_identifiers = Set.new
96
+ interpretation = @interpreter.apply(
97
+ ast_tree,
98
+ doc: @bindings,
99
+ var_identifiers: var_identifiers
100
+ )
101
+ if ENV["HESABU_DEBUG"]
102
+ log expression
103
+ log JSON.pretty_generate(ast_tree)
104
+ end
105
+ @equations[name] = Equation.new(name, interpretation, var_identifiers, raw_expression)
106
+ end
107
+
108
+ def unbound_message(node)
109
+ ref = first_reference(node)
110
+ "Unbound variable : #{node} used by #{ref.name} (#{ref.raw_expression})"
111
+ end
112
+
113
+ def first_reference(variable_name)
114
+ @equations.values.select { |v| v.dependencies.include?(variable_name) }.take(1).first
68
115
  end
69
116
  end
70
117
  end
@@ -2,7 +2,7 @@ module Hesabu
2
2
  module Types
3
3
  FloatLit = Struct.new(:float) do
4
4
  def eval
5
- float.to_f
5
+ Hesabu::Types.as_bigdecimal(float)
6
6
  end
7
7
  end
8
8
  end
@@ -1,6 +1,11 @@
1
1
  module Hesabu
2
2
  module Types
3
- class IfFunction
3
+ class Function
4
+ def divide(num, denum)
5
+ num / denum
6
+ end
7
+ end
8
+ class IfFunction < Function
4
9
  def call(args)
5
10
  raise "expected args #{name} : #{args}" unless args.size != 2
6
11
  condition_expression = args[0]
@@ -9,14 +14,14 @@ module Hesabu
9
14
  end
10
15
  end
11
16
 
12
- class SumFunction
17
+ class SumFunction < Function
13
18
  def call(args)
14
19
  values = args.map(&:eval)
15
20
  values.reduce(0, :+)
16
21
  end
17
22
  end
18
23
 
19
- class ScoreTableFunction
24
+ class ScoreTableFunction < Function
20
25
  def call(args)
21
26
  values = args.map(&:eval)
22
27
  target = values.shift
@@ -27,21 +32,21 @@ module Hesabu
27
32
  end
28
33
  end
29
34
 
30
- class AvgFunction
35
+ class AvgFunction < Function
31
36
  def call(args)
32
37
  values = args.map(&:eval)
33
38
  values.inject(0.0) { |acc, elem| acc + elem } / values.size
34
39
  end
35
40
  end
36
41
 
37
- class SafeDivFunction
42
+ class SafeDivFunction < Function
38
43
  def call(args)
39
44
  eval_denom = args[1].eval
40
45
  if eval_denom == 0
41
46
  0
42
47
  else
43
48
  eval_num = args[0].eval
44
- eval_num / eval_denom.to_f
49
+ eval_denom.zero? ? 0 : (eval_num / eval_denom)
45
50
  end
46
51
  end
47
52
  end
@@ -83,6 +88,15 @@ module Hesabu
83
88
  end
84
89
  end
85
90
 
91
+ class RoundFunction
92
+ def call(args)
93
+ raise "expected args #{self.class.name} : #{args}" if args.size > 2 || args.empty?
94
+ values = args.map(&:eval)
95
+ decimals = args.size == 2 ? values[1] : 0
96
+ values.first.round(decimals)
97
+ end
98
+ end
99
+
86
100
  FUNCTIONS = {
87
101
  "if" => IfFunction.new,
88
102
  "sum" => SumFunction.new,
@@ -93,7 +107,8 @@ module Hesabu
93
107
  "randbetween" => RandbetweenFunction.new,
94
108
  "score_table" => ScoreTableFunction.new,
95
109
  "abs" => AbsFunction.new,
96
- "access" => AccessFunction.new,
110
+ "access" => AccessFunction.new,
111
+ "round" => RoundFunction.new
97
112
  }.freeze
98
113
 
99
114
  FunCall = Struct.new(:name, :args) do
@@ -2,7 +2,7 @@ module Hesabu
2
2
  module Types
3
3
  IntLit = Struct.new(:int) do
4
4
  def eval
5
- int.to_i
5
+ ::Hesabu::Types.as_bigdecimal(int)
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,34 @@
1
+ require "bigdecimal"
2
+ module Hesabu
3
+ module Types
4
+ MAXDECIMAL = Float::DIG + 1
5
+
6
+ def self.as_numeric!(value)
7
+ numeric = as_numeric(value)
8
+ return numeric if numeric
9
+ raise "Not a numeric : '#{value}' (#{value.class.name})"
10
+ end
11
+
12
+ def self.as_numeric(value)
13
+ if value.is_a?(::Numeric)
14
+ return value.to_i if value.to_i == value
15
+ return value
16
+ end
17
+ value = value.str if value.is_a?(::Parslet::Slice)
18
+ if value.is_a?(::String)
19
+ number = value[/\A-?\d*\.?\d+\z/]
20
+ if number
21
+ if number.include?(".")
22
+ return BigDecimal(number, MAXDECIMAL)
23
+ else
24
+ return number.to_i
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.as_bigdecimal(number)
31
+ BigDecimal(number, MAXDECIMAL)
32
+ end
33
+ end
34
+ end
@@ -3,30 +3,39 @@ module Hesabu
3
3
  Operation = Struct.new(:left, :operator, :right) do
4
4
  def eval
5
5
  op = operator.str.strip
6
+ result(op, left.eval, right.eval)
7
+ end
8
+
9
+ private
6
10
 
7
- result = if op == "+"
8
- left.eval + right.eval
9
- elsif op == "-"
10
- left.eval - right.eval
11
- elsif op == "*"
12
- left.eval * right.eval
13
- elsif op == "/"
14
- left.eval / right.eval.to_f
15
- elsif op == ">"
16
- left.eval > right.eval
17
- elsif op == "<"
18
- left.eval < right.eval
19
- elsif op == ">="
20
- left.eval >= right.eval
21
- elsif op == "<="
22
- left.eval <= right.eval
23
- elsif op == "=" || op == "=="
24
- left.eval == right.eval
25
- else
26
- raise "unsupported operand : #{operator} : #{left} #{operator} #{right}"
27
- end
28
- # puts "#{left.eval} #{op} #{right.eval} => #{result}"
29
- result
11
+ def result(op, leftval, rightval)
12
+ case op
13
+ when "+"
14
+ leftval + rightval
15
+ when "-"
16
+ leftval - rightval
17
+ when "*"
18
+ leftval * rightval
19
+ when "/"
20
+ raise DivideByZeroError, "division by 0 : #{leftval}/0" if rightval.zero?
21
+ leftval / rightval
22
+ when ">"
23
+ leftval > rightval
24
+ when "<"
25
+ leftval < rightval
26
+ when ">="
27
+ leftval >= rightval
28
+ when "<="
29
+ leftval <= rightval
30
+ when "=", "=="
31
+ leftval == rightval
32
+ when "!="
33
+ leftval != rightval
34
+ when "AND"
35
+ leftval && rightval
36
+ else
37
+ raise "unsupported operand : #{op} : #{left} #{operator} #{right}"
38
+ end
30
39
  end
31
40
  end
32
41
  end
@@ -0,0 +1,9 @@
1
+ module Hesabu
2
+ module Types
3
+ StringLit = Struct.new(:string) do
4
+ def eval
5
+ string
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Hesabu
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hesabu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stéphan Mestach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-03 00:00:00.000000000 Z
11
+ date: 2018-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -157,11 +157,13 @@ executables: []
157
157
  extensions: []
158
158
  extra_rdoc_files: []
159
159
  files:
160
+ - ".github/PULL_REQUEST_TEMPLATE.md"
160
161
  - ".gitignore"
161
162
  - ".rspec"
162
163
  - ".rubocop.yml"
163
164
  - ".ruby-version"
164
165
  - ".travis.yml"
166
+ - CODE_OF_CONDUCT.md
165
167
  - Gemfile
166
168
  - Gemfile.lock
167
169
  - LICENSE.txt
@@ -171,6 +173,7 @@ files:
171
173
  - bin/setup
172
174
  - hesabu.gemspec
173
175
  - lib/hesabu.rb
176
+ - lib/hesabu/errors.rb
174
177
  - lib/hesabu/interpreter.rb
175
178
  - lib/hesabu/parser.rb
176
179
  - lib/hesabu/solver.rb
@@ -178,7 +181,9 @@ files:
178
181
  - lib/hesabu/types/fun_call.rb
179
182
  - lib/hesabu/types/indentifier_lit.rb
180
183
  - lib/hesabu/types/int_lit.rb
184
+ - lib/hesabu/types/numeric.rb
181
185
  - lib/hesabu/types/operation.rb
186
+ - lib/hesabu/types/string_lit.rb
182
187
  - lib/hesabu/version.rb
183
188
  homepage: https://github.com/BLSQ/hesabu
184
189
  licenses: