taxger 0.2.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +8 -0
  8. data/README.md +89 -0
  9. data/Rakefile +22 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/taxger/einkommensteuer.rb +104 -0
  13. data/lib/taxger/lohnsteuer/bigdecimal.rb +70 -0
  14. data/lib/taxger/lohnsteuer/lohnsteuer2006.rb +891 -0
  15. data/lib/taxger/lohnsteuer/lohnsteuer2007.rb +917 -0
  16. data/lib/taxger/lohnsteuer/lohnsteuer2008.rb +983 -0
  17. data/lib/taxger/lohnsteuer/lohnsteuer2009.rb +975 -0
  18. data/lib/taxger/lohnsteuer/lohnsteuer2010.rb +1026 -0
  19. data/lib/taxger/lohnsteuer/lohnsteuer2011.rb +1070 -0
  20. data/lib/taxger/lohnsteuer/lohnsteuer2011dezember.rb +1082 -0
  21. data/lib/taxger/lohnsteuer/lohnsteuer2012.rb +1118 -0
  22. data/lib/taxger/lohnsteuer/lohnsteuer2013.rb +1120 -0
  23. data/lib/taxger/lohnsteuer/lohnsteuer2014.rb +1123 -0
  24. data/lib/taxger/lohnsteuer/lohnsteuer2015.rb +1144 -0
  25. data/lib/taxger/lohnsteuer/lohnsteuer2015dezember.rb +1339 -0
  26. data/lib/taxger/lohnsteuer/lohnsteuer2016.rb +1144 -0
  27. data/lib/taxger/lohnsteuer.rb +28 -0
  28. data/lib/taxger/version.rb +3 -0
  29. data/lib/taxger.rb +9 -0
  30. data/src/README.md +141 -0
  31. data/src/code_tree.rb +122 -0
  32. data/src/converter.rb +61 -0
  33. data/src/generated/.keep +0 -0
  34. data/src/node/class_node.rb +30 -0
  35. data/src/node/comment_node.rb +19 -0
  36. data/src/node/conditional_node.rb +32 -0
  37. data/src/node/expr_node.rb +13 -0
  38. data/src/node/initializer_node.rb +37 -0
  39. data/src/node/method_call_node.rb +12 -0
  40. data/src/node/method_node.rb +21 -0
  41. data/src/node/node.rb +47 -0
  42. data/src/node/pseudo_code_parser.rb +88 -0
  43. data/src/node/source_node.rb +43 -0
  44. data/src/node/var_block_node.rb +39 -0
  45. data/taxger.gemspec +34 -0
  46. metadata +146 -0
