calco 0.1.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 +34 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +22 -0
- data/README.md +360 -0
- data/Rakefile +1 -0
- data/calco.gemspec +23 -0
- data/examples/ages.ods +0 -0
- data/examples/compute_cells.rb +61 -0
- data/examples/data.csv +8 -0
- data/examples/example.rb +97 -0
- data/examples/multiplication_tables.ods +0 -0
- data/examples/multiplication_tables.rb +73 -0
- data/examples/register_function.rb +44 -0
- data/examples/using_date_functions.rb +42 -0
- data/examples/write_csv.rb +68 -0
- data/examples/write_ods.rb +69 -0
- data/lib/calco.rb +17 -0
- data/lib/calco/core_ext/fixnum.rb +22 -0
- data/lib/calco/core_ext/float.rb +22 -0
- data/lib/calco/core_ext/range.rb +15 -0
- data/lib/calco/core_ext/string.rb +20 -0
- data/lib/calco/date_functions.rb +13 -0
- data/lib/calco/definition_dsl.rb +127 -0
- data/lib/calco/elements/aggregator.rb +17 -0
- data/lib/calco/elements/builtin_function.rb +84 -0
- data/lib/calco/elements/constant.rb +31 -0
- data/lib/calco/elements/current.rb +19 -0
- data/lib/calco/elements/element.rb +31 -0
- data/lib/calco/elements/empty.rb +9 -0
- data/lib/calco/elements/formula.rb +42 -0
- data/lib/calco/elements/if.rb +26 -0
- data/lib/calco/elements/operation.rb +34 -0
- data/lib/calco/elements/operator.rb +17 -0
- data/lib/calco/elements/or.rb +26 -0
- data/lib/calco/elements/value_extractor.rb +42 -0
- data/lib/calco/elements/variable.rb +35 -0
- data/lib/calco/engines/calculator_builtin_functions.rb +32 -0
- data/lib/calco/engines/csv_engine.rb +80 -0
- data/lib/calco/engines/default_engine.rb +140 -0
- data/lib/calco/engines/office_engine.rb +263 -0
- data/lib/calco/engines/simple_calculator_engine.rb +151 -0
- data/lib/calco/math_functions.rb +9 -0
- data/lib/calco/sheet.rb +363 -0
- data/lib/calco/spreadsheet.rb +172 -0
- data/lib/calco/string_functions.rb +9 -0
- data/lib/calco/style.rb +15 -0
- data/lib/calco/time_functions.rb +12 -0
- data/lib/calco/version.rb +3 -0
- data/spec/absolute_references_spec.rb +86 -0
- data/spec/builtin_functions_spec.rb +161 -0
- data/spec/calculator_engine_spec.rb +251 -0
- data/spec/conditions_spec.rb +118 -0
- data/spec/content_change_spec.rb +190 -0
- data/spec/csv_engine_spec.rb +324 -0
- data/spec/default_engine_spec.rb +135 -0
- data/spec/definitions_spec.rb +65 -0
- data/spec/errors_spec.rb +189 -0
- data/spec/functions_spec.rb +251 -0
- data/spec/header_row_spec.rb +63 -0
- data/spec/range_spec.rb +189 -0
- data/spec/sheet_selections_spec.rb +49 -0
- data/spec/sheet_spec.rb +229 -0
- data/spec/smart_types_spec.rb +43 -0
- data/spec/spreadsheet_spec.rb +80 -0
- data/spec/styles_spec.rb +29 -0
- data/spec/variables_spec.rb +41 -0
- metadata +158 -0
data/lib/calco.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "calco/version"
|
2
|
+
|
3
|
+
require "calco/spreadsheet"
|
4
|
+
require "calco/time_functions"
|
5
|
+
require "calco/date_functions"
|
6
|
+
require "calco/math_functions"
|
7
|
+
require "calco/string_functions"
|
8
|
+
|
9
|
+
def spreadsheet engine = Calco::DefaultEngine.new, &block
|
10
|
+
|
11
|
+
document = Calco::Spreadsheet.new(engine)
|
12
|
+
|
13
|
+
document.instance_eval(&block)
|
14
|
+
|
15
|
+
document
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Fixnum
|
2
|
+
|
3
|
+
alias_method '_calco_original_+', :+
|
4
|
+
alias_method '_calco_original_-', :-
|
5
|
+
alias_method '_calco_original_*', :*
|
6
|
+
alias_method '_calco_original_/', :/
|
7
|
+
|
8
|
+
%w[+ - / *].each do |op|
|
9
|
+
|
10
|
+
define_method(op) do |arg|
|
11
|
+
|
12
|
+
if arg.respond_to?(:generate)
|
13
|
+
Calco::Operation.new(op, self, arg)
|
14
|
+
else
|
15
|
+
self.send("_calco_original_#{op}", arg)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Float
|
2
|
+
|
3
|
+
alias_method '_calco_original_+', :+
|
4
|
+
alias_method '_calco_original_-', :-
|
5
|
+
alias_method '_calco_original_*', :*
|
6
|
+
alias_method '_calco_original_/', :/
|
7
|
+
|
8
|
+
%w[+ - / *].each do |op|
|
9
|
+
|
10
|
+
define_method(op) do |arg|
|
11
|
+
|
12
|
+
if arg.respond_to?(:generate)
|
13
|
+
Calco::Operation.new(op, self, arg)
|
14
|
+
else
|
15
|
+
self.send("_calco_original_#{op}", arg)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class String
|
2
|
+
|
3
|
+
alias_method '_calco_original_+', :+
|
4
|
+
alias_method '_calco_original_*', :*
|
5
|
+
|
6
|
+
%w[+ *].each do |op|
|
7
|
+
|
8
|
+
define_method(op) do |arg|
|
9
|
+
|
10
|
+
if arg.respond_to?(:generate)
|
11
|
+
Calco::Operation.new(op, self, arg)
|
12
|
+
else
|
13
|
+
self.send("_calco_original_#{op}", arg)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require_relative 'elements/if'
|
5
|
+
require_relative 'elements/or'
|
6
|
+
require_relative 'elements/empty'
|
7
|
+
require_relative 'elements/constant'
|
8
|
+
require_relative 'elements/current'
|
9
|
+
require_relative 'elements/element'
|
10
|
+
require_relative 'elements/formula'
|
11
|
+
require_relative 'elements/variable'
|
12
|
+
require_relative 'elements/operation'
|
13
|
+
require_relative 'elements/value_extractor'
|
14
|
+
require_relative 'elements/builtin_function'
|
15
|
+
|
16
|
+
require_relative 'core_ext/float'
|
17
|
+
require_relative 'core_ext/range'
|
18
|
+
require_relative 'core_ext/fixnum'
|
19
|
+
require_relative 'core_ext/string'
|
20
|
+
|
21
|
+
module Calco
|
22
|
+
|
23
|
+
module DefinitionDSL
|
24
|
+
|
25
|
+
def set variable_assign
|
26
|
+
|
27
|
+
name = variable_assign.first[0]
|
28
|
+
value = variable_assign.first[1]
|
29
|
+
|
30
|
+
raise "Variable '#{name}' already set" if @variables.include?(name)
|
31
|
+
|
32
|
+
if value =~ /^\d{1,2}:\d{1,2}(:\d{1,2})?$/
|
33
|
+
value = Time.parse(value)
|
34
|
+
elsif value =~ /^\d{1,4}[-\/]\d{1,2}[-\/]\d{1,2}$/
|
35
|
+
value = Date.parse(value)
|
36
|
+
end
|
37
|
+
|
38
|
+
@variables[name] = Variable.new(name, value)
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def function formula_definition
|
43
|
+
|
44
|
+
name = formula_definition.first[0]
|
45
|
+
value = formula_definition.first[1]
|
46
|
+
|
47
|
+
raise "Function '#{name}' already defined" if @formulas.include?(name)
|
48
|
+
|
49
|
+
@formulas[name] = Formula.new(value)
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
module BuilderDSL
|
56
|
+
|
57
|
+
def _if condition, then_, else_
|
58
|
+
If.new(condition, then_, else_)
|
59
|
+
end
|
60
|
+
|
61
|
+
def _or condition1, condition2
|
62
|
+
Or.new(condition1, condition2)
|
63
|
+
end
|
64
|
+
|
65
|
+
def builtin_function? sym
|
66
|
+
BuiltinFunction.exists_function?(sym)
|
67
|
+
end
|
68
|
+
|
69
|
+
def builtin_function sym, args
|
70
|
+
BuiltinFunction.create_function(sym, args)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class Definitions
|
76
|
+
|
77
|
+
include BuilderDSL
|
78
|
+
include DefinitionDSL
|
79
|
+
|
80
|
+
attr_reader :variables, :formulas
|
81
|
+
|
82
|
+
def initialize spreadsheet
|
83
|
+
|
84
|
+
@formulas = {}
|
85
|
+
@variables = {}
|
86
|
+
|
87
|
+
@spreadsheet = spreadsheet
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def resolve! sym, *args
|
92
|
+
|
93
|
+
if @variables.include?(sym)
|
94
|
+
@variables[sym]
|
95
|
+
elsif @formulas.include?(sym)
|
96
|
+
@formulas[sym]
|
97
|
+
elsif builtin_function?(sym)
|
98
|
+
builtin_function(sym, args)
|
99
|
+
else
|
100
|
+
raise "Unknown function or variable '#{sym}'"
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
def formula? name
|
106
|
+
@formulas.include?(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def formula name
|
110
|
+
@formulas[name]
|
111
|
+
end
|
112
|
+
|
113
|
+
def variable? name
|
114
|
+
@variables.include?(name)
|
115
|
+
end
|
116
|
+
|
117
|
+
def variable name
|
118
|
+
@variables[name]
|
119
|
+
end
|
120
|
+
|
121
|
+
def method_missing sym, *args
|
122
|
+
resolve! sym, *args
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
|
3
|
+
module Calco
|
4
|
+
|
5
|
+
class Aggregator < Element
|
6
|
+
|
7
|
+
def initialize variable, range
|
8
|
+
@variable, @range = variable, range
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate row
|
12
|
+
@engine.range_reference(@variable, row, @range)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
|
3
|
+
module Calco
|
4
|
+
|
5
|
+
class BuiltinFunction < Element
|
6
|
+
|
7
|
+
@@builtin_functions = {}
|
8
|
+
|
9
|
+
# arity is an integer or :n for a variable number of arguments
|
10
|
+
# type is the return type and expected to be a class object or an array
|
11
|
+
# of class objects
|
12
|
+
def self.declare name, arity, type = :any
|
13
|
+
|
14
|
+
unless arity == :n or arity.is_a?(Integer)
|
15
|
+
raise ArgumentError, "Artity must be an integer or :n but was a #{arity.class}"
|
16
|
+
end
|
17
|
+
unless type.is_a?(Class) or (type.respond_to?(:all?) and type.all?{|t| t.is_a?(Class)})
|
18
|
+
raise ArgumentError, "Type should be a Class or an array of Class objects"
|
19
|
+
end
|
20
|
+
|
21
|
+
@@builtin_functions[name.to_s.downcase] = [arity, type]
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.undeclare name
|
26
|
+
@@builtin_functions.delete(name.to_s.downcase)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.exists_function? name
|
30
|
+
@@builtin_functions.include?(name.to_s.downcase)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.create_function name, args
|
34
|
+
|
35
|
+
raise NameError, "Builtin function not found '#{name}'" unless exists_function?(name)
|
36
|
+
|
37
|
+
name = name.to_s.upcase
|
38
|
+
|
39
|
+
definition = @@builtin_functions[name.to_s.downcase]
|
40
|
+
|
41
|
+
self.new(name, definition, args)
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
include Operators
|
46
|
+
|
47
|
+
def initialize name, definition, args
|
48
|
+
|
49
|
+
@arity = definition[0]
|
50
|
+
@return_type = definition[1]
|
51
|
+
|
52
|
+
unless @arity == :n
|
53
|
+
|
54
|
+
if @arity != args.length
|
55
|
+
raise ArgumentError, "Function #{name} requires #{@arity}, was #{args.length} (#{args.inspect})"
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
@name, @args = name, args
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate row
|
65
|
+
|
66
|
+
sep = ''
|
67
|
+
arguments = ''
|
68
|
+
|
69
|
+
@args.each do |arg|
|
70
|
+
|
71
|
+
arguments << sep
|
72
|
+
arguments << (arg.respond_to?(:generate) ? arg.generate(row) : @engine.value(arg))
|
73
|
+
|
74
|
+
sep = ', '
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
"#{@name}(#{arguments})"
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
|
3
|
+
module Calco
|
4
|
+
|
5
|
+
class Constant < Element
|
6
|
+
|
7
|
+
include Operators
|
8
|
+
|
9
|
+
attr_accessor :value
|
10
|
+
|
11
|
+
def initialize value
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate row
|
16
|
+
@engine.value(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.wrap value
|
20
|
+
|
21
|
+
if value.respond_to?(:generate)
|
22
|
+
value
|
23
|
+
else
|
24
|
+
Constant.new(value)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'operator'
|
2
|
+
|
3
|
+
module Calco
|
4
|
+
|
5
|
+
class Element
|
6
|
+
|
7
|
+
attr_accessor :reference_type, :column, :absolute_row
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@absolute_row = nil
|
11
|
+
@reference_type = :normal
|
12
|
+
end
|
13
|
+
|
14
|
+
def absolute_row= row_number
|
15
|
+
@absolute_row = row_number
|
16
|
+
@reference_type = :absolute
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_operand o, row
|
20
|
+
|
21
|
+
if o.respond_to?(:as_operand)
|
22
|
+
o.as_operand(row)
|
23
|
+
else
|
24
|
+
o.generate(row)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'element'
|
2
|
+
|
3
|
+
module Calco
|
4
|
+
|
5
|
+
class Formula < Element
|
6
|
+
|
7
|
+
include Operators
|
8
|
+
|
9
|
+
def initialize statement
|
10
|
+
|
11
|
+
super()
|
12
|
+
|
13
|
+
@statement = Constant.wrap(statement)
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def [] range
|
18
|
+
|
19
|
+
raise ArgumentError, "Expected Range got #{range.class}" unless range.is_a?(Range)
|
20
|
+
raise ArgumentError, "Invalid start of range (must be > 0, was #{range.first})" unless range.first > 0
|
21
|
+
|
22
|
+
Aggregator.new(self, range)
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate row
|
27
|
+
|
28
|
+
if self.reference_type == :absolute and self.absolute_row != row
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
@statement.generate(row)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def as_operand row
|
37
|
+
@engine.column_reference(self, row)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|