lxl 0.3.1 → 0.3.4

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 (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.