require 'date' require 'iconv' require 'strscan' module Eventual class WdayMatchError < StandardError; end VERSION = '0.4.9' extend self WDAY_LIST = %w(domingo lunes martes miércoles jueves viernes sábado).freeze MESES = %w(enero febrero marzo abril mayo junio julio agosto septiembre octubre noviembre diciembre).unshift(nil).freeze DIAS_DE_LA_SEMANA = WDAY_LIST.map{ |d| d.match(/s$/) ? d : "#{d}s?" } def rango_de a, b = nil a = a.join('|') if Array === a b ||= a /(?:de |del )?(?:#{ a }) (?:a|al) (?:#{ b })/ end def lista_de elementos elementos = elementos.join('|') if Array === elementos /(?:#{ elementos }) # Primer elemento -requerido- (?: # Cualquier número de (?-x:, | y ) # concatenador (?:#{ elementos }) # y elemento. )*/ix end def plural_opcional elementos elementos.map{ |d| d.match(/s$/) ? d : "#{d}s?" } end year = /(?-x:del |de )?(\d{4})/i horarios = /a las (#{ lista_de '\d{1,2}:\d{2}' })(?: horas| hrs)?/i DAY_LIST = / (#{ lista_de %r"(?:(?:#{ DIAS_DE_LA_SEMANA.join('|') }) )?\d{1,2}" })\s de\s(#{ MESES.compact.join('|') }) # Mes (?:\s#{ year })? # Año opcional (\s#{ horarios })? # Hora opcional /ixo DAY_PERIOD = / (?: ( (?:#{ rango_de DIAS_DE_LA_SEMANA })\s | (?:#{ lista_de DIAS_DE_LA_SEMANA })\s ) (?-x:de |del ) )? (?-x:(?:de |del )?(?:(#{ DIAS_DE_LA_SEMANA.join('|') }) )?(\d{1,2})(?: de (#{ MESES.compact.join('|') })(?: #{ year })?)?) (?-x: (?:a|al) (?:(#{ DIAS_DE_LA_SEMANA.join('|') }) )?(\d{1,2}) de (#{ MESES.compact.join('|') })(?: #{ year })?) (?:\s#{ horarios })? /ixo WHOLE_MONTH = / ( # Dias de la semana opcionales (?-x:los |todos los )? (?: (?:#{ rango_de DIAS_DE_LA_SEMANA }) | (?:#{ lista_de DIAS_DE_LA_SEMANA }) )\s (?-x:de|durante|durante todo)\s )? (?: (?:#{ rango_de %r"(#{ MESES.compact.join('|') })(?:\s#{ year })?" }) | (#{ lista_de MESES.compact })(?:\s#{ year })? ) (?:\s#{ horarios })? # Hora opcional /ixo def event_parse string, opts = {}, &block parser = opts.delete(:parser) || (self == Eventual ? DateTime : self) use_trailing = opts.delete(:use_trailing) string = string.gsub('miercoles', 'miércoles').gsub('sabado', 'sábado').gsub(/'/, '').gsub(/(\s)+/, '\1') results = [] scanner = StringScanner.new string map_months = lambda{ |months| months.scan(/#{ MESES.compact.join('|') }/).map{ |m| MESES.index(m.downcase) } } make_range = lambda{ |first, last, min, max| first > last ? (first..max).map + (min..last).map : (first..last).map } extract_wdays = lambda do |wdays| wdays_array = wdays ? wdays.scan( /#{ WDAY_LIST.join('|') }/ ).map{ |d| WDAY_LIST.index d.downcase } : [] next wdays_array unless rango_de(DIAS_DE_LA_SEMANA) === wdays make_range[wdays_array.first, wdays_array.last, 0, 6] end until scanner.eos? case match = scanner.scan(/.*?(?:#{ DAY_LIST }|#{ DAY_PERIOD }|#{ WHOLE_MONTH })/m) when DAY_PERIOD wdays, first_wday, first_day, first_month, first_year, last_wday, last_day, last_month, last_year, times = $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 wdays_array = extract_wdays[wdays] last_year ||= string.match(/\d{4}/) ? $& : Date.today.year first_year ||= last_year first_month ||= last_month last_month = MESES.index last_month.downcase first_month = MESES.index first_month.downcase make_days = lambda do |hour, minute| first = make_day parser, first_year, first_month, first_day, hour, minute last = make_day parser, last_year, last_month, last_day, hour, minute [first, last].zip([first_wday, last_wday]).each do |day, wday| raise WdayMatchError.new("El día #{ day } cae en #{ DIAS_DE_LA_SEMANA[day.wday] }, no en #{ wday.downcase }") unless day.wday == WDAY_LIST.index(wday.downcase) if wday end next (first..last).map if wdays_array.empty? (first..last).select{ |day| wdays_array.include? day.wday } end when DAY_LIST daynums, month, year, times = $1, MESES.index($2.downcase), $3, $4 year ||= string.match(/\d{4}/) ? $& : Date.today.year make_days = lambda do |hour, minute| daynums.scan(/(?:(#{ DIAS_DE_LA_SEMANA.join('|') }) )?(\d{1,2})/).collect do |wday, daynum| day = make_day(parser, year, month, daynum, hour, minute) raise WdayMatchError.new("El día #{ day } cae en #{ DIAS_DE_LA_SEMANA[day.wday] }, no en #{ wday.downcase }") unless day.wday == WDAY_LIST.index(wday.downcase) if wday day end end when WHOLE_MONTH wdays, month_range_start, starting_year, month_range_end, ending_year, months, year, times = $1, $2, $3, $4, $5, $6, $7, $8 wdays_array = extract_wdays[wdays] month_array = if month_range_start ending_year ||= Date.today.year first = Date.civil( (starting_year || ending_year).to_i, MESES.index(month_range_start.downcase) ) last = Date.civil ending_year.to_i, MESES.index(month_range_end.downcase) (first..last) else year ||= Date.today.year months.scan(/#{ MESES.compact.join('|') }/).map do |m| Date.civil year.to_i, MESES.index(m.downcase) end end make_days = lambda do |hour, minute| months = month_array.map do |date| first = make_day(parser, date.year, date.month, 1, hour, minute) last = (first >> 1) - 1 next (first..last).map if wdays_array.empty? (first..last).select{ |day| wdays_array.include? day.wday } end.flatten end else break end extra = scanner.scan(/.*?(?=#{ DAY_LIST }|#{ DAY_PERIOD }|\z)/m).to_s.chomp extra.gsub!(/^(,|\n|\.)/, '') days = if times times.scan( /(\d{2}):(\d{2})/ ).map{ |hour, minute| make_days.call hour, minute }.flatten else make_days.call nil, nil end days.each { |day| day.instance_variable_set('@extra', extra) } results += days end raise ArgumentError.new( 'El formato de las fechas parece ser incorrecto' ) if results.empty? results.uniq! results.sort! use_trailing ? results.map!{ |day| yield day, day.instance_variable_get('@extra') } : results.map!{ |day| yield day } if block_given? results end protected def make_day *args maker = args.shift if maker == Date maker.civil *args.compact.collect{ |a| a.to_i }[0...4] else maker.civil *args.compact.collect{ |a| a.to_i } end end end