eventual 0.4.9 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ eventual.gemspec
21
+
22
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Macario Ortega
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc CHANGED
@@ -9,75 +9,114 @@ Reconocimiento de fechas y periodos en lenguaje natural. Útil para crear interf
9
9
  == SINOPSIS:
10
10
 
11
11
  El método event_parse del modulo Eventual reconoce y convierte una fecha o periodo expresado en lenguaje natural en objetos _Date_ o _DateTime_
12
- Ejemplos:
13
12
 
13
+ Ejemplos:
14
14
  require 'rubygems'
15
- require 'eventual'
15
+ require 'eventual'
16
16
 
17
- Eventual.event_parse( 'del 5 al 7 de junio' )
17
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2009' ).map
18
18
  => [#<DateTime: 4909975/2,0,2299161>, #<DateTime: 4909977/2,0,2299161>, #<DateTime: 4909979/2,0,2299161>]
19
19
 
20
- Eventual.event_parse( 'del 5 al 7 de junio 2009', Date )
21
- => [#<Date: 4909975/2,0,2299161>, #<Date: 4909977/2,0,2299161>, #<Date: 4909979/2,0,2299161>]
20
+ # Si no se especifica el año se usará el año actual
21
+ EsDatesParser.new.parse( 'del 5 al 7 de junio' ).map{ |d| d.to_s } # Quizá mas tarde se localize a otros idiomas
22
+ => ["2010-06-05", "2010-06-06", "2010-06-07"]
23
+
24
+ # Se puede especificar un año por omisión
25
+ dates = EsDatesParser.new.parse( 'del 5 al 7 de junio' )
26
+ dates.year = 2007
27
+ dates.map{ |d| d.to_s }
28
+ => ["2007-06-05", "2007-06-06", "2007-06-07"]
22
29
 
23
- Eventual.event_parse( 'del 5 al 7 de junio del 2009 a las 16:00 y 18:00 horas' ){ |d| d.to_s }
30
+ # Si se especifica la hora el resultado será un Array de DateTime
31
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2009 a las 16:00 y 18:00 horas' ).map
32
+ => [#<DateTime: 14729929/6,0,2299161>, #<DateTime: 9819953/4,0,2299161>, #<DateTime: 14729935/6,0,2299161>, #<DateTime: 9819957/4,0,2299161>, #<DateTime: 14729941/6,0,2299161>, #<DateTime: 9819961/4,0,2299161>]
33
+
34
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2009 a las 16:00 y 18:00 horas' ).map{ |d| d.to_s }
24
35
  => ["2009-06-05T16:00:00+00:00", "2009-06-05T18:00:00+00:00", "2009-06-06T16:00:00+00:00", "2009-06-06T18:00:00+00:00", "2009-06-07T16:00:00+00:00", "2009-06-07T18:00:00+00:00"]
25
36
 
26
- Eventual.event_parse( 'del 5 al 7 de junio 2009' ){ |d| Eventual::WDAY_LIST[ d.wday ] }
27
- => ['viernes', 'sabado', 'domingo']
37
+ # Se pueden restringir los resultados a ciertos dias
38
+ EsDatesParser.new.parse('lunes y martes de diciembre del 2001 a las 15:00').map{ |d| d.to_s }
39
+ => ["2010-12-06T15:00:00+00:00", "2010-12-07T15:00:00+00:00", "2010-12-13T15:00:00+00:00", "2010-12-14T15:00:00+00:00", "2010-12-20T15:00:00+00:00", "2010-12-21T15:00:00+00:00", "2010-12-27T15:00:00+00:00", "2010-12-28T15:00:00+00:00"]
28
40
 
29
- Ejemplos de formatos reconocidos:
30
-
31
- * 1 de enero
32
- * 21, 22 y 23 de enero del 2009
33
- * 21, 22 y 23 de enero a las 20:00 horas
34
- * 21, 22 y 23 de enero a las 20:00 y 22:00 horas
35
- * viernes 1, sábado 2, domingo 3 y lunes 4 de enero del 2010
36
- * martes y miércoles del 1 al 20 de junio del 2009 a las 16:00 y 18:00 horas
37
- * sábados y domingos del 1 de enero al 31 de diciembre del 2008 a las 16:00
38
- * sábado y domingo del 1 de enero al 31 de diciembre
39
- * lunes a viernes del 1 de enero al 31 de diciembre del 2008 a las 16:00
40
- * todos los sábados de diciembre del 2009
41
- * lunes a viernes de enero a abril
42
- * del viernes 1 al domingo 3 de enero del 2010
41
+ # Se puede checar si las fechas reconocidas incluyen cierta fecha, la comparación es "perezosa" es decir no instancia todos los objetos Date o DateTime
42
+ # como hace map y por lo tanto es mas eficiente
43
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007' ).include? Date.civil(2007, 6, 6)
44
+ => true
43
45
 
44
- Se puede extender _Date_ y _DateTime_ con Eventual requiriendo 'eventual/date_time' y 'eventual/date':
46
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007' ).include? Date.civil(2006, 6, 6)
47
+ => false
45
48
 
46
- require 'rubygems'
47
- require 'eventual'
48
- require 'eventual/date_time'
49
- require 'eventual/date'
50
-
51
- DateTime.event_parse( 'del 5 al 7 de junio' )
52
- => [#<DateTime: 4909975/2,0,2299161>, #<DateTime: 4909977/2,0,2299161>, #<DateTime: 4909979/2,0,2299161>]
49
+ # Se toma en cuenta la hora
50
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' ).include? DateTime.civil(2007, 6, 6, 16, 0)
51
+ => true
53
52
 
54
- Date.event_parse( 'del 5 al 7 de junio 2009' )
55
- => [#<Date: 4909975/2,0,2299161>, #<Date: 4909977/2,0,2299161>, #<Date: 4909979/2,0,2299161>]
56
-
57
-
58
- Si se pasa un bloque se puede especificar si se desea usar el texto que sigue a la definición de fechas mediante pasando la opción _use_trailing_ => _true_:
53
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' ).include? DateTime.civil(2007, 6, 6, 15, 0)
54
+ => false
55
+
56
+ # Si se pasa un Date que corresponda al periodo la comparación es positiva
57
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' ).include? Date.civil(2007, 6, 6)
58
+ => true
59
+
60
+ # El evento tiene una duración por omisión de 60 minutos
61
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' ).include? DateTime.civil(2007, 6, 6, 16, 59)
62
+ => true
63
+
64
+ EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' ).include? DateTime.civil(2007, 6, 6, 17, 00)
65
+ => false
66
+
67
+ # Pero se puede cambiar
68
+ dates = EsDatesParser.new.parse( 'del 5 al 7 de junio del 2007 a las 16:00' )
69
+ dates.time_span = 120
70
+ dates.include? DateTime.civil(2007, 6, 6, 17, 00)
71
+ => true
72
+
73
+ Ejemplos de formatos reconocidos:
59
74
 
60
- str = "1 de enero del 2009\nSede:El tercer lugar\n2 de enero del 2009\nSede:El tercer lugar"
61
- Eventual.event_parse( str, :use_trailing => true ) do |dia, texto_extra|
62
- [dia.to_s, texto_extra]
63
- end
64
- => [["2009-01-01T00:00:00+00:00", "Sede:El tercer lugar"], ["2009-01-02T00:00:00+00:00", "Sede:El tercer lugar"]]
75
+ * marzo
76
+ * marzo de 2009
77
+ * marzo del 2009
78
+ * todo marzo 2009
79
+ * marzo, 2009
80
+ * marzo '09
81
+ * lunes y martes marzo del 2010
82
+ * todos los lunes y martes de marzo del 2010
83
+ * lunes y martes durante marzo del 2010
84
+ * lunes y martes durante todo marzo del 2010
85
+ * lunes y martes, marzo del 2010
86
+ * 21 de marzo
87
+ * 21 marzo
88
+ * domingo 21 de marzo
89
+ * 1, 2 y 3 de marzo
90
+ * 1, 2 y 3 marzo
91
+ * lunes 1, martes 2 y miercoles 3 de marzo
92
+ * 1 al 3 de marzo
93
+ * 1 al 3, marzo
94
+ * del 1 al 3 de marzo
95
+ * del 1 al 3, marzo
96
+ * 24 de febrero al 3 de marzo del 2010
97
+ * 24 de diciembre del 2009 al 3 de enero del 2010
98
+ * lunes y martes del 1 al 22 de marzo del '10
99
+ * fines de semana del 1 al 22 de marzo del '10
100
+ * entre semana del 1 al 22 de marzo del '10
101
+ * lunes y martes del 1 al 22 de marzo del '10
102
+ * todos los lunes y martes del 1 al 22 de marzo del '10
103
+ * los lunes y martes del 1 al 22 de marzo del '10
104
+ * los lunes y los martes del 1 al 22 de marzo del '10
105
+ * lunes y martes de diciembre a las 15
106
+ * lunes y martes de diciembre a las 15:30 hrs.
107
+ * lunes y martes de diciembre a las 15:00 y 16:00 horas
108
+ * lunes y martes de diciembre a las 3 am
109
+ * lunes y martes de diciembre a las 3:15 p.m.
65
110
 
66
111
  == TODO:
67
112
 
68
- * No estoy seguro de que Iconv funcione en windows, lo arreglaré pronto
69
-
70
113
  Formatos a reconocer
71
114
 
72
- * todos los lunes de junio
73
- * todo junio
74
- * domingos de septiembre
75
- * martes y miércoles de agosto
76
115
  * todo el año
77
116
 
78
117
  == INSTALACIÓN:
79
118
 
80
- sudo gem install maca-eventual -s http://gems.github.com
119
+ [sudo] gem install eventual -s http://gemcutter.org
81
120
 
82
121
  == LICENCIA:
83
122
 
data/Rakefile CHANGED
@@ -1,18 +1,45 @@
1
- require 'rubygems' unless ENV['NO_RUBYGEMS']
2
- %w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
3
- require File.dirname(__FILE__) + '/lib/eventual'
4
-
5
-
6
- $hoe = Hoe.new('eventual', Eventual::VERSION) do |p|
7
- p.developer('Macario Ortega', 'macarui@gmail.com')
8
- p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
- p.rubyforge_name = p.name
10
-
11
- p.clean_globs |= %w[**/.DS_Store tmp *.log]
12
- p.rsync_args = '-av --delete --ignore-errors'
13
- end
14
-
15
- require 'newgem/tasks' # load /tasks/*.rake
16
- Dir['tasks/**/*.rake'].each { |t| load t }
17
-
18
- task :default => :spec
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "eventual"
8
+ gem.summary = %Q{ Reconocimiento de eventos y periodos de tiempo en lengua natural. Natural date event parsing in spanish so far. }
9
+ gem.description = %Q{ Reconocimiento de eventos y periodos de tiempo en lengua natural. Natural date event parsing in spanish so far. }
10
+ gem.email = "macarui@gmail.com"
11
+ gem.homepage = "http://github.com/maca/eventual"
12
+ gem.authors = ["Macario Ortega"]
13
+ gem.post_install_message = %{ \n\n***********************************\nPor favor tenga en cuenta que el API ha cambiado, consulte la página del proyecto: http://github.com/maca/eventual. English implementation is due.\n***********************************\n\n }
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "eventual #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,83 @@
1
+ grammar EsDates
2
+ rule root
3
+ (date_list / dates) year? times? <Eventual::Node>
4
+ end
5
+
6
+ rule dates
7
+ (period / date)
8
+ end
9
+
10
+ rule period
11
+ weekdays_constrain? (range / month)
12
+ end
13
+
14
+ rule date
15
+ weekday_constrain? day_number month_name?
16
+ end
17
+
18
+ rule date_list
19
+ dates (',' space dates)* (space 'y' space dates) / dates (space? "\n" space? dates)*
20
+ end
21
+
22
+ rule range
23
+ (('del' / 'de' / ',') space)? (month / date) year? space ('al' / 'a') space (month / date) <Eventual::DatePeriod>
24
+ end
25
+
26
+ rule month
27
+ month_name '' <Eventual::MonthPeriod>
28
+ end
29
+
30
+ rule day_number
31
+ ([0-2] [0-9] / '3' [0-1] / [1-9]) '' <Eventual::Day>
32
+ end
33
+
34
+ ##########
35
+ rule times
36
+ space 'a' space ('las' / 'la') (time_12 / time_24) ((space 'y' / ',') (time_12 / time_24))* <Eventual::Times>
37
+ end
38
+
39
+ rule time_24
40
+ space ([0-1] [0-9] / '2' [0-4] / [0-9]) (':' [0-5] [0-9])? (space? ('hrs.' / 'hrs' / 'horas'))? <Eventual::Time>
41
+ end
42
+
43
+ rule time_12
44
+ space ('0' [0-9] / '1' [0-2] / [0-9]) (':' [0-5] [0-9])? space? period:(('a'/'p') '.'? space? 'm' '.'? space?) <Eventual::Time12>
45
+ end
46
+
47
+ ##########
48
+ rule month_name
49
+ (((space 'de') / ',')? space)? ('enero' / 'febrero' / 'marzo' / 'abril' / 'mayo' / 'junio' / 'julio' / 'agosto' / 'septiembre' / 'octubre' / 'noviembre' / 'diciembre') <Eventual::MonthName>
50
+ end
51
+
52
+ rule year
53
+ ((space 'de' 'l'?) / ',')? space ([1-9] [0-9] [0-9] [0-9] / "'" [0-9] [0-9]) <Eventual::Year>
54
+ end
55
+
56
+ rule weekdays_constrain
57
+ wdays_node:(weekday_list / weekdays / weekday) (',' / (space ('del' / 'de' / 'durante todo' / 'durante')))? space <Eventual::WeekdayConstrain>
58
+ end
59
+
60
+ rule weekday_constrain
61
+ wdays_node:weekday space <Eventual::WeekdayConstrain>
62
+ end
63
+
64
+ rule weekday_list
65
+ (weekday_constrain_sugar? weekday (',' space weekday_constrain_sugar? weekday)* (space 'y' space weekday_constrain_sugar? weekday)?)
66
+ end
67
+
68
+ rule weekdays
69
+ weekday_constrain_sugar? ((('dias' space)? 'entre' space 'semana') / 'fines' space 'de' space 'semana')
70
+ end
71
+
72
+ rule weekday_constrain_sugar
73
+ (('todos' space)? 'los' space)
74
+ end
75
+
76
+ rule weekday
77
+ (('lun' 'es'? ) / ('mar' 'tes'? ) / ('mi' 'ercoles'? ) / ('jue' 'ves'? ) / ('vie' 'rnes'? ) / ('sab' 'ado'? 's'? ) / ('dom' 'ingo'? 's'? ))
78
+ end
79
+
80
+ rule space
81
+ ' '+
82
+ end
83
+ end
@@ -0,0 +1 @@
1
+ Treetop.load "#{ File.dirname __FILE__ }/es/event_parser"
@@ -0,0 +1,217 @@
1
+ module Eventual
2
+ Weekdays = %w(domingo lunes martes miércoles jueves viernes sábado).freeze
3
+ MonthNames = %w(enero febrero marzo abril mayo junio julio agosto septiembre noviembre).unshift(nil).freeze
4
+ ShortMonthNames = %w(ene feb mar abr may jun jul ago sept oct nov dic).freeze
5
+ WdaysR = [/d/, /l/, /ma/, /mi/, /j/, /v/, /s/].freeze
6
+ WdayListR = /\b(?:#{ WdaysR.join('|') })/.freeze
7
+
8
+ class WdayMatchError < StandardError
9
+ def initialize value, wday_index
10
+ @value, @wday_index = value, wday_index
11
+ end
12
+
13
+ def to_s
14
+ "El #{@value.day} de #{MonthNames[@value.month]} del #{@value.year} cae en #{Weekdays[@value.wday]} no #{Weekdays[@wday_index]}"
15
+ end
16
+ end
17
+
18
+ class Year < Treetop::Runtime::SyntaxNode
19
+ def value
20
+ match = text_value.match(/(')?(\d{2,4})/)
21
+ value = match[2].to_i
22
+ value += 2000 if match[1]
23
+ value
24
+ end
25
+ end
26
+
27
+ class WeekdayConstrain < Treetop::Runtime::SyntaxNode
28
+ def value
29
+ text = wdays_node.text_value.sub('semana', '')
30
+ days = text.scan(WdayListR).map{ |d| WdaysR.index /#{d}/ }
31
+ days += (1..5).map if text.include?('entre')
32
+ days += [6,0] if text.include?('fines')
33
+ days.uniq
34
+ end
35
+ end
36
+
37
+ class MonthName < Treetop::Runtime::SyntaxNode
38
+ def value
39
+ ShortMonthNames.index(text_value.downcase.match(/#{ ShortMonthNames.join('|') }/).to_s) + 1
40
+ end
41
+ end
42
+
43
+ class Node < Treetop::Runtime::SyntaxNode
44
+ attr_accessor :year
45
+ attr_accessor :time_span
46
+
47
+ attr_accessor :month
48
+ attr_accessor :weekdays
49
+ attr_accessor :times
50
+
51
+ def last
52
+ to_a.last
53
+ end
54
+
55
+ def first
56
+ to_a.first
57
+ end
58
+
59
+ def to_a
60
+ map
61
+ end
62
+
63
+ def date_within_weekdays? date
64
+ return true unless weekdays
65
+ weekdays.include?(date.wday)
66
+ end
67
+
68
+ def map &block
69
+ walk { |elements| elements.map &block }
70
+ end
71
+
72
+ def include? date
73
+ result = false
74
+ walk { |elements| break result = true if elements.include? date }
75
+
76
+ unless date.class == Date or times.nil? or times.empty?
77
+ @time_span ||= 60
78
+ within_time = times.inject(nil) { |memo, time|
79
+ first = ::Time.local date.year, date.month, date.day, time.hour, time.minute
80
+ time = ::Time.local date.year, date.month, date.day, date.hour, date.min
81
+ break true if time >= first and time < first + 60 * @time_span
82
+ }
83
+ return false unless within_time
84
+ end
85
+ result
86
+ end
87
+
88
+ private
89
+ def walk &block
90
+ year = self.year || Date.today.year
91
+ month = nil
92
+
93
+ walk = lambda do |elements|
94
+ break unless elements
95
+ weekdays = elements.first.value if elements.first.class == WeekdayConstrain
96
+
97
+ elements.reverse.map do |element|
98
+ case element
99
+ when Day, Period
100
+ element.weekdays = weekdays
101
+ element.year = year
102
+ element.month = month
103
+ element.times = @times
104
+
105
+ yield element
106
+ when Year
107
+ year = element.value
108
+ next nil
109
+ when MonthName
110
+ month = element.value
111
+ next nil
112
+ when WeekdayConstrain
113
+ next nil
114
+ when Times
115
+ @times = element.map
116
+ next nil
117
+ else
118
+ walk.call element.elements
119
+ end
120
+ end.reverse
121
+ end
122
+ walk.call(elements).flatten.compact
123
+ end
124
+ end
125
+
126
+ class Day < Node
127
+ def map &block
128
+ dates = times ? times.map{ |time| DateTime.civil year, month, text_value.to_i, time.hour, time.minute } : [Date.civil(year, month, text_value.to_i)]
129
+ raise WdayMatchError.new(dates.first, weekdays.first) unless date_within_weekdays? dates.first
130
+ dates.map(&block)
131
+ end
132
+
133
+ def include? date
134
+ to_a.include? date
135
+ end
136
+ end
137
+
138
+ class Period < Node
139
+ def range
140
+ (first..last)
141
+ end
142
+
143
+ def include? date
144
+ return false unless date_within_weekdays? date
145
+ range.include? date
146
+ end
147
+
148
+ alias node_map map
149
+ private :node_map
150
+
151
+ def map
152
+ array = []
153
+ range.each do |date|
154
+ next unless date_within_weekdays? date
155
+ next array.push(block_given? ? yield(date) : date) unless times
156
+
157
+ times.each do |time|
158
+ new_date = DateTime.civil date.year, date.month, date.day, time.hour, time.minute
159
+ array.push block_given? ? yield(new_date) : new_date
160
+ end
161
+ end
162
+ array
163
+ end
164
+ end
165
+
166
+ class MonthPeriod < Period
167
+ def first
168
+ return Date.civil(year, month_name.value) unless times and !times.empty?
169
+ time = times.first
170
+ return DateTime.civil(year, month_name.value, 1, time.hour, time.minute)
171
+ end
172
+
173
+ def last
174
+ date = (first >> 1) - 1
175
+ return date unless times and !times.empty?
176
+ time = times.last
177
+ DateTime.civil(date.year, date.month, date.day, time.hour, time.minute)
178
+ end
179
+ end
180
+
181
+ class DatePeriod < Period
182
+ def first
183
+ node_map.first
184
+ end
185
+
186
+ def last
187
+ node_map.last
188
+ end
189
+ end
190
+
191
+ class Times < Treetop::Runtime::SyntaxNode
192
+ def map
193
+ walk_times = lambda do |elements|
194
+ break unless elements
195
+ elements.map { |e| Time === e ? e.value : walk_times.call(e.elements) }
196
+ end
197
+ walk_times.call(elements).flatten.compact.sort_by{ |t| '%02d%02d' % [t.hour, t.minute] }
198
+ end
199
+ end
200
+
201
+ class Time < Treetop::Runtime::SyntaxNode
202
+ attr_accessor :hour, :minute
203
+ def value
204
+ @hour, @minute = text_value.scan(/\d+/).map(&:to_i)
205
+ @minute ||= 0
206
+ self
207
+ end
208
+ end
209
+
210
+ class Time12 < Time
211
+ def value
212
+ super
213
+ @hour += 12 if period.text_value.gsub(/[^a-z]/, '') == 'pm'
214
+ self
215
+ end
216
+ end
217
+ end