lxl 0.3.1 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/CHANGES +12 -0
  2. data/VERSION +1 -1
  3. data/lib/lxl.rb +102 -32
  4. data/lxl.gemspec +1 -0
  5. data/test/lxl_test.rb +35 -6
  6. metadata +5 -4
  7. data/lxl.rdoc +0 -114
data/CHANGES CHANGED
@@ -1,3 +1,15 @@
1
+ 0.3.4
2
+
3
+ - Now recognizes complex Range formats
4
+
5
+ B3:D5
6
+ Sheet1!B3:D5
7
+ [Book1.xls]Sheet1!B3:D5
8
+
9
+ LXL::Range has attr_accessors for :book and :sheet
10
+
11
+ - Documentation
12
+
1
13
  0.3.1
2
14
 
3
15
  - LXL::LittleLexer => LXL::Lexer, types is a string again.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.3.4
data/lib/lxl.rb CHANGED
@@ -20,12 +20,17 @@
20
20
  #
21
21
  # =Ranges
22
22
  #
23
- # B3:D5 => LXL::Range.new("B3","D5")
24
- #
25
- # LXL::Range is a subclass of Range which overrides
26
- # the #each method to return Excel-style ranges.
23
+ # LXL::Range provides Excel-style ranges.
24
+ #
25
+ # <b>Valid Formats</b>
26
+ #
27
+ # B3:D5
28
+ # Sheet1!B3:D5
29
+ # [Book1.xls]Sheet1!B3:D5
27
30
  #
28
- # # Standard Ruby Range
31
+ # <b>Collection</b>
32
+ #
33
+ # # Ruby Range
29
34
  # Range.new("B3", "D5").collect
30
35
  # # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
31
36
  # "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
@@ -33,7 +38,7 @@
33
38
  #
34
39
  # # LXL Range
35
40
  # LXL::Range.new("B3", "D5").collect
36
- # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
41
+ # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
37
42
  #
38
43
  # =Percentages
39
44
  #
@@ -71,9 +76,9 @@
71
76
  # a > b # Greater than
72
77
  # a <= b # Less than or equal to
73
78
  # a >= b # Greater than or equal to
74
- # a & b # String concentation
75
- # n ^ n # Exponential
76
- #
79
+ # a & b # String concentation
80
+ # a ^ b # Exponential
81
+ #
77
82
  # =Constants
78
83
  #
79
84
  # TRUE # true
@@ -98,7 +103,7 @@
98
103
  # =List Functions
99
104
  #
100
105
  # LIST (a,b,..) # Variable length list
101
- # IN (find,list) # True if find is found in the given list/string
106
+ # IN (find,list) # True if find is in the given list (or string)
102
107
  #
103
108
  # =Notes
104
109
  #
@@ -196,12 +201,13 @@ class LXL::Parser
196
201
  def initialize(namespace=self.class.namespace_class.new)
197
202
  @namespace = namespace
198
203
  ops = EXCEL_OPERATORS.collect { |v| Regexp.quote(v) }.join('|')
