File: calc.rb

package info (click to toggle)
ruby-parslet 1.6.1-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 908 kB
  • ctags: 473
  • sloc: ruby: 5,220; makefile: 2
file content (153 lines) | stat: -rw-r--r-- 3,493 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# A simple integer calculator to answer the question about how to do 
# left and right associativity in parslet (PEG) once and for all. 

$:.unshift File.dirname(__FILE__) + "/../lib"

require 'rspec'
require 'parslet'
require 'parslet/rig/rspec'

# This is the parsing stage. It expresses left associativity by compiling
# list of things that have the same associativity. 
class CalcParser < Parslet::Parser
  root :addition
  
  rule(:addition) {
    multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
    multiplication
  }
  
  rule(:multiplication) { 
    integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
    integer }
  
  rule(:integer) { digit.repeat(1).as(:i) >> space? }

  rule(:mult_op) { match['*/'].as(:o) >> space? }
  rule(:add_op) { match['+-'].as(:o) >> space? }

  rule(:digit) { match['0-9'] }
  rule(:space?) { match['\s'].repeat }
end

# Classes for the abstract syntax tree.
Int    = Struct.new(:int) {
  def eval; self end
  def op(operation, other)
    left = int
    right = other.int 

    Int.new(
      case operation
        when '+'
          left + right
        when '-'
          left - right
        when '*'
          left * right
        when '/'
          left / right
      end)
  end
  def to_i
    int
  end
}
Seq    = Struct.new(:sequence) {
  def eval
    sequence.reduce { |accum, operation| 
      operation.call(accum) }
  end
}
LeftOp = Struct.new(:operation, :right) {
  def call(left)
    left = left.eval
    right = self.right.eval

    left.op(operation, right)
  end
}

# Transforming intermediary syntax tree into a real AST.
class CalcTransform < Parslet::Transform
  rule(i: simple(:i)) { Int.new(Integer(i)) }
  rule(o: simple(:o), r: simple(:i)) { LeftOp.new(o, i) }
  rule(l: simple(:i)) { i }
  rule(sequence(:seq)) { Seq.new(seq) }
end

# And this calls everything in the right order.
def calculate(str)
  intermediary_tree = CalcParser.new.parse(str)
  abstract_tree = CalcTransform.new.apply(intermediary_tree)
  result = abstract_tree.eval
  
  result.to_i
end

# A test suite for the above parser
describe CalcParser do
  let(:p) { described_class.new }
  describe '#integer' do
    let(:i) { p.integer }
    it "parses integers" do
      i.should parse('1')
      i.should parse('123')
    end 
    it "consumes trailing white space" do
      i.should parse('123   ')
    end 
    it "doesn't parse floats" do
      i.should_not parse('1.3')
    end 
  end
  describe '#multiplication' do
    let(:m) { p.multiplication }
    it "parses simple multiplication" do
      m.should parse('1*2')
    end
    it "parses division" do
      m.should parse('1/2')
    end 
  end
  describe '#addition' do
    let(:a) { p.addition }
    
    it "parses simple addition" do
      a.should parse('1+2')
      a.should parse('1+2+3-4')
    end 
  end
end
describe CalcTransform do
  def t(obj)
    described_class.new.apply(obj)
  end
  
  it "transforms integers" do
    t(i: '1').should == Int.new(1)
  end 
  it "unwraps left operand" do
    t(l: :obj).should == :obj
  end 
end
describe 'whole computation specs' do
  def self.result_of(str, int)
    it(str) { calculate(str).should == int } 
  end

  result_of '1+1', 2
  result_of '1-1-1', -1
  result_of '1+1+3*5/2', 9
  result_of '123*2', 246
end


# Enable these if you want to change the code.
# RSpec::Core::Runner.run([], $stderr, $stdout)

str = ARGV.join
str = '123*2' if str.match(/^\s*$/)

print "#{str} (command line): -> "
puts calculate(str)