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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +8 -0
- data/README.md +89 -0
- data/Rakefile +22 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/taxger/einkommensteuer.rb +104 -0
- data/lib/taxger/lohnsteuer/bigdecimal.rb +70 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2006.rb +891 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2007.rb +917 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2008.rb +983 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2009.rb +975 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2010.rb +1026 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2011.rb +1070 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2011dezember.rb +1082 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2012.rb +1118 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2013.rb +1120 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2014.rb +1123 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2015.rb +1144 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2015dezember.rb +1339 -0
- data/lib/taxger/lohnsteuer/lohnsteuer2016.rb +1144 -0
- data/lib/taxger/lohnsteuer.rb +28 -0
- data/lib/taxger/version.rb +3 -0
- data/lib/taxger.rb +9 -0
- data/src/README.md +141 -0
- data/src/code_tree.rb +122 -0
- data/src/converter.rb +61 -0
- data/src/generated/.keep +0 -0
- data/src/node/class_node.rb +30 -0
- data/src/node/comment_node.rb +19 -0
- data/src/node/conditional_node.rb +32 -0
- data/src/node/expr_node.rb +13 -0
- data/src/node/initializer_node.rb +37 -0
- data/src/node/method_call_node.rb +12 -0
- data/src/node/method_node.rb +21 -0
- data/src/node/node.rb +47 -0
- data/src/node/pseudo_code_parser.rb +88 -0
- data/src/node/source_node.rb +43 -0
- data/src/node/var_block_node.rb +39 -0
- data/taxger.gemspec +34 -0
- 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
|
data/lib/taxger.rb
ADDED
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
|
data/src/generated/.keep
ADDED
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,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,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
|