204
+ xlr = Regexp.new('\A'+LXL::Range::EXCEL_RANGE.source, true)
199
205
  @lexer = self.class.lexer_class.new([
200
206
  [/\A[\s,]+/, ?w], # Whitespace (includes Commas)
201
207
  [/\A;/, ?;], # Semi-Colon (statement separator)
202
208
  [/\A(#{ops})/, ?o], # Operator
203
209
  [/\A("([^"]|"")*")/m, ?s], # String
204
- [/\A\w+:\w+/, ?r], # Range
210
+ [xlr, ?r], # Range
205
211
  [/\A\d+(\.\d+)?%/, ?p], # Percentage
206
212
  [/\A\d+\.\d+/, ?f], # Float
207
213
  [/\A\d+/, ?i], # Integer
@@ -273,9 +279,9 @@ class LXL::Parser
273
279
  when ?f then token.to_f
274
280
  when ?i then token.to_i
275
281
  when ?t then
276
- upper = token.to_s.upcase.to_sym
277
- lower = token.to_s.downcase.to_sym
278
- raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(token.to_s)
282
+ upper = token.to_s.upcase
283
+ lower = token.to_s.downcase
284
+ raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
279
285
  if @namespace.respond_to?(lower)
280
286
  if tokens[i+1] != '('
281
287
  raise ArgumentError, "wrong number of arguments for #{token}"
@@ -303,31 +309,91 @@ class LXL::Parser
303
309
 
304
310
  end
305
311
 
306
- # Excel-style ranges.
312
+ # Excel-style ranges.
313
+ #
314
+ # =Valid formats
315
+ #
316
+ # B3:D5
317
+ # Sheet1!B3:D5
318
+ # [Book1.xls]Sheet1!B3:D5
319
+ #
320
+ # =Collection
307
321
  #
322
+ # # Ruby Range
308
323
  # Range.new("B3", "D5").collect
309
324
  # # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
310
325
  # "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
311
326
  # "D0", "D1", "D2", "D3", "D4", "D5"]
312
- #
327
+ #
328
+ # # LXL Range
313
329
  # LXL::Range.new("B3", "D5").collect
314
- # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
315
- #
330
+ # # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
331
+ #
316
332
  class LXL::Range < Range
317
333
 
318
- EXCEL_RANGE = /[A-Z]+[1-9]+/
334
+ # B3, Sheet1!B3, [Book1.xls]Sheet1!B3
335
+ EXCEL_VECTOR = /(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+)/i
336
+
337
+ # B3:D5, Sheet1!B3:D5, [Book1.xls]Sheet1!B3:D5
338
+ EXCEL_RANGE = /(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+):([A-Z]+[1-9]+)/i
339
+
340
+ # True if this is an Excel range.
341
+ attr_accessor :excel
342
+
343
+ # Given workbook name.
344
+ attr_accessor :book
345
+
346
+ # Given worksheet name.
347
+ attr_accessor :sheet
348
+
349
+ def self.new(first, last)
350
+ excel = (first =~ EXCEL_VECTOR && last =~ EXCEL_VECTOR)
351
+ if excel
352
+ first =~ EXCEL_VECTOR
353
+ book,sheet,first = $2,$4,$5
354
+ obj = super(*[first.upcase,last.upcase].sort)
355
+ obj.excel = true
356
+ obj.book = book
357
+ obj.sheet = sheet
358
+ else
359
+ obj = super
360
+ obj.excel = false
361
+ end
362
+ obj
363
+ end
364
+
365
+ def excel?
366
+ @excel ? true : false
367
+ end
368
+
369
+ def first_column
370
+ first.to_s.upcase.gsub(/[^A-Z]/, '')
371
+ end
372
+
373
+ def last_column
374
+ last.to_s.upcase.gsub(/[^A-Z]/, '')
375
+ end
376
+
377
+ def first_cell
378
+ first.to_s.gsub(/[^1-9]/, '')
379
+ end
380
+
381
+ def last_cell
382
+ last.to_s.gsub(/[^1-9]/, '')
383
+ end
319
384
 
320
385
  def each
321
- range = [self.begin,self.end].collect! { |x| x.to_s.upcase }.sort
322
- if range.first =~ EXCEL_RANGE and range.last =~ EXCEL_RANGE
323
- fcol,lcol = range.collect { |x| x.gsub(/\d/, '') }
324
- fcel,lcel = range.collect { |x| x.gsub(/\D/, '') }
325
- Range.new(fcol,lcol).each{ |col| Range.new(fcel,lcel).each { |cel| yield col+cel } }
386
+ if excel?
387
+ Range.new(first_column, last_column).each do |column|
388
+ Range.new(first_cell, last_cell).each do |cell|
389
+ yield column+cell
390
+ end
391
+ end
326
392
  else
327
393
  super
328
394
  end
329
395
  end
330
-
396
+
331
397
  end
332
398
 
333
399
  # Snapshots the symbol/arguments of a function call for later use.
@@ -353,9 +419,19 @@ end
353
419
  #
354
420
  class LXL::EmptyNamespace
355
421
 
356
- # Remove all unessecary methods
422
+ # Ensure all constants are included (inherited, extended, included, etc).
423
+ #
424
+ def self.const_defined?(symbol) #:nodoc:
425
+ constants.include?(symbol.to_s)
426
+ end
427
+
428
+ # Create instance methods for constant-related methods
357
429
  ['constants', 'const_defined?', 'const_get', 'const_set'].each { |m| define_method(m) { |s| self.class.send(m,s) } }
430
+
431
+ # Preserved methods
358
432
  METHODS = ['__id__', '__send__', 'constants', 'const_defined?', 'const_get', 'const_set', 'class', 'instance_methods', 'method', 'methods', 'respond_to?', 'to_s']
433
+
434
+ # Clear all unessecary methods
359
435
  (instance_methods-METHODS).sort.each { |m| undef_method m }
360
436
 
361
437
  # Default deferred class.
@@ -364,12 +440,6 @@ class LXL::EmptyNamespace
364
440
  LXL::Deferred
365
441
  end
366
442
 
367
- # Ensure all constants are included (inherited, extended, included, etc).
368
- #
369
- def self.const_defined?(symbol) #:nodoc:
370
- constants.include?(symbol.to_s)
371
- end
372
-
373
443
  # Register lowercase methods which return a Deferred function call.
374
444
  #
375
445
  # class MyNamespace < LXL::Namespace
data/lxl.gemspec CHANGED
@@ -14,6 +14,7 @@ spec = Gem::Specification.new do |s|
14
14
  s.files = Dir['**/*'].delete_if { |f| f =~ /(cvs|gem|svn)$/i }
15
15
  s.require_path = 'lib'
16
16
  s.test_file = "test/lxl_test.rb"
17
+ s.rdoc_options << '--all' << '--inline-source'
17
18
  s.has_rdoc = true
18
19
  s.rubyforge_project = "lxl"
19
20
  end
data/test/lxl_test.rb CHANGED
@@ -51,10 +51,15 @@ class LXLTest < Test::Unit::TestCase
51
51
  def test_range
52
52
  lxl = LXL::Parser.new(MyNamespace.new)
53
53
  res = lxl.eval('=LIST(d5:b3, "x")')
54
- assert_equal(LXL::Range, res.first.class)
55
- assert_equal("B3", res.first.min)
56
- assert_equal("D5", res.first.max)
57
- assert_equal(res.first.collect.join(','), "B3,B4,B5,C3,C4,C5,D3,D4,D5")
54
+ range = res.first
55
+ assert_equal(LXL::Range, range.class)
56
+ assert_equal("B3", range.min)
57
+ assert_equal("D5", range.max)
58
+ assert_equal(range.collect.join(','), "B3,B4,B5,C3,C4,C5,D3,D4,D5")
59
+ assert_equal(range.first_column, 'B')
60
+ assert_equal(range.last_column, 'D')
61
+ assert_equal(range.first_cell, '3')
62
+ assert_equal(range.last_cell, '5')
58
63
  end
59
64
 
60
65
  def test_deferred
@@ -65,6 +70,26 @@ class LXLTest < Test::Unit::TestCase
65
70
  assert_equal(:testing, res.symbol)
66
71
  assert_equal(3, res.args.last)
67
72
  end
73
+
74
+ def test_range_naming
75
+ lxl = LXL::Parser.new(MyNamespace.new)
76
+ range = lxl.eval('=B1:D2')
77
+ assert_equal('B', range.first_column)
78
+ range = lxl.eval('=Sheet1!B3:D5')
79
+ assert_equal('B', range.first_column)
80
+ assert_equal('5', range.last_cell)
81
+ assert_equal('Sheet1', range.sheet)
82
+ range = lxl.eval('=[Book.xls]Sheet2!B5:D6')
83
+ assert_equal('B', range.first_column)
84
+ assert_equal('6', range.last_cell)
85
+ assert_equal('Book.xls', range.book)
86
+ assert_equal('Sheet2', range.sheet)
87
+ range = lxl.eval('=[Book1]Sheet3!B7:D8')
88
+ assert_equal('B', range.first_column)
89
+ assert_equal('8', range.last_cell)
90
+ assert_equal('Book1', range.book)
91
+ assert_equal('Sheet3', range.sheet)
92
+ end
68
93
 
69
94
  def test_ns_methods
70
95
  ns = MyNamespace.new
@@ -91,8 +116,12 @@ class LXLTest < Test::Unit::TestCase
91
116
  end
92
117
 
93
118
  def test_ns_access
94
- ns = MyNamespace.new
95
- assert_equal(:failed, begin ns.__get__(:class) rescue :failed end)
119
+ res = begin
120
+ LXL.eval('=CLASS()')
121
+ rescue NoMethodError => e
122
+ :failed
123
+ end
124
+ assert_equal(:failed, res)
96
125
  end
97
126
 
98
127
  class MyLXLNamespace < LXL::Namespace
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.3
3
3
  specification_version: 1
4
4
  name: lxl
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.1
7
- date: 2005-02-13
6
+ version: 0.3.4
7
+ date: 2005-02-14
8
8
  summary: LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas. Easily extended with new constants and functions.
9
9
  require_paths:
10
10
  - lib
@@ -34,12 +34,13 @@ files:
34
34
  - lxl.gemspec
35
35
  - setup.rb
36
36
  - README.en
37
- - lxl.rdoc
38
37
  - lib/lxl.rb
39
38
  - test/lxl_test.rb
40
39
  test_files:
41
40
  - test/lxl_test.rb
42
- rdoc_options: []
41
+ rdoc_options:
42
+ - "--all"
43
+ - "--inline-source"
43
44
  extra_rdoc_files: []
44
45
  executables: []
45
46
  extensions: []
data/lxl.rdoc DELETED
@@ -1,114 +0,0 @@
1
- LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas
2
- and is easily extended with new constants and functions.
3
-
4
- =Namespaces
5
-
6
- Extending the language is as easy as defining new constants
7
- and methods on an LXL::Namespace class.
8
-
9
- class MyLXLNamespace < LXL::Namespace
10
- NAME = 'John Doe'
11
- def upper(text) text.to_s.upcase end
12
- end
13
-
14
- class MyLXL < LXL::Parser
15
- def self.namespace_class() MyLXLNamespace end
16
- end
17
-
18
- MyLXL.eval('=UPPER(NAME)')
19
- # => JOHN DOE
20
-
21
- =Ranges
22
-
23
- B3:D5 => LXL::Range.new("B3","D5")
24
-
25
- LXL::Range is a subclass of Range which overrides
26
- the #each method to return Excel-style ranges.
27
-
28
- # Standard Ruby Range
29
- Range.new("B3", "D5").collect
30
- # => ["B3", "B4", "B5", "B6", "B7", "B8", "B9",
31
- "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9",
32
- "D0", "D1", "D2", "D3", "D4", "D5"]
33
-
34
- # LXL Range
35
- LXL::Range.new("B3", "D5").collect
36
- # => ["B3", "B4", "B5", "C3", "C4", "C5", "D3", "D4", "D5"]
37
-
38
- =Percentages
39
-
40
- 25% => .25
41
-
42
- =Deferred function calls
43
-
44
- LXL::Deferred snapshots the symbol/arguments of a function call for later use.
45
-
46
- class MyNamespace < LXL::Namespace
47
- register_deferred :foo
48
- end
49
- LXL.new(MyNamespace.new).eval('=FOO(1, "two", 3)').inspect
50
- # => <LXL::Deferred @args=[1, "two", 3] @symbol=:foo>
51
-
52
- =Symbol registration
53
-
54
- Register uppercase constants of the same name and value.
55
-
56
- class MyNamespace < LXL::Namespace
57
- register_symbols :foo, :bar
58
- end
59
- LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)').inspect
60
- # => [:FOO, :BAR]
61
-
62
- =Operators
63
-
64
- a + b # Add
65
- a - b # Subtract
66
- a * b # Multiply
67
- a / b # Divide
68
- a = b # Equal to
69
- a <> b # Not equal to
70
- a < b # Less than
71
- a > b # Greater than
72
- a <= b # Less than or equal to
73
- a >= b # Greater than or equal to
74
- a & b # String concentation
75
- n ^ n # Exponential
76
-
77
- =Constants
78
-
79
- TRUE # true
80
- FALSE # false
81
- NULL # nil
82
-
83
- =Logical Functions
84
-
85
- AND (a,b)
86
- OR (a,b)
87
- NOT (cond)
88
- IF (cond,true,false)
89
-
90
- =Date/Time Functions
91
-
92
- TODAY () # current date value
93
- NOW () # current date/time value
94
- DATE (y,m,d) # date value
95
- TIME (h,m,s) # time value
96
- DATETIME (text) # convert a date/time string into a date/time value
97
-
98
- =List Functions
99
-
100
- LIST (a,b,..) # Variable length list
101
- IN (find,list) # True if find is found in the given list/string
102
-
103
- =Notes
104
-
105
- * Values prefixed with = are formulas, anything else is assumed to be text.
106
-
107
- LXL.eval("5+5") # => '5+5'
108
- LXL.eval("=5+5") # => 10
109
-
110
- * Constant and Function names are case-insensitive.
111
-
112
- * The number zero is interpereted as FALSE.
113
-
114
- * Separating formulas with a semi-colon returns an Array of results.