spreadshit 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 +1 -0
- data/Gemfile +5 -0
- data/README.md +25 -0
- data/Rakefile +1 -0
- data/bin/console +18 -0
- data/bin/demo +34 -0
- data/bin/setup +6 -0
- data/lib/spreadshit.rb +112 -0
- data/lib/spreadshit/formula.rb +113 -0
- data/lib/spreadshit/formula.treetop +61 -0
- data/lib/spreadshit/functions.rb +68 -0
- data/lib/spreadshit/version.rb +3 -0
- data/lib/spreadshit/window.rb +263 -0
- data/spreadshit.gemspec +26 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8458cafa69246fc6038f2ac8a6823569e39de820
|
4
|
+
data.tar.gz: 2982c754d56740b6234255cf2c90c50b141f78cd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0cc85771b717af20087bc7ee1a64da872a26143f6e40d2682acf8afe093eb850193e2737ab2676f5ff1a657b9c3f2523592028583d0516a5c30c2497aab575a7
|
7
|
+
data.tar.gz: 497cb849983596238a0a49b742517c126ddc4473abe2a730ba4540a03a1faee053a0eb74be2fedb0cdc05a538d875424e32fb84c7e9f8db58fac5919c2ab2a0b
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Spreadsheet
|
2
|
+
|
3
|
+
Esse projeto é um exercício sugerido pelo [Casssiano](https://github.com/cassiano), que consiste na implementação em uma planilha de cálculos.
|
4
|
+
|
5
|
+
Por ora, o projeto está em fase alpha, e não trata quase nenhum caso de excessão.
|
6
|
+
|
7
|
+
[](https://asciinema.org/a/4677dtu9ippkmzgttr85vnj22)
|
8
|
+
|
9
|
+
## Implementação
|
10
|
+
|
11
|
+
Foram utilizados vários conceitos descritos no paper [Deprecating the Observer Pattern](http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf) para implementação, já que cada célula representa um `Signal` descrito no paper.
|
12
|
+
|
13
|
+
Para o parsing das fórmulas da planilha, foi desenvolvido um simples parser com o [Treetop](http://treetop.rubyforge.org/).
|
14
|
+
|
15
|
+
A UI (ainda bastante simplista) foi produzida com a ajuda da biblioteca [Curses](https://en.wikipedia.org/wiki/Curses_(programming_library)).
|
16
|
+
|
17
|
+
## Demo
|
18
|
+
|
19
|
+
Para rodar o demo, primeiro instale as dependências com:
|
20
|
+
|
21
|
+
$ bundle install
|
22
|
+
|
23
|
+
E depois execute
|
24
|
+
|
25
|
+
$ ./demo.rb
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "spreadshit"
|
5
|
+
require "pry"
|
6
|
+
|
7
|
+
@sheet = Spreadshit.new
|
8
|
+
@sheet[:A1] = 10
|
9
|
+
@sheet[:A2] = "0"
|
10
|
+
@sheet[:A3] = "=A1 + A2"
|
11
|
+
|
12
|
+
@sheet[:B1] = 0
|
13
|
+
@sheet[:B2] = 1
|
14
|
+
3.upto(300).each do |n|
|
15
|
+
@sheet["B#{n}"] = "=B#{n - 1} + B#{n - 2}"
|
16
|
+
end
|
17
|
+
|
18
|
+
Pry.start
|
data/bin/demo
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "spreadshit"
|
5
|
+
require "spreadshit/window"
|
6
|
+
|
7
|
+
sheet = Spreadshit.new
|
8
|
+
[
|
9
|
+
"Sum",
|
10
|
+
10,
|
11
|
+
20,
|
12
|
+
30,
|
13
|
+
"",
|
14
|
+
"=A2 + A3 + A4",
|
15
|
+
"=SUM(A2; A3; A4)",
|
16
|
+
"=SUM(A2:A4)"
|
17
|
+
].each_with_index do |cel, index|
|
18
|
+
sheet["A#{index + 1}"] = cel
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fibonacci
|
22
|
+
sheet[:B1] = "Fibonacci"
|
23
|
+
sheet[:B2] = 0
|
24
|
+
sheet[:B3] = 1
|
25
|
+
4.upto(30).each do |n|
|
26
|
+
sheet["B#{n}"] = "=B#{n - 1} + B#{n - 2}"
|
27
|
+
end
|
28
|
+
|
29
|
+
window = Spreadshit::Window.new do |delegate|
|
30
|
+
delegate.cell_updated { |address, value| sheet[address] = value }
|
31
|
+
delegate.cell_value { |address| sheet[address] }
|
32
|
+
delegate.cell_content { |address| sheet.cell_at(address).raw }
|
33
|
+
end
|
34
|
+
window.start
|
data/bin/setup
ADDED
data/lib/spreadshit.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
class Spreadshit
|
2
|
+
require "spreadshit/formula"
|
3
|
+
require "spreadshit/functions"
|
4
|
+
|
5
|
+
def initialize(parser = Formula.new, functions = Functions.new)
|
6
|
+
@cells = Hash.new { |hash, key| hash[key] = Cell.new }
|
7
|
+
@parser = parser
|
8
|
+
@functions = functions
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](address)
|
12
|
+
@cells[address.to_sym].value
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(address, value)
|
16
|
+
@cells[address.to_sym].update(value) { parse(value) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def cell_at(address)
|
20
|
+
@cells[address.to_sym]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse(value)
|
26
|
+
if value.to_s.start_with? "="
|
27
|
+
eval @parser.parse(value[1..-1])
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def eval(expression, refs = Set.new)
|
34
|
+
case expression
|
35
|
+
when Formula::Literal
|
36
|
+
expression.content
|
37
|
+
when Formula::Addition
|
38
|
+
@functions.add(eval(expression.left, refs), eval(expression.right, refs))
|
39
|
+
when Formula::Subtraction
|
40
|
+
@functions.minus(eval(expression.left, refs), eval(expression.right, refs))
|
41
|
+
when Formula::Multiplication
|
42
|
+
@functions.multiply(eval(expression.left, refs), eval(expression.right, refs))
|
43
|
+
when Formula::Division
|
44
|
+
@functions.divide(eval(expression.left, refs), eval(expression.right, refs))
|
45
|
+
when Formula::Function
|
46
|
+
@functions.send(expression.name.downcase, *expression.arguments.map { |arg| eval arg, refs })
|
47
|
+
when Formula::Reference
|
48
|
+
if refs.include? expression.address
|
49
|
+
"CYCLIC"
|
50
|
+
else
|
51
|
+
refs << expression.address
|
52
|
+
self[expression.address]
|
53
|
+
end
|
54
|
+
when Formula::Range
|
55
|
+
expand_range(expression.top, expression.bottom).map { |ref| eval ref, refs }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def expand_range(top, bottom)
|
60
|
+
cols = top.col..bottom.col
|
61
|
+
rows = top.row..bottom.row
|
62
|
+
cols.flat_map do |col|
|
63
|
+
rows.map { |row| Formula::Reference.new(col, row) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Cell
|
69
|
+
attr_reader :raw
|
70
|
+
|
71
|
+
def initialize(raw = "", &expression)
|
72
|
+
@raw = raw
|
73
|
+
@observers = Set.new
|
74
|
+
@observed = []
|
75
|
+
update(&expression) if block_given?
|
76
|
+
end
|
77
|
+
|
78
|
+
def value
|
79
|
+
if @@caller
|
80
|
+
@observers << @@caller
|
81
|
+
@@caller.observed << self
|
82
|
+
end
|
83
|
+
@value
|
84
|
+
end
|
85
|
+
|
86
|
+
def update(value = raw || "", &expression)
|
87
|
+
@raw = value
|
88
|
+
@expression = expression
|
89
|
+
compute
|
90
|
+
@value
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
attr_reader :observers, :observed
|
96
|
+
|
97
|
+
def compute
|
98
|
+
@observed.each { |observed| observed.observers.delete(self) }
|
99
|
+
@observed = []
|
100
|
+
|
101
|
+
@@caller = self
|
102
|
+
new_value = @expression.call
|
103
|
+
@@caller = nil
|
104
|
+
|
105
|
+
if new_value != @value
|
106
|
+
@value = new_value
|
107
|
+
observers = @observers
|
108
|
+
@observers = Set.new
|
109
|
+
observers.each { |observer| observer.compute }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "treetop"
|
2
|
+
|
3
|
+
class Spreadshit::Formula
|
4
|
+
module Nodes
|
5
|
+
class Node < Treetop::Runtime::SyntaxNode; end
|
6
|
+
class NumberNode < Node; end
|
7
|
+
class StringNode < Node; end
|
8
|
+
class GroupNode < Node; end
|
9
|
+
class FunctionNode < Node; end
|
10
|
+
class ArgumentListNode < Node; end
|
11
|
+
class AdditiveNode < Node; end
|
12
|
+
class MultiplicativeNode < Node; end
|
13
|
+
class RangeNode < Node; end
|
14
|
+
class ReferenceNode < Node; end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Literal < Struct.new(:content); end
|
18
|
+
class String < Literal; end
|
19
|
+
class Number < Literal; end
|
20
|
+
class Integer < Number; end
|
21
|
+
class Decimal < Number; end
|
22
|
+
|
23
|
+
class BinaryOperation < Struct.new(:left, :right); end
|
24
|
+
class Addition < BinaryOperation; end
|
25
|
+
class Subtraction < BinaryOperation; end
|
26
|
+
class Multiplication < BinaryOperation; end
|
27
|
+
class Division < BinaryOperation; end
|
28
|
+
class Function < Struct.new(:name, :arguments); end
|
29
|
+
|
30
|
+
class Reference < Struct.new(:col, :row)
|
31
|
+
def address
|
32
|
+
[col, row].join
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
address
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Range < Struct.new(:top, :bottom)
|
41
|
+
def to_s
|
42
|
+
[top.address, bottom.address].join(":")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@@parser = Treetop.load(File.join(File.dirname(__FILE__), "formula.treetop")).new
|
47
|
+
|
48
|
+
def parse(formula)
|
49
|
+
process @@parser.parse(formula)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def process(node)
|
55
|
+
case node
|
56
|
+
when Nodes::NumberNode
|
57
|
+
node.text_value.include?(".") ?
|
58
|
+
Decimal.new(node.text_value.to_f) :
|
59
|
+
Integer.new(node.text_value.to_i)
|
60
|
+
|
61
|
+
when Nodes::StringNode
|
62
|
+
String.new(node.chars.text_value)
|
63
|
+
|
64
|
+
when Nodes::GroupNode
|
65
|
+
process node.content
|
66
|
+
|
67
|
+
when Nodes::FunctionNode
|
68
|
+
Function.new(
|
69
|
+
node.name.text_value.upcase,
|
70
|
+
process(node.arguments) || []
|
71
|
+
)
|
72
|
+
|
73
|
+
when Nodes::ArgumentListNode
|
74
|
+
if node.tail.elements.empty?
|
75
|
+
[process(node.head)]
|
76
|
+
else
|
77
|
+
[process(node.head)] + process(node.tail.elements[0].arguments)
|
78
|
+
end
|
79
|
+
|
80
|
+
when Nodes::RangeNode
|
81
|
+
Range.new(process(node.top), process(node.bottom))
|
82
|
+
|
83
|
+
when Nodes::ReferenceNode
|
84
|
+
Reference.new(node.col.text_value.upcase, node.row.text_value.to_i)
|
85
|
+
|
86
|
+
when Nodes::AdditiveNode
|
87
|
+
if node.tail.elements.empty?
|
88
|
+
process node.head
|
89
|
+
else
|
90
|
+
node.tail.elements.reduce(process(node.head)) do |left, node|
|
91
|
+
right = process(node.operand)
|
92
|
+
case node.operator.text_value
|
93
|
+
when "+" then Addition.new(left, right)
|
94
|
+
when "-" then Subtraction.new(left, right)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
when Nodes::MultiplicativeNode
|
100
|
+
if node.tail.elements.empty?
|
101
|
+
process node.head
|
102
|
+
else
|
103
|
+
node.tail.elements.reduce(process(node.head)) do |left, node|
|
104
|
+
right = process(node.operand)
|
105
|
+
case node.operator.text_value
|
106
|
+
when "*" then Multiplication.new(left, right)
|
107
|
+
when "/" then Division.new(left, right)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
grammar FormulaGrammar
|
2
|
+
rule additive
|
3
|
+
head:multiplicative tail:(space* operator:[+-] space* operand:multiplicative)* <Spreadshit::Formula::Nodes::AdditiveNode>
|
4
|
+
end
|
5
|
+
|
6
|
+
rule multiplicative
|
7
|
+
head:operand tail:(space* operator:[*/] space* operand:operand)* <Spreadshit::Formula::Nodes::MultiplicativeNode>
|
8
|
+
end
|
9
|
+
|
10
|
+
rule operand
|
11
|
+
reference / signed_number / string / group / function
|
12
|
+
end
|
13
|
+
|
14
|
+
rule group
|
15
|
+
'(' space* content:additive space* ')' <Spreadshit::Formula::Nodes::GroupNode>
|
16
|
+
end
|
17
|
+
|
18
|
+
rule function
|
19
|
+
name:[a-zA-Z]+ '(' space* arguments:arguments_list? space* ')' <Spreadshit::Formula::Nodes::FunctionNode>
|
20
|
+
end
|
21
|
+
|
22
|
+
rule arguments_list
|
23
|
+
head:argument tail:(space* ';' space* arguments:arguments_list)* <Spreadshit::Formula::Nodes::ArgumentListNode>
|
24
|
+
end
|
25
|
+
|
26
|
+
rule argument
|
27
|
+
range / additive / operand
|
28
|
+
end
|
29
|
+
|
30
|
+
rule range
|
31
|
+
top:reference ':' bottom:reference <Spreadshit::Formula::Nodes::RangeNode>
|
32
|
+
end
|
33
|
+
|
34
|
+
rule reference
|
35
|
+
col:[a-zA-Z]+ row:([1-9] [0-9]+ / [1-9]) <Spreadshit::Formula::Nodes::ReferenceNode>
|
36
|
+
end
|
37
|
+
|
38
|
+
rule signed_number
|
39
|
+
sign? number <Spreadshit::Formula::Nodes::NumberNode>
|
40
|
+
end
|
41
|
+
|
42
|
+
rule number
|
43
|
+
([0-9]+ '.')? [0-9]+
|
44
|
+
end
|
45
|
+
|
46
|
+
rule sign
|
47
|
+
[+-]
|
48
|
+
end
|
49
|
+
|
50
|
+
rule string
|
51
|
+
quote chars:(!quote .)* quote <Spreadshit::Formula::Nodes::StringNode>
|
52
|
+
end
|
53
|
+
|
54
|
+
rule quote
|
55
|
+
'"'
|
56
|
+
end
|
57
|
+
|
58
|
+
rule space
|
59
|
+
' '
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Spreadshit::Functions
|
2
|
+
[[:+, :add], [:-, :minus], [:*, :multiply]].each do |operator, name|
|
3
|
+
define_method(name) { |left, right| to_number(left).send(operator, to_number(right)) }
|
4
|
+
end
|
5
|
+
|
6
|
+
def divide(left, right)
|
7
|
+
right = to_number(right)
|
8
|
+
return Float::NAN if right.zero?
|
9
|
+
to_number(to_number(left) / right.to_f)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sum(*args)
|
13
|
+
to_number args.flatten.map { |arg| to_number(arg) }.reduce(:+)
|
14
|
+
end
|
15
|
+
|
16
|
+
def average(*args)
|
17
|
+
to_number(sum(*args) / args.size.to_f)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sqrt(number)
|
21
|
+
Math.sqrt to_number(number)
|
22
|
+
end
|
23
|
+
|
24
|
+
def ln(number)
|
25
|
+
Math.log to_number(number)
|
26
|
+
end
|
27
|
+
|
28
|
+
def var(*args)
|
29
|
+
average = average(*args)
|
30
|
+
|
31
|
+
args.flatten.map { |arg| to_number(arg) }.reduce(0) do |variance, value|
|
32
|
+
variance + (value - average) ** 2
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def stdev(*args)
|
37
|
+
sqrt(var(*args))
|
38
|
+
end
|
39
|
+
|
40
|
+
def date(year, month, date)
|
41
|
+
Date.new(year, month, date)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def to_number(value)
|
47
|
+
case value
|
48
|
+
when Float::INFINITY
|
49
|
+
value
|
50
|
+
when nil
|
51
|
+
0
|
52
|
+
when Numeric
|
53
|
+
if !value.to_f.nan? && value.to_f - value.to_i == 0
|
54
|
+
value.to_i
|
55
|
+
else
|
56
|
+
value.to_f
|
57
|
+
end
|
58
|
+
when Date
|
59
|
+
value
|
60
|
+
when -> string { string.to_s =~ /\A[-+]?([0-9]+\.)?[0-9]+\z/ }
|
61
|
+
to_number(value.to_f)
|
62
|
+
when String && -> string { string.strip.empty? }
|
63
|
+
0
|
64
|
+
else
|
65
|
+
Float::NAN
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require "curses"
|
2
|
+
|
3
|
+
class Spreadshit::Window
|
4
|
+
class Address < Struct.new(:col, :row)
|
5
|
+
def to_s
|
6
|
+
[col, row].join
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_sym
|
10
|
+
to_s.to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class SpreadsheetDelegate
|
15
|
+
def initialize
|
16
|
+
@cell_updated, @cell_value, @cell_content = Proc.new {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def cell_updated(&block)
|
20
|
+
if block_given?
|
21
|
+
@cell_updated = block
|
22
|
+
else
|
23
|
+
@cell_updated
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cell_value(&block)
|
28
|
+
if block_given?
|
29
|
+
@cell_value = block
|
30
|
+
else
|
31
|
+
@cell_value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cell_content(&block)
|
36
|
+
if block_given?
|
37
|
+
@cell_content = block
|
38
|
+
else
|
39
|
+
@cell_content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
include Curses
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@mode = :navigation
|
48
|
+
@x, @y = 0, 0
|
49
|
+
@sx, @sy = 0, 0
|
50
|
+
@col_width = 13
|
51
|
+
@letters = ("A".."ZZZ").to_a
|
52
|
+
@spreadsheet_delegate = SpreadsheetDelegate.new
|
53
|
+
yield @spreadsheet_delegate
|
54
|
+
end
|
55
|
+
|
56
|
+
def start
|
57
|
+
init_screen
|
58
|
+
start_color
|
59
|
+
init_pair(COLOR_WHITE, COLOR_BLACK, COLOR_WHITE)
|
60
|
+
init_pair(COLOR_BLUE, COLOR_BLACK, COLOR_BLUE)
|
61
|
+
init_pair(COLOR_GREEN, COLOR_BLACK, COLOR_GREEN)
|
62
|
+
init_pair(COLOR_RED, COLOR_BLACK, COLOR_MAGENTA)
|
63
|
+
use_default_colors
|
64
|
+
redraw
|
65
|
+
|
66
|
+
loop { capture_input }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def max_cols
|
72
|
+
cols / @col_width
|
73
|
+
end
|
74
|
+
|
75
|
+
def max_rows
|
76
|
+
lines - 3
|
77
|
+
end
|
78
|
+
|
79
|
+
def address
|
80
|
+
Address.new(@letters[@x], @y + 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
def current_cell_value
|
84
|
+
cell_value_at(address)
|
85
|
+
end
|
86
|
+
|
87
|
+
def cell_value_at(address)
|
88
|
+
@spreadsheet_delegate.cell_value.call(address)
|
89
|
+
end
|
90
|
+
|
91
|
+
def current_cell_content
|
92
|
+
@spreadsheet_delegate.cell_content.call(address)
|
93
|
+
end
|
94
|
+
|
95
|
+
def current_cell_content=(value)
|
96
|
+
@spreadsheet_delegate.cell_updated.call(address, value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def capture_input
|
100
|
+
case @mode
|
101
|
+
when :navigation
|
102
|
+
navigate
|
103
|
+
when :edit
|
104
|
+
read_cell_definition
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def navigate
|
109
|
+
cbreak
|
110
|
+
noecho
|
111
|
+
stdscr.keypad = true
|
112
|
+
case getch
|
113
|
+
when KEY_UP
|
114
|
+
@y -= 1
|
115
|
+
@y = 0 if @y < 0
|
116
|
+
@sy -= 1 if @y < @sy
|
117
|
+
@sy = 0 if @sy < 0
|
118
|
+
when KEY_DOWN
|
119
|
+
@y += 1
|
120
|
+
@sy += 1 if @y >= max_rows
|
121
|
+
when KEY_LEFT
|
122
|
+
@x -= 1
|
123
|
+
@x = 0 if @x < 0
|
124
|
+
@sx -= 1 if @x < @sx
|
125
|
+
@sx = 0 if @sx < 0
|
126
|
+
when KEY_RIGHT
|
127
|
+
@x += 1
|
128
|
+
@sx += 1 if @x >= max_cols
|
129
|
+
when 10
|
130
|
+
@mode = :edit
|
131
|
+
when 27
|
132
|
+
exit 0
|
133
|
+
else
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
redraw
|
138
|
+
end
|
139
|
+
|
140
|
+
def read_cell_definition
|
141
|
+
echo
|
142
|
+
self.current_cell_content = getstr
|
143
|
+
@mode = :navigation
|
144
|
+
redraw
|
145
|
+
end
|
146
|
+
|
147
|
+
def redraw
|
148
|
+
draw_cells
|
149
|
+
draw_letters_header
|
150
|
+
draw_numbers_header
|
151
|
+
draw_text_field
|
152
|
+
cursor_to_input_line
|
153
|
+
refresh
|
154
|
+
end
|
155
|
+
|
156
|
+
def draw_text_field
|
157
|
+
setpos(divider_line, 0)
|
158
|
+
|
159
|
+
case @mode
|
160
|
+
when :navigation
|
161
|
+
draw_divider(
|
162
|
+
color: color_pair(COLOR_RED) | A_NORMAL,
|
163
|
+
left_text: current_cell_value.to_s,
|
164
|
+
center_text: "Press ENTER to edit #{address}",
|
165
|
+
)
|
166
|
+
cursor_to_input_line
|
167
|
+
addstr(current_cell_content.to_s)
|
168
|
+
clrtoeol
|
169
|
+
when :edit
|
170
|
+
draw_divider(
|
171
|
+
color: color_pair(COLOR_GREEN) | A_NORMAL,
|
172
|
+
left_text: current_cell_content.to_s,
|
173
|
+
center_text: "Editing #{address}"
|
174
|
+
)
|
175
|
+
cursor_to_input_line
|
176
|
+
clrtoeol
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def draw_divider(color: color_pair(COLOR_GREEN) | A_NORMAL, left_text: "", right_text: "", center_text: "")
|
181
|
+
attron color do
|
182
|
+
setpos(divider_line, 0)
|
183
|
+
addstr(" " * cols)
|
184
|
+
|
185
|
+
setpos(divider_line, 2)
|
186
|
+
addstr(left_text.ljust(cols / 3))
|
187
|
+
|
188
|
+
setpos(divider_line, cols / 3)
|
189
|
+
addstr(center_text.center(cols / 3))
|
190
|
+
|
191
|
+
setpos(divider_line, cols - right_text.size - 2)
|
192
|
+
addstr(right_text)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def visible_letters
|
197
|
+
(@sx...max_cols + @sx).map { |col| @letters[col] }
|
198
|
+
end
|
199
|
+
|
200
|
+
def selected?(row, col)
|
201
|
+
address.col == col && address.row == (@sy + row)
|
202
|
+
end
|
203
|
+
|
204
|
+
def draw_cells(padding: 4)
|
205
|
+
1.upto(max_rows).each do |row|
|
206
|
+
visible_letters.each.with_index do |col, index|
|
207
|
+
setpos(row, padding + index * @col_width)
|
208
|
+
|
209
|
+
if selected? row, col
|
210
|
+
attron(color_pair(@mode == :edit ? COLOR_GREEN : COLOR_WHITE) | A_TOP) do
|
211
|
+
draw_cell @sy + row, col
|
212
|
+
end
|
213
|
+
else
|
214
|
+
draw_cell @sy + row, col
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def draw_letters_header(padding: 4, color: COLOR_BLUE)
|
221
|
+
visible_letters.each.with_index do |letter, index|
|
222
|
+
setpos(0, padding + index * @col_width)
|
223
|
+
|
224
|
+
attron(color_pair(color) | A_TOP) do
|
225
|
+
addstr(letter.center(@col_width))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def draw_numbers_header(padding: 4, color: COLOR_BLUE)
|
231
|
+
1.upto(max_rows).each.with_index do |row, index|
|
232
|
+
setpos(row, 0)
|
233
|
+
|
234
|
+
attron(color_pair(COLOR_BLUE) | A_TOP) do
|
235
|
+
addstr (@sy + row).to_s.rjust(padding)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def draw_cell(row, col)
|
241
|
+
value = cell_value_at(Address.new(col, row)).to_s
|
242
|
+
|
243
|
+
if value == Float::NAN.to_s
|
244
|
+
addstr("#VALUE!".center(@col_width))
|
245
|
+
elsif value.size >= @col_width
|
246
|
+
addstr(value.chars.last(@col_width).join)
|
247
|
+
else
|
248
|
+
addstr(value.rjust(@col_width))
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def input_line
|
253
|
+
lines - 1
|
254
|
+
end
|
255
|
+
|
256
|
+
def divider_line
|
257
|
+
lines - 2
|
258
|
+
end
|
259
|
+
|
260
|
+
def cursor_to_input_line
|
261
|
+
setpos(input_line, 2)
|
262
|
+
end
|
263
|
+
end
|
data/spreadshit.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "spreadshit/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "spreadshit"
|
8
|
+
spec.version = Spreadshit::VERSION
|
9
|
+
spec.authors = ["Rodrigo Navarro"]
|
10
|
+
spec.email = ["rnavarro@rnavarro.com.br"]
|
11
|
+
|
12
|
+
spec.summary = %q{Simple spreadsheet implementation}
|
13
|
+
spec.description = %q{Simple spreadsheet implementation}
|
14
|
+
spec.homepage = "https://github.com/reu/spreadshit"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "treetop", "~> 1.6"
|
22
|
+
spec.add_dependency "curses"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spreadshit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rodrigo Navarro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: treetop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: curses
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.9'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
description: Simple spreadsheet implementation
|
70
|
+
email:
|
71
|
+
- rnavarro@rnavarro.com.br
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Gemfile
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- bin/console
|
81
|
+
- bin/demo
|
82
|
+
- bin/setup
|
83
|
+
- lib/spreadshit.rb
|
84
|
+
- lib/spreadshit/formula.rb
|
85
|
+
- lib/spreadshit/formula.treetop
|
86
|
+
- lib/spreadshit/functions.rb
|
87
|
+
- lib/spreadshit/version.rb
|
88
|
+
- lib/spreadshit/window.rb
|
89
|
+
- spreadshit.gemspec
|
90
|
+
homepage: https://github.com/reu/spreadshit
|
91
|
+
licenses: []
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.4.5
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Simple spreadsheet implementation
|
113
|
+
test_files: []
|