mirror of
https://github.com/mistergibson/gxg-web-client.git
synced 2026-08-15 03:26:00 -07:00
3448 lines
100 KiB
Ruby
3448 lines
100 KiB
Ruby
#
|
|
require 'time'
|
|
require 'date'
|
|
#
|
|
module Chronic
|
|
class Anchor
|
|
#
|
|
end
|
|
#
|
|
module Handlers
|
|
module_function
|
|
|
|
# Handle month/day
|
|
def handle_m_d(month, day, time_tokens, options)
|
|
month.start = self.now
|
|
span = month.this(options[:context])
|
|
year, month = span.begin.year, span.begin.month
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
|
|
day_or_time(day_start, time_tokens, options)
|
|
end
|
|
|
|
# Handle repeater-month-name/scalar-day
|
|
def handle_rmn_sd(tokens, options)
|
|
month = tokens[0].get_tag(RepeaterMonthName)
|
|
day = tokens[1].get_tag(ScalarDay).type
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[2..tokens.size], options)
|
|
end
|
|
|
|
# Handle repeater-month-name/scalar-day with separator-on
|
|
def handle_rmn_sd_on(tokens, options)
|
|
if tokens.size > 3
|
|
month = tokens[2].get_tag(RepeaterMonthName)
|
|
day = tokens[3].get_tag(ScalarDay).type
|
|
token_range = 0..1
|
|
else
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(ScalarDay).type
|
|
token_range = 0..0
|
|
end
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[token_range], options)
|
|
end
|
|
|
|
# Handle repeater-month-name/ordinal-day
|
|
def handle_rmn_od(tokens, options)
|
|
month = tokens[0].get_tag(RepeaterMonthName)
|
|
day = tokens[1].get_tag(OrdinalDay).type
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[2..tokens.size], options)
|
|
end
|
|
|
|
# Handle ordinal this month
|
|
def handle_od_rm(tokens, options)
|
|
day = tokens[0].get_tag(OrdinalDay).type
|
|
month = tokens[2].get_tag(RepeaterMonth)
|
|
handle_m_d(month, day, tokens[3..tokens.size], options)
|
|
end
|
|
|
|
# Handle ordinal-day/repeater-month-name
|
|
def handle_od_rmn(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[0].get_tag(OrdinalDay).type
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[2..tokens.size], options)
|
|
end
|
|
|
|
def handle_sy_rmn_od(tokens, options)
|
|
year = tokens[0].get_tag(ScalarYear).type
|
|
month = tokens[1].get_tag(RepeaterMonthName).index
|
|
day = tokens[2].get_tag(OrdinalDay).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle scalar-day/repeater-month-name
|
|
def handle_sd_rmn(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[0].get_tag(ScalarDay).type
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[2..tokens.size], options)
|
|
end
|
|
|
|
# Handle repeater-month-name/ordinal-day with separator-on
|
|
def handle_rmn_od_on(tokens, options)
|
|
if tokens.size > 3
|
|
month = tokens[2].get_tag(RepeaterMonthName)
|
|
day = tokens[3].get_tag(OrdinalDay).type
|
|
token_range = 0..1
|
|
else
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(OrdinalDay).type
|
|
token_range = 0..0
|
|
end
|
|
|
|
return if month_overflow?(self.now.year, month.index, day)
|
|
|
|
handle_m_d(month, day, tokens[token_range], options)
|
|
end
|
|
|
|
# Handle repeater-month-name/scalar-year
|
|
def handle_rmn_sy(tokens, options)
|
|
month = tokens[0].get_tag(RepeaterMonthName).index
|
|
year = tokens[1].get_tag(ScalarYear).type
|
|
|
|
if month == 12
|
|
next_month_year = year + 1
|
|
next_month_month = 1
|
|
else
|
|
next_month_year = year
|
|
next_month_month = month + 1
|
|
end
|
|
|
|
begin
|
|
end_time = Chronic.time_class.local(next_month_year, next_month_month)
|
|
Span.new(Chronic.time_class.local(year, month), end_time)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle generic timestamp (ruby 1.8)
|
|
def handle_generic(tokens, options)
|
|
t = Chronic.time_class.parse(options[:text])
|
|
Span.new(t, t + 1)
|
|
rescue ArgumentError => e
|
|
raise e unless e.message =~ /out of range/
|
|
end
|
|
|
|
# Handle repeater-month-name/scalar-day/scalar-year
|
|
def handle_rmn_sd_sy(tokens, options)
|
|
month = tokens[0].get_tag(RepeaterMonthName).index
|
|
day = tokens[1].get_tag(ScalarDay).type
|
|
year = tokens[2].get_tag(ScalarYear).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle repeater-month-name/ordinal-day/scalar-year
|
|
def handle_rmn_od_sy(tokens, options)
|
|
month = tokens[0].get_tag(RepeaterMonthName).index
|
|
day = tokens[1].get_tag(OrdinalDay).type
|
|
year = tokens[2].get_tag(ScalarYear).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle oridinal-day/repeater-month-name/scalar-year
|
|
def handle_od_rmn_sy(tokens, options)
|
|
day = tokens[0].get_tag(OrdinalDay).type
|
|
month = tokens[1].get_tag(RepeaterMonthName).index
|
|
year = tokens[2].get_tag(ScalarYear).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle scalar-day/repeater-month-name/scalar-year
|
|
def handle_sd_rmn_sy(tokens, options)
|
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
handle_rmn_sd_sy(new_tokens + time_tokens, options)
|
|
end
|
|
|
|
# Handle scalar-month/scalar-day/scalar-year (endian middle)
|
|
def handle_sm_sd_sy(tokens, options)
|
|
month = tokens[0].get_tag(ScalarMonth).type
|
|
day = tokens[1].get_tag(ScalarDay).type
|
|
year = tokens[2].get_tag(ScalarYear).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle scalar-day/scalar-month/scalar-year (endian little)
|
|
def handle_sd_sm_sy(tokens, options)
|
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
handle_sm_sd_sy(new_tokens + time_tokens, options)
|
|
end
|
|
|
|
# Handle scalar-year/scalar-month/scalar-day
|
|
def handle_sy_sm_sd(tokens, options)
|
|
new_tokens = [tokens[1], tokens[2], tokens[0]]
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
handle_sm_sd_sy(new_tokens + time_tokens, options)
|
|
end
|
|
|
|
# Handle scalar-month/scalar-day
|
|
def handle_sm_sd(tokens, options)
|
|
month = tokens[0].get_tag(ScalarMonth).type
|
|
day = tokens[1].get_tag(ScalarDay).type
|
|
year = self.now.year
|
|
time_tokens = tokens.last(tokens.size - 2)
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_start = Chronic.time_class.local(year + 1, month, day) if options[:context] == :future && day_start < now
|
|
day_or_time(day_start, time_tokens, options)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle scalar-day/scalar-month
|
|
def handle_sd_sm(tokens, options)
|
|
new_tokens = [tokens[1], tokens[0]]
|
|
time_tokens = tokens.last(tokens.size - 2)
|
|
handle_sm_sd(new_tokens + time_tokens, options)
|
|
end
|
|
|
|
def handle_year_and_month(year, month)
|
|
if month == 12
|
|
next_month_year = year + 1
|
|
next_month_month = 1
|
|
else
|
|
next_month_year = year
|
|
next_month_month = month + 1
|
|
end
|
|
|
|
begin
|
|
end_time = Chronic.time_class.local(next_month_year, next_month_month)
|
|
Span.new(Chronic.time_class.local(year, month), end_time)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle scalar-month/scalar-year
|
|
def handle_sm_sy(tokens, options)
|
|
month = tokens[0].get_tag(ScalarMonth).type
|
|
year = tokens[1].get_tag(ScalarYear).type
|
|
handle_year_and_month(year, month)
|
|
end
|
|
|
|
# Handle scalar-year/scalar-month
|
|
def handle_sy_sm(tokens, options)
|
|
year = tokens[0].get_tag(ScalarYear).type
|
|
month = tokens[1].get_tag(ScalarMonth).type
|
|
handle_year_and_month(year, month)
|
|
end
|
|
|
|
# Handle RepeaterDayName RepeaterMonthName OrdinalDay
|
|
def handle_rdn_rmn_od(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(OrdinalDay).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
year = self.now.year
|
|
|
|
return if month_overflow?(year, month.index, day)
|
|
|
|
begin
|
|
if time_tokens.empty?
|
|
start_time = Chronic.time_class.local(year, month.index, day)
|
|
end_time = time_with_rollover(year, month.index, day + 1)
|
|
Span.new(start_time, end_time)
|
|
else
|
|
day_start = Chronic.time_class.local(year, month.index, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
end
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle RepeaterDayName RepeaterMonthName OrdinalDay ScalarYear
|
|
def handle_rdn_rmn_od_sy(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(OrdinalDay).type
|
|
year = tokens[3].get_tag(ScalarYear).type
|
|
|
|
return if month_overflow?(year, month.index, day)
|
|
|
|
begin
|
|
start_time = Chronic.time_class.local(year, month.index, day)
|
|
end_time = time_with_rollover(year, month.index, day + 1)
|
|
Span.new(start_time, end_time)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle RepeaterDayName OrdinalDay
|
|
def handle_rdn_od(tokens, options)
|
|
day = tokens[1].get_tag(OrdinalDay).type
|
|
time_tokens = tokens.last(tokens.size - 2)
|
|
year = self.now.year
|
|
month = self.now.month
|
|
if options[:context] == :future
|
|
self.now.day > day ? month += 1 : month
|
|
end
|
|
|
|
return if month_overflow?(year, month, day)
|
|
|
|
begin
|
|
if time_tokens.empty?
|
|
start_time = Chronic.time_class.local(year, month, day)
|
|
end_time = time_with_rollover(year, month, day + 1)
|
|
Span.new(start_time, end_time)
|
|
else
|
|
day_start = Chronic.time_class.local(year, month, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
end
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle RepeaterDayName RepeaterMonthName ScalarDay
|
|
def handle_rdn_rmn_sd(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(ScalarDay).type
|
|
time_tokens = tokens.last(tokens.size - 3)
|
|
year = self.now.year
|
|
|
|
return if month_overflow?(year, month.index, day)
|
|
|
|
begin
|
|
if time_tokens.empty?
|
|
start_time = Chronic.time_class.local(year, month.index, day)
|
|
end_time = time_with_rollover(year, month.index, day + 1)
|
|
Span.new(start_time, end_time)
|
|
else
|
|
day_start = Chronic.time_class.local(year, month.index, day)
|
|
day_or_time(day_start, time_tokens, options)
|
|
end
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Handle RepeaterDayName RepeaterMonthName ScalarDay ScalarYear
|
|
def handle_rdn_rmn_sd_sy(tokens, options)
|
|
month = tokens[1].get_tag(RepeaterMonthName)
|
|
day = tokens[2].get_tag(ScalarDay).type
|
|
year = tokens[3].get_tag(ScalarYear).type
|
|
|
|
return if month_overflow?(year, month.index, day)
|
|
|
|
begin
|
|
start_time = Chronic.time_class.local(year, month.index, day)
|
|
end_time = time_with_rollover(year, month.index, day + 1)
|
|
Span.new(start_time, end_time)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
end
|
|
|
|
def handle_sm_rmn_sy(tokens, options)
|
|
day = tokens[0].get_tag(ScalarDay).type
|
|
month = tokens[1].get_tag(RepeaterMonthName).index
|
|
year = tokens[2].get_tag(ScalarYear).type
|
|
if tokens.size > 3
|
|
time = get_anchor([tokens.last], options).begin
|
|
h, m, s = time.hour, time.min, time.sec
|
|
time = Chronic.time_class.local(year, month, day, h, m, s)
|
|
end_time = Chronic.time_class.local(year, month, day + 1, h, m, s)
|
|
else
|
|
time = Chronic.time_class.local(year, month, day)
|
|
day += 1 unless day >= 31
|
|
end_time = Chronic.time_class.local(year, month, day)
|
|
end
|
|
Span.new(time, end_time)
|
|
end
|
|
|
|
# anchors
|
|
|
|
# Handle repeaters
|
|
def handle_r(tokens, options)
|
|
dd_tokens = dealias_and_disambiguate_times(tokens, options)
|
|
get_anchor(dd_tokens, options)
|
|
end
|
|
|
|
# Handle repeater/grabber/repeater
|
|
def handle_r_g_r(tokens, options)
|
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
|
handle_r(new_tokens, options)
|
|
end
|
|
|
|
# arrows
|
|
|
|
# Handle scalar/repeater/pointer helper
|
|
def handle_srp(tokens, span, options)
|
|
distance = tokens[0].get_tag(Scalar).type
|
|
repeater = tokens[1].get_tag(Repeater)
|
|
pointer = tokens[2].get_tag(Pointer).type
|
|
|
|
repeater.offset(span, distance, pointer) if repeater.respond_to?(:offset)
|
|
end
|
|
|
|
# Handle scalar/repeater/pointer
|
|
def handle_s_r_p(tokens, options)
|
|
span = Span.new(self.now, self.now + 1)
|
|
|
|
handle_srp(tokens, span, options)
|
|
end
|
|
|
|
# Handle pointer/scalar/repeater
|
|
def handle_p_s_r(tokens, options)
|
|
new_tokens = [tokens[1], tokens[2], tokens[0]]
|
|
handle_s_r_p(new_tokens, options)
|
|
end
|
|
|
|
# Handle scalar/repeater/pointer/anchor
|
|
def handle_s_r_p_a(tokens, options)
|
|
anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
|
|
handle_srp(tokens, anchor_span, options)
|
|
end
|
|
|
|
def handle_s_r_a_s_r_p_a(tokens, options)
|
|
anchor_span = get_anchor(tokens[4..tokens.size - 1], options)
|
|
|
|
span = handle_srp(tokens[0..1]+tokens[4..6], anchor_span, options)
|
|
handle_srp(tokens[2..3]+tokens[4..6], span, options)
|
|
end
|
|
|
|
# narrows
|
|
|
|
# Handle oridinal repeaters
|
|
def handle_orr(tokens, outer_span, options)
|
|
repeater = tokens[1].get_tag(Repeater)
|
|
repeater.start = outer_span.begin - 1
|
|
ordinal = tokens[0].get_tag(Ordinal).type
|
|
span = nil
|
|
|
|
ordinal.times do
|
|
span = repeater.next(:future)
|
|
|
|
if span.begin >= outer_span.end
|
|
span = nil
|
|
break
|
|
end
|
|
end
|
|
|
|
span
|
|
end
|
|
|
|
# Handle ordinal/repeater/separator/repeater
|
|
def handle_o_r_s_r(tokens, options)
|
|
outer_span = get_anchor([tokens[3]], options)
|
|
handle_orr(tokens[0..1], outer_span, options)
|
|
end
|
|
|
|
# Handle ordinal/repeater/grabber/repeater
|
|
def handle_o_r_g_r(tokens, options)
|
|
outer_span = get_anchor(tokens[2..3], options)
|
|
handle_orr(tokens[0..1], outer_span, options)
|
|
end
|
|
|
|
# support methods
|
|
|
|
def day_or_time(day_start, time_tokens, options)
|
|
outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
|
|
|
|
unless time_tokens.empty?
|
|
self.now = outer_span.begin
|
|
get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future))
|
|
else
|
|
outer_span
|
|
end
|
|
end
|
|
|
|
def get_anchor(tokens, options)
|
|
grabber = Grabber.new(:this)
|
|
pointer = :future
|
|
repeaters = get_repeaters(tokens)
|
|
repeaters.size.times { tokens.pop }
|
|
|
|
if tokens.first && tokens.first.get_tag(Grabber)
|
|
grabber = tokens.shift.get_tag(Grabber)
|
|
end
|
|
|
|
head = repeaters.shift
|
|
head.start = self.now
|
|
|
|
case grabber.type
|
|
when :last
|
|
outer_span = head.next(:past)
|
|
when :this
|
|
if options[:context] != :past and repeaters.size > 0
|
|
outer_span = head.this(:none)
|
|
else
|
|
outer_span = head.this(options[:context])
|
|
end
|
|
when :next
|
|
outer_span = head.next(:future)
|
|
else
|
|
raise "Invalid grabber"
|
|
end
|
|
|
|
if Chronic.debug
|
|
puts "Handler-class: #{head.class}"
|
|
puts "--#{outer_span}"
|
|
end
|
|
|
|
find_within(repeaters, outer_span, pointer)
|
|
end
|
|
|
|
def get_repeaters(tokens)
|
|
tokens.map { |token| token.get_tag(Repeater) }.compact.sort.reverse
|
|
end
|
|
|
|
def month_overflow?(year, month, day)
|
|
if ::Date.leap?(year)
|
|
day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
|
|
else
|
|
day > RepeaterMonth::MONTH_DAYS[month - 1]
|
|
end
|
|
rescue ArgumentError
|
|
false
|
|
end
|
|
|
|
# Recursively finds repeaters within other repeaters.
|
|
# Returns a Span representing the innermost time span
|
|
# or nil if no repeater union could be found
|
|
def find_within(tags, span, pointer)
|
|
puts "--#{span}" if Chronic.debug
|
|
return span if tags.empty?
|
|
|
|
head = tags.shift
|
|
head.start = (pointer == :future ? span.begin : span.end)
|
|
h = head.this(:none)
|
|
|
|
if span.cover?(h.begin) || span.cover?(h.end)
|
|
find_within(tags, h, pointer)
|
|
end
|
|
end
|
|
|
|
def time_with_rollover(year, month, day)
|
|
date_parts =
|
|
if month_overflow?(year, month, day)
|
|
if month == 12
|
|
[year + 1, 1, 1]
|
|
else
|
|
[year, month + 1, 1]
|
|
end
|
|
else
|
|
[year, month, day]
|
|
end
|
|
Chronic.time_class.local(*date_parts)
|
|
end
|
|
|
|
def dealias_and_disambiguate_times(tokens, options)
|
|
# handle aliases of am/pm
|
|
# 5:00 in the morning -> 5:00 am
|
|
# 7:00 in the evening -> 7:00 pm
|
|
|
|
day_portion_index = nil
|
|
tokens.each_with_index do |t, i|
|
|
if t.get_tag(RepeaterDayPortion)
|
|
day_portion_index = i
|
|
break
|
|
end
|
|
end
|
|
|
|
time_index = nil
|
|
tokens.each_with_index do |t, i|
|
|
if t.get_tag(RepeaterTime)
|
|
time_index = i
|
|
break
|
|
end
|
|
end
|
|
|
|
if day_portion_index && time_index
|
|
t1 = tokens[day_portion_index]
|
|
t1tag = t1.get_tag(RepeaterDayPortion)
|
|
|
|
case t1tag.type
|
|
when :morning
|
|
puts '--morning->am' if Chronic.debug
|
|
t1.untag(RepeaterDayPortion)
|
|
t1.tag(RepeaterDayPortion.new(:am))
|
|
when :afternoon, :evening, :night
|
|
puts "--#{t1tag.type}->pm" if Chronic.debug
|
|
t1.untag(RepeaterDayPortion)
|
|
t1.tag(RepeaterDayPortion.new(:pm))
|
|
end
|
|
end
|
|
|
|
# handle ambiguous times if :ambiguous_time_range is specified
|
|
if options[:ambiguous_time_range] != :none
|
|
ambiguous_tokens = []
|
|
|
|
tokens.each_with_index do |token, i|
|
|
ambiguous_tokens << token
|
|
next_token = tokens[i + 1]
|
|
|
|
if token.get_tag(RepeaterTime) && token.get_tag(RepeaterTime).type.ambiguous? && (!next_token || !next_token.get_tag(RepeaterDayPortion))
|
|
distoken = Token.new('disambiguator')
|
|
|
|
distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
|
|
ambiguous_tokens << distoken
|
|
end
|
|
end
|
|
|
|
tokens = ambiguous_tokens
|
|
end
|
|
|
|
tokens
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
#
|
|
module Chronic
|
|
class Parser
|
|
include Handlers
|
|
|
|
# Hash of default configuration options.
|
|
DEFAULT_OPTIONS = {
|
|
:context => :future,
|
|
:now => nil,
|
|
:hours24 => nil,
|
|
:guess => true,
|
|
:ambiguous_time_range => 6,
|
|
:endian_precedence => [:middle, :little],
|
|
:ambiguous_year_future_bias => 50
|
|
}
|
|
|
|
attr_accessor :now
|
|
attr_reader :options
|
|
|
|
# options - An optional Hash of configuration options:
|
|
# :context - If your string represents a birthday, you can set
|
|
# this value to :past and if an ambiguous string is
|
|
# given, it will assume it is in the past.
|
|
# :now - Time, all computations will be based off of time
|
|
# instead of Time.now.
|
|
# :hours24 - Time will be parsed as it would be 24 hour clock.
|
|
# :guess - By default the parser will guess a single point in time
|
|
# for the given date or time. If you'd rather have the
|
|
# entire time span returned, set this to false
|
|
# and a Chronic::Span will be returned. Setting :guess to :end
|
|
# will return last time from Span, to :middle for middle (same as just true)
|
|
# and :begin for first time from span.
|
|
# :ambiguous_time_range - If an Integer is given, ambiguous times
|
|
# (like 5:00) will be assumed to be within the range of
|
|
# that time in the AM to that time in the PM. For
|
|
# example, if you set it to `7`, then the parser will
|
|
# look for the time between 7am and 7pm. In the case of
|
|
# 5:00, it would assume that means 5:00pm. If `:none`
|
|
# is given, no assumption will be made, and the first
|
|
# matching instance of that time will be used.
|
|
# :endian_precedence - By default, Chronic will parse "03/04/2011"
|
|
# as the fourth day of the third month. Alternatively you
|
|
# can tell Chronic to parse this as the third day of the
|
|
# fourth month by setting this to [:little, :middle].
|
|
# :ambiguous_year_future_bias - When parsing two digit years
|
|
# (ie 79) unlike Rubys Time class, Chronic will attempt
|
|
# to assume the full year using this figure. Chronic will
|
|
# look x amount of years into the future and past. If the
|
|
# two digit year is `now + x years` it's assumed to be the
|
|
# future, `now - x years` is assumed to be the past.
|
|
def initialize(options = {})
|
|
@options = DEFAULT_OPTIONS.merge(options)
|
|
@now = options.delete(:now) || Chronic.time_class.now
|
|
end
|
|
|
|
# Parse "text" with the given options
|
|
# Returns either a Time or Chronic::Span, depending on the value of options[:guess]
|
|
def parse(text)
|
|
tokens = tokenize(text, options)
|
|
span = tokens_to_span(tokens, options.merge(:text => text))
|
|
|
|
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
|
|
|
|
guess(span, options[:guess]) if span
|
|
end
|
|
|
|
# Clean up the specified text ready for parsing.
|
|
#
|
|
# Clean up the string by stripping unwanted characters, converting
|
|
# idioms to their canonical form, converting number words to numbers
|
|
# (three => 3), and converting ordinal words to numeric
|
|
# ordinals (third => 3rd)
|
|
#
|
|
# text - The String text to normalize.
|
|
#
|
|
# Examples:
|
|
#
|
|
# Chronic.pre_normalize('first day in May')
|
|
# #=> "1st day in may"
|
|
#
|
|
# Chronic.pre_normalize('tomorrow after noon')
|
|
# #=> "next day future 12:00"
|
|
#
|
|
# Chronic.pre_normalize('one hundred and thirty six days from now')
|
|
# #=> "136 days future this second"
|
|
#
|
|
# Returns a new String ready for Chronic to parse.
|
|
def pre_normalize(text)
|
|
text = text.to_s.downcase
|
|
text = text.gsub(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
|
|
text = text.gsub(/\b([ap])\.m\.?/, '\1m')
|
|
text = text.gsub(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
|
|
text = text.gsub(/\./, ':')
|
|
text = text.gsub(/([ap]):m:?/, '\1m')
|
|
text = text.gsub(/['"]/, '')
|
|
text = text.gsub(/,/, ' ')
|
|
text = text.gsub(/^second /, '2nd ')
|
|
text = text.gsub(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
|
|
text = Numerizer.numerize(text)
|
|
text = text.gsub(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
|
text = text.gsub(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
|
|
text = text.gsub(/\btoday\b/, 'this day')
|
|
text = text.gsub(/\btomm?orr?ow\b/, 'next day')
|
|
text = text.gsub(/\byesterday\b/, 'last day')
|
|
text = text.gsub(/\bnoon\b/, '12:00pm')
|
|
text = text.gsub(/\bmidnight\b/, '24:00')
|
|
text = text.gsub(/\bnow\b/, 'this second')
|
|
text = text.gsub('quarter', '15')
|
|
text = text.gsub('half', '30')
|
|
text = text.gsub(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
|
|
text = text.gsub(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
|
|
text = text.gsub(/\b(?:ago|before(?: now)?)\b/, 'past')
|
|
text = text.gsub(/\bthis (?:last|past)\b/, 'last')
|
|
text = text.gsub(/\b(?:in|during) the (morning)\b/, '\1')
|
|
text = text.gsub(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
|
|
text = text.gsub(/\btonight\b/, 'this night')
|
|
text = text.gsub(/\b\d+:?\d*[ap]\b/,'\0m')
|
|
text = text.gsub(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
|
|
text = text.gsub(/(\d)([ap]m|oclock)\b/, '\1 \2')
|
|
text = text.gsub(/\b(hence|after|from)\b/, 'future')
|
|
text = text.gsub(/^\s?an? /i, '1 ')
|
|
text = text.gsub(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
|
|
text = text.gsub(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
|
|
text
|
|
end
|
|
|
|
# Guess a specific time within the given span.
|
|
#
|
|
# span - The Chronic::Span object to calcuate a guess from.
|
|
#
|
|
# Returns a new Time object.
|
|
def guess(span, mode = :middle)
|
|
return span if not mode
|
|
return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
|
|
return span.end if mode == :end
|
|
span.begin
|
|
end
|
|
|
|
# List of Handler definitions. See Chronic.parse for a list of options this
|
|
# method accepts.
|
|
#
|
|
# options - An optional Hash of configuration options.
|
|
#
|
|
# Returns a Hash of Handler definitions.
|
|
def definitions(options = {})
|
|
options[:endian_precedence] ||= [:middle, :little]
|
|
|
|
@@definitions ||= {
|
|
:time => [
|
|
Handler.new([:repeater_time, :repeater_day_portion?], nil)
|
|
],
|
|
|
|
:date => [
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd),
|
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od),
|
|
Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od),
|
|
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic),
|
|
Handler.new([:ordinal_day], :handle_generic),
|
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
|
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
|
|
Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
|
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
|
Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
|
|
Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
|
|
Handler.new([:ordinal_day, :grabber?, :repeater_month, :separator_at?, 'time?'], :handle_od_rm),
|
|
Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
|
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
|
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
|
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
|
Handler.new([:scalar_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
|
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
|
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm),
|
|
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy),
|
|
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
|
|
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic),
|
|
],
|
|
|
|
:anchor => [
|
|
Handler.new([:separator_on?, :grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
|
Handler.new([:grabber?, :repeater, :repeater, :separator?, :repeater?, :repeater?], :handle_r),
|
|
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
|
|
],
|
|
|
|
:arrow => [
|
|
Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
|
|
Handler.new([:scalar, :repeater, :separator_and?, :scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_a_s_r_p_a),
|
|
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
|
|
Handler.new([:scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_p_a)
|
|
],
|
|
|
|
:narrow => [
|
|
Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
|
|
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
|
|
]
|
|
}
|
|
|
|
endians = [
|
|
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
|
|
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd),
|
|
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm),
|
|
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
|
]
|
|
|
|
case endian = Array(options[:endian_precedence]).first
|
|
when :little
|
|
@@definitions.merge(:endian => endians.reverse)
|
|
when :middle
|
|
@@definitions.merge(:endian => endians)
|
|
else
|
|
raise ArgumentError, "Unknown endian option '#{endian}'"
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def tokenize(text, options)
|
|
text = pre_normalize(text)
|
|
tokens = text.split(' ').map { |word| Token.new(word) }
|
|
[Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok|
|
|
tok.scan(tokens, options)
|
|
end
|
|
tokens.select { |token| token.tagged? }
|
|
end
|
|
|
|
def tokens_to_span(tokens, options)
|
|
definitions = definitions(options)
|
|
|
|
(definitions[:endian] + definitions[:date]).each do |handler|
|
|
if handler.match(tokens, definitions)
|
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
return handler.invoke(:date, good_tokens, self, options)
|
|
end
|
|
end
|
|
|
|
definitions[:anchor].each do |handler|
|
|
if handler.match(tokens, definitions)
|
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
return handler.invoke(:anchor, good_tokens, self, options)
|
|
end
|
|
end
|
|
|
|
definitions[:arrow].each do |handler|
|
|
if handler.match(tokens, definitions)
|
|
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
|
|
return handler.invoke(:arrow, good_tokens, self, options)
|
|
end
|
|
end
|
|
|
|
definitions[:narrow].each do |handler|
|
|
if handler.match(tokens, definitions)
|
|
return handler.invoke(:narrow, tokens, self, options)
|
|
end
|
|
end
|
|
|
|
puts "-none" if Chronic.debug
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Time
|
|
HOUR_SECONDS = 3600 # 60 * 60
|
|
MINUTE_SECONDS = 60
|
|
SECOND_SECONDS = 1 # haha, awesome
|
|
SUBSECOND_SECONDS = 0.001
|
|
|
|
# Checks if given number could be hour
|
|
def self.could_be_hour?(hour)
|
|
hour >= 0 && hour <= 24
|
|
end
|
|
|
|
# Checks if given number could be minute
|
|
def self.could_be_minute?(minute)
|
|
minute >= 0 && minute <= 60
|
|
end
|
|
|
|
# Checks if given number could be second
|
|
def self.could_be_second?(second)
|
|
second >= 0 && second <= 60
|
|
end
|
|
|
|
# Checks if given number could be subsecond
|
|
def self.could_be_subsecond?(subsecond)
|
|
subsecond >= 0 && subsecond <= 999999
|
|
end
|
|
|
|
# normalize offset in seconds to offset as string +mm:ss or -mm:ss
|
|
def self.normalize_offset(offset)
|
|
return offset if offset.is_a?(String)
|
|
offset = Chronic.time_class.now.to_time.utc_offset unless offset # get current system's UTC offset if offset is nil
|
|
sign = '+'
|
|
sign = '-' if offset < 0
|
|
hours = (offset.abs / 3600).to_i.to_s.rjust(2,'0')
|
|
minutes = (offset.abs % 3600).to_s.rjust(2,'0')
|
|
sign + hours + minutes
|
|
end
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Date
|
|
YEAR_MONTHS = 12
|
|
MONTH_DAYS = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
MONTH_DAYS_LEAP = [nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
YEAR_SECONDS = 31_536_000 # 365 * 24 * 60 * 60
|
|
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
|
|
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
|
|
FORTNIGHT_SECONDS = 1_209_600 # 14 * 24 * 60 * 60
|
|
WEEK_SECONDS = 604_800 # 7 * 24 * 60 * 60
|
|
WEEKEND_SECONDS = 172_800 # 2 * 24 * 60 * 60
|
|
DAY_SECONDS = 86_400 # 24 * 60 * 60
|
|
MONTHS = {
|
|
:january => 1,
|
|
:february => 2,
|
|
:march => 3,
|
|
:april => 4,
|
|
:may => 5,
|
|
:june => 6,
|
|
:july => 7,
|
|
:august => 8,
|
|
:september => 9,
|
|
:october => 10,
|
|
:november => 11,
|
|
:december => 12
|
|
}
|
|
DAYS = {
|
|
:sunday => 0,
|
|
:monday => 1,
|
|
:tuesday => 2,
|
|
:wednesday => 3,
|
|
:thursday => 4,
|
|
:friday => 5,
|
|
:saturday => 6
|
|
}
|
|
|
|
# Checks if given number could be day
|
|
def self.could_be_day?(day)
|
|
day >= 1 && day <= 31
|
|
end
|
|
|
|
# Checks if given number could be month
|
|
def self.could_be_month?(month)
|
|
month >= 1 && month <= 12
|
|
end
|
|
|
|
# Checks if given number could be year
|
|
def self.could_be_year?(year)
|
|
year >= 1 && year <= 9999
|
|
end
|
|
|
|
# Build a year from a 2 digit suffix.
|
|
#
|
|
# year - The two digit Integer year to build from.
|
|
# bias - The Integer amount of future years to bias.
|
|
#
|
|
# Examples:
|
|
#
|
|
# make_year(96, 50) #=> 1996
|
|
# make_year(79, 20) #=> 2079
|
|
# make_year(00, 50) #=> 2000
|
|
#
|
|
# Returns The Integer 4 digit year.
|
|
def self.make_year(year, bias)
|
|
return year if year.to_s.size > 2
|
|
start_year = Chronic.time_class.now.year - bias
|
|
century = (start_year / 100) * 100
|
|
full_year = century + year
|
|
full_year += 100 if full_year < start_year
|
|
full_year
|
|
end
|
|
|
|
def self.month_overflow?(year, month, day)
|
|
if ::Date.leap?(year)
|
|
day > Date::MONTH_DAYS_LEAP[month]
|
|
else
|
|
day > Date::MONTH_DAYS[month]
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Handler
|
|
|
|
attr_reader :pattern
|
|
|
|
attr_reader :handler_method
|
|
|
|
# pattern - An Array of patterns to match tokens against.
|
|
# handler_method - A Symbol representing the method to be invoked
|
|
# when a pattern matches.
|
|
def initialize(pattern, handler_method)
|
|
@pattern = pattern
|
|
@handler_method = handler_method
|
|
end
|
|
|
|
# tokens - An Array of tokens to process.
|
|
# definitions - A Hash of definitions to check against.
|
|
#
|
|
# Returns true if a match is found.
|
|
def match(tokens, definitions)
|
|
token_index = 0
|
|
@pattern.each do |elements|
|
|
was_optional = false
|
|
elements = [elements] unless elements.is_a?(Array)
|
|
|
|
elements.each_index do |i|
|
|
name = elements[i].to_s
|
|
optional = name[-1, 1] == '?'
|
|
name = name.chop if optional
|
|
|
|
case elements[i]
|
|
when Symbol
|
|
if tags_match?(name, tokens, token_index)
|
|
token_index += 1
|
|
break
|
|
else
|
|
if optional
|
|
was_optional = true
|
|
next
|
|
elsif i + 1 < elements.count
|
|
next
|
|
else
|
|
return false unless was_optional
|
|
end
|
|
end
|
|
|
|
when String
|
|
return true if optional && token_index == tokens.size
|
|
|
|
if definitions.key?(name.to_sym)
|
|
sub_handlers = definitions[name.to_sym]
|
|
else
|
|
raise "Invalid subset #{name} specified"
|
|
end
|
|
|
|
sub_handlers.each do |sub_handler|
|
|
return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
|
|
end
|
|
else
|
|
raise "Invalid match type: #{elements[i].class}"
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
return false if token_index != tokens.size
|
|
return true
|
|
end
|
|
|
|
def invoke(type, tokens, parser, options)
|
|
if Chronic.debug
|
|
puts "-#{type}"
|
|
puts "Handler: #{@handler_method}"
|
|
end
|
|
|
|
parser.send(@handler_method, tokens, options)
|
|
end
|
|
|
|
# other - The other Handler object to compare.
|
|
#
|
|
# Returns true if these Handlers match.
|
|
def ==(other)
|
|
@pattern == other.pattern
|
|
end
|
|
|
|
private
|
|
|
|
def tags_match?(name, tokens, token_index)
|
|
klass = Chronic.const_get(name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
|
|
|
|
if tokens[token_index]
|
|
!tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class MiniDate
|
|
attr_accessor :month, :day
|
|
|
|
def self.from_time(time)
|
|
new(time.month, time.day)
|
|
end
|
|
|
|
def initialize(month, day)
|
|
unless (1..12).include?(month)
|
|
raise ArgumentError, "1..12 are valid months"
|
|
end
|
|
|
|
@month = month
|
|
@day = day
|
|
end
|
|
|
|
def is_between?(md_start, md_end)
|
|
return false if (@month == md_start.month && @month == md_end.month) &&
|
|
(@day < md_start.day || @day > md_end.day)
|
|
return true if (@month == md_start.month && @day >= md_start.day) ||
|
|
(@month == md_end.month && @day <= md_end.day)
|
|
|
|
i = (md_start.month % 12) + 1
|
|
|
|
until i == md_end.month
|
|
return true if @month == i
|
|
i = (i % 12) + 1
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
def equals?(other)
|
|
@month == other.month and @day == other.day
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
# Tokens are tagged with subclassed instances of this class when
|
|
# they match specific criteria.
|
|
class Tag
|
|
|
|
attr_accessor :type
|
|
|
|
# type - The Symbol type of this tag.
|
|
def initialize(type, options = {})
|
|
@type = type
|
|
@options = options
|
|
end
|
|
|
|
# time - Set the start Time for this Tag.
|
|
def start=(time)
|
|
@now = time
|
|
end
|
|
|
|
class << self
|
|
private
|
|
|
|
def scan_for(token, klass, items={}, options = {})
|
|
case items
|
|
when Regexp
|
|
return klass.new(token.word, options) if items =~ token.word
|
|
when Hash
|
|
items.each do |item, symbol|
|
|
return klass.new(symbol, options) if item =~ token.word
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
# A Span represents a range of time. Since this class extends
|
|
# Range, you can use #begin and #end to get the beginning and
|
|
# ending times of the span (they will be of class Time)
|
|
class Span < Range
|
|
# Returns the width of this span in seconds
|
|
def width
|
|
(self.end - self.begin).to_i
|
|
end
|
|
|
|
# Add a number of seconds to this span, returning the
|
|
# resulting Span
|
|
def +(seconds)
|
|
Span.new(self.begin + seconds, self.end + seconds)
|
|
end
|
|
|
|
# Subtract a number of seconds to this span, returning the
|
|
# resulting Span
|
|
def -(seconds)
|
|
self + -seconds
|
|
end
|
|
|
|
# Prints this span in a nice fashion
|
|
def to_s
|
|
('(' + self.begin.to_s + '..' + self.end.to_s + ')')
|
|
end
|
|
|
|
alias :cover? :include? if RUBY_VERSION =~ /^1.8/
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Token
|
|
|
|
attr_accessor :word
|
|
attr_accessor :tags
|
|
|
|
def initialize(word)
|
|
@word = word
|
|
@tags = []
|
|
end
|
|
|
|
# Tag this token with the specified tag.
|
|
#
|
|
# new_tag - The new Tag object.
|
|
#
|
|
# Returns nothing.
|
|
def tag(new_tag)
|
|
@tags << new_tag
|
|
end
|
|
|
|
# Remove all tags of the given class.
|
|
#
|
|
# tag_class - The tag Class to remove.
|
|
#
|
|
# Returns nothing.
|
|
def untag(tag_class)
|
|
@tags.delete_if { |m| m.kind_of? tag_class }
|
|
end
|
|
|
|
# Returns true if this token has any tags.
|
|
def tagged?
|
|
@tags.size > 0
|
|
end
|
|
|
|
# tag_class - The tag Class to search for.
|
|
#
|
|
# Returns The first Tag that matches the given class.
|
|
def get_tag(tag_class)
|
|
@tags.find { |m| m.kind_of? tag_class }
|
|
end
|
|
|
|
# Print this Token in a pretty way
|
|
def to_s
|
|
@word = (@word + '(' + @tags.join(', ') + ') ')
|
|
end
|
|
|
|
def inspect
|
|
to_s
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Grabber < Tag
|
|
|
|
# Scan an Array of Tokens and apply any necessary Grabber tags to
|
|
# each token.
|
|
#
|
|
# tokens - An Array of Token objects to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of Token objects.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_all(token) then token.tag(t); next end
|
|
end
|
|
end
|
|
|
|
# token - The Token object to scan.
|
|
#
|
|
# Returns a new Grabber object.
|
|
def self.scan_for_all(token)
|
|
scan_for token, self,
|
|
{
|
|
/last/ => :last,
|
|
/this/ => :this,
|
|
/next/ => :next
|
|
}
|
|
end
|
|
|
|
def to_s
|
|
('grabber-' + @type.to_s)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Pointer < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary Pointer
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_all(token) then token.tag(t) end
|
|
end
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Pointer object.
|
|
def self.scan_for_all(token)
|
|
scan_for token, self,
|
|
{
|
|
/\bpast\b/ => :past,
|
|
/\b(?:future|in)\b/ => :future,
|
|
}
|
|
end
|
|
|
|
def to_s
|
|
('pointer-' + @type.to_s)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Scalar < Tag
|
|
DAY_PORTIONS = %w( am pm morning afternoon evening night )
|
|
|
|
# Scan an Array of Token objects and apply any necessary Scalar
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each_index do |i|
|
|
token = tokens[i]
|
|
post_token = tokens[i + 1]
|
|
if token.word =~ /^\d+$/
|
|
scalar = token.word.to_i
|
|
token.tag(Scalar.new(scalar))
|
|
token.tag(ScalarSubsecond.new(scalar)) if Chronic::Time::could_be_subsecond?(scalar)
|
|
token.tag(ScalarSecond.new(scalar)) if Chronic::Time::could_be_second?(scalar)
|
|
token.tag(ScalarMinute.new(scalar)) if Chronic::Time::could_be_minute?(scalar)
|
|
token.tag(ScalarHour.new(scalar)) if Chronic::Time::could_be_hour?(scalar)
|
|
unless post_token and DAY_PORTIONS.include?(post_token.word)
|
|
token.tag(ScalarDay.new(scalar)) if Chronic::Date::could_be_day?(scalar)
|
|
token.tag(ScalarMonth.new(scalar)) if Chronic::Date::could_be_month?(scalar)
|
|
if Chronic::Date::could_be_year?(scalar)
|
|
year = Chronic::Date::make_year(scalar, options[:ambiguous_year_future_bias])
|
|
token.tag(ScalarYear.new(year.to_i))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
'scalar'
|
|
end
|
|
end
|
|
|
|
class ScalarSubsecond < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-subsecond-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarSecond < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-second-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarMinute < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-minute-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarHour < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-hour-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarDay < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-day-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarMonth < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-month-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class ScalarYear < Scalar #:nodoc:
|
|
def to_s
|
|
(super + '-year-' + @type.to_s)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Ordinal < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary Ordinal
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each_index do |i|
|
|
if tokens[i].word =~ /^(\d+)(st|nd|rd|th|\.)$/
|
|
ordinal = $1.to_i
|
|
tokens[i].tag(Ordinal.new(ordinal))
|
|
tokens[i].tag(OrdinalDay.new(ordinal)) if Chronic::Date::could_be_day?(ordinal)
|
|
tokens[i].tag(OrdinalMonth.new(ordinal)) if Chronic::Date::could_be_month?(ordinal)
|
|
if Chronic::Date::could_be_year?(ordinal)
|
|
year = Chronic::Date::make_year(ordinal, options[:ambiguous_year_future_bias])
|
|
tokens[i].tag(OrdinalYear.new(year.to_i))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
'ordinal'
|
|
end
|
|
end
|
|
|
|
class OrdinalDay < Ordinal #:nodoc:
|
|
def to_s
|
|
(super + '-day-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class OrdinalMonth < Ordinal #:nodoc:
|
|
def to_s
|
|
(super + '-month-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class OrdinalYear < Ordinal #:nodoc:
|
|
def to_s
|
|
(super + '-year-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
end
|
|
#
|
|
module Chronic
|
|
class Separator < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary Separator
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_commas(token) then token.tag(t); next end
|
|
if t = scan_for_dots(token) then token.tag(t); next end
|
|
if t = scan_for_colon(token) then token.tag(t); next end
|
|
if t = scan_for_space(token) then token.tag(t); next end
|
|
if t = scan_for_slash(token) then token.tag(t); next end
|
|
if t = scan_for_dash(token) then token.tag(t); next end
|
|
if t = scan_for_quote(token) then token.tag(t); next end
|
|
if t = scan_for_at(token) then token.tag(t); next end
|
|
if t = scan_for_in(token) then token.tag(t); next end
|
|
if t = scan_for_on(token) then token.tag(t); next end
|
|
if t = scan_for_and(token) then token.tag(t); next end
|
|
if t = scan_for_t(token) then token.tag(t); next end
|
|
if t = scan_for_w(token) then token.tag(t); next end
|
|
end
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorComma object.
|
|
def self.scan_for_commas(token)
|
|
scan_for token, SeparatorComma, { /^,$/ => :comma }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorDot object.
|
|
def self.scan_for_dots(token)
|
|
scan_for token, SeparatorDot, { /^\.$/ => :dot }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorColon object.
|
|
def self.scan_for_colon(token)
|
|
scan_for token, SeparatorColon, { /^:$/ => :colon }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorSpace object.
|
|
def self.scan_for_space(token)
|
|
scan_for token, SeparatorSpace, { /^ $/ => :space }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorSlash object.
|
|
def self.scan_for_slash(token)
|
|
scan_for token, SeparatorSlash, { /^\/$/ => :slash }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorDash object.
|
|
def self.scan_for_dash(token)
|
|
scan_for token, SeparatorDash, { /^-$/ => :dash }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorQuote object.
|
|
def self.scan_for_quote(token)
|
|
scan_for token, SeparatorQuote,
|
|
{
|
|
/^'$/ => :single_quote,
|
|
/^"$/ => :double_quote
|
|
}
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorAt object.
|
|
def self.scan_for_at(token)
|
|
scan_for token, SeparatorAt, { /^(at|@)$/ => :at }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorIn object.
|
|
def self.scan_for_in(token)
|
|
scan_for token, SeparatorIn, { /^in$/ => :in }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeparatorOn object.
|
|
def self.scan_for_on(token)
|
|
scan_for token, SeparatorOn, { /^on$/ => :on }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeperatorAnd Object object.
|
|
def self.scan_for_and(token)
|
|
scan_for token, SeparatorAnd, { /^and$/ => :and }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeperatorT Object object.
|
|
def self.scan_for_t(token)
|
|
scan_for token, SeparatorT, { /^t$/ => :T }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SeperatorW Object object.
|
|
def self.scan_for_w(token)
|
|
scan_for token, SeparatorW, { /^w$/ => :W }
|
|
end
|
|
|
|
def to_s
|
|
'separator'
|
|
end
|
|
end
|
|
|
|
class SeparatorComma < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-comma')
|
|
end
|
|
end
|
|
|
|
class SeparatorDot < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-dot')
|
|
end
|
|
end
|
|
|
|
class SeparatorColon < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-colon')
|
|
end
|
|
end
|
|
|
|
class SeparatorSpace < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-space')
|
|
end
|
|
end
|
|
|
|
class SeparatorSlash < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-slash')
|
|
end
|
|
end
|
|
|
|
class SeparatorDash < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-dash')
|
|
end
|
|
end
|
|
|
|
class SeparatorQuote < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-quote-' + @type.to_s)
|
|
end
|
|
end
|
|
|
|
class SeparatorAt < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-at')
|
|
end
|
|
end
|
|
|
|
class SeparatorIn < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-in')
|
|
end
|
|
end
|
|
|
|
class SeparatorOn < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-on')
|
|
end
|
|
end
|
|
|
|
class SeparatorAnd < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-and')
|
|
end
|
|
end
|
|
|
|
class SeparatorT < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-T')
|
|
end
|
|
end
|
|
|
|
class SeparatorW < Separator #:nodoc:
|
|
def to_s
|
|
(super + '-W')
|
|
end
|
|
end
|
|
|
|
end
|
|
#
|
|
module Chronic
|
|
class Sign < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary Sign
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_plus(token) then token.tag(t); next end
|
|
if t = scan_for_minus(token) then token.tag(t); next end
|
|
end
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SignPlus object.
|
|
def self.scan_for_plus(token)
|
|
scan_for token, SignPlus, { /^\+$/ => :plus }
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new SignMinus object.
|
|
def self.scan_for_minus(token)
|
|
scan_for token, SignMinus, { /^-$/ => :minus }
|
|
end
|
|
|
|
def to_s
|
|
'sign'
|
|
end
|
|
end
|
|
|
|
class SignPlus < Sign #:nodoc:
|
|
def to_s
|
|
(super + '-plus')
|
|
end
|
|
end
|
|
|
|
class SignMinus < Sign #:nodoc:
|
|
def to_s
|
|
(super + '-minus')
|
|
end
|
|
end
|
|
|
|
end
|
|
#
|
|
module Chronic
|
|
class TimeZone < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary TimeZone
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_all(token) then token.tag(t); next end
|
|
end
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Pointer object.
|
|
def self.scan_for_all(token)
|
|
scan_for token, self,
|
|
{
|
|
/[PMCE][DS]T|UTC/i => :tz,
|
|
/(tzminus)?\d{2}:?\d{2}/ => :tz
|
|
}
|
|
end
|
|
|
|
def to_s
|
|
'timezone'
|
|
end
|
|
end
|
|
end
|
|
#
|
|
require 'strscan'
|
|
|
|
module Chronic
|
|
class Numerizer
|
|
|
|
DIRECT_NUMS = [
|
|
['eleven', '11'],
|
|
['twelve', '12'],
|
|
['thirteen', '13'],
|
|
['fourteen', '14'],
|
|
['fifteen', '15'],
|
|
['sixteen', '16'],
|
|
['seventeen', '17'],
|
|
['eighteen', '18'],
|
|
['nineteen', '19'],
|
|
['ninteen', '19'], # Common mis-spelling
|
|
['zero', '0'],
|
|
['one', '1'],
|
|
['two', '2'],
|
|
['three', '3'],
|
|
['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
|
|
['five', '5'],
|
|
['six(\W|$)', '6\1'],
|
|
['seven(\W|$)', '7\1'],
|
|
['eight(\W|$)', '8\1'],
|
|
['nine(\W|$)', '9\1'],
|
|
['ten', '10'],
|
|
['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
|
|
]
|
|
|
|
ORDINALS = [
|
|
['first', '1'],
|
|
['third', '3'],
|
|
['fourth', '4'],
|
|
['fifth', '5'],
|
|
['sixth', '6'],
|
|
['seventh', '7'],
|
|
['eighth', '8'],
|
|
['ninth', '9'],
|
|
['tenth', '10'],
|
|
['twelfth', '12'],
|
|
['twentieth', '20'],
|
|
['thirtieth', '30'],
|
|
['fourtieth', '40'],
|
|
['fiftieth', '50'],
|
|
['sixtieth', '60'],
|
|
['seventieth', '70'],
|
|
['eightieth', '80'],
|
|
['ninetieth', '90']
|
|
]
|
|
|
|
TEN_PREFIXES = [
|
|
['twenty', 20],
|
|
['thirty', 30],
|
|
['forty', 40],
|
|
['fourty', 40], # Common mis-spelling
|
|
['fifty', 50],
|
|
['sixty', 60],
|
|
['seventy', 70],
|
|
['eighty', 80],
|
|
['ninety', 90]
|
|
]
|
|
|
|
BIG_PREFIXES = [
|
|
['hundred', 100],
|
|
['thousand', 1000],
|
|
['million', 1_000_000],
|
|
['billion', 1_000_000_000],
|
|
['trillion', 1_000_000_000_000],
|
|
]
|
|
|
|
def self.numerize(string)
|
|
string = string.dup
|
|
|
|
# preprocess
|
|
string = string.gsub(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
|
|
string = string.gsub(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
|
|
|
|
# easy/direct replacements
|
|
|
|
DIRECT_NUMS.each do |dn|
|
|
string = string.gsub(/#{dn[0]}/i, '<num>' + dn[1])
|
|
end
|
|
|
|
ORDINALS.each do |on|
|
|
string = string.gsub(/#{on[0]}/i, '<num>' + on[1] + on[0][-2, 2])
|
|
end
|
|
|
|
# ten, twenty, etc.
|
|
|
|
TEN_PREFIXES.each do |tp|
|
|
string = string.gsub(/(?:#{tp[0]}) *<num>(\d(?=[^\d]|$))*/i) { '<num>' + (tp[1] + $1.to_i).to_s }
|
|
end
|
|
|
|
TEN_PREFIXES.each do |tp|
|
|
string = string.gsub(/#{tp[0]}/i) { '<num>' + tp[1].to_s }
|
|
end
|
|
|
|
# hundreds, thousands, millions, etc.
|
|
|
|
BIG_PREFIXES.each do |bp|
|
|
string = string.gsub(/(?:<num>)?(\d*) *#{bp[0]}/i) { $1.empty? ? bp[1] : '<num>' + (bp[1] * $1.to_i).to_s}
|
|
andition(string)
|
|
end
|
|
|
|
# fractional addition
|
|
# I'm not combining this with the previous block as using float addition complicates the strings
|
|
# (with extraneous .0's and such )
|
|
string = string.gsub(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
|
|
|
|
string.gsub(/<num>/, '')
|
|
end
|
|
|
|
class << self
|
|
private
|
|
|
|
def andition(string)
|
|
sc = StringScanner.new(string)
|
|
|
|
while sc.scan_until(/<num>(\d+)( | and )<num>(\d+)(?=[^\w]|$)/i)
|
|
if sc[2] =~ /and/ || sc[1].size > sc[3].size
|
|
string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '<num>' + (sc[1].to_i + sc[3].to_i).to_s
|
|
sc.reset
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Season
|
|
|
|
attr_reader :start
|
|
attr_reader :end
|
|
|
|
def initialize(start_date, end_date)
|
|
@start = start_date
|
|
@end = end_date
|
|
end
|
|
|
|
def self.find_next_season(season, pointer)
|
|
lookup = [:spring, :summer, :autumn, :winter]
|
|
next_season_num = (lookup.index(season) + 1 * pointer) % 4
|
|
lookup[next_season_num]
|
|
end
|
|
|
|
def self.season_after(season)
|
|
find_next_season(season, +1)
|
|
end
|
|
|
|
def self.season_before(season)
|
|
find_next_season(season, -1)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class Repeater < Tag
|
|
|
|
# Scan an Array of Token objects and apply any necessary Repeater
|
|
# tags to each token.
|
|
#
|
|
# tokens - An Array of tokens to scan.
|
|
# options - The Hash of options specified in Chronic::parse.
|
|
#
|
|
# Returns an Array of tokens.
|
|
def self.scan(tokens, options)
|
|
tokens.each do |token|
|
|
if t = scan_for_season_names(token, options) then token.tag(t); next end
|
|
if t = scan_for_month_names(token, options) then token.tag(t); next end
|
|
if t = scan_for_day_names(token, options) then token.tag(t); next end
|
|
if t = scan_for_day_portions(token, options) then token.tag(t); next end
|
|
if t = scan_for_times(token, options) then token.tag(t); next end
|
|
if t = scan_for_units(token, options) then token.tag(t); next end
|
|
end
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_season_names(token, options = {})
|
|
scan_for token, RepeaterSeasonName,
|
|
{
|
|
/^springs?$/ => :spring,
|
|
/^summers?$/ => :summer,
|
|
/^(autumn)|(fall)s?$/ => :autumn,
|
|
/^winters?$/ => :winter
|
|
}, options
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_month_names(token, options = {})
|
|
scan_for token, RepeaterMonthName,
|
|
{
|
|
/^jan[:\.]?(uary)?$/ => :january,
|
|
/^feb[:\.]?(ruary)?$/ => :february,
|
|
/^mar[:\.]?(ch)?$/ => :march,
|
|
/^apr[:\.]?(il)?$/ => :april,
|
|
/^may$/ => :may,
|
|
/^jun[:\.]?e?$/ => :june,
|
|
/^jul[:\.]?y?$/ => :july,
|
|
/^aug[:\.]?(ust)?$/ => :august,
|
|
/^sep[:\.]?(t[:\.]?|tember)?$/ => :september,
|
|
/^oct[:\.]?(ober)?$/ => :october,
|
|
/^nov[:\.]?(ember)?$/ => :november,
|
|
/^dec[:\.]?(ember)?$/ => :december
|
|
}, options
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_day_names(token, options = {})
|
|
scan_for token, RepeaterDayName,
|
|
{
|
|
/^m[ou]n(day)?$/ => :monday,
|
|
/^t(ue|eu|oo|u|)s?(day)?$/ => :tuesday,
|
|
/^we(d|dnes|nds|nns)(day)?$/ => :wednesday,
|
|
/^th(u|ur|urs|ers)(day)?$/ => :thursday,
|
|
/^fr[iy](day)?$/ => :friday,
|
|
/^sat(t?[ue]rday)?$/ => :saturday,
|
|
/^su[nm](day)?$/ => :sunday
|
|
}, options
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_day_portions(token, options = {})
|
|
scan_for token, RepeaterDayPortion,
|
|
{
|
|
/^ams?$/ => :am,
|
|
/^pms?$/ => :pm,
|
|
/^mornings?$/ => :morning,
|
|
/^afternoons?$/ => :afternoon,
|
|
/^evenings?$/ => :evening,
|
|
/^(night|nite)s?$/ => :night
|
|
}, options
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_times(token, options = {})
|
|
scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/, options
|
|
end
|
|
|
|
# token - The Token object we want to scan.
|
|
#
|
|
# Returns a new Repeater object.
|
|
def self.scan_for_units(token, options = {})
|
|
{
|
|
/^years?$/ => :year,
|
|
/^seasons?$/ => :season,
|
|
/^months?$/ => :month,
|
|
/^fortnights?$/ => :fortnight,
|
|
/^weeks?$/ => :week,
|
|
/^weekends?$/ => :weekend,
|
|
/^(week|business)days?$/ => :weekday,
|
|
/^days?$/ => :day,
|
|
/^hrs?$/ => :hour,
|
|
/^hours?$/ => :hour,
|
|
/^mins?$/ => :minute,
|
|
/^minutes?$/ => :minute,
|
|
/^secs?$/ => :second,
|
|
/^seconds?$/ => :second
|
|
}.each do |item, symbol|
|
|
if item =~ token.word
|
|
klass_name = 'Repeater' + symbol.to_s.capitalize
|
|
klass = Chronic.const_get(klass_name)
|
|
return klass.new(symbol, options)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def <=>(other)
|
|
width <=> other.width
|
|
end
|
|
|
|
# returns the width (in seconds or months) of this repeatable.
|
|
def width
|
|
raise("Repeater#width must be overridden in subclasses")
|
|
end
|
|
|
|
# returns the next occurance of this repeatable.
|
|
def next(pointer)
|
|
raise("Start point must be set before calling #next") unless @now
|
|
end
|
|
|
|
def this(pointer)
|
|
raise("Start point must be set before calling #this") unless @now
|
|
end
|
|
|
|
def to_s
|
|
'repeater'
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterYear < Repeater #:nodoc:
|
|
YEAR_SECONDS = 31536000 # 365 * 24 * 60 * 60
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_year_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_year_start
|
|
case pointer
|
|
when :future
|
|
@current_year_start = Chronic.construct(@now.year + 1)
|
|
when :past
|
|
@current_year_start = Chronic.construct(@now.year - 1)
|
|
end
|
|
else
|
|
diff = pointer == :future ? 1 : -1
|
|
@current_year_start = Chronic.construct(@current_year_start.year + diff)
|
|
end
|
|
|
|
Span.new(@current_year_start, Chronic.construct(@current_year_start.year + 1))
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
this_year_start = Chronic.construct(@now.year, @now.month, @now.day + 1)
|
|
this_year_end = Chronic.construct(@now.year + 1, 1, 1)
|
|
when :past
|
|
this_year_start = Chronic.construct(@now.year, 1, 1)
|
|
this_year_end = Chronic.construct(@now.year, @now.month, @now.day)
|
|
when :none
|
|
this_year_start = Chronic.construct(@now.year, 1, 1)
|
|
this_year_end = Chronic.construct(@now.year + 1, 1, 1)
|
|
end
|
|
|
|
Span.new(this_year_start, this_year_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
new_begin = build_offset_time(span.begin, amount, direction)
|
|
new_end = build_offset_time(span.end, amount, direction)
|
|
Span.new(new_begin, new_end)
|
|
end
|
|
|
|
def width
|
|
YEAR_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-year')
|
|
end
|
|
|
|
private
|
|
|
|
def build_offset_time(time, amount, direction)
|
|
year = time.year + (amount * direction)
|
|
days = month_days(year, time.month)
|
|
day = time.day > days ? days : time.day
|
|
Chronic.construct(year, time.month, day, time.hour, time.min, time.sec)
|
|
end
|
|
|
|
def month_days(year, month)
|
|
if ::Date.leap?(year)
|
|
RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
|
|
else
|
|
RepeaterMonth::MONTH_DAYS[month - 1]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterSeason < Repeater #:nodoc:
|
|
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
|
|
SEASONS = {
|
|
:spring => Season.new(MiniDate.new(3,20), MiniDate.new(6,20)),
|
|
:summer => Season.new(MiniDate.new(6,21), MiniDate.new(9,22)),
|
|
:autumn => Season.new(MiniDate.new(9,23), MiniDate.new(12,21)),
|
|
:winter => Season.new(MiniDate.new(12,22), MiniDate.new(3,19))
|
|
}
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@next_season_start = nil
|
|
@next_season_end = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
next_season = Season.find_next_season(find_current_season(MiniDate.from_time(@now)), direction)
|
|
|
|
find_next_season_span(direction, next_season)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
today = Chronic.construct(@now.year, @now.month, @now.day)
|
|
this_ssn = find_current_season(MiniDate.from_time(@now))
|
|
case pointer
|
|
when :past
|
|
this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
|
|
this_ssn_end = today
|
|
when :future
|
|
this_ssn_start = today + RepeaterDay::DAY_SECONDS
|
|
this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
|
|
when :none
|
|
this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
|
|
this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
|
|
end
|
|
|
|
construct_season(this_ssn_start, this_ssn_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
|
|
end
|
|
|
|
def offset_by(time, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
time + amount * direction * SEASON_SECONDS
|
|
end
|
|
|
|
def width
|
|
SEASON_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-season')
|
|
end
|
|
|
|
private
|
|
|
|
def find_next_season_span(direction, next_season)
|
|
unless @next_season_start || @next_season_end
|
|
@next_season_start = Chronic.construct(@now.year, @now.month, @now.day)
|
|
@next_season_end = Chronic.construct(@now.year, @now.month, @now.day)
|
|
end
|
|
|
|
@next_season_start += direction * num_seconds_til_start(next_season, direction)
|
|
@next_season_end += direction * num_seconds_til_end(next_season, direction)
|
|
|
|
construct_season(@next_season_start, @next_season_end)
|
|
end
|
|
|
|
def find_current_season(md)
|
|
[:spring, :summer, :autumn, :winter].find do |season|
|
|
md.is_between?(SEASONS[season].start, SEASONS[season].end)
|
|
end
|
|
end
|
|
|
|
def num_seconds_til(goal, direction)
|
|
start = Chronic.construct(@now.year, @now.month, @now.day)
|
|
seconds = 0
|
|
|
|
until MiniDate.from_time(start + direction * seconds).equals?(goal)
|
|
seconds += RepeaterDay::DAY_SECONDS
|
|
end
|
|
|
|
seconds
|
|
end
|
|
|
|
def num_seconds_til_start(season_symbol, direction)
|
|
num_seconds_til(SEASONS[season_symbol].start, direction)
|
|
end
|
|
|
|
def num_seconds_til_end(season_symbol, direction)
|
|
num_seconds_til(SEASONS[season_symbol].end, direction)
|
|
end
|
|
|
|
def construct_season(start, finish)
|
|
Span.new(
|
|
Chronic.construct(start.year, start.month, start.day),
|
|
Chronic.construct(finish.year, finish.month, finish.day)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterSeasonName < RepeaterSeason #:nodoc:
|
|
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
|
|
DAY_SECONDS = 86_400 # (24 * 60 * 60)
|
|
|
|
def next(pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
find_next_season_span(direction, @type)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
today = Chronic.construct(@now.year, @now.month, @now.day)
|
|
goal_ssn_start = today + direction * num_seconds_til_start(@type, direction)
|
|
goal_ssn_end = today + direction * num_seconds_til_end(@type, direction)
|
|
curr_ssn = find_current_season(MiniDate.from_time(@now))
|
|
case pointer
|
|
when :past
|
|
this_ssn_start = goal_ssn_start
|
|
this_ssn_end = (curr_ssn == @type) ? today : goal_ssn_end
|
|
when :future
|
|
this_ssn_start = (curr_ssn == @type) ? today + RepeaterDay::DAY_SECONDS : goal_ssn_start
|
|
this_ssn_end = goal_ssn_end
|
|
when :none
|
|
this_ssn_start = goal_ssn_start
|
|
this_ssn_end = goal_ssn_end
|
|
end
|
|
|
|
construct_season(this_ssn_start, this_ssn_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
|
|
end
|
|
|
|
def offset_by(time, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
time + amount * direction * RepeaterYear::YEAR_SECONDS
|
|
end
|
|
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterMonth < Repeater #:nodoc:
|
|
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
|
|
YEAR_MONTHS = 12
|
|
MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
MONTH_DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_month_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_month_start
|
|
@current_month_start = offset_by(Chronic.construct(@now.year, @now.month), 1, pointer)
|
|
else
|
|
@current_month_start = offset_by(Chronic.construct(@current_month_start.year, @current_month_start.month), 1, pointer)
|
|
end
|
|
|
|
Span.new(@current_month_start, Chronic.construct(@current_month_start.year, @current_month_start.month + 1))
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
month_start = Chronic.construct(@now.year, @now.month, @now.day + 1)
|
|
month_end = self.offset_by(Chronic.construct(@now.year, @now.month), 1, :future)
|
|
when :past
|
|
month_start = Chronic.construct(@now.year, @now.month)
|
|
month_end = Chronic.construct(@now.year, @now.month, @now.day)
|
|
when :none
|
|
month_start = Chronic.construct(@now.year, @now.month)
|
|
month_end = self.offset_by(Chronic.construct(@now.year, @now.month), 1, :future)
|
|
end
|
|
|
|
Span.new(month_start, month_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
|
|
end
|
|
|
|
def offset_by(time, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
amount_years = direction * amount / YEAR_MONTHS
|
|
amount_months = direction * amount % YEAR_MONTHS
|
|
|
|
new_year = time.year + amount_years
|
|
new_month = time.month + amount_months
|
|
if new_month > YEAR_MONTHS
|
|
new_year += 1
|
|
new_month -= YEAR_MONTHS
|
|
end
|
|
|
|
days = month_days(new_year, new_month)
|
|
new_day = time.day > days ? days : time.day
|
|
|
|
Chronic.construct(new_year, new_month, new_day, time.hour, time.min, time.sec)
|
|
end
|
|
|
|
def width
|
|
MONTH_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-month')
|
|
end
|
|
|
|
private
|
|
|
|
def month_days(year, month)
|
|
::Date.leap?(year) ? MONTH_DAYS_LEAP[month - 1] : MONTH_DAYS[month - 1]
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterMonthName < Repeater #:nodoc:
|
|
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
|
|
MONTHS = {
|
|
:january => 1,
|
|
:february => 2,
|
|
:march => 3,
|
|
:april => 4,
|
|
:may => 5,
|
|
:june => 6,
|
|
:july => 7,
|
|
:august => 8,
|
|
:september => 9,
|
|
:october => 10,
|
|
:november => 11,
|
|
:december => 12
|
|
}
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_month_begin = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_month_begin
|
|
case pointer
|
|
when :future
|
|
if @now.month < index
|
|
@current_month_begin = Chronic.construct(@now.year, index)
|
|
elsif @now.month > index
|
|
@current_month_begin = Chronic.construct(@now.year + 1, index)
|
|
end
|
|
when :none
|
|
if @now.month <= index
|
|
@current_month_begin = Chronic.construct(@now.year, index)
|
|
elsif @now.month > index
|
|
@current_month_begin = Chronic.construct(@now.year + 1, index)
|
|
end
|
|
when :past
|
|
if @now.month >= index
|
|
@current_month_begin = Chronic.construct(@now.year, index)
|
|
elsif @now.month < index
|
|
@current_month_begin = Chronic.construct(@now.year - 1, index)
|
|
end
|
|
end
|
|
@current_month_begin || raise("Current month should be set by now")
|
|
else
|
|
case pointer
|
|
when :future
|
|
@current_month_begin = Chronic.construct(@current_month_begin.year + 1, @current_month_begin.month)
|
|
when :past
|
|
@current_month_begin = Chronic.construct(@current_month_begin.year - 1, @current_month_begin.month)
|
|
end
|
|
end
|
|
|
|
cur_month_year = @current_month_begin.year
|
|
cur_month_month = @current_month_begin.month
|
|
|
|
if cur_month_month == 12
|
|
next_month_year = cur_month_year + 1
|
|
next_month_month = 1
|
|
else
|
|
next_month_year = cur_month_year
|
|
next_month_month = cur_month_month + 1
|
|
end
|
|
|
|
Span.new(@current_month_begin, Chronic.construct(next_month_year, next_month_month))
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :past
|
|
self.next(pointer)
|
|
when :future, :none
|
|
self.next(:none)
|
|
end
|
|
end
|
|
|
|
def width
|
|
MONTH_SECONDS
|
|
end
|
|
|
|
def index
|
|
@index ||= MONTHS[@type]
|
|
end
|
|
|
|
def to_s
|
|
(super + '-monthname-' + @type.to_s)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterFortnight < Repeater #:nodoc:
|
|
FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_fortnight_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_fortnight_start
|
|
case pointer
|
|
when :future
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
next_sunday_span = sunday_repeater.next(:future)
|
|
@current_fortnight_start = next_sunday_span.begin
|
|
when :past
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = (@now + RepeaterDay::DAY_SECONDS)
|
|
2.times { sunday_repeater.next(:past) }
|
|
last_sunday_span = sunday_repeater.next(:past)
|
|
@current_fortnight_start = last_sunday_span.begin
|
|
end
|
|
else
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_fortnight_start += direction * FORTNIGHT_SECONDS
|
|
end
|
|
|
|
Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
pointer = :future if pointer == :none
|
|
|
|
case pointer
|
|
when :future
|
|
this_fortnight_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
sunday_repeater.this(:future)
|
|
this_sunday_span = sunday_repeater.this(:future)
|
|
this_fortnight_end = this_sunday_span.begin
|
|
Span.new(this_fortnight_start, this_fortnight_end)
|
|
when :past
|
|
this_fortnight_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
last_sunday_span = sunday_repeater.next(:past)
|
|
this_fortnight_start = last_sunday_span.begin
|
|
Span.new(this_fortnight_start, this_fortnight_end)
|
|
end
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * FORTNIGHT_SECONDS
|
|
end
|
|
|
|
def width
|
|
FORTNIGHT_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-fortnight')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterWeek < Repeater #:nodoc:
|
|
WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_week_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_week_start
|
|
case pointer
|
|
when :future
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
next_sunday_span = sunday_repeater.next(:future)
|
|
@current_week_start = next_sunday_span.begin
|
|
when :past
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = (@now + RepeaterDay::DAY_SECONDS)
|
|
sunday_repeater.next(:past)
|
|
last_sunday_span = sunday_repeater.next(:past)
|
|
@current_week_start = last_sunday_span.begin
|
|
end
|
|
else
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_week_start += direction * WEEK_SECONDS
|
|
end
|
|
|
|
Span.new(@current_week_start, @current_week_start + WEEK_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
this_week_start = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
this_sunday_span = sunday_repeater.this(:future)
|
|
this_week_end = this_sunday_span.begin
|
|
Span.new(this_week_start, this_week_end)
|
|
when :past
|
|
this_week_end = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour)
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
last_sunday_span = sunday_repeater.next(:past)
|
|
this_week_start = last_sunday_span.begin
|
|
Span.new(this_week_start, this_week_end)
|
|
when :none
|
|
sunday_repeater = RepeaterDayName.new(:sunday)
|
|
sunday_repeater.start = @now
|
|
last_sunday_span = sunday_repeater.next(:past)
|
|
this_week_start = last_sunday_span.begin
|
|
Span.new(this_week_start, this_week_start + WEEK_SECONDS)
|
|
end
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * WEEK_SECONDS
|
|
end
|
|
|
|
def width
|
|
WEEK_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-week')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterWeekend < Repeater #:nodoc:
|
|
WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60)
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_week_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_week_start
|
|
case pointer
|
|
when :future
|
|
saturday_repeater = RepeaterDayName.new(:saturday)
|
|
saturday_repeater.start = @now
|
|
next_saturday_span = saturday_repeater.next(:future)
|
|
@current_week_start = next_saturday_span.begin
|
|
when :past
|
|
saturday_repeater = RepeaterDayName.new(:saturday)
|
|
saturday_repeater.start = (@now + RepeaterDay::DAY_SECONDS)
|
|
last_saturday_span = saturday_repeater.next(:past)
|
|
@current_week_start = last_saturday_span.begin
|
|
end
|
|
else
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_week_start += direction * RepeaterWeek::WEEK_SECONDS
|
|
end
|
|
|
|
Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future, :none
|
|
saturday_repeater = RepeaterDayName.new(:saturday)
|
|
saturday_repeater.start = @now
|
|
this_saturday_span = saturday_repeater.this(:future)
|
|
Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS)
|
|
when :past
|
|
saturday_repeater = RepeaterDayName.new(:saturday)
|
|
saturday_repeater.start = @now
|
|
last_saturday_span = saturday_repeater.this(:past)
|
|
Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS)
|
|
end
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
weekend = RepeaterWeekend.new(:weekend)
|
|
weekend.start = span.begin
|
|
start = weekend.next(pointer).begin + (amount - 1) * direction * RepeaterWeek::WEEK_SECONDS
|
|
Span.new(start, start + (span.end - span.begin))
|
|
end
|
|
|
|
def width
|
|
WEEKEND_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-weekend')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterWeekday < Repeater #:nodoc:
|
|
DAY_SECONDS = 86400 # (24 * 60 * 60)
|
|
DAYS = {
|
|
:sunday => 0,
|
|
:monday => 1,
|
|
:tuesday => 2,
|
|
:wednesday => 3,
|
|
:thursday => 4,
|
|
:friday => 5,
|
|
:saturday => 6
|
|
}
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_weekday_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
unless @current_weekday_start
|
|
@current_weekday_start = Chronic.construct(@now.year, @now.month, @now.day)
|
|
@current_weekday_start += direction * DAY_SECONDS
|
|
|
|
until is_weekday?(@current_weekday_start.wday)
|
|
@current_weekday_start += direction * DAY_SECONDS
|
|
end
|
|
else
|
|
loop do
|
|
@current_weekday_start += direction * DAY_SECONDS
|
|
break if is_weekday?(@current_weekday_start.wday)
|
|
end
|
|
end
|
|
|
|
Span.new(@current_weekday_start, @current_weekday_start + DAY_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :past
|
|
self.next(:past)
|
|
when :future, :none
|
|
self.next(:future)
|
|
end
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
num_weekdays_passed = 0; offset = 0
|
|
until num_weekdays_passed == amount
|
|
offset += direction * DAY_SECONDS
|
|
num_weekdays_passed += 1 if is_weekday?((span.begin+offset).wday)
|
|
end
|
|
|
|
span + offset
|
|
end
|
|
|
|
def width
|
|
DAY_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-weekday')
|
|
end
|
|
|
|
private
|
|
|
|
def is_weekend?(day)
|
|
day == symbol_to_number(:saturday) || day == symbol_to_number(:sunday)
|
|
end
|
|
|
|
def is_weekday?(day)
|
|
!is_weekend?(day)
|
|
end
|
|
|
|
def symbol_to_number(sym)
|
|
DAYS[sym] || raise("Invalid symbol specified")
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterDay < Repeater #:nodoc:
|
|
DAY_SECONDS = 86_400 # (24 * 60 * 60)
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_day_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_day_start
|
|
@current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day)
|
|
end
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_day_start += direction * DAY_SECONDS
|
|
|
|
Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
day_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
|
|
day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
|
|
when :past
|
|
day_begin = Chronic.construct(@now.year, @now.month, @now.day)
|
|
day_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
|
|
when :none
|
|
day_begin = Chronic.construct(@now.year, @now.month, @now.day)
|
|
day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
|
|
end
|
|
|
|
Span.new(day_begin, day_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * DAY_SECONDS
|
|
end
|
|
|
|
def width
|
|
DAY_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-day')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterDayName < Repeater #:nodoc:
|
|
DAY_SECONDS = 86400 # (24 * 60 * 60)
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_date = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
unless @current_date
|
|
@current_date = ::Date.new(@now.year, @now.month, @now.day)
|
|
@current_date += direction
|
|
|
|
day_num = symbol_to_number(@type)
|
|
|
|
while @current_date.wday != day_num
|
|
@current_date += direction
|
|
end
|
|
else
|
|
@current_date += direction * 7
|
|
end
|
|
next_date = @current_date.succ
|
|
Span.new(Chronic.construct(@current_date.year, @current_date.month, @current_date.day), Chronic.construct(next_date.year, next_date.month, next_date.day))
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
pointer = :future if pointer == :none
|
|
self.next(pointer)
|
|
end
|
|
|
|
def width
|
|
DAY_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-dayname-' + @type.to_s)
|
|
end
|
|
|
|
private
|
|
|
|
def symbol_to_number(sym)
|
|
lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
|
|
lookup[sym] || raise("Invalid symbol specified")
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterDayPortion < Repeater #:nodoc:
|
|
PORTIONS = {
|
|
:am => 0..(12 * 60 * 60 - 1),
|
|
:pm => (12 * 60 * 60)..(24 * 60 * 60 - 1),
|
|
:morning => (6 * 60 * 60)..(12 * 60 * 60), # 6am-12am,
|
|
:afternoon => (13 * 60 * 60)..(17 * 60 * 60), # 1pm-5pm,
|
|
:evening => (17 * 60 * 60)..(20 * 60 * 60), # 5pm-8pm,
|
|
:night => (20 * 60 * 60)..(24 * 60 * 60), # 8pm-12pm
|
|
}
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_span = nil
|
|
|
|
if type.kind_of? Integer
|
|
@range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
|
|
else
|
|
@range = PORTIONS[type]
|
|
@range || raise("Invalid type '#{type}' for RepeaterDayPortion")
|
|
end
|
|
|
|
@range || raise("Range should have been set by now")
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_span
|
|
now_seconds = @now - Chronic.construct(@now.year, @now.month, @now.day)
|
|
if now_seconds < @range.begin
|
|
case pointer
|
|
when :future
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
|
|
when :past
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
|
|
end
|
|
elsif now_seconds > @range.end
|
|
case pointer
|
|
when :future
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
|
|
when :past
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
|
|
end
|
|
else
|
|
case pointer
|
|
when :future
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
|
|
when :past
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
|
|
end
|
|
end
|
|
offset = (@range.end - @range.begin)
|
|
range_end = construct_date_from_reference_and_offset(range_start, offset)
|
|
@current_span = Span.new(range_start, range_end)
|
|
else
|
|
days_to_shift_window =
|
|
case pointer
|
|
when :future
|
|
1
|
|
when :past
|
|
-1
|
|
end
|
|
|
|
new_begin = Chronic.construct(@current_span.begin.year, @current_span.begin.month, @current_span.begin.day + days_to_shift_window, @current_span.begin.hour, @current_span.begin.min, @current_span.begin.sec)
|
|
new_end = Chronic.construct(@current_span.end.year, @current_span.end.month, @current_span.end.day + days_to_shift_window, @current_span.end.hour, @current_span.end.min, @current_span.end.sec)
|
|
@current_span = Span.new(new_begin, new_end)
|
|
end
|
|
end
|
|
|
|
def this(context = :future)
|
|
super
|
|
|
|
range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
|
|
range_end = construct_date_from_reference_and_offset(range_start)
|
|
@current_span = Span.new(range_start, range_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
@now = span.begin
|
|
portion_span = self.next(pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
portion_span + (direction * (amount - 1) * RepeaterDay::DAY_SECONDS)
|
|
end
|
|
|
|
def width
|
|
@range || raise("Range has not been set")
|
|
return @current_span.width if @current_span
|
|
if @type.kind_of? Integer
|
|
return (12 * 60 * 60)
|
|
else
|
|
@range.end - @range.begin
|
|
end
|
|
end
|
|
|
|
def to_s
|
|
(super + '-dayportion-' + @type.to_s)
|
|
end
|
|
|
|
private
|
|
def construct_date_from_reference_and_offset(reference, offset = nil)
|
|
elapsed_seconds_for_range = offset || (@range.end - @range.begin)
|
|
second_hand = ((elapsed_seconds_for_range - (12 * 60))) % 60
|
|
minute_hand = (elapsed_seconds_for_range - second_hand) / (60) % 60
|
|
hour_hand = (elapsed_seconds_for_range - minute_hand - second_hand) / (60 * 60) + reference.hour % 24
|
|
Chronic.construct(reference.year, reference.month, reference.day, hour_hand, minute_hand, second_hand)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterHour < Repeater #:nodoc:
|
|
HOUR_SECONDS = 3600 # 60 * 60
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_hour_start = nil
|
|
end
|
|
|
|
def next(pointer)
|
|
super
|
|
|
|
unless @current_hour_start
|
|
case pointer
|
|
when :future
|
|
@current_hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour + 1)
|
|
when :past
|
|
@current_hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour - 1)
|
|
end
|
|
else
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_hour_start += direction * HOUR_SECONDS
|
|
end
|
|
|
|
Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
|
|
hour_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour + 1)
|
|
when :past
|
|
hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
|
|
hour_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
|
|
when :none
|
|
hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
|
|
hour_end = hour_start + HOUR_SECONDS
|
|
end
|
|
|
|
Span.new(hour_start, hour_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * HOUR_SECONDS
|
|
end
|
|
|
|
def width
|
|
HOUR_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-hour')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterMinute < Repeater #:nodoc:
|
|
MINUTE_SECONDS = 60
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@current_minute_start = nil
|
|
end
|
|
|
|
def next(pointer = :future)
|
|
super
|
|
|
|
unless @current_minute_start
|
|
case pointer
|
|
when :future
|
|
@current_minute_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
|
|
when :past
|
|
@current_minute_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1)
|
|
end
|
|
else
|
|
direction = pointer == :future ? 1 : -1
|
|
@current_minute_start += direction * MINUTE_SECONDS
|
|
end
|
|
|
|
Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
case pointer
|
|
when :future
|
|
minute_begin = @now
|
|
minute_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
|
|
when :past
|
|
minute_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
|
|
minute_end = @now
|
|
when :none
|
|
minute_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
|
|
minute_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS
|
|
end
|
|
|
|
Span.new(minute_begin, minute_end)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * MINUTE_SECONDS
|
|
end
|
|
|
|
def width
|
|
MINUTE_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-minute')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterSecond < Repeater #:nodoc:
|
|
SECOND_SECONDS = 1 # haha, awesome
|
|
|
|
def initialize(type, options = {})
|
|
super
|
|
@second_start = nil
|
|
end
|
|
|
|
def next(pointer = :future)
|
|
super
|
|
|
|
direction = pointer == :future ? 1 : -1
|
|
|
|
unless @second_start
|
|
@second_start = @now + (direction * SECOND_SECONDS)
|
|
else
|
|
@second_start += SECOND_SECONDS * direction
|
|
end
|
|
|
|
Span.new(@second_start, @second_start + SECOND_SECONDS)
|
|
end
|
|
|
|
def this(pointer = :future)
|
|
super
|
|
|
|
Span.new(@now, @now + 1)
|
|
end
|
|
|
|
def offset(span, amount, pointer)
|
|
direction = pointer == :future ? 1 : -1
|
|
span + direction * amount * SECOND_SECONDS
|
|
end
|
|
|
|
def width
|
|
SECOND_SECONDS
|
|
end
|
|
|
|
def to_s
|
|
(super + '-second')
|
|
end
|
|
end
|
|
end
|
|
#
|
|
module Chronic
|
|
class RepeaterTime < Repeater #:nodoc:
|
|
class Tick #:nodoc:
|
|
attr_accessor :time
|
|
|
|
def initialize(time, ambiguous = false)
|
|
@time = time
|
|
@ambiguous = ambiguous
|
|
end
|
|
|
|
def ambiguous?
|
|
@ambiguous
|
|
end
|
|
|
|
def *(other)
|
|
Tick.new(@time * other, @ambiguous)
|
|
end
|
|
|
|
def to_f
|
|
@time.to_f
|
|
end
|
|
|
|
def to_s
|
|
@time.to_s + (@ambiguous ? '?' : '')
|
|
end
|
|
|
|
end
|
|
|
|
def initialize(time, options = {})
|
|
@current_time = nil
|
|
@options = options
|
|
time_parts = time.split(':')
|
|
raise ArgumentError, "Time cannot have more than 4 groups of ':'" if time_parts.count > 4
|
|
|
|
if time_parts.first.length > 2 and time_parts.count == 1
|
|
if time_parts.first.length > 4
|
|
second_index = time_parts.first.length - 2
|
|
time_parts.insert(1, time_parts.first[second_index..time_parts.first.length])
|
|
time_parts[0] = time_parts.first[0..second_index - 1]
|
|
end
|
|
minute_index = time_parts.first.length - 2
|
|
time_parts.insert(1, time_parts.first[minute_index..time_parts.first.length])
|
|
time_parts[0] = time_parts.first[0..minute_index - 1]
|
|
end
|
|
|
|
ambiguous = false
|
|
hours = time_parts.first.to_i
|
|
|
|
if @options[:hours24].nil? or (not @options[:hours24].nil? and @options[:hours24] != true)
|
|
ambiguous = true if (time_parts.first.length == 1 and hours > 0) or (hours >= 10 and hours <= 12) or (@options[:hours24] == false and hours > 0)
|
|
hours = 0 if hours == 12 and ambiguous
|
|
end
|
|
|
|
hours *= 60 * 60
|
|
minutes = 0
|
|
seconds = 0
|
|
subseconds = 0
|
|
|
|
minutes = time_parts[1].to_i * 60 if time_parts.count > 1
|
|
seconds = time_parts[2].to_i if time_parts.count > 2
|
|
subseconds = time_parts[3].to_f / (10 ** time_parts[3].length) if time_parts.count > 3
|
|
|
|
@type = Tick.new(hours + minutes + seconds + subseconds, ambiguous)
|
|
end
|
|
|
|
# Return the next past or future Span for the time that this Repeater represents
|
|
# pointer - Symbol representing which temporal direction to fetch the next day
|
|
# must be either :past or :future
|
|
def next(pointer)
|
|
super
|
|
|
|
half_day = 60 * 60 * 12
|
|
full_day = 60 * 60 * 24
|
|
|
|
first = false
|
|
|
|
unless @current_time
|
|
first = true
|
|
midnight = Chronic.time_class.local(@now.year, @now.month, @now.day)
|
|
|
|
yesterday_midnight = midnight - full_day
|
|
tomorrow_midnight = midnight + full_day
|
|
|
|
offset_fix = midnight.gmt_offset - tomorrow_midnight.gmt_offset
|
|
tomorrow_midnight += offset_fix
|
|
|
|
catch :done do
|
|
if pointer == :future
|
|
if @type.ambiguous?
|
|
[midnight + @type.time + offset_fix, midnight + half_day + @type.time + offset_fix, tomorrow_midnight + @type.time].each do |t|
|
|
(@current_time = t; throw :done) if t >= @now
|
|
end
|
|
else
|
|
[midnight + @type.time + offset_fix, tomorrow_midnight + @type.time].each do |t|
|
|
(@current_time = t; throw :done) if t >= @now
|
|
end
|
|
end
|
|
else # pointer == :past
|
|
if @type.ambiguous?
|
|
[midnight + half_day + @type.time + offset_fix, midnight + @type.time + offset_fix, yesterday_midnight + @type.time + half_day].each do |t|
|
|
(@current_time = t; throw :done) if t <= @now
|
|
end
|
|
else
|
|
[midnight + @type.time + offset_fix, yesterday_midnight + @type.time].each do |t|
|
|
(@current_time = t; throw :done) if t <= @now
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@current_time || raise("Current time cannot be nil at this point")
|
|
end
|
|
|
|
unless first
|
|
increment = @type.ambiguous? ? half_day : full_day
|
|
@current_time += pointer == :future ? increment : -increment
|
|
end
|
|
|
|
Span.new(@current_time, @current_time + width)
|
|
end
|
|
|
|
def this(context = :future)
|
|
super
|
|
|
|
context = :future if context == :none
|
|
|
|
self.next(context)
|
|
end
|
|
|
|
def width
|
|
1
|
|
end
|
|
|
|
def to_s
|
|
(super + '-time-' + @type.to_s)
|
|
end
|
|
end
|
|
end
|
|
#
|
|
|
|
# Parse natural language dates and times into Time or Chronic::Span objects.
|
|
#
|
|
# Examples:
|
|
#
|
|
# require 'chronic'
|
|
#
|
|
# Time.now #=> Sun Aug 27 23:18:25 PDT 2006
|
|
#
|
|
# Chronic.parse('tomorrow')
|
|
# #=> Mon Aug 28 12:00:00 PDT 2006
|
|
#
|
|
# Chronic.parse('monday', :context => :past)
|
|
# #=> Mon Aug 21 12:00:00 PDT 2006
|
|
module Chronic
|
|
VERSION = "0.10.2"
|
|
|
|
class << self
|
|
|
|
# Returns true when debug mode is enabled.
|
|
attr_accessor :debug
|
|
|
|
# Examples:
|
|
#
|
|
# require 'chronic'
|
|
# require 'active_support/time'
|
|
#
|
|
# Time.zone = 'UTC'
|
|
# Chronic.time_class = Time.zone
|
|
# Chronic.parse('June 15 2006 at 5:54 AM')
|
|
# # => Thu, 15 Jun 2006 05:45:00 UTC +00:00
|
|
#
|
|
# Returns The Time class Chronic uses internally.
|
|
attr_accessor :time_class
|
|
end
|
|
|
|
self.debug = false
|
|
self.time_class = ::Time
|
|
|
|
|
|
# Parses a string containing a natural language date or time.
|
|
#
|
|
# If the parser can find a date or time, either a Time or Chronic::Span
|
|
# will be returned (depending on the value of `:guess`). If no
|
|
# date or time can be found, `nil` will be returned.
|
|
#
|
|
# text - The String text to parse.
|
|
# opts - An optional Hash of configuration options passed to Parser::new.
|
|
def self.parse(text, options = {})
|
|
Parser.new(options).parse(text)
|
|
end
|
|
|
|
# Construct a new time object determining possible month overflows
|
|
# and leap years.
|
|
#
|
|
# year - Integer year.
|
|
# month - Integer month.
|
|
# day - Integer day.
|
|
# hour - Integer hour.
|
|
# minute - Integer minute.
|
|
# second - Integer second.
|
|
#
|
|
# Returns a new Time object constructed from these params.
|
|
def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, offset = nil)
|
|
if second >= 60
|
|
minute += second / 60
|
|
second = second % 60
|
|
end
|
|
|
|
if minute >= 60
|
|
hour += minute / 60
|
|
minute = minute % 60
|
|
end
|
|
|
|
if hour >= 24
|
|
day += hour / 24
|
|
hour = hour % 24
|
|
end
|
|
|
|
# determine if there is a day overflow. this is complicated by our crappy calendar
|
|
# system (non-constant number of days per month)
|
|
day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
|
|
if day > 28 # no month ever has fewer than 28 days, so only do this if necessary
|
|
days_this_month = ::Date.leap?(year) ? Date::MONTH_DAYS_LEAP[month] : Date::MONTH_DAYS[month]
|
|
if day > days_this_month
|
|
month += day / days_this_month
|
|
day = day % days_this_month
|
|
end
|
|
end
|
|
|
|
if month > 12
|
|
if month % 12 == 0
|
|
year += (month - 12) / 12
|
|
month = 12
|
|
else
|
|
year += month / 12
|
|
month = month % 12
|
|
end
|
|
end
|
|
if Chronic.time_class.name == "Date"
|
|
Chronic.time_class.new(year, month, day)
|
|
elsif not Chronic.time_class.respond_to?(:new) or (RUBY_VERSION.to_f < 1.9 and Chronic.time_class.name == "Time")
|
|
Chronic.time_class.local(year, month, day, hour, minute, second)
|
|
else
|
|
offset = Time::normalize_offset(offset) if Chronic.time_class.name == "DateTime"
|
|
Chronic.time_class.new(year, month, day, hour, minute, second, offset)
|
|
end
|
|
end
|
|
|
|
end
|