@@ -0,0 +1,28 @@
1
+ require 'taxger/lohnsteuer/bigdecimal'
2
+ require 'taxger/lohnsteuer/lohnsteuer2006'
3
+ require 'taxger/lohnsteuer/lohnsteuer2007'
4
+ require 'taxger/lohnsteuer/lohnsteuer2008'
5
+ require 'taxger/lohnsteuer/lohnsteuer2009'
6
+ require 'taxger/lohnsteuer/lohnsteuer2010'
7
+ require 'taxger/lohnsteuer/lohnsteuer2011'
8
+ require 'taxger/lohnsteuer/lohnsteuer2011dezember'
9
+ require 'taxger/lohnsteuer/lohnsteuer2012'
10
+ require 'taxger/lohnsteuer/lohnsteuer2013'
11
+ require 'taxger/lohnsteuer/lohnsteuer2014'
12
+ require 'taxger/lohnsteuer/lohnsteuer2015'
13
+ require 'taxger/lohnsteuer/lohnsteuer2015dezember'
14
+ require 'taxger/lohnsteuer/lohnsteuer2016'
15
+
16
+ module Taxger
17
+ module Lohnsteuer
18
+ extend self
19
+
20
+ def calculate(year, input)
21
+ input = Hash[input.map do |key, value|
22
+ [key, Taxger::Lohnsteuer::BigDecimal.new(value)]
23
+ end]
24
+ lst = Object.const_get("Taxger::Lohnsteuer::Lohnsteuer#{year}")
25
+ lst.new(input)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Taxger
2
+ VERSION = "0.2.0"
3
+ end
data/lib/taxger.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'bigdecimal'
2
+ require 'taxger/version'
3
+ require 'taxger/lohnsteuer'
4
+ require 'taxger/einkommensteuer'
5
+
6
+ module Taxger
7
+ # Your code goes here...
8
+ end
9
+
data/src/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Taxger Source Generation
2
+
3
+ The Lohnsteuer calculator is created from [https://www.bmf-steuerrechner.de/interface/pseudocode.jsp](pseudo code) offered by the
4
+ Ministery of Finance.
5
+
6
+ To rebuild these files (or create files for years not yet included in
7
+ this gem), execute the following steps:
8
+
9
+ ### Download the pseudo code
10
+
11
+ Download the files by running
12
+
13
+ ```
14
+ $ rake taxger:source:download
15
+ ```
16
+
17
+ The files will be stored in `src/xml/`. The rake task assumes you have
18
+ `curl` installed. If not, check `src/converter.rb` to see where to
19
+ download the files manually.
20
+
21
+ ### Parse and generate Ruby code
22
+
23
+ To create Ruby code from the pseudo code, run:
24
+
25
+ ```
26
+ $ rake taxger:source:generate
27
+ ```
28
+
29
+ For every year, a Ruby class will be generated in `src/generated/`.
30
+
31
+ ### Copy generated sources to gem sources
32
+
33
+ Inspect the automatically generated files in `src/generated/` and apply
34
+ changes if necessary.
35
+
36
+ To make them usable for the actual gem, copy them to `lib/lohnsteuer/`
37
+ and adjust `lib/taxger.rb` to make sure these files are loaded.
38
+
39
+ ## About the parser
40
+
41
+ The pseudo code consists of two different structural layers. Declaration
42
+ of variables, method bodies and control flow (IF/THEN/ELSE) is specified
43
+ in XML.
44
+
45
+ ### Parsing XML
46
+
47
+ The XML tags contain Java-like pseudo code (for example to specify a
48
+ boolean expression for a conditional statement or an assignment).
49
+
50
+ The parser uses Nokogiri to parse the XML and generates its own nodes
51
+ (classes in `src/node` to translate the structure).
52
+
53
+ These nodes build up a tree structure (for example a `MethodNode`
54
+ containing a `ConditionalNode` which in turn contains a `MethodCallNode`
55
+ etc.).
56
+
57
+ ### Parsing Java-like pseudo code
58
+
59
+ Every `Node` exposes a `render` method that returns the actual Ruby code
60
+ (and is mostly fed by content of attributes of the underlying XML tag).
61
+
62
+ This pseudo code has a Java like syntax. `PseudoCodeParser` uses Ruby's
63
+ internal `StringScanner` class to tokenize the input.
64
+
65
+ The parser is nowhere near complete but enough to parse the existing
66
+ pseudo code XML files from the last 10 years.
67
+
68
+ It distinguishes between instance variables and constants. While in
69
+ pseudo code, all these are uppercase, the parser translates instance
70
+ variables to proper lowercase names prefixed with `@`.
71
+
72
+ Constants are left in uppercase.
73
+
74
+ Instantiations of `BigDecimal` are translated into a Ruby syntax (`new
75
+ BigDecimal(0)` becomes `BigDecimal.new(0)`.
76
+
77
+ *Please note that BigDecimal has been monkey-patched to make it
78
+ compatible with the Java-like syntax of doing calculations with
79
+ designated methods (i.e. `a.substract(b)` instead of `a - b`!)*
80
+
81
+ The parser accepts an optional `;` at EOL, but other unknown symbols
82
+ will trigger an error (to make sure it roughly understands whats going
83
+ on).
84
+
85
+ ### Parser warnings
86
+
87
+ You will encounter the following messages for older files:
88
+
89
+ ```
90
+ WARNING: Orphaned ELSE block found, but assigning it to previous IF statement` in some older files.
91
+ ```
92
+
93
+ These can be safely ignored. The reason is that older files use an
94
+ ambigous XML nesting like this:
95
+
96
+ ```xml
97
+ <IF>
98
+ <THEN> ... </THEN>
99
+ </IF>
100
+ <ELSE> ... </ELSE>
101
+ ```
102
+
103
+ The parser checks if the `ELSE` tag is follwing an `IF` tag immediatly.
104
+ If so, it is attached there and the warning is shown.
105
+
106
+ Newer files from the Ministery of Finance use the following syntax:
107
+
108
+ ```xml
109
+ <IF>
110
+ <THEN> ... </THEN>
111
+ <ELSE> ... </ELSE>
112
+ </IF>
113
+ ```
114
+
115
+ ### TODO
116
+
117
+ `Lohnsteuer2012.xml` contains namespace references to *some* variables.
118
+ The parser doesn't deal with it, so they must be removed manually from
119
+ the Ruby source.
120
+
121
+ `lib/taxger/lohnsteuer/lohnsteuer2012.rb` Line 818:
122
+
123
+ ```ruby
124
+ if @zre4vp.compare_to(Lohnsteuer2012Big.RENTBEMESSUNGSGR_WEST) == 1
125
+ ```
126
+ should be changed to
127
+ ```ruby
128
+ if @zre4vp.compare_to(RENTBEMESSUNGSGR_WEST) == 1
129
+ ```
130
+ etc.
131
+
132
+ This should be handled automatically in the future.
133
+
134
+ ### Final words
135
+
136
+ I would not consider this code to be a text book example on how to do
137
+ something like this (the whole thing could use some refactoring). It is
138
+ more of a quick hack, but this does not affect the quality of the
139
+ resulting Ruby files.
140
+
141
+ They are 100% compatible with the pseudo code provided.
data/src/code_tree.rb ADDED
@@ -0,0 +1,122 @@
1
+ class CodeTree
2
+ attr_accessor :nodes
3
+
4
+ def initialize(xml, class_name = nil)
5
+ @class_name = class_name
6
+ @xml = xml
7
+ @nodes = []
8
+ @internals = []
9
+ @var = {}
10
+ @var[:outputs] = @xml.css('OUTPUT').map { |e| e.attr('name') }
11
+ @var[:inputs] = @xml.css('INPUT').map { |e| e.attr('name') }
12
+ @var[:internals] = @xml.css('INTERNAL').map { |e| e.attr('name') }
13
+ @var[:constants] = @xml.css('CONSTANT').map { |e| e.attr('name') }
14
+ @instance_vars = []
15
+ PseudoCode.set_vars(@var)
16
+ parse_vars
17
+ parse_methods
18
+ end
19
+
20
+ def render
21
+ ClassNode.new(@class_name || @xml.css('PAP').attr('name'), @nodes, ['Taxger', 'Lohnsteuer']).render
22
+ end
23
+
24
+ private
25
+
26
+ def parse_vars
27
+ @instance_vars << VarBlockNode.new(@xml.css('INPUTS').first, tag: :input, show_type: true)
28
+ @instance_vars << VarBlockNode.new(@xml.css('OUTPUTS').first, tag: :output, show_type: true)
29
+ @instance_vars << VarBlockNode.new(@xml.css('INTERNALS').first, tag: :internal)
30
+ @nodes << VarBlockNode.new(@xml.css('CONSTANTS').first, tag: :constant)
31
+ end
32
+
33
+ def parse_methods
34
+ comments = []
35
+ @nodes << InitializerNode.new(@xml.xpath('/PAP/METHODS/MAIN').first, @instance_vars, @var)
36
+ @xml.xpath('/PAP/METHODS/METHOD').each do |line|
37
+ if Node.is_comment?(line)
38
+ comments << CommentNode.new(line)
39
+ elsif Node.is_text?(line)
40
+ if line.inner_text.strip != ''
41
+ raise UnknownTextError.new(line)
42
+ end
43
+ elsif line.name == 'METHOD'
44
+ @nodes << MethodNode.new(line, description: comments)
45
+ comments = []
46
+ else
47
+ raise UnknownTagError.new(line)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ class ParserError < StandardError
54
+ end
55
+
56
+ class UnknownTextError < ParserError
57
+ end
58
+
59
+ class UnknownTagError < ParserError
60
+ end
61
+
62
+ class PseudoCode
63
+ def self.set_vars(var)
64
+ @outputs = var[:outputs].sort { |b, a| a.length <=> b.length }
65
+ @inputs = var[:inputs].sort { |b, a| a.length <=> b.length }
66
+ @internals = var[:internals].sort { |b, a| a.length <=> b.length }
67
+ @instance_vars = (@outputs + @inputs + @internals).sort { |b, a| a.length <=> b.length }
68
+ end
69
+
70
+ def self.parse_expr(pseudo_code)
71
+ parser = PseudoCodeParser.new(pseudo_code.strip, @instance_vars)
72
+ parser.tokens.join
73
+ end
74
+
75
+ def self.parse_method_name(name)
76
+ "#{name.downcase}"
77
+ end
78
+
79
+ def self.parse_var(name)
80
+ if @instance_vars.include?(name)
81
+ "@#{name.downcase}"
82
+ else
83
+ name
84
+ end
85
+ end
86
+
87
+ def self.parse_default(default, type)
88
+ if default
89
+ parser = PseudoCodeParser.new(default.strip, @instance_vars)
90
+ parser.tokens.join
91
+ else
92
+ case type
93
+ when 'int'
94
+ '0'
95
+ when 'double'
96
+ '0.0'
97
+ when 'float'
98
+ '0.0'
99
+ when 'BigDecimal'
100
+ 'BigDecimal.new(0)'
101
+ end
102
+ end
103
+ end
104
+
105
+ def self.parse_constant_value(value)
106
+ if value[0] == '{'
107
+ list = value[1..-2].split(',').map do |field|
108
+ PseudoCodeParser.new(field.strip, @instance_vars).tokens.join
109
+ end
110
+ lines = []
111
+ while (list != [])
112
+ lines << list.shift(4)
113
+ end
114
+ '[' + lines.map { |items| items.join(', ') }.join(",\n#{' ' * 20}") + ']'
115
+ else
116
+ PseudoCodeParser.new(value.strip, @instance_vars).tokens.join
117
+ end
118
+ end
119
+ end
120
+
121
+
122
+
data/src/converter.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'nokogiri'
2
+ require 'strscan'
3
+ require 'bigdecimal'
4
+
5
+ require 'node/node'
6
+ require 'node/comment_node.rb'
7
+ require 'node/expr_node.rb'
8
+ require 'node/method_node.rb'
9
+ require 'node/initializer_node.rb'
10
+ require 'node/method_call_node.rb'
11
+ require 'node/conditional_node.rb'
12
+ require 'node/source_node.rb'
13
+ require 'node/class_node.rb'
14
+ require 'node/var_block_node.rb'
15
+ require 'node/pseudo_code_parser.rb'
16
+
17
+ require 'code_tree.rb'
18
+
19
+ module Taxger
20
+ class Converter
21
+ XML_PATH = File.expand_path('../xml/', __FILE__)
22
+ GENERATED_PATH = File.expand_path('../generated/', __FILE__)
23
+ URI = 'https://www.bmf-steuerrechner.de/pruefdaten/'
24
+ FILES = {
25
+ 'Lohnsteuer2016.xml' => 'Lohnsteuer2016',
26
+ 'Lohnsteuer2015Dezember.xml' => 'Lohnsteuer2015Dezember',
27
+ 'Lohnsteuer2015BisNovember.xml' => 'Lohnsteuer2015',
28
+ 'Lohnsteuer2014.xml' => 'Lohnsteuer2014',
29
+ 'Lohnsteuer2013_2.xml' => 'Lohnsteuer2013',
30
+ 'Lohnsteuer2012.xml' => 'Lohnsteuer2012',
31
+ 'Lohnsteuer2011Dezember.xml' => 'Lohnsteuer2011Dezember',
32
+ 'Lohnsteuer2011BisNovember.xml' => 'Lohnsteuer2011',
33
+ 'Lohnsteuer2010Big.xml' => 'Lohnsteuer2010',
34
+ 'Lohnsteuer2009Big.xml' => 'Lohnsteuer2009',
35
+ 'Lohnsteuer2008Big.xml' => 'Lohnsteuer2008',
36
+ 'Lohnsteuer2007Big.xml' => 'Lohnsteuer2007',
37
+ 'Lohnsteuer2006Big.xml' => 'Lohnsteuer2006'}
38
+
39
+ def self.download_all!
40
+ FILES.keys.each do |file|
41
+ puts "Downloading #{file}"
42
+ `curl #{URI}#{file} -s -o #{File.join(XML_PATH, file)}`
43
+ end
44
+ end
45
+
46
+ def self.generate_all!
47
+ FILES.each do |file, class_name|
48
+ generate_file(file, class_name)
49
+ end
50
+ end
51
+
52
+ def self.generate_file(file, class_name)
53
+ code = CodeTree.new(Nokogiri::XML(File.read(File.join(XML_PATH, file))), class_name)
54
+ name = file.split('.').first.downcase
55
+ puts "Generating #{name}.rb"
56
+ File.open(File.join(GENERATED_PATH, "#{class_name.downcase}.rb"), 'w+') do |f|
57
+ f.puts code.render
58
+ end
59
+ end
60
+ end
61
+ end
File without changes
@@ -0,0 +1,30 @@
1
+ class ClassNode < Node
2
+ def initialize(name, content, namespaces)
3
+ @namespaces = namespaces.reverse
4
+ @name = name
5
+ @content = content
6
+ super()
7
+ end
8
+
9
+ def render
10
+ render_with_namespace(@namespaces)
11
+ output_buffer
12
+ end
13
+
14
+ private
15
+
16
+ def render_with_namespace(namespaces)
17
+ if namespaces.size > 0
18
+ output "module #{namespaces.pop}"
19
+ ident { render_with_namespace(namespaces) }
20
+ output "end"
21
+ else
22
+ output "class #{@name}"
23
+ ident do
24
+ output(@content)
25
+ end
26
+ output 'end'
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,19 @@
1
+ class CommentNode < Node
2
+ def initialize(element)
3
+ if element.is_a?(String)
4
+ @comments = [element]
5
+ else
6
+ @comments = element.inner_text.split("\n").map(&:strip)
7
+ end
8
+ super()
9
+ end
10
+
11
+ def render
12
+ @comments.each do |line|
13
+ comment(line)
14
+ end
15
+ #linefeed if @comments.size > 0
16
+ output_buffer
17
+ end
18
+ end
19
+
@@ -0,0 +1,32 @@
1
+ class ConditionalNode < Node
2
+ def initialize(element)
3
+ @cond_expr = element.attr('expr')
4
+ @then = SourceNode.new(element.xpath('./THEN/*'))
5
+ @else = SourceNode.new(element.xpath('./ELSE/*'))
6
+ super()
7
+ end
8
+
9
+ def attach_else(elements)
10
+ if @else.lines.count > 0
11
+ raise "Cannot attach another ELSE block to this conditional statement: #{elements}"
12
+ else
13
+ @else = SourceNode.new(elements)
14
+ end
15
+ end
16
+
17
+ def render
18
+ output("if #{PseudoCode.parse_expr(@cond_expr)}")
19
+ ident do
20
+ output @then
21
+ end
22
+ if @else.lines.count > 0
23
+ output('else')
24
+ ident do
25
+ output @else
26
+ end
27
+ end
28
+ output('end')
29
+ output_buffer
30
+ end
31
+ end
32
+
@@ -0,0 +1,13 @@
1
+ class ExprNode < Node
2
+ def initialize(element)
3
+ @expression = element.attr('exec')
4
+ # TODO: parse expression hard core
5
+ super()
6
+ end
7
+
8
+ def render
9
+ output(PseudoCode.parse_expr(@expression))
10
+ #linefeed
11
+ output_buffer
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ class InitializerNode < Node
2
+ def initialize(element, instance_vars, var, options = {})
3
+ @options = options
4
+ @name = element.attr('name')
5
+ @instance_vars = instance_vars
6
+ @var = var
7
+ @body = SourceNode.new(element.xpath('./*'))
8
+ super()
9
+ end
10
+
11
+ def render
12
+ @var[:outputs].map(&:downcase).each do |field|
13
+ output "attr_accessor :#{field}"
14
+ end
15
+ linefeed
16
+ output('INPUT_VARS = ' + @var[:inputs].map(&:downcase).map(&:to_sym).inspect)
17
+ output('OUTPUT_VARS = ' + @var[:outputs].map(&:downcase).map(&:to_sym).inspect)
18
+ if @options[:description] && @options[:description].length > 0
19
+ output(@options[:description])
20
+ end
21
+ output('def initialize(params)')
22
+ ident do
23
+ output 'raise "Unknown parameters: #{params.keys - INPUT_VARS}" if params.keys - INPUT_VARS != []'
24
+ output @instance_vars
25
+ output 'params.each do |key, value|'
26
+ output ' instance_variable_set("@#{key}", value)'
27
+ output 'end'
28
+ linefeed
29
+ output @body
30
+ end
31
+ output('end')
32
+ linefeed
33
+ output('private')
34
+ linefeed
35
+ output_buffer
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ class MethodCallNode < Node
2
+ def initialize(element)
3
+ @method = element.attr('method')
4
+ super()
5
+ end
6
+
7
+ def render
8
+ output("#{PseudoCode.parse_method_name(@method)}")
9
+ output_buffer
10
+ end
11
+ end
12
+
@@ -0,0 +1,21 @@
1
+ class MethodNode < Node
2
+ def initialize(element, options = {})
3
+ @options = options
4
+ @name = element.attr('name')
5
+ @body = SourceNode.new(element.xpath('./*'))
6
+ super()
7
+ end
8
+
9
+ def render
10
+ if @options[:description] && @options[:description].length > 0
11
+ output(@options[:description])
12
+ end
13
+ output("def #{PseudoCode.parse_method_name(@name)}")
14
+ ident do
15
+ output(@body)
16
+ end
17
+ output('end')
18
+ linefeed
19
+ output_buffer
20
+ end
21
+ end
data/src/node/node.rb ADDED
@@ -0,0 +1,47 @@
1
+ class Node
2
+ @@ident = 0
3
+ def initialize
4
+ @output = []
5
+ end
6
+
7
+ def output_buffer
8
+ @output
9
+ end
10
+
11
+ def self.is_comment?(element)
12
+ element.is_a?(Nokogiri::XML::Comment)
13
+ end
14
+
15
+ def self.is_text?(element)
16
+ element.is_a?(Nokogiri::XML::Text)
17
+ end
18
+
19
+ protected
20
+
21
+ def ident(&block)
22
+ @@ident += 1
23
+ yield
24
+ @@ident -= 1
25
+ end
26
+
27
+ def output(node)
28
+ if node.is_a?(String)
29
+ @output << "#{' ' * @@ident}#{node}".rstrip
30
+ elsif node.is_a?(Node)
31
+ @output += node.render
32
+ elsif node.is_a?(Array)
33
+ node.each { |n| output(n) }
34
+ end
35
+ end
36
+
37
+ def comment(str)
38
+ (str.is_a?(Array) ? str : [str]).map(&:strip).compact.each do |line|
39
+ output("# #{str}")
40
+ end
41
+ end
42
+
43
+ def linefeed
44
+ output('')
45
+ end
46
+ end
47
+
@@ -0,0 +1,88 @@
1
+ class PseudoCodeParser
2
+ attr_reader :tokens
3
+
4
+ NUMBER = /^-?\d+/
5
+ IDENTIFIER = /\w+/
6
+ RESERVED = {
7
+ multiply: 'multiply',
8
+ divide: 'divide',
9
+ substract: 'substract',
10
+ add: 'add',
11
+ valueOf: 'value_of',
12
+ compareTo: 'compare_to',
13
+ setScale: 'set_scale'
14
+ }
15
+
16
+ class ParserError < StandardError
17
+ end
18
+
19
+ def initialize(source, instance_vars)
20
+ @buffer = StringScanner.new(source)
21
+ @instance_vars = instance_vars
22
+ @tokens = []
23
+ parse
24
+ end
25
+
26
+ private
27
+
28
+ def parse
29
+ until @buffer.eos?
30
+ parse_next
31
+ end
32
+ end
33
+
34
+ def parse_next
35
+ skip_spaces
36
+ if @buffer.scan(/new BigDecimal/)
37
+ @tokens << 'BigDecimal.new'
38
+ elsif parse_number_or_identifier
39
+ # ...
40
+ elsif @buffer.scan(/,/)
41
+ @tokens << ', '
42
+ elsif ['&&', '>=', '<=', '==', '!='].include?(@buffer.peek(2))
43
+ @tokens << " #{@buffer.getch + @buffer.getch} "
44
+ elsif ['+', '-', '/', '*', '<' ,'>', '='].include?(@buffer.peek(1))
45
+ @tokens << " #{@buffer.getch} "
46
+ elsif ['.', '(', ')', '[', ']'].include?(@buffer.peek(1))
47
+ @tokens << @buffer.getch
48
+ elsif @buffer.rest == ';'
49
+ # some older files (< 2012) have semicolons at the end of some lines.
50
+ # we ignore this if it is the last character.
51
+ @buffer.getch
52
+ else
53
+ error('Unexpected token')
54
+ end
55
+ end
56
+
57
+ def parse_number_or_identifier
58
+ skip_spaces
59
+ if @buffer.check(NUMBER)
60
+ result = @tokens.push(@buffer.scan(NUMBER))
61
+
62
+ # remove numeric literal data types (like '0.01D'). these occur in the 2011 XML file
63
+ if %w(D L).include?(@buffer.peek(1))
64
+ @buffer.getch
65
+ end
66
+
67
+ result
68
+ elsif @buffer.check(IDENTIFIER)
69
+ token = @buffer.scan(IDENTIFIER)
70
+ if RESERVED[token.to_sym]
71
+ token = RESERVED[token.to_sym]
72
+ else
73
+ token = "@#{token.downcase}" if @instance_vars.include?(token)
74
+ end
75
+ @tokens.push(token)
76
+ else
77
+ nil
78
+ end
79
+ end
80
+
81
+ def skip_spaces
82
+ @buffer.skip(/\s+/)
83
+ end
84
+
85
+ def error(description)
86
+ raise ParserError.new("#{description} at position #{@buffer.pos+1}: \"#{@buffer.string}\"")
87
+ end
88
+ end