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/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/calco.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'calco/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "calco"
|
8
|
+
gem.version = Calco::VERSION
|
9
|
+
gem.authors = ["Jean Lazarou"]
|
10
|
+
gem.email = ["jean.lazarou@alef1.org"]
|
11
|
+
gem.description = %q{Implements a DSL used to create and define the content of spreadsheet documents}
|
12
|
+
gem.summary = %q{DSL for spreadsheet documents}
|
13
|
+
gem.homepage = "https://github.com/jeanlazarou/calco"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency('rspec')
|
21
|
+
|
22
|
+
gem.add_dependency 'rubyzip', '~> 1.1.0'
|
23
|
+
end
|
data/examples/ages.ods
ADDED
Binary file
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'calco/engines/simple_calculator_engine'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Next example uses a SimpleCaclulatorEngine that does not produce a spreadsheet
|
5
|
+
# with formulas but computes the calculations instead.
|
6
|
+
#
|
7
|
+
|
8
|
+
engine = Calco::SimpleCalculatorEngine.new
|
9
|
+
|
10
|
+
doc = spreadsheet(engine) do
|
11
|
+
|
12
|
+
definitions do
|
13
|
+
|
14
|
+
set tax_rate: 0
|
15
|
+
|
16
|
+
set basic_price: 0
|
17
|
+
set quantity: 1
|
18
|
+
|
19
|
+
function price: (basic_price * quantity) * (1 + (tax_rate / 100.0))
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
sheet do
|
24
|
+
|
25
|
+
column value_of(:tax_rate, :absolute)
|
26
|
+
|
27
|
+
column value_of(:basic_price)
|
28
|
+
column value_of(:quantity)
|
29
|
+
|
30
|
+
column skip
|
31
|
+
|
32
|
+
column :price, title: 'price'
|
33
|
+
|
34
|
+
tax_rate.absolute_row = 1
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
data = [
|
41
|
+
[12, 3],
|
42
|
+
[10.3, 2],
|
43
|
+
[34, 4]
|
44
|
+
]
|
45
|
+
|
46
|
+
doc.save($stdout) do |spreadsheet|
|
47
|
+
|
48
|
+
sheet = spreadsheet.current
|
49
|
+
|
50
|
+
sheet[:tax_rate] = 100
|
51
|
+
|
52
|
+
data.each_with_index do |item, i|
|
53
|
+
|
54
|
+
sheet[:basic_price] = item[0]
|
55
|
+
sheet[:quantity] = item[1]
|
56
|
+
|
57
|
+
sheet.write_row i + 1
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/examples/data.csv
ADDED
data/examples/example.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'calco'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Next example shows different uses with formulas (functions) and styles.
|
5
|
+
#
|
6
|
+
# It 'implements' a calculator that evaluates which of the train or the car
|
7
|
+
# is better, depending on several parameters.
|
8
|
+
#
|
9
|
+
# It prints out the definition.
|
10
|
+
# It then prints out a CSV format after changing the engine.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'calco/engines/csv_engine'
|
14
|
+
|
15
|
+
doc = spreadsheet do
|
16
|
+
|
17
|
+
definitions do
|
18
|
+
|
19
|
+
set distance: 20 # km
|
20
|
+
set fuel_price: 1.4 # €
|
21
|
+
set consumption: 6 # l/100km
|
22
|
+
set trip_duration: 50 # min
|
23
|
+
|
24
|
+
set train_price: 8 # €
|
25
|
+
set train_duration: 65 # min
|
26
|
+
|
27
|
+
function trip_consumption: consumption * (distance / 100)
|
28
|
+
|
29
|
+
function trip_price: consumption * fuel_price * (distance / 100)
|
30
|
+
function benefit: (train_price - trip_price) / train_price
|
31
|
+
|
32
|
+
function best_choice: _if(trip_duration < train_duration, 'car', 'train')
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
sheet do
|
37
|
+
|
38
|
+
column value_of(:distance), :title => 'km'
|
39
|
+
column value_of(:trip_duration), :title => 'min'
|
40
|
+
column skip
|
41
|
+
|
42
|
+
column value_of(:train_price), type: '$EUR', :title => 'train'
|
43
|
+
column value_of(:train_duration), :title => 'min'
|
44
|
+
column skip
|
45
|
+
|
46
|
+
column :trip_consumption, :title => 'L'
|
47
|
+
column :trip_price, style: _if(current > train_price, 'Red', 'default'), type: '$EUR', :title => 'price'
|
48
|
+
column :benefit, :type => '%', :title => 'benefit'
|
49
|
+
column skip
|
50
|
+
|
51
|
+
column :best_choice, :title => 'choice'
|
52
|
+
column skip
|
53
|
+
|
54
|
+
column value_of(:fuel_price), type: '$EUR', :title => 'fuel price (1L)'
|
55
|
+
column value_of(:consumption), :title => 'L/100km'
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "Layout definition:"
|
62
|
+
|
63
|
+
doc.current[:distance] = 40
|
64
|
+
cells = doc.row(3)
|
65
|
+
|
66
|
+
cells.each_index do |i|
|
67
|
+
|
68
|
+
print " #{Calco::COLUMNS[i]}: "
|
69
|
+
|
70
|
+
cell = cells[i]
|
71
|
+
|
72
|
+
puts cell
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "\nCSV output:"
|
77
|
+
|
78
|
+
data = [
|
79
|
+
{:distance => 40, :trip_duration => 30, :train_price => 10, :train_duration => 30, :fuel_price => 1.8, :consumption => 7},
|
80
|
+
{:distance => 25, :trip_duration => 30, :train_price => 19, :train_duration => 70, :fuel_price => 1.3, :consumption => 5.4},
|
81
|
+
]
|
82
|
+
|
83
|
+
doc.engine = Calco::CSVEngine.new
|
84
|
+
|
85
|
+
doc.save($stdout) do |spreadsheet|
|
86
|
+
|
87
|
+
sheet = doc.current
|
88
|
+
|
89
|
+
sheet.write_row 0
|
90
|
+
|
91
|
+
data.each_with_index do |entry, i|
|
92
|
+
|
93
|
+
sheet.write_row i + 1, entry
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
Binary file
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Next example shows the use of the office engine, no headers and empty row.
|
6
|
+
#
|
7
|
+
# It saves an office file in the temporary directory named "res.ods" and uses
|
8
|
+
# a template file named "multiplication_tables.ods"
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'calco/engines/office_engine'
|
12
|
+
|
13
|
+
output_file = File.join(Dir.tmpdir, "res.ods")
|
14
|
+
|
15
|
+
def relative_path file
|
16
|
+
File.join(File.dirname(__FILE__), file)
|
17
|
+
end
|
18
|
+
|
19
|
+
engine = Calco::OfficeEngine.new(relative_path('multiplication_tables.ods'), false)
|
20
|
+
|
21
|
+
doc = spreadsheet(engine) do
|
22
|
+
|
23
|
+
definitions do
|
24
|
+
|
25
|
+
set multiplicand: 7
|
26
|
+
set multiplier: 0
|
27
|
+
|
28
|
+
function result: multiplicand * multiplier
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
sheet('x') do
|
33
|
+
|
34
|
+
column value_of(:multiplier)
|
35
|
+
column 'x'
|
36
|
+
column value_of(:multiplicand)
|
37
|
+
column '='
|
38
|
+
|
39
|
+
column :result
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
count = 0
|
46
|
+
|
47
|
+
doc.save(output_file) do |spreadsheet|
|
48
|
+
|
49
|
+
sheet = doc.current
|
50
|
+
|
51
|
+
7.step(10) do |multiplicand|
|
52
|
+
|
53
|
+
sheet[:multiplicand] = multiplicand
|
54
|
+
|
55
|
+
1.step(10) do |multiplier|
|
56
|
+
|
57
|
+
sheet[:multiplier] = multiplier
|
58
|
+
|
59
|
+
sheet.write_row count
|
60
|
+
|
61
|
+
count += 1
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
count += 1
|
66
|
+
|
67
|
+
sheet.empty_row
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "Wrote #{output_file} (#{count} rows)"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
require 'calco'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Next example shows how to extend the DSL with new built-in functions.
|
7
|
+
# It outputs as simple text the resulting spreadsheet (two rows: the headers
|
8
|
+
# and one row of data)
|
9
|
+
#
|
10
|
+
|
11
|
+
# Next functions does not exist in a spreadsheet software.
|
12
|
+
# we only show how to declare functions that are missing...
|
13
|
+
Calco::BuiltinFunction.declare :now, 0, Integer
|
14
|
+
Calco::BuiltinFunction.declare :age, 1, Integer
|
15
|
+
|
16
|
+
doc = spreadsheet do
|
17
|
+
|
18
|
+
definitions do
|
19
|
+
|
20
|
+
set some_date: Date.today
|
21
|
+
|
22
|
+
function now: now
|
23
|
+
function age: age(some_date)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
sheet do
|
28
|
+
|
29
|
+
column :now, :title => "Today"
|
30
|
+
|
31
|
+
column value_of(:some_date)
|
32
|
+
column :age, :title => "Age"
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
hash_printer = proc {|k, v| puts "#{k}: #{v}"}
|
39
|
+
|
40
|
+
doc.row_to_hash(0).each &hash_printer
|
41
|
+
|
42
|
+
doc.current[:some_date] = Date.new(1934, 10, 3)
|
43
|
+
puts
|
44
|
+
doc.row_to_hash(1).each &hash_printer
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
require 'calco'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Next example uses date functions and outputs to the console the result as
|
7
|
+
# simple text.
|
8
|
+
#
|
9
|
+
|
10
|
+
doc = spreadsheet do
|
11
|
+
|
12
|
+
definitions do
|
13
|
+
|
14
|
+
set some_date: Date.today
|
15
|
+
|
16
|
+
function some_year: year(some_date)
|
17
|
+
function age: year(today) - year(some_date)
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
sheet do
|
22
|
+
|
23
|
+
column value_of(:some_date)
|
24
|
+
|
25
|
+
column :some_year
|
26
|
+
column :age
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
doc.save($stdout) do
|
33
|
+
|
34
|
+
sheet = doc.current
|
35
|
+
|
36
|
+
sheet[:some_date] = Date.new(1934, 10, 3)
|
37
|
+
sheet.write_row 3
|
38
|
+
|
39
|
+
sheet[:some_date] = Date.new(2004, 6, 19)
|
40
|
+
sheet.write_row 5
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
require 'calco/engines/csv_engine'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Next example presents uses the CSV engine that outputs formulas and save time
|
7
|
+
# values as formulas. LibreOffice opens CSV files containing formulas and is
|
8
|
+
# able to apply them.
|
9
|
+
#
|
10
|
+
# The example shows how to define an aggregation function (sum) using a Range
|
11
|
+
# object marked with the as_grouping method (extension added by Calco to the
|
12
|
+
# Ruby Range class).
|
13
|
+
#
|
14
|
+
# The example uses the Sheet#replace_and_clear method that changes the
|
15
|
+
# definition set for some column.
|
16
|
+
#
|
17
|
+
# The spreadsheet is a kind ot timesheet.
|
18
|
+
#
|
19
|
+
|
20
|
+
doc = spreadsheet(Calco::CSVEngine.new) do
|
21
|
+
|
22
|
+
definitions do
|
23
|
+
|
24
|
+
set start_time: ''
|
25
|
+
set end_time: ''
|
26
|
+
|
27
|
+
function duration: end_time - start_time
|
28
|
+
|
29
|
+
function total: sum(duration[(1..-1).as_grouping])
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
sheet do
|
34
|
+
|
35
|
+
column value_of(:start_time), :title => "Start"
|
36
|
+
column value_of(:end_time), :title => "End"
|
37
|
+
|
38
|
+
column :duration, :title => "Duration", :id => :duration
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
data = [
|
45
|
+
{:start_time => Time.parse("12:10"), :end_time => Time.parse("15:30")},
|
46
|
+
{:start_time => Time.parse("11:00"), :end_time => Time.parse("16:30")},
|
47
|
+
{:start_time => Time.parse("10:01"), :end_time => Time.parse("12:05")},
|
48
|
+
]
|
49
|
+
|
50
|
+
doc.save($stdout) do |spreadsheet|
|
51
|
+
|
52
|
+
sheet = doc.current
|
53
|
+
|
54
|
+
sheet.write_row 0
|
55
|
+
|
56
|
+
data.each_with_index do |entry, i|
|
57
|
+
|
58
|
+
sheet.write_row i + 1, entry
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
sheet.empty_row
|
63
|
+
|
64
|
+
sheet.replace_and_clear :duration => :total
|
65
|
+
|
66
|
+
sheet.write_row data.length + 1
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'date'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Next example uses the use of the office engine and reads data from a CSV file.
|
7
|
+
#
|
8
|
+
# It saves an office file in the temporary directory named "res.ods" and uses
|
9
|
+
# a template file named "ages.ods"
|
10
|
+
#
|
11
|
+
# It shows the definition of a cell with a conditional style. Depending on the
|
12
|
+
# current value of the cell the style name "adult" or "default" is applied.
|
13
|
+
# The "adult" style should be defined in the template file.
|
14
|
+
#
|
15
|
+
|
16
|
+
require 'calco/engines/office_engine'
|
17
|
+
|
18
|
+
output_file = File.join(Dir.tmpdir, "res.ods")
|
19
|
+
|
20
|
+
def relative_path file
|
21
|
+
File.join(File.dirname(__FILE__), file)
|
22
|
+
end
|
23
|
+
|
24
|
+
engine = Calco::OfficeEngine.new(relative_path('ages.ods'))
|
25
|
+
|
26
|
+
doc = spreadsheet(engine) do
|
27
|
+
|
28
|
+
definitions do
|
29
|
+
|
30
|
+
set name: ''
|
31
|
+
set birth_date: ''
|
32
|
+
|
33
|
+
function age: year(today) - year(birth_date)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
sheet('Main') do
|
38
|
+
|
39
|
+
has_titles true
|
40
|
+
|
41
|
+
column value_of(:name)
|
42
|
+
column value_of(:birth_date)
|
43
|
+
|
44
|
+
column :age, style: _if(current > 19, '"adult"', '"default"')
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
count = 0
|
51
|
+
|
52
|
+
doc.save(output_file) do |spreadsheet|
|
53
|
+
|
54
|
+
sheet = doc.current
|
55
|
+
|
56
|
+
CSV.foreach(relative_path('data.csv'), :headers => true) do |row|
|
57
|
+
|
58
|
+
count += 1
|
59
|
+
|
60
|
+
sheet[:name] = row[0]
|
61
|
+
sheet[:birth_date] = Date.parse(row[1])
|
62
|
+
|
63
|
+
sheet.write_row count
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "Wrote #{output_file} (#{count} rows)"
|