calco 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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)"
|