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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +26 -0
  5. data/LICENSE +21 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +360 -0
  8. data/Rakefile +1 -0
  9. data/calco.gemspec +23 -0
  10. data/examples/ages.ods +0 -0
  11. data/examples/compute_cells.rb +61 -0
  12. data/examples/data.csv +8 -0
  13. data/examples/example.rb +97 -0
  14. data/examples/multiplication_tables.ods +0 -0
  15. data/examples/multiplication_tables.rb +73 -0
  16. data/examples/register_function.rb +44 -0
  17. data/examples/using_date_functions.rb +42 -0
  18. data/examples/write_csv.rb +68 -0
  19. data/examples/write_ods.rb +69 -0
  20. data/lib/calco.rb +17 -0
  21. data/lib/calco/core_ext/fixnum.rb +22 -0
  22. data/lib/calco/core_ext/float.rb +22 -0
  23. data/lib/calco/core_ext/range.rb +15 -0
  24. data/lib/calco/core_ext/string.rb +20 -0
  25. data/lib/calco/date_functions.rb +13 -0
  26. data/lib/calco/definition_dsl.rb +127 -0
  27. data/lib/calco/elements/aggregator.rb +17 -0
  28. data/lib/calco/elements/builtin_function.rb +84 -0
  29. data/lib/calco/elements/constant.rb +31 -0
  30. data/lib/calco/elements/current.rb +19 -0
  31. data/lib/calco/elements/element.rb +31 -0
  32. data/lib/calco/elements/empty.rb +9 -0
  33. data/lib/calco/elements/formula.rb +42 -0
  34. data/lib/calco/elements/if.rb +26 -0
  35. data/lib/calco/elements/operation.rb +34 -0
  36. data/lib/calco/elements/operator.rb +17 -0
  37. data/lib/calco/elements/or.rb +26 -0
  38. data/lib/calco/elements/value_extractor.rb +42 -0
  39. data/lib/calco/elements/variable.rb +35 -0
  40. data/lib/calco/engines/calculator_builtin_functions.rb +32 -0
  41. data/lib/calco/engines/csv_engine.rb +80 -0
  42. data/lib/calco/engines/default_engine.rb +140 -0
  43. data/lib/calco/engines/office_engine.rb +263 -0
  44. data/lib/calco/engines/simple_calculator_engine.rb +151 -0
  45. data/lib/calco/math_functions.rb +9 -0
  46. data/lib/calco/sheet.rb +363 -0
  47. data/lib/calco/spreadsheet.rb +172 -0
  48. data/lib/calco/string_functions.rb +9 -0
  49. data/lib/calco/style.rb +15 -0
  50. data/lib/calco/time_functions.rb +12 -0
  51. data/lib/calco/version.rb +3 -0
  52. data/spec/absolute_references_spec.rb +86 -0
  53. data/spec/builtin_functions_spec.rb +161 -0
  54. data/spec/calculator_engine_spec.rb +251 -0
  55. data/spec/conditions_spec.rb +118 -0
  56. data/spec/content_change_spec.rb +190 -0
  57. data/spec/csv_engine_spec.rb +324 -0
  58. data/spec/default_engine_spec.rb +135 -0
  59. data/spec/definitions_spec.rb +65 -0
  60. data/spec/errors_spec.rb +189 -0
  61. data/spec/functions_spec.rb +251 -0
  62. data/spec/header_row_spec.rb +63 -0
  63. data/spec/range_spec.rb +189 -0
  64. data/spec/sheet_selections_spec.rb +49 -0
  65. data/spec/sheet_spec.rb +229 -0
  66. data/spec/smart_types_spec.rb +43 -0
  67. data/spec/spreadsheet_spec.rb +80 -0
  68. data/spec/styles_spec.rb +29 -0
  69. data/spec/variables_spec.rb +41 -0
  70. metadata +158 -0
@@ -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,15 @@
1
+ class Range
2
+
3
+ def as_grouping
4
+
5
+ @grouping_range = true
6
+
7
+ self
8
+
9
+ end
10
+
11
+ def grouping_range?
12
+ ! @grouping_range.nil?
13
+ end
14
+
15
+ 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,13 @@
1
+ require 'date'
2
+
3
+ module Calco
4
+
5
+ class BuiltinFunction
6
+
7
+ declare :today, 0, Date
8
+ declare :year, 1, Integer
9
+ declare :datevalue, 1, Date
10
+
11
+ end
12
+
13
+ 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,19 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class Current < Element
6
+
7
+ include Operators
8
+
9
+ def initialize
10
+ @reference_type = :current
11
+ end
12
+
13
+ def generate row
14
+ @engine.current
15
+ end
16
+
17
+ end
18
+
19
+ 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,9 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class Empty < Element
6
+ include Singleton
7
+ end
8
+
9
+ 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