From: Chuck Hagenbuch Date: Sun, 21 Dec 2008 00:36:05 +0000 (-0500) Subject: Horde_Date_Parser -> Date_Parser X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=87bb5605b73173745324a5c2ce2fd36e1c7a3ebb;p=horde.git Horde_Date_Parser -> Date_Parser Horde_Form -> Form VC -> Vcs (Horde_Vcs, full rewrite not yet done) --- diff --git a/framework/Date_Parser/chronic/History.txt b/framework/Date_Parser/chronic/History.txt new file mode 100644 index 000000000..0f2364676 --- /dev/null +++ b/framework/Date_Parser/chronic/History.txt @@ -0,0 +1,53 @@ += 0.2.3 + +* fixed 12am/12pm (by Nicholas Schlueter) + += 0.2.2 + +* added missing files (damn you manifest) + += 0.2.1 + +* fixed time overflow issue +* implemented "next" for minute repeater +* generalized time dealiasing to dealias regardless of day portion and time position +* added additional token match for cases like "friday evening at 7" and "tomorrow evening at 7" +* added support for Time#to_s output format: "Mon Apr 02 17:00:00 PDT 2007" + += 0.2.0 2007-03-20 + +* implemented numerizer, allowing the use of number words (e.g. five weeks ago) (by shalev) + += 0.1.6 2006-01-15 + +* added 'weekend' support (by eventualbuddha) + += 0.1.5 2006-12-20 + +* fixed 'aug 20' returning next year if current month is august +* modified behavior of 'from now' +* added support for seconds on times, and thus db timestamp format: "2006-12-20 18:04:23" +* made Hoe compliant + += 0.1.4 + +* removed verbose error checking code. oops. :-/ + += 0.1.3 + +* improved regexes for word variations (by Josh Goebel) +* fixed a bug that caused "today at 3am" to return nil if current time is after 3am + += 0.1.2 + +* removed Date dependency (now works on windows properly without fiddling) + += 0.1.1 + +* run to_s on incoming object +* fixed loop loading of repeaters files (out of order on some machines) +* fixed find_within to use this instead of next (was breaking "today at 6pm") + += 0.1.0 + +* initial release \ No newline at end of file diff --git a/framework/Date_Parser/chronic/README.txt b/framework/Date_Parser/chronic/README.txt new file mode 100644 index 000000000..2e4f1791f --- /dev/null +++ b/framework/Date_Parser/chronic/README.txt @@ -0,0 +1,149 @@ +Chronic + http://chronic.rubyforge.org/ + by Tom Preston-Werner + +== DESCRIPTION: + +Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse. + +== INSTALLATION: + +Chronic can be installed via RubyGems: + + $ sudo gem install chronic + +== USAGE: + +You can parse strings containing a natural language date using the Chronic.parse method. + + require 'rubygems' + 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 + + Chronic.parse('this tuesday 5:00') + #=> Tue Aug 29 17:00:00 PDT 2006 + + Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none) + #=> Tue Aug 29 05:00:00 PDT 2006 + + Chronic.parse('may 27th', :now => Time.local(2000, 1, 1)) + #=> Sat May 27 12:00:00 PDT 2000 + + Chronic.parse('may 27th', :guess => false) + #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007 + +See Chronic.parse for detailed usage instructions. + +== EXAMPLES: + +Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings. + +Simple + + thursday + november + summer + friday 13:00 + mon 2:35 + 4pm + 6 in the morning + friday 1pm + sat 7 in the evening + yesterday + today + tomorrow + this tuesday + next month + last winter + this morning + last night + this second + yesterday at 4:00 + last friday at 20:00 + last week tuesday + tomorrow at 6:45pm + afternoon yesterday + thursday last week + +Complex + + 3 years ago + 5 months before now + 7 hours ago + 7 days from now + 1 week hence + in 3 hours + 1 year ago tomorrow + 3 months ago saturday at 5:00 pm + 7 hours before tomorrow at noon + 3rd wednesday in november + 3rd month next year + 3rd thursday this september + 4th day last week + +Specific Dates + + January 5 + dec 25 + may 27th + October 2006 + oct 06 + jan 3 2010 + february 14, 2004 + 3 jan 2000 + 17 april 85 + 5/27/1979 + 27/5/1979 + 05/06 + 1979-05-27 + Friday + 5 + 4:00 + 17:00 + 0800 + +Specific Times (many of the above with an added time) + + January 5 at 7pm + 1979-05-27 05:00:00 + etc + +== LIMITATIONS: + +Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release. + +Time zones other than the local one are not currently supported. Support for other time zones is planned for a future release. + +== LICENSE: + +(The MIT License) + +Copyright (c) 2006 Ryan Davis, Zen Spider Software + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/framework/Date_Parser/chronic/lib/chronic.rb b/framework/Date_Parser/chronic/lib/chronic.rb new file mode 100644 index 000000000..6d0e7ba76 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic.rb @@ -0,0 +1,125 @@ +#============================================================================= +# +# Name: Chronic +# Author: Tom Preston-Werner +# Purpose: Parse natural language dates and times into Time or +# Chronic::Span objects +# +#============================================================================= + +$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed + +require 'chronic/chronic' +require 'chronic/handlers' + +require 'chronic/repeater' +require 'chronic/repeaters/repeater_year' +require 'chronic/repeaters/repeater_season' +require 'chronic/repeaters/repeater_season_name' +require 'chronic/repeaters/repeater_month' +require 'chronic/repeaters/repeater_month_name' +require 'chronic/repeaters/repeater_fortnight' +require 'chronic/repeaters/repeater_week' +require 'chronic/repeaters/repeater_weekend' +require 'chronic/repeaters/repeater_day' +require 'chronic/repeaters/repeater_day_name' +require 'chronic/repeaters/repeater_day_portion' +require 'chronic/repeaters/repeater_hour' +require 'chronic/repeaters/repeater_minute' +require 'chronic/repeaters/repeater_second' +require 'chronic/repeaters/repeater_time' + +require 'chronic/grabber' +require 'chronic/pointer' +require 'chronic/scalar' +require 'chronic/ordinal' +require 'chronic/separator' +require 'chronic/time_zone' + +require 'numerizer/numerizer' + +module Chronic + VERSION = "0.2.3" + + def self.debug; false; end +end + +alias p_orig p + +def p(val) + p_orig val + puts +end + +# class Time +# def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) +# # extra_seconds = second > 60 ? second - 60 : 0 +# # extra_minutes = minute > 59 ? minute - 59 : 0 +# # extra_hours = hour > 23 ? hour - 23 : 0 +# # extra_days = day > +# +# if month > 12 +# if month % 12 == 0 +# year += (month - 12) / 12 +# month = 12 +# else +# year += month / 12 +# month = month % 12 +# end +# end +# +# base = Time.local(year, month) +# puts base +# offset = ((day - 1) * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + second +# puts offset.to_s +# date = base + offset +# puts date +# date +# end +# end + +class Time + def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) + 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 + leap_year = (year % 4 == 0) && !(year % 100 == 0) + leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1] + 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 + + Time.local(year, month, day, hour, minute, second) + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/chronic.rb b/framework/Date_Parser/chronic/lib/chronic/chronic.rb new file mode 100644 index 000000000..5e7779f63 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/chronic.rb @@ -0,0 +1,239 @@ +module Chronic + class << self + + # 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. + # + # Options are: + # + # [:context] + # :past or :future (defaults to :future) + # + # If your string represents a birthday, you can set :context to :past + # and if an ambiguous string is given, it will assume it is in the + # past. Specify :future or omit to set a future context. + # + # [:now] + # Time (defaults to Time.now) + # + # By setting :now to a Time, all computations will be based off + # of that time instead of Time.now + # + # [:guess] + # +true+ or +false+ (defaults to +true+) + # + # 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 :guess to +false+ and a Chronic::Span will be returned. + # + # [:ambiguous_time_range] + # Integer or :none (defaults to 6 (6am-6pm)) + # + # 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. + def parse(text, specified_options = {}) + # get options and set defaults if necessary + default_options = {:context => :future, + :now => Time.now, + :guess => true, + :ambiguous_time_range => 6} + options = default_options.merge specified_options + + # ensure the specified options are valid + specified_options.keys.each do |key| + default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.") + end + [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.") + + # store now for later =) + @now = options[:now] + + # put the text into a normal format to ease scanning + text = self.pre_normalize(text) + + # get base tokens for each word + @tokens = self.base_tokenize(text) + + # scan the tokens with each token scanner + [Repeater].each do |tokenizer| + @tokens = tokenizer.scan(@tokens, options) + end + + [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer| + @tokens = tokenizer.scan(@tokens) + end + + # strip any non-tagged tokens + @tokens = @tokens.select { |token| token.tagged? } + + if Chronic.debug + puts "+---------------------------------------------------" + puts "| " + @tokens.to_s + puts "+---------------------------------------------------" + end + + # do the heavy lifting + begin + span = self.tokens_to_span(@tokens, options) + rescue + raise + return nil + end + + # guess a time within a span if required + if options[:guess] + return self.guess(span) + else + return span + end + end + + # Clean up the specified input text 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) + def pre_normalize(text) #:nodoc: + normalized_text = text.to_s.downcase + normalized_text = numericize_numbers(normalized_text) + normalized_text.gsub!(/['"\.]/, '') + normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' } + normalized_text.gsub!(/\btoday\b/, 'this day') + normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day') + normalized_text.gsub!(/\byesterday\b/, 'last day') + normalized_text.gsub!(/\bnoon\b/, '12:00') + normalized_text.gsub!(/\bmidnight\b/, '24:00') + normalized_text.gsub!(/\bbefore now\b/, 'past') + normalized_text.gsub!(/\bnow\b/, 'this second') + normalized_text.gsub!(/\b(ago|before)\b/, 'past') + normalized_text.gsub!(/\bthis past\b/, 'last') + normalized_text.gsub!(/\bthis last\b/, 'last') + normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1') + normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1') + normalized_text.gsub!(/\btonight\b/, 'this night') + normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1') + normalized_text.gsub!(/\b(hence|after|from)\b/, 'future') + normalized_text = numericize_ordinals(normalized_text) + end + + # Convert number words to numbers (three => 3) + def numericize_numbers(text) #:nodoc: + Numerizer.numerize(text) + end + + # Convert ordinal words to numeric ordinals (third => 3rd) + def numericize_ordinals(text) #:nodoc: + text + end + + # Split the text on spaces and convert each word into + # a Token + def base_tokenize(text) #:nodoc: + text.split(' ').map { |word| Token.new(word) } + end + + # Guess a specific time within the given span + def guess(span) #:nodoc: + return nil if span.nil? + if span.width > 1 + span.begin + (span.width / 2) + else + span.begin + end + end + end + + class Token #:nodoc: + attr_accessor :word, :tags + + def initialize(word) + @word = word + @tags = [] + end + + # Tag this token with the specified tag + def tag(new_tag) + @tags << new_tag + end + + # Remove all tags of the given class + def untag(tag_class) + @tags = @tags.select { |m| !m.kind_of? tag_class } + end + + # Return true if this token has any tags + def tagged? + @tags.size > 0 + end + + # Return the Tag that matches the given class + def get_tag(tag_class) + matches = @tags.select { |m| m.kind_of? tag_class } + #matches.size < 2 || raise("Multiple identical tags found") + return matches.first + end + + # Print this Token in a pretty way + def to_s + @word << '(' << @tags.join(', ') << ') ' + end + end + + # 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 + end + + # Tokens are tagged with subclassed instances of this class when + # they match specific criteria + class Tag #:nodoc: + attr_accessor :type + + def initialize(type) + @type = type + end + + def start=(s) + @now = s + end + end + + # Internal exception + class ChronicPain < Exception #:nodoc: + + end + + # This exception is raised if an invalid argument is provided to + # any of Chronic's methods + class InvalidArgumentException < Exception + + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/grabber.rb b/framework/Date_Parser/chronic/lib/chronic/grabber.rb new file mode 100644 index 000000000..4162a260b --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/grabber.rb @@ -0,0 +1,26 @@ +#module Chronic + + class Chronic::Grabber < Chronic::Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/last/ => :last, + /this/ => :this, + /next/ => :next} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'grabber-' << @type.to_s + end + end + +#end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/handlers.rb b/framework/Date_Parser/chronic/lib/chronic/handlers.rb new file mode 100644 index 000000000..551d632fa --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/handlers.rb @@ -0,0 +1,469 @@ +module Chronic + + class << self + + def definitions #:nodoc: + @definitions ||= + {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)], + + :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + 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_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy), + Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)], + + # tonight at 7pm + :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)], + + # 3 weeks from now, in 2 months + :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)], + + # 3rd week in march + :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)] + } + end + + def tokens_to_span(tokens, options) #:nodoc: + # maybe it's a specific date + + self.definitions[:date].each do |handler| + if handler.match(tokens, self.definitions) + puts "-date" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # I guess it's not a specific date, maybe it's just an anchor + + self.definitions[:anchor].each do |handler| + if handler.match(tokens, self.definitions) + puts "-anchor" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an anchor, perhaps it's an arrow + + self.definitions[:arrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-arrow" if Chronic.debug + good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an arrow, let's hope it's a narrow + + self.definitions[:narrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-narrow" if Chronic.debug + #good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, tokens, options) + end + end + + # I guess you're out of luck! + puts "-none" if Chronic.debug + return nil + end + + #-------------- + + def day_or_time(day_start, time_tokens, options) + outer_span = Span.new(day_start, day_start + (24 * 60 * 60)) + + if !time_tokens.empty? + @now = outer_span.begin + time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options) + return time + else + return outer_span + end + end + + #-------------- + + def handle_m_d(month, day, time_tokens, options) #:nodoc: + month.start = @now + span = month.this(options[:context]) + + day_start = Time.local(span.begin.year, span.begin.month, day) + + day_or_time(day_start, time_tokens, options) + end + + def handle_rmn_sd(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_od(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_sy(tokens, options) #:nodoc: + 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 + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc: + month = tokens[1].get_tag(RepeaterMonthName).index + day = tokens[2].get_tag(ScalarDay).type + year = tokens[5].get_tag(ScalarYear).type + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, [tokens[3]], options) + rescue ArgumentError + nil + end + end + + def handle_rmn_sd_sy(tokens, options) #:nodoc: + 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) + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_rmn_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_rmn_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sd_sy(tokens, options) #:nodoc: + 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) + + begin + day_start = Time.local(year, month, day) #:nodoc: + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_sm_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sy_sm_sd(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + 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 + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + # anchors + + def handle_r(tokens, options) #:nodoc: + dd_tokens = dealias_and_disambiguate_times(tokens, options) + self.get_anchor(dd_tokens, options) + end + + def handle_r_g_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + self.handle_r(new_tokens, options) + end + + # arrows + + def handle_srp(tokens, span, options) #:nodoc: + 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) + end + + def handle_s_r_p(tokens, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + + # span = + # case true + # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class) + # self.parse("this hour", :guess => false, :now => @now) + # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class) + # self.parse("this minute", :guess => false, :now => @now) + # when [RepeaterMinute, RepeaterSecond].include?(repeater.class) + # self.parse("this second", :guess => false, :now => @now) + # else + # raise(ChronicPain, "Invalid repeater: #{repeater.class}") + # end + + span = self.parse("this second", :guess => false, :now => @now) + + self.handle_srp(tokens, span, options) + end + + def handle_p_s_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + self.handle_s_r_p(new_tokens, options) + end + + def handle_s_r_p_a(tokens, options) #:nodoc: + anchor_span = get_anchor(tokens[3..tokens.size - 1], options) + self.handle_srp(tokens, anchor_span, options) + end + + # narrows + + def handle_orr(tokens, outer_span, options) #:nodoc: + 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 + + def handle_o_r_s_r(tokens, options) #:nodoc: + outer_span = get_anchor([tokens[3]], options) + handle_orr(tokens[0..1], outer_span, options) + end + + def handle_o_r_g_r(tokens, options) #:nodoc: + outer_span = get_anchor(tokens[2..3], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # support methods + + def get_anchor(tokens, options) #:nodoc: + grabber = Grabber.new(:this) + pointer = :future + + repeaters = self.get_repeaters(tokens) + repeaters.size.times { tokens.pop } + + if tokens.first && tokens.first.get_tag(Grabber) + grabber = tokens.first.get_tag(Grabber) + tokens.pop + end + + head = repeaters.shift + head.start = @now + + case grabber.type + when :last + outer_span = head.next(:past) + when :this + if 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(ChronicPain, "Invalid grabber") + end + + puts "--#{outer_span}" if Chronic.debug + anchor = find_within(repeaters, outer_span, pointer) + end + + def get_repeaters(tokens) #:nodoc: + repeaters = [] + tokens.each do |token| + if t = token.get_tag(Repeater) + repeaters << t + end + end + repeaters.sort.reverse + 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) #:nodoc: + puts "--#{span}" if Chronic.debug + return span if tags.empty? + + head, *rest = tags + head.start = pointer == :future ? span.begin : span.end + h = head.this(:none) + + if span.include?(h.begin) || span.include?(h.end) + return find_within(rest, h, pointer) + else + return nil + end + end + + def dealias_and_disambiguate_times(tokens, options) #:nodoc: + # 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) + + if [:morning].include?(t1tag.type) + puts '--morning->am' if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:am)) + elsif [:afternoon, :evening, :night].include?(t1tag.type) + puts "--#{t1tag.type}->pm" if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:pm)) + end + end + + # tokens.each_with_index do |t0, i| + # t1 = tokens[i + 1] + # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime) + # if [:morning].include?(t1tag.type) + # puts '--morning->am' if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:am)) + # elsif [:afternoon, :evening, :night].include?(t1tag.type) + # puts "--#{t1tag.type}->pm" if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:pm)) + # end + # end + # end + + # handle ambiguous times if :ambiguous_time_range is specified + if options[:ambiguous_time_range] != :none + ttokens = [] + tokens.each_with_index do |t0, i| + ttokens << t0 + t1 = tokens[i + 1] + if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion)) + distoken = Token.new('disambiguator') + distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range])) + ttokens << distoken + end + end + tokens = ttokens + end + + tokens + end + + end + + class Handler #:nodoc: + attr_accessor :pattern, :handler_method + + def initialize(pattern, handler_method) + @pattern = pattern + @handler_method = handler_method + end + + def constantize(name) + camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase } + ::Chronic.module_eval(camel, __FILE__, __LINE__) + end + + def match(tokens, definitions) + token_index = 0 + @pattern.each do |element| + name = element.to_s + optional = name.reverse[0..0] == '?' + name = name.chop if optional + if element.instance_of? Symbol + klass = constantize(name) + match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty? + return false if !match && !optional + (token_index += 1; next) if match + next if !match && optional + elsif element.instance_of? String + return true if optional && token_index == tokens.size + sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified") + sub_handlers.each do |sub_handler| + return true if sub_handler.match(tokens[token_index..tokens.size], definitions) + end + return false + else + raise(ChronicPain, "Invalid match type: #{element.class}") + end + end + return false if token_index != tokens.size + return true + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/ordinal.rb b/framework/Date_Parser/chronic/lib/chronic/ordinal.rb new file mode 100644 index 000000000..45b8148e4 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/ordinal.rb @@ -0,0 +1,40 @@ +module Chronic + + class Ordinal < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_ordinals(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + return Ordinal.new($1.to_i) + end + return nil + end + + def self.scan_for_days(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + unless $1.to_i > 31 + return OrdinalDay.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'ordinal' + end + end + + class OrdinalDay < Ordinal #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/pointer.rb b/framework/Date_Parser/chronic/lib/chronic/pointer.rb new file mode 100644 index 000000000..224efaf96 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/pointer.rb @@ -0,0 +1,27 @@ +module Chronic + + class Pointer < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/\bpast\b/ => :past, + /\bfuture\b/ => :future, + /\bin\b/ => :future} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'pointer-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeater.rb b/framework/Date_Parser/chronic/lib/chronic/repeater.rb new file mode 100644 index 000000000..9f80daf2f --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeater.rb @@ -0,0 +1,115 @@ +class Chronic::Repeater < Chronic::Tag #:nodoc: + def self.scan(tokens, options) + # for each token + tokens.each_index do |i| + if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end + if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_month_names(token) + scanner = {/^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} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_names(token) + scanner = {/^m[ou]n(day)?$/ => :monday, + /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday, + /^tue$/ => :tuesday, + /^we(dnes|nds|nns)day$/ => :wednesday, + /^wed$/ => :wednesday, + /^th(urs|ers)day$/ => :thursday, + /^thu$/ => :thursday, + /^fr[iy](day)?$/ => :friday, + /^sat(t?[ue]rday)?$/ => :saturday, + /^su[nm](day)?$/ => :sunday} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_portions(token) + scanner = {/^ams?$/ => :am, + /^pms?$/ => :pm, + /^mornings?$/ => :morning, + /^afternoons?$/ => :afternoon, + /^evenings?$/ => :evening, + /^(night|nite)s?$/ => :night} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_times(token, options) + if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/ + return Chronic::RepeaterTime.new(token.word, options) + end + return nil + end + + def self.scan_for_units(token) + scanner = {/^years?$/ => :year, + /^seasons?$/ => :season, + /^months?$/ => :month, + /^fortnights?$/ => :fortnight, + /^weeks?$/ => :week, + /^weekends?$/ => :weekend, + /^days?$/ => :day, + /^hours?$/ => :hour, + /^minutes?$/ => :minute, + /^seconds?$/ => :second} + scanner.keys.each do |scanner_item| + if scanner_item =~ token.word + klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize + klass = eval(klass_name) + return klass.new(scanner[scanner_item]) + end + end + return nil + end + + def <=>(other) + width <=> other.width + end + + # returns the width (in seconds or months) of this repeatable. + def width + raise("Repeatable#width must be overridden in subclasses") + end + + # returns the next occurance of this repeatable. + def next(pointer) + !@now.nil? || raise("Start point must be set before calling #next") + [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future") + #raise("Repeatable#next must be overridden in subclasses") + end + + def this(pointer) + !@now.nil? || raise("Start point must be set before calling #this") + [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none") + end + + def to_s + 'repeater' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb new file mode 100644 index 000000000..a92d83f63 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb @@ -0,0 +1,47 @@ +class Chronic::RepeaterDay < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def next(pointer) + super + + if !@current_day_start + @current_day_start = Time.local(@now.year, @now.month, @now.day) + end + + direction = pointer == :future ? 1 : -1 + @current_day_start += direction * DAY_SECONDS + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + when :past + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + when :none + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb new file mode 100644 index 000000000..0486a4ddf --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb @@ -0,0 +1,46 @@ +class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + if !@current_day_start + @current_day_start = Time.construct(@now.year, @now.month, @now.day) + @current_day_start += direction * DAY_SECONDS + + day_num = symbol_to_number(@type) + + while @current_day_start.wday != day_num + @current_day_start += direction * DAY_SECONDS + end + else + @current_day_start += direction * 7 * DAY_SECONDS + end + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + 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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb new file mode 100644 index 000000000..c854933ad --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb @@ -0,0 +1,93 @@ +class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc: + @@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) + super + + if type.kind_of? Integer + @range = (@type * 60 * 60)..((@type + 12) * 60 * 60) + else + lookup = {:am => 0..(12 * 60 * 60 - 1), + :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1), + :morning => @@morning, + :afternoon => @@afternoon, + :evening => @@evening, + :night => @@night} + @range = lookup[type] + lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion") + end + @range || raise("Range should have been set by now") + end + + def next(pointer) + super + + full_day = 60 * 60 * 24 + + if !@current_span + now_seconds = @now - Time.construct(@now.year, @now.month, @now.day) + if now_seconds < @range.begin + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + elsif now_seconds > @range.end + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + end + else + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + end + + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + else + case pointer + when :future + @current_span += full_day + when :past + @current_span -= full_day + end + end + end + + def this(context = :future) + super + + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + end + + def offset(span, amount, pointer) + @now = span.begin + portion_span = self.next(pointer) + direction = pointer == :future ? 1 : -1 + portion_span + (direction * (amount - 1) * Chronic::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 +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb new file mode 100644 index 000000000..058fbb904 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb @@ -0,0 +1,65 @@ +class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc: + FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_fortnight_start + case pointer + when :future + sunday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::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 + + Chronic::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 = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::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 + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + when :past + this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_fortnight_start = last_sunday_span.begin + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb new file mode 100644 index 000000000..f38a3f825 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb @@ -0,0 +1,52 @@ +class Chronic::RepeaterHour < Chronic::Repeater #:nodoc: + HOUR_SECONDS = 3600 # 60 * 60 + + def next(pointer) + super + + if !@current_hour_start + case pointer + when :future + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_hour_start += direction * HOUR_SECONDS + end + + Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :none + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = hour_begin + HOUR_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb new file mode 100644 index 000000000..342d3cd41 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb @@ -0,0 +1,52 @@ +class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc: + MINUTE_SECONDS = 60 + + def next(pointer = :future) + super + + if !@current_minute_start + case pointer + when :future + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + when :past + @current_minute_start = Time.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 + + Chronic::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 = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :past + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = @now + when :none + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb new file mode 100644 index 000000000..edd89eeb2 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb @@ -0,0 +1,61 @@ +class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + YEAR_MONTHS = 12 + + def next(pointer) + super + + if !@current_month_start + @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer) + else + @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer) + end + + Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + month_start = Time.construct(@now.year, @now.month, @now.day + 1) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + when :past + month_start = Time.construct(@now.year, @now.month) + month_end = Time.construct(@now.year, @now.month, @now.day) + when :none + month_start = Time.construct(@now.year, @now.month) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + end + + Chronic::Span.new(month_start, month_end) + end + + def offset(span, amount, pointer) + Chronic::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 + Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec) + end + + def width + MONTH_SECONDS + end + + def to_s + super << '-month' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb new file mode 100644 index 000000000..1f8b748a9 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb @@ -0,0 +1,93 @@ +class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + + def next(pointer) + super + + if !@current_month_begin + target_month = symbol_to_number(@type) + case pointer + when :future + if @now.month < target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :none + if @now.month <= target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :past + if @now.month > target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month < target_month + @current_month_begin = Time.construct(@now.year - 1, target_month) + end + end + @current_month_begin || raise("Current month should be set by now") + else + case pointer + when :future + @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month) + when :past + @current_month_begin = Time.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 + + Chronic::Span.new(@current_month_begin, Time.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 + symbol_to_number(@type) + end + + def to_s + super << '-monthname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb new file mode 100644 index 000000000..a255865fb --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb @@ -0,0 +1,23 @@ +class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + + def next(pointer) + super + + raise 'Not implemented' + end + + def this(pointer = :future) + super + + raise 'Not implemented' + end + + def width + SEASON_SECONDS + end + + def to_s + super << '-season' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb new file mode 100644 index 000000000..adfd1f281 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb @@ -0,0 +1,24 @@ +class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc: + @summer = ['jul 21', 'sep 22'] + @autumn = ['sep 23', 'dec 21'] + @winter = ['dec 22', 'mar 19'] + @spring = ['mar 20', 'jul 20'] + + def next(pointer) + super + raise 'Not implemented' + end + + def this(pointer = :future) + super + raise 'Not implemented' + end + + def width + (91 * 24 * 60 * 60) + end + + def to_s + super << '-season-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb new file mode 100644 index 000000000..6d05545ca --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb @@ -0,0 +1,36 @@ +class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc: + SECOND_SECONDS = 1 # haha, awesome + + def next(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + if !@second_start + @second_start = @now + (direction * SECOND_SECONDS) + else + @second_start += SECOND_SECONDS * direction + end + + Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS) + end + + def this(pointer = :future) + super + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb new file mode 100644 index 000000000..f8560141c --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb @@ -0,0 +1,117 @@ +class Chronic::RepeaterTime < Chronic::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 = {}) + t = time.gsub(/\:/, '') + @type = + if (1..2) === t.size + hours = t.to_i + hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true) + elsif t.size == 3 + Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true) + elsif t.size == 4 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous) + elsif t.size == 5 + Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true) + elsif t.size == 6 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) + else + raise("Time cannot exceed six digits") + end + 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 = Time.local(@now.year, @now.month, @now.day) + yesterday_midnight = midnight - full_day + tomorrow_midnight = midnight + full_day + + catch :done do + if pointer == :future + if @type.ambiguous? + [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + else + [midnight + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + end + else # pointer == :past + if @type.ambiguous? + [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t| + (@current_time = t; throw :done) if t <= @now + end + else + [midnight + @type, yesterday_midnight + @type].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 + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb new file mode 100644 index 000000000..ec88ff14b --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb @@ -0,0 +1,68 @@ +class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc: + WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + sunday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::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 + + Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + this_sunday_span = sunday_repeater.this(:future) + this_week_end = this_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :past + this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :none + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb new file mode 100644 index 000000000..f012267d9 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb @@ -0,0 +1,60 @@ +class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc: + WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + saturday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = (@now + Chronic::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 * Chronic::RepeaterWeek::WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future, :none + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + this_saturday_span = saturday_repeater.this(:future) + Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS) + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + last_saturday_span = saturday_repeater.this(:past) + Chronic::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 = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = span.begin + start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS + Chronic::Span.new(start, start + (span.end - span.begin)) + end + + def width + WEEKEND_SECONDS + end + + def to_s + super << '-weekend' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb new file mode 100644 index 000000000..426371f9b --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb @@ -0,0 +1,58 @@ +class Chronic::RepeaterYear < Chronic::Repeater #:nodoc: + + def next(pointer) + super + + if !@current_year_start + case pointer + when :future + @current_year_start = Time.construct(@now.year + 1) + when :past + @current_year_start = Time.construct(@now.year - 1) + end + else + diff = pointer == :future ? 1 : -1 + @current_year_start = Time.construct(@current_year_start.year + diff) + end + + Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS + this_year_end = Time.construct(@now.year + 1, 1, 1) + when :past + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year, @now.month, @now.day) + when :none + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year + 1, 1, 1) + end + + Chronic::Span.new(this_year_start, this_year_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + + sb = span.begin + new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec) + + se = span.end + new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec) + + Chronic::Span.new(new_begin, new_end) + end + + def width + (365 * 24 * 60 * 60) + end + + def to_s + super << '-year' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/scalar.rb b/framework/Date_Parser/chronic/lib/chronic/scalar.rb new file mode 100644 index 000000000..b08cfee18 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/scalar.rb @@ -0,0 +1,74 @@ +module Chronic + + class Scalar < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_scalars(token, post_token) + if token.word =~ /^\d*$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return Scalar.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_days(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarDay.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_months(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarMonth.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_years(token, post_token) + if token.word =~ /^([1-9]\d)?\d\d?$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return ScalarYear.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'scalar' + 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 \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/separator.rb b/framework/Date_Parser/chronic/lib/chronic/separator.rb new file mode 100644 index 000000000..86c56e33b --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/separator.rb @@ -0,0 +1,76 @@ +module Chronic + + class Separator < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_commas(token) + scanner = {/^,$/ => :comma} + scanner.keys.each do |scanner_item| + return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_slash_or_dash(token) + scanner = {/^-$/ => :dash, + /^\/$/ => :slash} + scanner.keys.each do |scanner_item| + return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_at(token) + scanner = {/^(at|@)$/ => :at} + scanner.keys.each do |scanner_item| + return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_in(token) + scanner = {/^in$/ => :in} + scanner.keys.each do |scanner_item| + return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'separator' + end + end + + class SeparatorComma < Separator #:nodoc: + def to_s + super << '-comma' + end + end + + class SeparatorSlashOrDash < Separator #:nodoc: + def to_s + super << '-slashordash-' << @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 + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/lib/chronic/time_zone.rb b/framework/Date_Parser/chronic/lib/chronic/time_zone.rb new file mode 100644 index 000000000..41041ef47 --- /dev/null +++ b/framework/Date_Parser/chronic/lib/chronic/time_zone.rb @@ -0,0 +1,22 @@ +module Chronic + class TimeZone < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/[PMCE][DS]T/i => :tz} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'timezone' + end + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/suite.rb b/framework/Date_Parser/chronic/test/suite.rb new file mode 100644 index 000000000..fa8bdaab5 --- /dev/null +++ b/framework/Date_Parser/chronic/test/suite.rb @@ -0,0 +1,9 @@ +require 'test/unit' + +tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"] +tests.delete_if { |o| o =~ /test_parsing/ } +tests.each do |file| + require file +end + +require File.dirname(__FILE__) + '/test_parsing.rb' \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_Chronic.rb b/framework/Date_Parser/chronic/test/test_Chronic.rb new file mode 100644 index 000000000..04fedb520 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_Chronic.rb @@ -0,0 +1,50 @@ +require 'chronic' +require 'test/unit' + +class TestChronic < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_post_normalize_am_pm_aliases + # affect wanted patterns + + tokens = [Chronic::Token.new("5:00"), Chronic::Token.new("morning")] + tokens[0].tag(Chronic::RepeaterTime.new("5:00")) + tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning)) + + assert_equal :morning, tokens[1].tags[0].type + + tokens = Chronic.dealias_and_disambiguate_times(tokens, {}) + + assert_equal :am, tokens[1].tags[0].type + assert_equal 2, tokens.size + + # don't affect unwanted patterns + + tokens = [Chronic::Token.new("friday"), Chronic::Token.new("morning")] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning)) + + assert_equal :morning, tokens[1].tags[0].type + + tokens = Chronic.dealias_and_disambiguate_times(tokens, {}) + + assert_equal :morning, tokens[1].tags[0].type + assert_equal 2, tokens.size + end + + def test_guess + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0)) + assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span) + + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1)) + assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span) + + span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12)) + assert_equal Time.local(2006, 11, 16), Chronic.guess(span) + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_Handler.rb b/framework/Date_Parser/chronic/test/test_Handler.rb new file mode 100644 index 000000000..4e36dfe25 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_Handler.rb @@ -0,0 +1,110 @@ +require 'chronic' +require 'test/unit' + +class TestHandler < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_handler_class_1 + handler = Chronic::Handler.new([:repeater], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_2 + handler = Chronic::Handler.new([:repeater, :repeater?], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[2].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_3 + handler = Chronic::Handler.new([:repeater, 'time?'], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_4 + handler = Chronic::Handler.new([:repeater_month_name, :scalar_day, 'time?'], :handler) + + tokens = [Chronic::Token.new('may')] + tokens[0].tag(Chronic::RepeaterMonthName.new(:may)) + + assert !handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('27') + tokens[1].tag(Chronic::ScalarDay.new(27)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_5 + handler = Chronic::Handler.new([:repeater, 'time?'], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('5:00') + tokens[1].tag(Chronic::RepeaterTime.new('5:00')) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('pm') + tokens[2].tag(Chronic::RepeaterDayPortion.new(:pm)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_6 + handler = Chronic::Handler.new([:scalar, :repeater, :pointer], :handler) + + tokens = [Chronic::Token.new('3'), + Chronic::Token.new('years'), + Chronic::Token.new('past')] + + tokens[0].tag(Chronic::Scalar.new(3)) + tokens[1].tag(Chronic::RepeaterYear.new(:year)) + tokens[2].tag(Chronic::Pointer.new(:past)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_constantize + handler = Chronic::Handler.new([], :handler) + assert_equal Chronic::RepeaterTime, handler.constantize(:repeater_time) + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterDayName.rb b/framework/Date_Parser/chronic/test/test_RepeaterDayName.rb new file mode 100644 index 000000000..8e119db30 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterDayName.rb @@ -0,0 +1,52 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterDayName < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_match + token = Chronic::Token.new('saturday') + repeater = Chronic::Repeater.scan_for_day_names(token) + assert_equal Chronic::RepeaterDayName, repeater.class + assert_equal :saturday, repeater.type + + token = Chronic::Token.new('sunday') + repeater = Chronic::Repeater.scan_for_day_names(token) + assert_equal Chronic::RepeaterDayName, repeater.class + assert_equal :sunday, repeater.type + end + + def test_next_future + mondays = Chronic::RepeaterDayName.new(:monday) + mondays.start = @now + + span = mondays.next(:future) + + assert_equal Time.local(2006, 8, 21), span.begin + assert_equal Time.local(2006, 8, 22), span.end + + span = mondays.next(:future) + + assert_equal Time.local(2006, 8, 28), span.begin + assert_equal Time.local(2006, 8, 29), span.end + end + + def test_next_past + mondays = Chronic::RepeaterDayName.new(:monday) + mondays.start = @now + + span = mondays.next(:past) + + assert_equal Time.local(2006, 8, 14), span.begin + assert_equal Time.local(2006, 8, 15), span.end + + span = mondays.next(:past) + + assert_equal Time.local(2006, 8, 7), span.begin + assert_equal Time.local(2006, 8, 8), span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterFortnight.rb b/framework/Date_Parser/chronic/test/test_RepeaterFortnight.rb new file mode 100644 index 000000000..cb80c43cf --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterFortnight.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterFortnight < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + next_fortnight = fortnights.next(:future) + assert_equal Time.local(2006, 8, 20), next_fortnight.begin + assert_equal Time.local(2006, 9, 3), next_fortnight.end + + next_next_fortnight = fortnights.next(:future) + assert_equal Time.local(2006, 9, 3), next_next_fortnight.begin + assert_equal Time.local(2006, 9, 17), next_next_fortnight.end + end + + def test_next_past + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + last_fortnight = fortnights.next(:past) + assert_equal Time.local(2006, 7, 30), last_fortnight.begin + assert_equal Time.local(2006, 8, 13), last_fortnight.end + + last_last_fortnight = fortnights.next(:past) + assert_equal Time.local(2006, 7, 16), last_last_fortnight.begin + assert_equal Time.local(2006, 7, 30), last_last_fortnight.end + end + + def test_this_future + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + this_fortnight = fortnights.this(:future) + assert_equal Time.local(2006, 8, 16, 15), this_fortnight.begin + assert_equal Time.local(2006, 8, 27), this_fortnight.end + end + + def test_this_past + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + this_fortnight = fortnights.this(:past) + assert_equal Time.local(2006, 8, 13, 0), this_fortnight.begin + assert_equal Time.local(2006, 8, 16, 14), this_fortnight.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 6, 14), offset_span.begin + assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterHour.rb b/framework/Date_Parser/chronic/test/test_RepeaterHour.rb new file mode 100644 index 000000000..48f37c429 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterHour.rb @@ -0,0 +1,65 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterHour < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + next_hour = hours.next(:future) + assert_equal Time.local(2006, 8, 16, 15), next_hour.begin + assert_equal Time.local(2006, 8, 16, 16), next_hour.end + + next_next_hour = hours.next(:future) + assert_equal Time.local(2006, 8, 16, 16), next_next_hour.begin + assert_equal Time.local(2006, 8, 16, 17), next_next_hour.end + end + + def test_next_past + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + past_hour = hours.next(:past) + assert_equal Time.local(2006, 8, 16, 13), past_hour.begin + assert_equal Time.local(2006, 8, 16, 14), past_hour.end + + past_past_hour = hours.next(:past) + assert_equal Time.local(2006, 8, 16, 12), past_past_hour.begin + assert_equal Time.local(2006, 8, 16, 13), past_past_hour.end + end + + def test_this + @now = Time.local(2006, 8, 16, 14, 30) + + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + this_hour = hours.this(:future) + assert_equal Time.local(2006, 8, 16, 14, 31), this_hour.begin + assert_equal Time.local(2006, 8, 16, 15), this_hour.end + + this_hour = hours.this(:past) + assert_equal Time.local(2006, 8, 16, 14), this_hour.begin + assert_equal Time.local(2006, 8, 16, 14, 30), this_hour.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 3, :future) + + assert_equal Time.local(2006, 8, 16, 17), offset_span.begin + assert_equal Time.local(2006, 8, 16, 17, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 24, :past) + + assert_equal Time.local(2006, 8, 15, 14), offset_span.begin + assert_equal Time.local(2006, 8, 15, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterMonth.rb b/framework/Date_Parser/chronic/test/test_RepeaterMonth.rb new file mode 100644 index 000000000..d0609c5f0 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterMonth.rb @@ -0,0 +1,47 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterMonth < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_offset_by + # future + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :future) + assert_equal Time.local(2006, 9, 16, 14), time + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 5, :future) + assert_equal Time.local(2007, 1, 16, 14), time + + # past + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :past) + assert_equal Time.local(2006, 7, 16, 14), time + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 10, :past) + assert_equal Time.local(2005, 10, 16, 14), time + end + + def test_offset + # future + + span = Chronic::Span.new(@now, @now + 60) + offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :future) + + assert_equal Time.local(2006, 9, 16, 14), offset_span.begin + assert_equal Time.local(2006, 9, 16, 14, 1), offset_span.end + + # past + + span = Chronic::Span.new(@now, @now + 60) + offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :past) + + assert_equal Time.local(2006, 7, 16, 14), offset_span.begin + assert_equal Time.local(2006, 7, 16, 14, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterMonthName.rb b/framework/Date_Parser/chronic/test/test_RepeaterMonthName.rb new file mode 100644 index 000000000..6326a4587 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterMonthName.rb @@ -0,0 +1,57 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterMonthName < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next + # future + + mays = Chronic::RepeaterMonthName.new(:may) + mays.start = @now + + next_may = mays.next(:future) + assert_equal Time.local(2007, 5), next_may.begin + assert_equal Time.local(2007, 6), next_may.end + + next_next_may = mays.next(:future) + assert_equal Time.local(2008, 5), next_next_may.begin + assert_equal Time.local(2008, 6), next_next_may.end + + decembers = Chronic::RepeaterMonthName.new(:december) + decembers.start = @now + + next_december = decembers.next(:future) + assert_equal Time.local(2006, 12), next_december.begin + assert_equal Time.local(2007, 1), next_december.end + + # past + + mays = Chronic::RepeaterMonthName.new(:may) + mays.start = @now + + assert_equal Time.local(2006, 5), mays.next(:past).begin + assert_equal Time.local(2005, 5), mays.next(:past).begin + end + + def test_this + octobers = Chronic::RepeaterMonthName.new(:october) + octobers.start = @now + + this_october = octobers.this(:future) + assert_equal Time.local(2006, 10, 1), this_october.begin + assert_equal Time.local(2006, 11, 1), this_october.end + + aprils = Chronic::RepeaterMonthName.new(:april) + aprils.start = @now + + this_april = aprils.this(:past) + assert_equal Time.local(2006, 4, 1), this_april.begin + assert_equal Time.local(2006, 5, 1), this_april.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterTime.rb b/framework/Date_Parser/chronic/test/test_RepeaterTime.rb new file mode 100644 index 000000000..bb2773588 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterTime.rb @@ -0,0 +1,72 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterTime < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + t = Chronic::RepeaterTime.new('4:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 16), t.next(:future).begin + assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin + + t = Chronic::RepeaterTime.new('13:00') + t.start = @now + + assert_equal Time.local(2006, 8, 17, 13), t.next(:future).begin + assert_equal Time.local(2006, 8, 18, 13), t.next(:future).begin + + t = Chronic::RepeaterTime.new('0400') + t.start = @now + + assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin + assert_equal Time.local(2006, 8, 18, 4), t.next(:future).begin + end + + def test_next_past + t = Chronic::RepeaterTime.new('4:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 4), t.next(:past).begin + assert_equal Time.local(2006, 8, 15, 16), t.next(:past).begin + + t = Chronic::RepeaterTime.new('13:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 13), t.next(:past).begin + assert_equal Time.local(2006, 8, 15, 13), t.next(:past).begin + end + + def test_type + t1 = Chronic::RepeaterTime.new('4') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('14') + assert_equal 50_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('4:00') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('4:30') + assert_equal 16_200, t1.type.time + + t1 = Chronic::RepeaterTime.new('1400') + assert_equal 50_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('0400') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('04') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('400') + assert_equal 14_400, t1.type.time + end + + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterWeek.rb b/framework/Date_Parser/chronic/test/test_RepeaterWeek.rb new file mode 100644 index 000000000..084ef4ee6 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterWeek.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterWeek < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + next_week = weeks.next(:future) + assert_equal Time.local(2006, 8, 20), next_week.begin + assert_equal Time.local(2006, 8, 27), next_week.end + + next_next_week = weeks.next(:future) + assert_equal Time.local(2006, 8, 27), next_next_week.begin + assert_equal Time.local(2006, 9, 3), next_next_week.end + end + + def test_next_past + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + last_week = weeks.next(:past) + assert_equal Time.local(2006, 8, 6), last_week.begin + assert_equal Time.local(2006, 8, 13), last_week.end + + last_last_week = weeks.next(:past) + assert_equal Time.local(2006, 7, 30), last_last_week.begin + assert_equal Time.local(2006, 8, 6), last_last_week.end + end + + def test_this_future + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + this_week = weeks.this(:future) + assert_equal Time.local(2006, 8, 16, 15), this_week.begin + assert_equal Time.local(2006, 8, 20), this_week.end + end + + def test_this_past + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + this_week = weeks.this(:past) + assert_equal Time.local(2006, 8, 13, 0), this_week.begin + assert_equal Time.local(2006, 8, 16, 14), this_week.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 6, 14), offset_span.begin + assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterWeekend.rb b/framework/Date_Parser/chronic/test/test_RepeaterWeekend.rb new file mode 100644 index 000000000..44dc08763 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterWeekend.rb @@ -0,0 +1,75 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterWeekend < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.next(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_next_past + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.next(:past) + assert_equal Time.local(2006, 8, 12), next_weekend.begin + assert_equal Time.local(2006, 8, 14), next_weekend.end + end + + def test_this_future + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_this_past + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:past) + assert_equal Time.local(2006, 8, 12), next_weekend.begin + assert_equal Time.local(2006, 8, 14), next_weekend.end + end + + def test_this_none + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 2), offset_span.begin + assert_equal Time.local(2006, 9, 2, 0, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 1, :past) + + assert_equal Time.local(2006, 8, 12), offset_span.begin + assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 0, :future) + + assert_equal Time.local(2006, 8, 12), offset_span.begin + assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_RepeaterYear.rb b/framework/Date_Parser/chronic/test/test_RepeaterYear.rb new file mode 100644 index 000000000..eaebe25a2 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_RepeaterYear.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterYear < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + next_year = years.next(:future) + assert_equal Time.local(2007, 1, 1), next_year.begin + assert_equal Time.local(2008, 1, 1), next_year.end + + next_next_year = years.next(:future) + assert_equal Time.local(2008, 1, 1), next_next_year.begin + assert_equal Time.local(2009, 1, 1), next_next_year.end + end + + def test_next_past + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + last_year = years.next(:past) + assert_equal Time.local(2005, 1, 1), last_year.begin + assert_equal Time.local(2006, 1, 1), last_year.end + + last_last_year = years.next(:past) + assert_equal Time.local(2004, 1, 1), last_last_year.begin + assert_equal Time.local(2005, 1, 1), last_last_year.end + end + + def test_this + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + this_year = years.this(:future) + assert_equal Time.local(2006, 8, 17), this_year.begin + assert_equal Time.local(2007, 1, 1), this_year.end + + this_year = years.this(:past) + assert_equal Time.local(2006, 1, 1), this_year.begin + assert_equal Time.local(2006, 8, 16), this_year.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterYear.new(:year).offset(span, 3, :future) + + assert_equal Time.local(2009, 8, 16, 14), offset_span.begin + assert_equal Time.local(2009, 8, 16, 14, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterYear.new(:year).offset(span, 10, :past) + + assert_equal Time.local(1996, 8, 16, 14), offset_span.begin + assert_equal Time.local(1996, 8, 16, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_Span.rb b/framework/Date_Parser/chronic/test/test_Span.rb new file mode 100644 index 000000000..099455a2d --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_Span.rb @@ -0,0 +1,24 @@ +require 'chronic' +require 'test/unit' + +class TestSpan < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_span_width + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0)) + assert_equal (60 * 60 * 24), span.width + end + + def test_span_math + s = Chronic::Span.new(1, 2) + assert_equal 2, (s + 1).begin + assert_equal 3, (s + 1).end + assert_equal 0, (s - 1).begin + assert_equal 1, (s - 1).end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_Time.rb b/framework/Date_Parser/chronic/test/test_Time.rb new file mode 100644 index 000000000..3ffc9c019 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_Time.rb @@ -0,0 +1,50 @@ +require 'chronic' +require 'test/unit' + +class TestTime < Test::Unit::TestCase + + def setup + end + + def test_normal + assert_equal Time.local(2006, 1, 2, 0, 0, 0), Time.construct(2006, 1, 2, 0, 0, 0) + assert_equal Time.local(2006, 1, 2, 3, 0, 0), Time.construct(2006, 1, 2, 3, 0, 0) + assert_equal Time.local(2006, 1, 2, 3, 4, 0), Time.construct(2006, 1, 2, 3, 4, 0) + assert_equal Time.local(2006, 1, 2, 3, 4, 5), Time.construct(2006, 1, 2, 3, 4, 5) + end + + def test_second_overflow + assert_equal Time.local(2006, 1, 1, 0, 1, 30), Time.construct(2006, 1, 1, 0, 0, 90) + assert_equal Time.local(2006, 1, 1, 0, 5, 0), Time.construct(2006, 1, 1, 0, 0, 300) + end + + def test_minute_overflow + assert_equal Time.local(2006, 1, 1, 1, 30), Time.construct(2006, 1, 1, 0, 90) + assert_equal Time.local(2006, 1, 1, 5), Time.construct(2006, 1, 1, 0, 300) + end + + def test_hour_overflow + assert_equal Time.local(2006, 1, 2, 12), Time.construct(2006, 1, 1, 36) + assert_equal Time.local(2006, 1, 7), Time.construct(2006, 1, 1, 144) + end + + def test_day_overflow + assert_equal Time.local(2006, 2, 1), Time.construct(2006, 1, 32) + assert_equal Time.local(2006, 3, 5), Time.construct(2006, 2, 33) + assert_equal Time.local(2004, 3, 4), Time.construct(2004, 2, 33) + assert_equal Time.local(2000, 3, 5), Time.construct(2000, 2, 33) + + assert_nothing_raised do + Time.construct(2006, 1, 56) + end + + assert_raise(RuntimeError) do + Time.construct(2006, 1, 57) + end + end + + def test_month_overflow + assert_equal Time.local(2006, 1), Time.construct(2005, 13) + assert_equal Time.local(2005, 12), Time.construct(2000, 72) + end +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_Token.rb b/framework/Date_Parser/chronic/test/test_Token.rb new file mode 100644 index 000000000..80463d1ee --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_Token.rb @@ -0,0 +1,26 @@ +require 'chronic' +require 'test/unit' + +class TestToken < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_token + token = Chronic::Token.new('foo') + assert_equal 0, token.tags.size + assert !token.tagged? + token.tag("mytag") + assert_equal 1, token.tags.size + assert token.tagged? + assert_equal String, token.get_tag(String).class + token.tag(5) + assert_equal 2, token.tags.size + token.untag(String) + assert_equal 1, token.tags.size + assert_equal 'foo', token.word + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/chronic/test/test_parsing.rb b/framework/Date_Parser/chronic/test/test_parsing.rb new file mode 100644 index 000000000..d2216d676 --- /dev/null +++ b/framework/Date_Parser/chronic/test/test_parsing.rb @@ -0,0 +1,614 @@ +require 'chronic' +require 'time' +require 'test/unit' + +class TestParsing < Test::Unit::TestCase + # Wed Aug 16 14:00:00 UTC 2006 + TIME_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0) + + def setup + @time_2006_08_16_14_00_00 = TIME_2006_08_16_14_00_00 + end + + def test_parse_guess_dates + # rm_sd + + time = parse_now("may 27") + assert_equal Time.local(2007, 5, 27, 12), time + + time = parse_now("may 28", :context => :past) + assert_equal Time.local(2006, 5, 28, 12), time + + time = parse_now("may 28 5pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("may 28 at 5pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("may 28 at 5:32.19pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17, 32, 19), time + + # rm_od + + time = parse_now("may 27th") + assert_equal Time.local(2007, 5, 27, 12), time + + time = parse_now("may 27th", :context => :past) + assert_equal Time.local(2006, 5, 27, 12), time + + time = parse_now("may 27th 5:00 pm", :context => :past) + assert_equal Time.local(2006, 5, 27, 17), time + + time = parse_now("may 27th at 5pm", :context => :past) + assert_equal Time.local(2006, 5, 27, 17), time + + time = parse_now("may 27th at 5", :ambiguous_time_range => :none) + assert_equal Time.local(2007, 5, 27, 5), time + + # rm_sy + + time = parse_now("June 1979") + assert_equal Time.local(1979, 6, 16, 0), time + + time = parse_now("dec 79") + assert_equal Time.local(1979, 12, 16, 12), time + + # rm_sd_sy + + time = parse_now("jan 3 2010") + assert_equal Time.local(2010, 1, 3, 12), time + + time = parse_now("jan 3 2010 midnight") + assert_equal Time.local(2010, 1, 4, 0), time + + time = parse_now("jan 3 2010 at midnight") + assert_equal Time.local(2010, 1, 4, 0), time + + time = parse_now("jan 3 2010 at 4", :ambiguous_time_range => :none) + assert_equal Time.local(2010, 1, 3, 4), time + + #time = parse_now("January 12, '00") + #assert_equal Time.local(2000, 1, 12, 12), time + + time = parse_now("may 27 79") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("may 27 79 4:30") + assert_equal Time.local(1979, 5, 27, 16, 30), time + + time = parse_now("may 27 79 at 4:30", :ambiguous_time_range => :none) + assert_equal Time.local(1979, 5, 27, 4, 30), time + + # sd_rm_sy + + time = parse_now("3 jan 2010") + assert_equal Time.local(2010, 1, 3, 12), time + + time = parse_now("3 jan 2010 4pm") + assert_equal Time.local(2010, 1, 3, 16), time + + # sm_sd_sy + + time = parse_now("5/27/1979") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("5/27/1979 4am") + assert_equal Time.local(1979, 5, 27, 4), time + + # sd_sm_sy + + time = parse_now("27/5/1979") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("27/5/1979 @ 0700") + assert_equal Time.local(1979, 5, 27, 7), time + + # sm_sy + + time = parse_now("05/06") + assert_equal Time.local(2006, 5, 16, 12), time + + time = parse_now("12/06") + assert_equal Time.local(2006, 12, 16, 12), time + + time = parse_now("13/06") + assert_equal nil, time + + # sy_sm_sd + + time = parse_now("2000-1-1") + assert_equal Time.local(2000, 1, 1, 12), time + + time = parse_now("2006-08-20") + assert_equal Time.local(2006, 8, 20, 12), time + + time = parse_now("2006-08-20 7pm") + assert_equal Time.local(2006, 8, 20, 19), time + + time = parse_now("2006-08-20 03:00") + assert_equal Time.local(2006, 8, 20, 3), time + + time = parse_now("2006-08-20 03:30:30") + assert_equal Time.local(2006, 8, 20, 3, 30, 30), time + + time = parse_now("2006-08-20 15:30:30") + assert_equal Time.local(2006, 8, 20, 15, 30, 30), time + + time = parse_now("2006-08-20 15:30.30") + assert_equal Time.local(2006, 8, 20, 15, 30, 30), time + + # rdn_rm_rd_rt_rtz_ry + + time = parse_now("Mon Apr 02 17:00:00 PDT 2007") + assert_equal Time.local(2007, 4, 2, 17), time + + now = Time.now + time = parse_now(now.to_s) + assert_equal now.to_s, time.to_s + + # rm_sd_rt + + #time = parse_now("jan 5 13:00") + #assert_equal Time.local(2007, 1, 5, 13), time + + # due to limitations of the Time class, these don't work + + time = parse_now("may 40") + assert_equal nil, time + + time = parse_now("may 27 40") + assert_equal nil, time + + time = parse_now("1800-08-20") + assert_equal nil, time + end + + def test_foo + Chronic.parse('two months ago this friday') + end + + def test_parse_guess_r + time = parse_now("friday") + assert_equal Time.local(2006, 8, 18, 12), time + + time = parse_now("tue") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("5") + assert_equal Time.local(2006, 8, 16, 17), time + + time = Chronic.parse("5", :now => Time.local(2006, 8, 16, 3, 0, 0, 0), :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 16, 5), time + + time = parse_now("13:00") + assert_equal Time.local(2006, 8, 17, 13), time + + time = parse_now("13:45") + assert_equal Time.local(2006, 8, 17, 13, 45), time + + time = parse_now("november") + assert_equal Time.local(2006, 11, 16), time + end + + def test_parse_guess_rr + time = parse_now("friday 13:00") + assert_equal Time.local(2006, 8, 18, 13), time + + time = parse_now("monday 4:00") + assert_equal Time.local(2006, 8, 21, 16), time + + time = parse_now("sat 4:00", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 19, 4), time + + time = parse_now("sunday 4:20", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 20, 4, 20), time + + time = parse_now("4 pm") + assert_equal Time.local(2006, 8, 16, 16), time + + time = parse_now("4 am", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 16, 4), time + + time = parse_now("12 pm") + assert_equal Time.local(2006, 8, 16, 12), time + + time = parse_now("12:01 pm") + assert_equal Time.local(2006, 8, 16, 12, 1), time + + time = parse_now("12:01 am") + assert_equal Time.local(2006, 8, 16, 0, 1), time + + time = parse_now("12 am") + assert_equal Time.local(2006, 8, 16), time + + time = parse_now("4:00 in the morning") + assert_equal Time.local(2006, 8, 16, 4), time + + time = parse_now("november 4") + assert_equal Time.local(2006, 11, 4, 12), time + + time = parse_now("aug 24") + assert_equal Time.local(2006, 8, 24, 12), time + end + + def test_parse_guess_rrr + time = parse_now("friday 1 pm") + assert_equal Time.local(2006, 8, 18, 13), time + + time = parse_now("friday 11 at night") + assert_equal Time.local(2006, 8, 18, 23), time + + time = parse_now("friday 11 in the evening") + assert_equal Time.local(2006, 8, 18, 23), time + + time = parse_now("sunday 6am") + assert_equal Time.local(2006, 8, 20, 6), time + + time = parse_now("friday evening at 7") + assert_equal Time.local(2006, 8, 18, 19), time + end + + def test_parse_guess_gr + # year + + time = parse_now("this year") + assert_equal Time.local(2006, 10, 24, 12, 30), time + + time = parse_now("this year", :context => :past) + assert_equal Time.local(2006, 4, 24, 12, 30), time + + # month + + time = parse_now("this month") + assert_equal Time.local(2006, 8, 24, 12), time + + time = parse_now("this month", :context => :past) + assert_equal Time.local(2006, 8, 8, 12), time + + time = Chronic.parse("next month", :now => Time.local(2006, 11, 15)) + assert_equal Time.local(2006, 12, 16, 12), time + + # month name + + time = parse_now("last november") + assert_equal Time.local(2005, 11, 16), time + + # fortnight + + time = parse_now("this fortnight") + assert_equal Time.local(2006, 8, 21, 19, 30), time + + time = parse_now("this fortnight", :context => :past) + assert_equal Time.local(2006, 8, 14, 19), time + + # week + + time = parse_now("this week") + assert_equal Time.local(2006, 8, 18, 7, 30), time + + time = parse_now("this week", :context => :past) + assert_equal Time.local(2006, 8, 14, 19), time + + # weekend + + time = parse_now("this weekend") + assert_equal Time.local(2006, 8, 20), time + + time = parse_now("this weekend", :context => :past) + assert_equal Time.local(2006, 8, 13), time + + time = parse_now("last weekend") + assert_equal Time.local(2006, 8, 13), time + + # day + + time = parse_now("this day") + assert_equal Time.local(2006, 8, 16, 19, 30), time + + time = parse_now("this day", :context => :past) + assert_equal Time.local(2006, 8, 16, 7), time + + time = parse_now("today") + assert_equal Time.local(2006, 8, 16, 19, 30), time + + time = parse_now("yesterday") + assert_equal Time.local(2006, 8, 15, 12), time + + time = parse_now("tomorrow") + assert_equal Time.local(2006, 8, 17, 12), time + + # day name + + time = parse_now("this tuesday") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("next tuesday") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("last tuesday") + assert_equal Time.local(2006, 8, 15, 12), time + + time = parse_now("this wed") + assert_equal Time.local(2006, 8, 23, 12), time + + time = parse_now("next wed") + assert_equal Time.local(2006, 8, 23, 12), time + + time = parse_now("last wed") + assert_equal Time.local(2006, 8, 9, 12), time + + # day portion + + time = parse_now("this morning") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("tonight") + assert_equal Time.local(2006, 8, 16, 22), time + + # minute + + time = parse_now("next minute") + assert_equal Time.local(2006, 8, 16, 14, 1, 30), time + + # second + + time = parse_now("this second") + assert_equal Time.local(2006, 8, 16, 14), time + + time = parse_now("this second", :context => :past) + assert_equal Time.local(2006, 8, 16, 14), time + + time = parse_now("next second") + assert_equal Time.local(2006, 8, 16, 14, 0, 1), time + + time = parse_now("last second") + assert_equal Time.local(2006, 8, 16, 13, 59, 59), time + end + + def test_parse_guess_grr + time = parse_now("yesterday at 4:00") + assert_equal Time.local(2006, 8, 15, 16), time + + time = parse_now("today at 9:00") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("today at 2100") + assert_equal Time.local(2006, 8, 16, 21), time + + time = parse_now("this day at 0900") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("tomorrow at 0900") + assert_equal Time.local(2006, 8, 17, 9), time + + time = parse_now("yesterday at 4:00", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 15, 4), time + + time = parse_now("last friday at 4:00") + assert_equal Time.local(2006, 8, 11, 16), time + + time = parse_now("next wed 4:00") + assert_equal Time.local(2006, 8, 23, 16), time + + time = parse_now("yesterday afternoon") + assert_equal Time.local(2006, 8, 15, 15), time + + time = parse_now("last week tuesday") + assert_equal Time.local(2006, 8, 8, 12), time + + time = parse_now("tonight at 7") + assert_equal Time.local(2006, 8, 16, 19), time + + time = parse_now("tonight 7") + assert_equal Time.local(2006, 8, 16, 19), time + + time = parse_now("7 tonight") + assert_equal Time.local(2006, 8, 16, 19), time + end + + def test_parse_guess_grrr + time = parse_now("today at 6:00pm") + assert_equal Time.local(2006, 8, 16, 18), time + + time = parse_now("today at 6:00am") + assert_equal Time.local(2006, 8, 16, 6), time + + time = parse_now("this day 1800") + assert_equal Time.local(2006, 8, 16, 18), time + + time = parse_now("yesterday at 4:00pm") + assert_equal Time.local(2006, 8, 15, 16), time + + time = parse_now("tomorrow evening at 7") + assert_equal Time.local(2006, 8, 17, 19), time + + time = parse_now("tomorrow morning at 5:30") + assert_equal Time.local(2006, 8, 17, 5, 30), time + + time = parse_now("next monday at 12:01 am") + assert_equal Time.local(2006, 8, 21, 00, 1), time + + time = parse_now("next monday at 12:01 pm") + assert_equal Time.local(2006, 8, 21, 12, 1), time + end + + def test_parse_guess_rgr + time = parse_now("afternoon yesterday") + assert_equal Time.local(2006, 8, 15, 15), time + + time = parse_now("tuesday last week") + assert_equal Time.local(2006, 8, 8, 12), time + end + + def test_parse_guess_s_r_p + # past + + time = parse_now("3 years ago") + assert_equal Time.local(2003, 8, 16, 14), time + + time = parse_now("1 month ago") + assert_equal Time.local(2006, 7, 16, 14), time + + time = parse_now("1 fortnight ago") + assert_equal Time.local(2006, 8, 2, 14), time + + time = parse_now("2 fortnights ago") + assert_equal Time.local(2006, 7, 19, 14), time + + time = parse_now("3 weeks ago") + assert_equal Time.local(2006, 7, 26, 14), time + + time = parse_now("2 weekends ago") + assert_equal Time.local(2006, 8, 5), time + + time = parse_now("3 days ago") + assert_equal Time.local(2006, 8, 13, 14), time + + #time = parse_now("1 monday ago") + #assert_equal Time.local(2006, 8, 14, 12), time + + time = parse_now("5 mornings ago") + assert_equal Time.local(2006, 8, 12, 9), time + + time = parse_now("7 hours ago") + assert_equal Time.local(2006, 8, 16, 7), time + + time = parse_now("3 minutes ago") + assert_equal Time.local(2006, 8, 16, 13, 57), time + + time = parse_now("20 seconds before now") + assert_equal Time.local(2006, 8, 16, 13, 59, 40), time + + # future + + time = parse_now("3 years from now") + assert_equal Time.local(2009, 8, 16, 14, 0, 0), time + + time = parse_now("6 months hence") + assert_equal Time.local(2007, 2, 16, 14), time + + time = parse_now("3 fortnights hence") + assert_equal Time.local(2006, 9, 27, 14), time + + time = parse_now("1 week from now") + assert_equal Time.local(2006, 8, 23, 14, 0, 0), time + + time = parse_now("1 weekend from now") + assert_equal Time.local(2006, 8, 19), time + + time = parse_now("2 weekends from now") + assert_equal Time.local(2006, 8, 26), time + + time = parse_now("1 day hence") + assert_equal Time.local(2006, 8, 17, 14), time + + time = parse_now("5 mornings hence") + assert_equal Time.local(2006, 8, 21, 9), time + + time = parse_now("1 hour from now") + assert_equal Time.local(2006, 8, 16, 15), time + + time = parse_now("20 minutes hence") + assert_equal Time.local(2006, 8, 16, 14, 20), time + + time = parse_now("20 seconds from now") + assert_equal Time.local(2006, 8, 16, 14, 0, 20), time + + time = Chronic.parse("2 months ago", :now => Time.parse("2007-03-07 23:30")) + assert_equal Time.local(2007, 1, 7, 23, 30), time + end + + def test_parse_guess_p_s_r + time = parse_now("in 3 hours") + assert_equal Time.local(2006, 8, 16, 17), time + end + + def test_parse_guess_s_r_p_a + # past + + time = parse_now("3 years ago tomorrow") + assert_equal Time.local(2003, 8, 17, 12), time + + time = parse_now("3 years ago this friday") + assert_equal Time.local(2003, 8, 18, 12), time + + time = parse_now("3 months ago saturday at 5:00 pm") + assert_equal Time.local(2006, 5, 19, 17), time + + time = parse_now("2 days from this second") + assert_equal Time.local(2006, 8, 18, 14), time + + time = parse_now("7 hours before tomorrow at midnight") + assert_equal Time.local(2006, 8, 17, 17), time + + # future + end + + def test_parse_guess_o_r_s_r + time = parse_now("3rd wednesday in november") + assert_equal Time.local(2006, 11, 15, 12), time + + time = parse_now("10th wednesday in november") + assert_equal nil, time + + # time = parse_now("3rd wednesday in 2007") + # assert_equal Time.local(2007, 1, 20, 12), time + end + + def test_parse_guess_o_r_g_r + time = parse_now("3rd month next year") + assert_equal Time.local(2007, 3, 16, 12, 30), time + + time = parse_now("3rd thursday this september") + assert_equal Time.local(2006, 9, 21, 12), time + + time = parse_now("4th day last week") + assert_equal Time.local(2006, 8, 9, 12), time + end + + def test_parse_guess_nonsense + time = parse_now("some stupid nonsense") + assert_equal nil, time + end + + def test_parse_span + span = parse_now("friday", :guess => false) + assert_equal Time.local(2006, 8, 18), span.begin + assert_equal Time.local(2006, 8, 19), span.end + + span = parse_now("november", :guess => false) + assert_equal Time.local(2006, 11), span.begin + assert_equal Time.local(2006, 12), span.end + + span = Chronic.parse("weekend" , :now => @time_2006_08_16_14_00_00, :guess => false) + assert_equal Time.local(2006, 8, 19), span.begin + assert_equal Time.local(2006, 8, 21), span.end + end + + def test_parse_words + assert_equal parse_now("33 days from now"), parse_now("thirty-three days from now") + assert_equal parse_now("2867532 seconds from now"), parse_now("two million eight hundred and sixty seven thousand five hundred and thirty two seconds from now") + assert_equal parse_now("may 10th"), parse_now("may tenth") + end + + def test_parse_only_complete_pointers + assert_equal parse_now("eat pasty buns today at 2pm"), @time_2006_08_16_14_00_00 + assert_equal parse_now("futuristically speaking today at 2pm"), @time_2006_08_16_14_00_00 + assert_equal parse_now("meeting today at 2pm"), @time_2006_08_16_14_00_00 + end + + def test_argument_validation + assert_raise(Chronic::InvalidArgumentException) do + time = Chronic.parse("may 27", :foo => :bar) + end + + assert_raise(Chronic::InvalidArgumentException) do + time = Chronic.parse("may 27", :context => :bar) + end + end + + private + def parse_now(string, options={}) + Chronic.parse(string, {:now => TIME_2006_08_16_14_00_00 }.merge(options)) + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser.php b/framework/Date_Parser/lib/Horde/Date/Parser.php new file mode 100644 index 000000000..6406ce56d --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser.php @@ -0,0 +1,52 @@ +:guess). If no date or time can be found, + # +nil+ will be returned. + # + # Options are: + # + # [:context] + # :past or :future (defaults to :future) + # + # If your string represents a birthday, you can set :context to :past + # and if an ambiguous string is given, it will assume it is in the + # past. Specify :future or omit to set a future context. + # + # [:now] + # Time (defaults to Time.now) + # + # By setting :now to a Time, all computations will be based off + # of that time instead of Time.now + # + # [:guess] + # +true+ or +false+ (defaults to +true+) + # + # 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 :guess to +false+ and a Chronic::Span will be returned. + # + # [:ambiguous_time_range] + # Integer or :none (defaults to 6 (6am-6pm)) + # + # 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. + def parse(text, specified_options = {}) + # get options and set defaults if necessary + default_options = {:context => :future, + :now => Time.now, + :guess => true, + :ambiguous_time_range => 6} + options = default_options.merge specified_options + + # ensure the specified options are valid + specified_options.keys.each do |key| + default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.") + end + [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.") + + # store now for later =) + @now = options[:now] + + # put the text into a normal format to ease scanning + text = self.pre_normalize(text) + + # get base tokens for each word + @tokens = self.base_tokenize(text) + + # scan the tokens with each token scanner + [Repeater].each do |tokenizer| + @tokens = tokenizer.scan(@tokens, options) + end + + [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer| + @tokens = tokenizer.scan(@tokens) + end + + # strip any non-tagged tokens + @tokens = @tokens.select { |token| token.tagged? } + + if Chronic.debug + puts "+---------------------------------------------------" + puts "| " + @tokens.to_s + puts "+---------------------------------------------------" + end + + # do the heavy lifting + begin + span = self.tokens_to_span(@tokens, options) + rescue + raise + return nil + end + + # guess a time within a span if required + if options[:guess] + return self.guess(span) + else + return span + end + end + + # Clean up the specified input text 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) + def pre_normalize(text) #:nodoc: + normalized_text = text.to_s.downcase + normalized_text = numericize_numbers(normalized_text) + normalized_text.gsub!(/['"\.]/, '') + normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' } + normalized_text.gsub!(/\btoday\b/, 'this day') + normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day') + normalized_text.gsub!(/\byesterday\b/, 'last day') + normalized_text.gsub!(/\bnoon\b/, '12:00') + normalized_text.gsub!(/\bmidnight\b/, '24:00') + normalized_text.gsub!(/\bbefore now\b/, 'past') + normalized_text.gsub!(/\bnow\b/, 'this second') + normalized_text.gsub!(/\b(ago|before)\b/, 'past') + normalized_text.gsub!(/\bthis past\b/, 'last') + normalized_text.gsub!(/\bthis last\b/, 'last') + normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1') + normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1') + normalized_text.gsub!(/\btonight\b/, 'this night') + normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1') + normalized_text.gsub!(/\b(hence|after|from)\b/, 'future') + normalized_text = numericize_ordinals(normalized_text) + end + + # Convert number words to numbers (three => 3) + def numericize_numbers(text) #:nodoc: + Numerizer.numerize(text) + end + + # Convert ordinal words to numeric ordinals (third => 3rd) + def numericize_ordinals(text) #:nodoc: + text + end + + # Split the text on spaces and convert each word into + # a Token + def base_tokenize(text) #:nodoc: + text.split(' ').map { |word| Token.new(word) } + end + + # Guess a specific time within the given span + def guess(span) #:nodoc: + return nil if span.nil? + if span.width > 1 + span.begin + (span.width / 2) + else + span.begin + end + end + end + +end diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Grabber.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Grabber.php new file mode 100644 index 000000000..4162a260b --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Grabber.php @@ -0,0 +1,26 @@ +#module Chronic + + class Chronic::Grabber < Chronic::Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/last/ => :last, + /this/ => :this, + /next/ => :next} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'grabber-' << @type.to_s + end + end + +#end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Handlers.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Handlers.php new file mode 100644 index 000000000..551d632fa --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Handlers.php @@ -0,0 +1,469 @@ +module Chronic + + class << self + + def definitions #:nodoc: + @definitions ||= + {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)], + + :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + 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_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy), + Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)], + + # tonight at 7pm + :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)], + + # 3 weeks from now, in 2 months + :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)], + + # 3rd week in march + :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)] + } + end + + def tokens_to_span(tokens, options) #:nodoc: + # maybe it's a specific date + + self.definitions[:date].each do |handler| + if handler.match(tokens, self.definitions) + puts "-date" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # I guess it's not a specific date, maybe it's just an anchor + + self.definitions[:anchor].each do |handler| + if handler.match(tokens, self.definitions) + puts "-anchor" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an anchor, perhaps it's an arrow + + self.definitions[:arrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-arrow" if Chronic.debug + good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an arrow, let's hope it's a narrow + + self.definitions[:narrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-narrow" if Chronic.debug + #good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, tokens, options) + end + end + + # I guess you're out of luck! + puts "-none" if Chronic.debug + return nil + end + + #-------------- + + def day_or_time(day_start, time_tokens, options) + outer_span = Span.new(day_start, day_start + (24 * 60 * 60)) + + if !time_tokens.empty? + @now = outer_span.begin + time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options) + return time + else + return outer_span + end + end + + #-------------- + + def handle_m_d(month, day, time_tokens, options) #:nodoc: + month.start = @now + span = month.this(options[:context]) + + day_start = Time.local(span.begin.year, span.begin.month, day) + + day_or_time(day_start, time_tokens, options) + end + + def handle_rmn_sd(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_od(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_sy(tokens, options) #:nodoc: + 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 + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc: + month = tokens[1].get_tag(RepeaterMonthName).index + day = tokens[2].get_tag(ScalarDay).type + year = tokens[5].get_tag(ScalarYear).type + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, [tokens[3]], options) + rescue ArgumentError + nil + end + end + + def handle_rmn_sd_sy(tokens, options) #:nodoc: + 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) + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_rmn_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_rmn_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sd_sy(tokens, options) #:nodoc: + 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) + + begin + day_start = Time.local(year, month, day) #:nodoc: + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_sm_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sy_sm_sd(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + 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 + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + # anchors + + def handle_r(tokens, options) #:nodoc: + dd_tokens = dealias_and_disambiguate_times(tokens, options) + self.get_anchor(dd_tokens, options) + end + + def handle_r_g_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + self.handle_r(new_tokens, options) + end + + # arrows + + def handle_srp(tokens, span, options) #:nodoc: + 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) + end + + def handle_s_r_p(tokens, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + + # span = + # case true + # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class) + # self.parse("this hour", :guess => false, :now => @now) + # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class) + # self.parse("this minute", :guess => false, :now => @now) + # when [RepeaterMinute, RepeaterSecond].include?(repeater.class) + # self.parse("this second", :guess => false, :now => @now) + # else + # raise(ChronicPain, "Invalid repeater: #{repeater.class}") + # end + + span = self.parse("this second", :guess => false, :now => @now) + + self.handle_srp(tokens, span, options) + end + + def handle_p_s_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + self.handle_s_r_p(new_tokens, options) + end + + def handle_s_r_p_a(tokens, options) #:nodoc: + anchor_span = get_anchor(tokens[3..tokens.size - 1], options) + self.handle_srp(tokens, anchor_span, options) + end + + # narrows + + def handle_orr(tokens, outer_span, options) #:nodoc: + 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 + + def handle_o_r_s_r(tokens, options) #:nodoc: + outer_span = get_anchor([tokens[3]], options) + handle_orr(tokens[0..1], outer_span, options) + end + + def handle_o_r_g_r(tokens, options) #:nodoc: + outer_span = get_anchor(tokens[2..3], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # support methods + + def get_anchor(tokens, options) #:nodoc: + grabber = Grabber.new(:this) + pointer = :future + + repeaters = self.get_repeaters(tokens) + repeaters.size.times { tokens.pop } + + if tokens.first && tokens.first.get_tag(Grabber) + grabber = tokens.first.get_tag(Grabber) + tokens.pop + end + + head = repeaters.shift + head.start = @now + + case grabber.type + when :last + outer_span = head.next(:past) + when :this + if 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(ChronicPain, "Invalid grabber") + end + + puts "--#{outer_span}" if Chronic.debug + anchor = find_within(repeaters, outer_span, pointer) + end + + def get_repeaters(tokens) #:nodoc: + repeaters = [] + tokens.each do |token| + if t = token.get_tag(Repeater) + repeaters << t + end + end + repeaters.sort.reverse + 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) #:nodoc: + puts "--#{span}" if Chronic.debug + return span if tags.empty? + + head, *rest = tags + head.start = pointer == :future ? span.begin : span.end + h = head.this(:none) + + if span.include?(h.begin) || span.include?(h.end) + return find_within(rest, h, pointer) + else + return nil + end + end + + def dealias_and_disambiguate_times(tokens, options) #:nodoc: + # 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) + + if [:morning].include?(t1tag.type) + puts '--morning->am' if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:am)) + elsif [:afternoon, :evening, :night].include?(t1tag.type) + puts "--#{t1tag.type}->pm" if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:pm)) + end + end + + # tokens.each_with_index do |t0, i| + # t1 = tokens[i + 1] + # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime) + # if [:morning].include?(t1tag.type) + # puts '--morning->am' if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:am)) + # elsif [:afternoon, :evening, :night].include?(t1tag.type) + # puts "--#{t1tag.type}->pm" if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:pm)) + # end + # end + # end + + # handle ambiguous times if :ambiguous_time_range is specified + if options[:ambiguous_time_range] != :none + ttokens = [] + tokens.each_with_index do |t0, i| + ttokens << t0 + t1 = tokens[i + 1] + if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion)) + distoken = Token.new('disambiguator') + distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range])) + ttokens << distoken + end + end + tokens = ttokens + end + + tokens + end + + end + + class Handler #:nodoc: + attr_accessor :pattern, :handler_method + + def initialize(pattern, handler_method) + @pattern = pattern + @handler_method = handler_method + end + + def constantize(name) + camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase } + ::Chronic.module_eval(camel, __FILE__, __LINE__) + end + + def match(tokens, definitions) + token_index = 0 + @pattern.each do |element| + name = element.to_s + optional = name.reverse[0..0] == '?' + name = name.chop if optional + if element.instance_of? Symbol + klass = constantize(name) + match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty? + return false if !match && !optional + (token_index += 1; next) if match + next if !match && optional + elsif element.instance_of? String + return true if optional && token_index == tokens.size + sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified") + sub_handlers.each do |sub_handler| + return true if sub_handler.match(tokens[token_index..tokens.size], definitions) + end + return false + else + raise(ChronicPain, "Invalid match type: #{element.class}") + end + end + return false if token_index != tokens.size + return true + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Ordinal.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Ordinal.php new file mode 100644 index 000000000..45b8148e4 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Ordinal.php @@ -0,0 +1,40 @@ +module Chronic + + class Ordinal < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_ordinals(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + return Ordinal.new($1.to_i) + end + return nil + end + + def self.scan_for_days(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + unless $1.to_i > 31 + return OrdinalDay.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'ordinal' + end + end + + class OrdinalDay < Ordinal #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Pointer.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Pointer.php new file mode 100644 index 000000000..224efaf96 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Pointer.php @@ -0,0 +1,27 @@ +module Chronic + + class Pointer < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/\bpast\b/ => :past, + /\bfuture\b/ => :future, + /\bin\b/ => :future} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'pointer-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeater.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeater.php new file mode 100644 index 000000000..9f80daf2f --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeater.php @@ -0,0 +1,115 @@ +class Chronic::Repeater < Chronic::Tag #:nodoc: + def self.scan(tokens, options) + # for each token + tokens.each_index do |i| + if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end + if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_month_names(token) + scanner = {/^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} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_names(token) + scanner = {/^m[ou]n(day)?$/ => :monday, + /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday, + /^tue$/ => :tuesday, + /^we(dnes|nds|nns)day$/ => :wednesday, + /^wed$/ => :wednesday, + /^th(urs|ers)day$/ => :thursday, + /^thu$/ => :thursday, + /^fr[iy](day)?$/ => :friday, + /^sat(t?[ue]rday)?$/ => :saturday, + /^su[nm](day)?$/ => :sunday} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_portions(token) + scanner = {/^ams?$/ => :am, + /^pms?$/ => :pm, + /^mornings?$/ => :morning, + /^afternoons?$/ => :afternoon, + /^evenings?$/ => :evening, + /^(night|nite)s?$/ => :night} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_times(token, options) + if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/ + return Chronic::RepeaterTime.new(token.word, options) + end + return nil + end + + def self.scan_for_units(token) + scanner = {/^years?$/ => :year, + /^seasons?$/ => :season, + /^months?$/ => :month, + /^fortnights?$/ => :fortnight, + /^weeks?$/ => :week, + /^weekends?$/ => :weekend, + /^days?$/ => :day, + /^hours?$/ => :hour, + /^minutes?$/ => :minute, + /^seconds?$/ => :second} + scanner.keys.each do |scanner_item| + if scanner_item =~ token.word + klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize + klass = eval(klass_name) + return klass.new(scanner[scanner_item]) + end + end + return nil + end + + def <=>(other) + width <=> other.width + end + + # returns the width (in seconds or months) of this repeatable. + def width + raise("Repeatable#width must be overridden in subclasses") + end + + # returns the next occurance of this repeatable. + def next(pointer) + !@now.nil? || raise("Start point must be set before calling #next") + [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future") + #raise("Repeatable#next must be overridden in subclasses") + end + + def this(pointer) + !@now.nil? || raise("Start point must be set before calling #this") + [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none") + end + + def to_s + 'repeater' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Day.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Day.php new file mode 100644 index 000000000..a92d83f63 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Day.php @@ -0,0 +1,47 @@ +class Chronic::RepeaterDay < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def next(pointer) + super + + if !@current_day_start + @current_day_start = Time.local(@now.year, @now.month, @now.day) + end + + direction = pointer == :future ? 1 : -1 + @current_day_start += direction * DAY_SECONDS + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + when :past + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + when :none + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayName.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayName.php new file mode 100644 index 000000000..0486a4ddf --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayName.php @@ -0,0 +1,46 @@ +class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + if !@current_day_start + @current_day_start = Time.construct(@now.year, @now.month, @now.day) + @current_day_start += direction * DAY_SECONDS + + day_num = symbol_to_number(@type) + + while @current_day_start.wday != day_num + @current_day_start += direction * DAY_SECONDS + end + else + @current_day_start += direction * 7 * DAY_SECONDS + end + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + 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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayPortion.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayPortion.php new file mode 100644 index 000000000..c854933ad --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/DayPortion.php @@ -0,0 +1,93 @@ +class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc: + @@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) + super + + if type.kind_of? Integer + @range = (@type * 60 * 60)..((@type + 12) * 60 * 60) + else + lookup = {:am => 0..(12 * 60 * 60 - 1), + :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1), + :morning => @@morning, + :afternoon => @@afternoon, + :evening => @@evening, + :night => @@night} + @range = lookup[type] + lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion") + end + @range || raise("Range should have been set by now") + end + + def next(pointer) + super + + full_day = 60 * 60 * 24 + + if !@current_span + now_seconds = @now - Time.construct(@now.year, @now.month, @now.day) + if now_seconds < @range.begin + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + elsif now_seconds > @range.end + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + end + else + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + end + + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + else + case pointer + when :future + @current_span += full_day + when :past + @current_span -= full_day + end + end + end + + def this(context = :future) + super + + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + end + + def offset(span, amount, pointer) + @now = span.begin + portion_span = self.next(pointer) + direction = pointer == :future ? 1 : -1 + portion_span + (direction * (amount - 1) * Chronic::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 +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Fortnight.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Fortnight.php new file mode 100644 index 000000000..058fbb904 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Fortnight.php @@ -0,0 +1,65 @@ +class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc: + FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_fortnight_start + case pointer + when :future + sunday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::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 + + Chronic::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 = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::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 + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + when :past + this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_fortnight_start = last_sunday_span.begin + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Hour.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Hour.php new file mode 100644 index 000000000..f38a3f825 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Hour.php @@ -0,0 +1,52 @@ +class Chronic::RepeaterHour < Chronic::Repeater #:nodoc: + HOUR_SECONDS = 3600 # 60 * 60 + + def next(pointer) + super + + if !@current_hour_start + case pointer + when :future + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_hour_start += direction * HOUR_SECONDS + end + + Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :none + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = hour_begin + HOUR_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Minute.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Minute.php new file mode 100644 index 000000000..342d3cd41 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Minute.php @@ -0,0 +1,52 @@ +class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc: + MINUTE_SECONDS = 60 + + def next(pointer = :future) + super + + if !@current_minute_start + case pointer + when :future + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + when :past + @current_minute_start = Time.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 + + Chronic::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 = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :past + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = @now + when :none + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS + end + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Month.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Month.php new file mode 100644 index 000000000..edd89eeb2 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Month.php @@ -0,0 +1,61 @@ +class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + YEAR_MONTHS = 12 + + def next(pointer) + super + + if !@current_month_start + @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer) + else + @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer) + end + + Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + month_start = Time.construct(@now.year, @now.month, @now.day + 1) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + when :past + month_start = Time.construct(@now.year, @now.month) + month_end = Time.construct(@now.year, @now.month, @now.day) + when :none + month_start = Time.construct(@now.year, @now.month) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + end + + Chronic::Span.new(month_start, month_end) + end + + def offset(span, amount, pointer) + Chronic::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 + Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec) + end + + def width + MONTH_SECONDS + end + + def to_s + super << '-month' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/MonthName.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/MonthName.php new file mode 100644 index 000000000..1f8b748a9 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/MonthName.php @@ -0,0 +1,93 @@ +class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + + def next(pointer) + super + + if !@current_month_begin + target_month = symbol_to_number(@type) + case pointer + when :future + if @now.month < target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :none + if @now.month <= target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :past + if @now.month > target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month < target_month + @current_month_begin = Time.construct(@now.year - 1, target_month) + end + end + @current_month_begin || raise("Current month should be set by now") + else + case pointer + when :future + @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month) + when :past + @current_month_begin = Time.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 + + Chronic::Span.new(@current_month_begin, Time.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 + symbol_to_number(@type) + end + + def to_s + super << '-monthname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Season.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Season.php new file mode 100644 index 000000000..a255865fb --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Season.php @@ -0,0 +1,23 @@ +class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + + def next(pointer) + super + + raise 'Not implemented' + end + + def this(pointer = :future) + super + + raise 'Not implemented' + end + + def width + SEASON_SECONDS + end + + def to_s + super << '-season' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/SeasonName.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/SeasonName.php new file mode 100644 index 000000000..adfd1f281 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/SeasonName.php @@ -0,0 +1,24 @@ +class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc: + @summer = ['jul 21', 'sep 22'] + @autumn = ['sep 23', 'dec 21'] + @winter = ['dec 22', 'mar 19'] + @spring = ['mar 20', 'jul 20'] + + def next(pointer) + super + raise 'Not implemented' + end + + def this(pointer = :future) + super + raise 'Not implemented' + end + + def width + (91 * 24 * 60 * 60) + end + + def to_s + super << '-season-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Second.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Second.php new file mode 100644 index 000000000..6d05545ca --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Second.php @@ -0,0 +1,36 @@ +class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc: + SECOND_SECONDS = 1 # haha, awesome + + def next(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + if !@second_start + @second_start = @now + (direction * SECOND_SECONDS) + else + @second_start += SECOND_SECONDS * direction + end + + Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS) + end + + def this(pointer = :future) + super + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Time.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Time.php new file mode 100644 index 000000000..f8560141c --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Time.php @@ -0,0 +1,117 @@ +class Chronic::RepeaterTime < Chronic::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 = {}) + t = time.gsub(/\:/, '') + @type = + if (1..2) === t.size + hours = t.to_i + hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true) + elsif t.size == 3 + Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true) + elsif t.size == 4 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous) + elsif t.size == 5 + Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true) + elsif t.size == 6 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) + else + raise("Time cannot exceed six digits") + end + 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 = Time.local(@now.year, @now.month, @now.day) + yesterday_midnight = midnight - full_day + tomorrow_midnight = midnight + full_day + + catch :done do + if pointer == :future + if @type.ambiguous? + [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + else + [midnight + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + end + else # pointer == :past + if @type.ambiguous? + [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t| + (@current_time = t; throw :done) if t <= @now + end + else + [midnight + @type, yesterday_midnight + @type].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 + + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Week.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Week.php new file mode 100644 index 000000000..ec88ff14b --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Week.php @@ -0,0 +1,68 @@ +class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc: + WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + sunday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::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 + + Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + this_sunday_span = sunday_repeater.this(:future) + this_week_end = this_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :past + this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :none + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Weekend.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Weekend.php new file mode 100644 index 000000000..f012267d9 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Weekend.php @@ -0,0 +1,60 @@ +class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc: + WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + saturday_repeater = Chronic::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 = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = (@now + Chronic::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 * Chronic::RepeaterWeek::WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future, :none + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + this_saturday_span = saturday_repeater.this(:future) + Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS) + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + last_saturday_span = saturday_repeater.this(:past) + Chronic::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 = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = span.begin + start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS + Chronic::Span.new(start, start + (span.end - span.begin)) + end + + def width + WEEKEND_SECONDS + end + + def to_s + super << '-weekend' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Year.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Year.php new file mode 100644 index 000000000..426371f9b --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Repeaters/Year.php @@ -0,0 +1,58 @@ +class Chronic::RepeaterYear < Chronic::Repeater #:nodoc: + + def next(pointer) + super + + if !@current_year_start + case pointer + when :future + @current_year_start = Time.construct(@now.year + 1) + when :past + @current_year_start = Time.construct(@now.year - 1) + end + else + diff = pointer == :future ? 1 : -1 + @current_year_start = Time.construct(@current_year_start.year + diff) + end + + Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS + this_year_end = Time.construct(@now.year + 1, 1, 1) + when :past + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year, @now.month, @now.day) + when :none + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year + 1, 1, 1) + end + + Chronic::Span.new(this_year_start, this_year_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + + sb = span.begin + new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec) + + se = span.end + new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec) + + Chronic::Span.new(new_begin, new_end) + end + + def width + (365 * 24 * 60 * 60) + end + + def to_s + super << '-year' + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Scalar.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Scalar.php new file mode 100644 index 000000000..b08cfee18 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Scalar.php @@ -0,0 +1,74 @@ +module Chronic + + class Scalar < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_scalars(token, post_token) + if token.word =~ /^\d*$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return Scalar.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_days(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarDay.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_months(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarMonth.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_years(token, post_token) + if token.word =~ /^([1-9]\d)?\d\d?$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return ScalarYear.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'scalar' + 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 \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Separator.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Separator.php new file mode 100644 index 000000000..86c56e33b --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Separator.php @@ -0,0 +1,76 @@ +module Chronic + + class Separator < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_commas(token) + scanner = {/^,$/ => :comma} + scanner.keys.each do |scanner_item| + return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_slash_or_dash(token) + scanner = {/^-$/ => :dash, + /^\/$/ => :slash} + scanner.keys.each do |scanner_item| + return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_at(token) + scanner = {/^(at|@)$/ => :at} + scanner.keys.each do |scanner_item| + return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_in(token) + scanner = {/^in$/ => :in} + scanner.keys.each do |scanner_item| + return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'separator' + end + end + + class SeparatorComma < Separator #:nodoc: + def to_s + super << '-comma' + end + end + + class SeparatorSlashOrDash < Separator #:nodoc: + def to_s + super << '-slashordash-' << @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 + +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Timezone.php b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Timezone.php new file mode 100644 index 000000000..41041ef47 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base/Timezone.php @@ -0,0 +1,22 @@ +module Chronic + class TimeZone < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/[PMCE][DS]T/i => :tz} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'timezone' + end + end +end \ No newline at end of file diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Tag.php b/framework/Date_Parser/lib/Horde/Date/Parser/Tag.php new file mode 100644 index 000000000..a0796408a --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Tag.php @@ -0,0 +1,21 @@ +type = $type; + } + + public function start($s) + { + $this->now = $s; + } + +} diff --git a/framework/Date_Parser/lib/Horde/Date/Parser/Token.php b/framework/Date_Parser/lib/Horde/Date/Parser/Token.php new file mode 100644 index 000000000..16b3b70a5 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Parser/Token.php @@ -0,0 +1,54 @@ +word = $word; + $this->tags = array(); + } + + /** + * Tag this token with the specified tag + */ + public function tag($new_tag) + { + $this->tags[] = $new_tag; + } + + /** + * Remove all tags of the given class + */ + public function untag($tag_class) + { + $this->tags = array_filter($this->tags, create_function('$t', 'return $t instanceof ' . $tag_class)); + } + + /** + * Return true if this token has any tags + */ + public function tagged() + { + return count($this->tags) > 0; + } + + /** + * Return the Tag that matches the given class + */ + public function getTag($tag_class) + { + $matches = array_filter($this->tags, create_function('$t', 'return $t instanceof ' . $tag_class)); + return array_shift($matches); + } + + /** + * Print this Token in a pretty way + */ + public function __toString() + { + return '(' . implode(', ', $this->tags) . ') '; + } + +} diff --git a/framework/Date_Parser/lib/Horde/Date/Span.php b/framework/Date_Parser/lib/Horde/Date/Span.php new file mode 100644 index 000000000..d61c24cc0 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Date/Span.php @@ -0,0 +1,27 @@ + # 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 + end + diff --git a/framework/Date_Parser/lib/Horde/Support/Numerizer.php b/framework/Date_Parser/lib/Horde/Support/Numerizer.php new file mode 100644 index 000000000..062f7046f --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Support/Numerizer.php @@ -0,0 +1,29 @@ + '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 + ); + + public $TEN_PREFIXES = array( + 'twenty' => 20, + 'thirty' => 30, + 'fourty' => 40, + 'fifty' => 50, + 'sixty' => 60, + 'seventy' => 70, + 'eighty' => 80, + 'ninety' => 90, + ); + + public $BIG_PREFIXES = array( + 'hundred' => 100, + 'thousand' => 1000, + 'million' => 1000000, + 'billion' => 1000000000, + 'trillion' => 1000000000000, + ); + + public function numerize($string) + { + // preprocess + $string = $this->_splitHyphenatedWords($string); + $string = $this->_hideAHalf($string); + + $string = $this->_directReplacements($string); + $string = $this->_replaceTenPrefixes($string); + $string = $this->_replaceBigPrefixes($string); + $string = $this->_fractionalAddition($string); + + return $string; + } + + /** + * will mutilate hyphenated-words but shouldn't matter for date extraction + */ + protected function _splitHyphenatedWords($string) + { + return preg_replace('/ +|([^\d])-([^d])/', '$1 $2', $string); + } + + /** + * take the 'a' out so it doesn't turn into a 1, save the half for the end + */ + protected function _hideAHalf($string) + { + return str_replace('a half', 'haAlf', $string); + } + + /** + * easy/direct replacements + */ + protected function _directReplacements($string) + { + foreach ($this->DIRECT_NUMS as $dn => $dn_replacement) { + $string = preg_replace("/$dn/i", $dn_replacement, $string); + } + return $string; + } + + /** + * ten, twenty, etc. + */ + protected function _replaceTenPrefixes($string) + { + foreach ($this->TEN_PREFIXES as $tp => $tp_replacement) { + $string = preg_replace_callback( + "/(?:$tp)( *\d(?=[^\d]|\$))*/i", + create_function( + '$m', + 'return ' . $tp_replacement . ' + (isset($m[1]) ? (int)$m[1] : 0);' + ), + $string); + } + return $string; + } + + /** + * hundreds, thousands, millions, etc. + */ + protected function _replaceBigPrefixes($string) + { + foreach ($this->BIG_PREFIXES as $bp => $bp_replacement) { + $string = preg_replace_callback( + '/(\d*) *' . $bp . '/i', + create_function( + '$m', + 'return ' . $bp_replacement . ' * (int)$m[1];' + ), + $string); + $string = $this->_andition($string); + } + return $string; + } + + protected function _andition($string) + { + while (true) { + if (preg_match('/(\d+)( | and )(\d+)(?=[^\w]|$)/i', $string, $sc, PREG_OFFSET_CAPTURE)) { + if (preg_match('/and/', $sc[2][0]) || $sc[1][0] > $sc[3][0]) { + $string = substr($string, 0, $sc[1][1]) . ((int)$sc[1][0] + (int)$sc[3][0]) . substr($string, $sc[3][1] + strlen($sc[3][0])); + continue; + } + } + break; + } + return $string; + } + + protected function _fractionalAddition($string) + { + return preg_replace_callback( + '/(\d+)(?: | and |-)*haAlf/i', + create_function( + '$m', + 'return (string)((float)$m[1] + 0.5);' + ), + $string); + } + +} diff --git a/framework/Date_Parser/lib/Horde/Support/Numerizer/Locale/De.php b/framework/Date_Parser/lib/Horde/Support/Numerizer/Locale/De.php new file mode 100644 index 000000000..2e13fae30 --- /dev/null +++ b/framework/Date_Parser/lib/Horde/Support/Numerizer/Locale/De.php @@ -0,0 +1,110 @@ + 13, + 'vierzehn' => 14, + 'fünfzehn' => 15, + 'sechzehn' => 16, + 'siebzehn' => 17, + 'achtzehn' => 18, + 'neunzehn' => 19, + 'eins' => 1, + 'zwei' => 2, + 'zwo' => 2, + 'drei' => 3, + 'vier' => 4, + 'fünf' => 5, + 'sechs' => 6, + 'sieben' => 7, + 'acht' => 8, + 'neun' => 9, + 'zehn' => 10, + 'elf' => 11, + 'zwölf' => 12, + 'eine?' => 1, + ); + + public $TEN_PREFIXES = array( + 'zwanzig' => 20, + 'dreißig' => 30, + 'vierzig' => 40, + 'fünfzig' => 50, + 'sechzig' => 60, + 'siebzig' => 70, + 'achtzig' => 80, + 'neunzig' => 90, + ); + + public $BIG_PREFIXES = array( + 'hundert' => 100, + 'tausend' => 1000, + 'million' => 1000000, + 'milliarde' => 1000000000, + 'billion' => 1000000000000, + ); + + /** + * Rules: + * + * - there are irregular word for 11 and 12 like in English + * - numbers below one million are written together (1 M = "eine Million", 100 = "einhundert") + * - "a" is declinable (see above, "one" = "eins", "a" = "ein/eine") + * - numbers below 100 are flipped compared to english, and have an "and = "und" (21 = "twenty-one" = "einundzwanzig") + */ + public function numerize($string) + { + // preprocess? + + $string = $this->_replaceTenPrefixes($string); + $string = $this->_directReplacements($string); + $string = $this->_replaceBigPrefixes($string); + $string = $this->_fractionalAddition($string); + $string = $this->_andition($string); + + return $string; + } + + /** + * ten, twenty, etc. + */ + protected function _replaceTenPrefixes($string) + { + foreach ($this->TEN_PREFIXES as $tp => $tp_replacement) { + $string = preg_replace_callback( + "/(?:$tp)( *\d(?=[^\d]|\$))*/i", + create_function( + '$m', + 'return ' . $tp_replacement . ' + (isset($m[1]) ? (int)$m[1] : 0);' + ), + $string); + } + return $string; + } + + /** + * hundreds, thousands, millions, etc. + */ + protected function _replaceBigPrefixes($string) + { + foreach ($this->BIG_PREFIXES as $bp => $bp_replacement) { + $string = preg_replace_callback( + '/(\d*) *' . $bp . '/i', + create_function( + '$m', + '$factor = (int)$m[1]; if (!$factor) $factor = 1; return (' . $bp_replacement . ' * $factor) . "und";' + ), + $string); + } + return $string; + } + + protected function _andition($string) + { + while (preg_match('/(\d+)((?:und)+)(\d*)(?=[^\w]|$)/i', $string, $sc, PREG_OFFSET_CAPTURE)) { + $string = substr($string, 0, $sc[1][1]) . ((int)$sc[1][0] + (int)$sc[3][0]) . substr($string, $sc[3][1] + strlen($sc[3][0])); + } + return $string; + } + +} diff --git a/framework/Date_Parser/package.xml b/framework/Date_Parser/package.xml new file mode 100644 index 000000000..e69de29bb diff --git a/framework/Date_Parser/test/Horde/Date/Parser/AllTests.php b/framework/Date_Parser/test/Horde/Date/Parser/AllTests.php new file mode 100644 index 000000000..53d8bde90 --- /dev/null +++ b/framework/Date_Parser/test/Horde/Date/Parser/AllTests.php @@ -0,0 +1,54 @@ +isFile() && preg_match('/Test.php$/', $file->getFilename())) { + $pathname = $file->getPathname(); + require $pathname; + + $class = str_replace(DIRECTORY_SEPARATOR, '_', + preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname)); + $suite->addTestSuite('Horde_Date_Parser_' . $class); + } + } + + return $suite; + } + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_Date_Parser_AllTests::main') { + Horde_Date_Parser_AllTests::main(); +} diff --git a/framework/Date_Parser/test/Horde/Support/AllTests.php b/framework/Date_Parser/test/Horde/Support/AllTests.php new file mode 100644 index 000000000..7fac8b189 --- /dev/null +++ b/framework/Date_Parser/test/Horde/Support/AllTests.php @@ -0,0 +1,54 @@ +isFile() && preg_match('/Test.php$/', $file->getFilename())) { + $pathname = $file->getPathname(); + require $pathname; + + $class = str_replace(DIRECTORY_SEPARATOR, '_', + preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname)); + $suite->addTestSuite('Horde_Support_' . $class); + } + } + + return $suite; + } + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_Support_AllTests::main') { + Horde_Support_AllTests::main(); +} diff --git a/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/BaseTest.php b/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/BaseTest.php new file mode 100644 index 000000000..0643e33d4 --- /dev/null +++ b/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/BaseTest.php @@ -0,0 +1,62 @@ + 'one', + 5 => 'five', + 10 => 'ten', + 11 => 'eleven', + 12 => 'twelve', + 13 => 'thirteen', + 14 => 'fourteen', + 15 => 'fifteen', + 16 => 'sixteen', + 17 => 'seventeen', + 18 => 'eighteen', + 19 => 'nineteen', + 20 => 'twenty', + 27 => 'twenty seven', + 31 => 'thirty-one', + 59 => 'fifty nine', + 100 => 'a hundred', + 100 => 'one hundred', + 150 => 'one hundred and fifty', + // 150 => 'one fifty', + 200 => 'two-hundred', + 500 => '5 hundred', + 999 => 'nine hundred and ninety nine', + 1000 => 'one thousand', + 1200 => 'twelve hundred', + 1200 => 'one thousand two hundred', + 17000 => 'seventeen thousand', + 21473 => 'twentyone-thousand-four-hundred-and-seventy-three', + 74002 => 'seventy four thousand and two', + 99999 => 'ninety nine thousand nine hundred ninety nine', + 100000 => '100 thousand', + 250000 => 'two hundred fifty thousand', + 1000000 => 'one million', + 1250007 => 'one million two hundred fifty thousand and seven', + 1000000000 => 'one billion', + 1000000001 => 'one billion and one', + ); + + foreach ($strings as $key => $string) { + $this->assertEquals($key, (int)$numerizer->numerize($string)); + } + } + +} diff --git a/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/DeTest.php b/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/DeTest.php new file mode 100644 index 000000000..806d9bb04 --- /dev/null +++ b/framework/Date_Parser/test/Horde/Support/Numerizer/Locale/DeTest.php @@ -0,0 +1,64 @@ + 'de')); + $strings = array( + array(1, 'eins'), + array(5, 'fünf'), + array(10, 'zehn'), + array(11, 'elf'), + array(12, 'zwölf'), + array(13, 'dreizehn'), + array(14, 'vierzehn'), + array(15, 'fünfzehn'), + array(16, 'sechzehn'), + array(17, 'siebzehn'), + array(18, 'achtzehn'), + array(19, 'neunzehn'), + array(20, 'zwanzig'), + array(27, 'siebenundzwanzig'), + array(31, 'einunddreißig'), + array(59, 'neunundfünfzig'), + array(100, 'einhundert'), + array(100, 'ein hundert'), + array(150, 'hundertundfünfzig'), + array(150, 'einhundertundfünfzig'), + array(200, 'zweihundert'), + array(500, 'fünfhundert'), + array(999, 'neunhundertneunundneunzig'), + array(1000, 'eintausend'), + array(1200, 'zwölfhundert'), + array(1200, 'eintausendzweihundert'), + array(17000, 'siebzehntausend'), + array(21473, 'einundzwanzigtausendvierhundertdreiundsiebzig'), + array(74002, 'vierundsiebzigtausendzwei'), + array(74002, 'vierundsiebzigtausendundzwei'), + array(99999, 'neunundneunzigtausendneunhundertneunundneunzig'), + array(100000, 'hunderttausend'), + array(100000, 'einhunderttausend'), + array(250000, 'zweihundertfünfzigtausend'), + array(1000000, 'eine million'), + array(1250007, 'eine million zweihundertfünfzigtausendundsieben'), + array(1000000000, 'eine milliarde'), + array(1000000001, 'eine milliarde und eins'), + ); + + foreach ($strings as $pair) { + $this->assertEquals((string)$pair[0], $numerizer->numerize($pair[1])); + } + } + +} diff --git a/framework/Form/lib/Horde/Form.php b/framework/Form/lib/Horde/Form.php new file mode 100644 index 000000000..f995eead7 --- /dev/null +++ b/framework/Form/lib/Horde/Form.php @@ -0,0 +1,3946 @@ + + * Copyright 2001-2008 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Robert E. Coyle + * @author Chuck Hagenbuch + * @since Horde 3.0 + * @package Horde_Form + */ +class Horde_Form { + + protected $_name = ''; + protected $_title = ''; + protected $_vars; + protected $_errors = array(); + protected $_submitted = null; + protected $_sections = array(); + protected $_open_section = null; + protected $_currentSection = array(); + protected $_variables = array(); + protected $_hiddenVariables = array(); + protected $_useFormToken = true; + protected $_autofilled = false; + protected $_help = false; + + public function __construct($vars, $title = '', $name = null) + { + if (is_null($name)) { + $name = String::lower(get_class($this)); + } + + $this->_vars = $vars; + $this->_title = $title; + $this->_name = $name; + } + + public function setVars($vars) + { + $this->_vars = $vars; + } + + public function getVars() + { + return $this->_vars; + } + + public function getTitle() + { + return $this->_title; + } + + public function setTitle($title) + { + $this->_title = $title; + } + + public function getName() + { + return $this->_name; + } + + /** + * Sets or gets whether the form should be verified by tokens. + * Tokens are used to verify that a form is only submitted once. + * + * @param boolean $token If specified, sets whether to use form tokens. + * + * @return boolean Whether form tokens are being used. + */ + public function useToken($token = null) + { + if (!is_null($token)) { + $this->_useFormToken = $token; + } + return $this->_useFormToken; + } + + /** + * Get the renderer for this form, either a custom renderer or the + * standard one. + * + * To use a custom form renderer, your form class needs to + * override this function: + * + * function getRenderer() + * { + * return new CustomFormRenderer(); + * } + * + * + * ... where CustomFormRenderer is the classname of the custom + * renderer class, which should extend Horde_Form_Renderer. + * + * @param array $params A hash of renderer-specific parameters. + * + * @return object Horde_Form_Renderer The form renderer. + */ + function getRenderer($params = array()) + { + return new Horde_Form_Renderer_Xhtml($params); + } + + function getType($type, $params = array()) + { + $type_class = 'Horde_Form_Type_' . $type; + if (!class_exists($type_class)) { + Horde::fatal(PEAR::raiseError(sprintf('Nonexistant class "%s" for field type "%s"', $type_class, $type)), __FILE__, __LINE__); + } + $type_ob = new $type_class(); + call_user_func_array(array(&$type_ob, 'init'), $params); + return $type_ob; + } + + public function setSection($section = '', $desc = '', $image = '', $expanded = true) + { + $this->_currentSection = $section; + if (!count($this->_sections) && !$this->getOpenSection()) { + $this->setOpenSection($section); + } + $this->_sections[$section]['desc'] = $desc; + $this->_sections[$section]['expanded'] = $expanded; + $this->_sections[$section]['image'] = $image; + } + + public function getSections() + { + return $this->_sections; + } + + public function getSectionDesc($section) + { + return $this->_sections[$section]['desc']; + } + + public function getSectionImage($section) + { + return $this->_sections[$section]['image']; + } + + public function setOpenSection($section) + { + $this->_vars->set('__formOpenSection', $section); + } + + public function getOpenSection() + { + return $this->_vars->get('__formOpenSection'); + } + + public function getSectionExpandedState($section, $boolean = false) + { + if ($boolean) { + /* Only the boolean value is required. */ + return $this->_sections[$section]['expanded']; + } + + /* Need to return the values for use in styles. */ + if ($this->_sections[$section]['expanded']) { + return 'block'; + } else { + return 'none'; + } + } + + /** + * TODO + */ + public function addVariable($humanName, $varName, $type, $required, + $readonly = false, $description = null, + $params = array()) + { + return $this->insertVariableBefore(null, $humanName, $varName, $type, + $required, $readonly, $description, + $params); + } + + /** + * TODO + */ + public function insertVariableBefore($before, $humanName, $varName, $type, + $required, $readonly = false, + $description = null, $params = array()) + { + $type = $this->getType($type, $params); + $var = new Horde_Form_Variable($humanName, $varName, $type, + $required, $readonly, $description); + + /* Set the form object reference in the var. */ + $var->setFormOb($this); + + if ($var->getType() instanceof Horde_Form_Type_enum && + count($var->getValues()) == 1) { + $vals = array_keys($var->getValues()); + $this->_vars->add($var->varName, $vals[0]); + $var->_autofilled = true; + } + if (empty($this->_currentSection)) { + $this->_currentSection = '__base'; + } + + if (is_null($before)) { + $this->_variables[$this->_currentSection][] = &$var; + } else { + $num = 0; + while (isset($this->_variables[$this->_currentSection][$num]) && + $this->_variables[$this->_currentSection][$num]->getVarName() != $before) { + $num++; + } + if (!isset($this->_variables[$this->_currentSection][$num])) { + $this->_variables[$this->_currentSection][] = &$var; + } else { + $this->_variables[$this->_currentSection] = array_merge( + array_slice($this->_variables[$this->_currentSection], 0, $num), + array(&$var), + array_slice($this->_variables[$this->_currentSection], $num)); + } + } + + return $var; + } + + /** + * Removes a variable from the form. + * + * As only variables can be passed by reference, you need to call this + * method this way if want to pass a variable name: + * + * $form->removeVariable($var = 'varname'); + * + * + * @param Horde_Form_Variable|string $var Either the variable's name or + * the variable to remove from the + * form. + * + * @return boolean True if the variable was found (and deleted). + */ + public function removeVariable(&$var) + { + foreach (array_keys($this->_variables) as $section) { + foreach (array_keys($this->_variables[$section]) as $i) { + if ((is_a($var, 'Horde_Form_Variable') && $this->_variables[$section][$i] === $var) || + ($this->_variables[$section][$i]->getVarName() == $var)) { + // Slice out the variable to be removed. + $this->_variables[$this->_currentSection] = array_merge( + array_slice($this->_variables[$this->_currentSection], 0, $i), + array_slice($this->_variables[$this->_currentSection], $i + 1)); + + return true; + } + } + } + + return false; + } + + /** + * TODO + */ + public function addHidden($varName, $type, $required, $params = array()) + { + $type = $this->getType($type, $params); + $var = new Horde_Form_Variable('', $varName, $type, $required); + $var->hide(); + $this->_hiddenVariables[] = &$var; + return $var; + } + + public function getVariables($flat = true, $withHidden = false) + { + if ($flat) { + $vars = array(); + foreach ($this->_variables as $section) { + foreach ($section as $var) { + $vars[] = $var; + } + } + if ($withHidden) { + foreach ($this->_hiddenVariables as $var) { + $vars[] = $var; + } + } + return $vars; + } else { + return $this->_variables; + } + } + + public function getHiddenVariables() + { + return $this->_hiddenVariables; + } + + /** + * Preserve the variables/values from another Horde_Form object. + */ + public function preserve(Horde_Form $form) + { + /* OLD IMPLEMENTATION + if ($this->_useFormToken) { + $this->_preserveVarByPost($this->_name . '_formToken', Horde_Token::generateId($this->_name)); + } + + $variables = $this->getVariables(); + foreach ($variables as $var) { + $varname = $var->getVarName(); + + switch (get_class($var->getType()) { + case 'passwordconfirm': + case 'emailconfirm': + $this->preserveVarByPost($this->_vars, $varname . '[original]'); + $this->preserveVarByPost($this->_vars, $varname . '[confirm]'); + break; + + case 'monthyear': + $this->preserveVarByPost($this->_vars, $varname . '[month]'); + $this->preserveVarByPost($this->_vars, $varname . '[year]'); + break; + + case 'monthdayyear': + $this->preserveVarByPost($this->_vars, $varname . '[month]'); + $this->preserveVarByPost($this->_vars, $varname . '[day]'); + $this->preserveVarByPost($this->_vars, $varname . '[year]'); + break; + } + + $this->preserveVarByPost($this->_vars, $varname); + } + foreach ($this->_hiddenVariables as $var) { + $this->preserveVarByPost($this->_vars, $var->getVarName()); + } + */ + } + + /** + * Does the action of validating the form, checking if it really has been + * submitted by calling isSubmitted() and if true does any onSubmit() + * calls for var types in the form. The _submitted var is then rechecked. + * + * @param boolean $canAutofill Can the form be valid without + * being submitted? + * + * @return boolean True if the form is valid. + */ + public function validate($canAutoFill = false) + { + /* Get submitted status. */ + if ($this->isSubmitted() || $canAutoFill) { + /* Form was submitted or can autofill; check for any variable + * types' onSubmit(). */ + $this->onSubmit($this->_vars); + + /* Recheck submitted status. */ + if (!$this->isSubmitted() && !$canAutoFill) { + return false; + } + } else { + /* Form has not been submitted; return false. */ + return false; + } + + $message = ''; + $this->_autofilled = true; + + if ($this->_useFormToken) { + global $conf; + if (isset($conf['token'])) { + /* If there is a configured token system, set it up. */ + $tokenSource = Horde_Token::factory($conf['token']['driver'], Horde::getDriverConfig('token', $conf['token']['driver'])); + } else { + /* Default to the file system if no config. */ + $tokenSource = Horde_Token::factory('file'); + } + if (!$tokenSource->verify($this->_vars->get($this->_name . '_formToken'))) { + $this->_errors['_formToken'] = _("This form has already been processed."); + } + } + + foreach ($this->getVariables() as $var) { + $this->_autofilled = $var->_autofilled && $this->_autofilled; + if (!$var->validate($this->_vars, $message)) { + $this->_errors[$var->getVarName()] = $message; + } + } + + if ($this->_autofilled) { + unset($this->_errors['_formToken']); + } + + foreach ($this->_hiddenVariables as $var) { + if (!$var->validate($this->_vars, $message)) { + $this->_errors[$var->getVarName()] = $message; + } + } + + return $this->isValid(); + } + + public function clearValidation() + { + $this->_errors = array(); + } + + public function getError($var) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + return isset($this->_errors[$name]) ? $this->_errors[$name] : null; + } + + public function setError($var, $message) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + $this->_errors[$name] = $message; + } + + public function clearError($var) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + unset($this->_errors[$name]); + } + + public function isValid() + { + return ($this->_autofilled || !count($this->_errors)); + } + + public function execute() + { + throw new Horde_Form_Exception('Subclass must overide execute()'); + } + + /** + * Fetch the field values of the submitted form. + * + * @param array $info Array to be filled with the submitted field + * values. + */ + public function getInfo(&$info) + { + $this->_getInfoFromVariables($this->getVariables(), $info); + $this->_getInfoFromVariables($this->_hiddenVariables, $info); + } + + /** + * Fetch the field values from a given array of variables. + * + * @access private + * + * @param array $variables An array of Horde_Form_Variable objects to + * fetch from. + * @param array $info The array to be filled with the submitted + * field values. + */ + protected function _getInfoFromVariables($variables, &$info) + { + foreach ($variables as $var) { + if ($var->isArrayVal()) { + $var->getInfo($this->_vars, $values); + if (is_array($values)) { + $varName = str_replace('[]', '', $var->getVarName()); + foreach ($values as $i => $val) { + $info[$i][$varName] = $val; + } + } + } else { + if (Horde_Array::getArrayParts($var->getVarName(), $base, $keys)) { + if (!isset($info[$base])) { + $info[$base] = array(); + } + $pointer = &$info[$base]; + while (count($keys)) { + $key = array_shift($keys); + if (!isset($pointer[$key])) { + $pointer[$key] = array(); + } + $pointer = &$pointer[$key]; + } + $var->getInfo($this->_vars, $pointer); + } else { + $var->getInfo($this->_vars, $info[$var->getVarName()]); + } + } + } + } + + public function hasHelp() + { + return $this->_help; + } + + /** + * Determines if this form has been submitted or not. If the class + * var _submitted is null then it will check for the presence of + * the formname in the form variables. + * + * Other events can explicitly set the _submitted variable to + * false to indicate a form submit but not for actual posting of + * data (eg. onChange events to update the display of fields). + * + * @return boolean True or false indicating if the form has been + * submitted. + */ + public function isSubmitted() + { + if (is_null($this->_submitted)) { + if ($this->_vars->get('formname') == $this->getName()) { + $this->_submitted = true; + } else { + $this->_submitted = false; + } + } + + return $this->_submitted; + } + + /** + * Checks if there is anything to do on the submission of the form by + * looping through each variable's onSubmit() function. + */ + public function onSubmit() + { + /* Loop through all vars and check if there's anything to do on + * submit. */ + $variables = $this->getVariables(); + foreach ($variables as $var) { + $var->type->onSubmit($var, $this->_vars); + /* If changes to var being tracked don't register the form as + * submitted if old value and new value differ. */ + if ($var->getOption('trackchange')) { + $varname = $var->getVarName(); + if (!is_null($this->_vars->get('formname')) && + $this->_vars->get($varname) != $this->_vars->get('__old_' . $varname)) { + $this->_submitted = false; + } + } + } + } + + /** + * Explicitly sets the state of the form submit. + * + * An event can override the automatic determination of the submit state + * in the isSubmitted() function. + * + * @param boolean $state Whether to set the state of the form as being + * submitted. + */ + public function setSubmitted($state = true) + { + $this->_submitted = $state; + } + +} + +/** + * Horde_Form_Type Class + * + * @author Robert E. Coyle + * @package Horde_Form + */ +class Horde_Form_Type { + + protected function __get($property) + { + $prop = '_' . $property; + return isset($this->$prop) ? $this->$prop : null; + } + + protected function __set($property, $value) + { + $prop = '_' . $property; + $this->$prop = $value; + } + + protected function __isset($property) + { + $prop = '_' . $property; + return isset($this->$prop); + } + + protected function __unset($property) + { + $prop = '_' . $property; + unset($this->$prop); + } + + public function init() + { + } + + public function onSubmit() + { + } + + public function isValid($var, $vars, $value, &$message) + { + $message = 'Error: Horde_Form_Type::isValid() called - should be overridden
'; + return false; + } + + function getInfo($vars, $var, &$info) + { + $info = $var->getValue($vars); + } + +} + +class Horde_Form_Type_number extends Horde_Form_Type { + + var $_fraction; + + function init($fraction = null) + { + $this->_fraction = $fraction; + } + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) { + $message = _("This field is required."); + return false; + } elseif (empty($value)) { + return true; + } + + /* If matched, then this is a correct numeric value. */ + if (preg_match($this->_getValidationPattern(), $value)) { + return true; + } + + $message = _("This field must be a valid number."); + return false; + } + + function _getValidationPattern() + { + static $pattern = ''; + if (!empty($pattern)) { + return $pattern; + } + + /* Get current locale information. */ + $linfo = NLS::getLocaleInfo(); + + /* Build the pattern. */ + $pattern = '(-)?'; + + /* Only check thousands separators if locale has any. */ + if (!empty($linfo['mon_thousands_sep'])) { + /* Regex to check for correct thousands separators (if any). */ + $pattern .= '((\d+)|((\d{0,3}?)([' . $linfo['mon_thousands_sep'] . ']\d{3})*?))'; + } else { + /* No locale thousands separator, check for only digits. */ + $pattern .= '(\d+)'; + } + /* If no decimal point specified default to dot. */ + if (empty($linfo['mon_decimal_point'])) { + $linfo['mon_decimal_point'] = '.'; + } + /* Regex to check for correct decimals (if any). */ + if (empty($this->_fraction)) { + $fraction = '*'; + } else { + $fraction = '{0,' . $this->_fraction . '}'; + } + $pattern .= '([' . $linfo['mon_decimal_point'] . '](\d' . $fraction . '))?'; + + /* Put together the whole regex pattern. */ + $pattern = '/^' . $pattern . '$/'; + + return $pattern; + } + + function getInfo($vars, $var, &$info) + { + $value = $vars->get($var->getVarName()); + $linfo = NLS::getLocaleInfo(); + $value = str_replace($linfo['mon_thousands_sep'], '', $value); + $info = str_replace($linfo['mon_decimal_point'], '.', $value); + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Number")); + } + +} + +class Horde_Form_Type_int extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-9]+$/', $value)) { + return true; + } + + $message = _("This field may only contain integers."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Integer")); + } + +} + +class Horde_Form_Type_octal extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-7]+$/', $value)) { + return true; + } + + $message = _("This field may only contain octal values."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Octal")); + } + +} + +class Horde_Form_Type_intlist extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if (empty($value) && $var->isRequired()) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-9 ,]+$/', $value)) { + return true; + } + + $message = _("This field must be a comma or space separated list of integers"); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Integer list")); + } + +} + +class Horde_Form_Type_text extends Horde_Form_Type { + + var $_regex; + var $_size; + var $_maxlength; + + /** + * The initialisation function for the text variable type. + * + * @access private + * + * @param string $regex Any valid PHP PCRE pattern syntax that + * needs to be matched for the field to be + * considered valid. If left empty validity + * will be checked only for required fields + * whether they are empty or not. + * If using this regex test it is advisable + * to enter a description for this field to + * warn the user what is expected, as the + * generated error message is quite generic + * and will not give any indication where + * the regex failed. + * @param integer $size The size of the input field. + * @param integer $maxlength The max number of characters. + */ + function init($regex = '', $size = 40, $maxlength = null) + { + $this->_regex = $regex; + $this->_size = $size; + $this->_maxlength = $maxlength; + } + + public function isValid($var, $vars, $value, &$message) + { + $valid = true; + + if (!empty($this->_maxlength) && String::length($value) > $this->_maxlength) { + $valid = false; + $message = sprintf(_("Value is over the maximum length of %s."), $this->_maxlength); + } elseif ($var->isRequired() && empty($this->_regex)) { + if (!($valid = strlen(trim($value)) > 0)) { + $message = _("This field is required."); + } + } elseif (strlen($this->_regex)) { + if (!($valid = preg_match($this->_regex, $value))) { + $message = _("You must enter a valid value."); + } + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Text"), + 'params' => array( + 'regex' => array('label' => _("Regex"), + 'type' => 'text'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'maxlength' => array('label' => _("Maximum length"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_stringlist extends Horde_Form_Type_text { + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("String list"), + 'params' => array( + 'regex' => array('label' => _("Regex"), + 'type' => 'text'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'maxlength' => array('label' => _("Maximum length"), + 'type' => 'int')), + ); + } + +} + +class Horde_Form_Type_phone extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + $valid = true; + + if ($var->isRequired()) { + $valid = strlen(trim($value)) > 0; + if (!$valid) { + $message = _("This field is required."); + } + } else { + $valid = preg_match('/^\+?[\d()\-\/ ]*$/', $value); + if (!$valid) { + $message = _("You must enter a valid phone number, digits only with an optional '+' for the international dialing prefix."); + } + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Phone number")); + } + +} + +class Horde_Form_Type_cellphone extends Horde_Form_Type_phone { + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Mobile phone number")); + } + +} + +class Horde_Form_Type_ipaddress extends Horde_Form_Type_text { + + function isValid($var, $vars, $value, &$message) + { + $valid = true; + + if (strlen(trim($value)) > 0) { + $ip = explode('.', $value); + $valid = (count($ip) == 4); + if ($valid) { + foreach ($ip as $part) { + if (!is_numeric($part) || + $part > 255 || + $part < 0) { + $valid = false; + break; + } + } + } + + if (!$valid) { + $message = _("Please enter a valid IP address."); + } + } elseif ($var->isRequired()) { + $valid = false; + $message = _("This field is required."); + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("IP address")); + } + +} + +class Horde_Form_Type_longtext extends Horde_Form_Type_text { + + var $_rows; + var $_cols; + var $_helper = array(); + + function init($rows = 8, $cols = 80, $helper = array()) + { + if (!is_array($helper)) { + $helper = array($helper); + } + + $this->_rows = $rows; + $this->_cols = $cols; + $this->_helper = $helper; + } + + function hasHelper($option = '') + { + if (empty($option)) { + /* No option specified, check if any helpers have been + * activated. */ + return !empty($this->_helper); + } elseif (empty($this->_helper)) { + /* No helpers activated at all, return false. */ + return false; + } else { + /* Check if given helper has been activated. */ + return in_array($option, $this->_helper); + } + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Long text"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'), + 'helper' => array('label' => _("Helper?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_countedtext extends Horde_Form_Type_longtext { + + var $_chars; + + function init($rows = null, $cols = null, $chars = 1000) + { + parent::init($rows, $cols); + $this->_chars = $chars; + } + + function isValid($var, $vars, $value, &$message) + { + $valid = true; + + $length = String::length(trim($value)); + + if ($var->isRequired() && $length <= 0) { + $valid = false; + $message = _("This field is required."); + } elseif ($length > $this->_chars) { + $valid = false; + $message = sprintf(_("There are too many characters in this field. You have entered %s characters; you must enter less than %s."), String::length(trim($value)), $this->_chars); + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Counted text"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'), + 'chars' => array('label' => _("Number of characters"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_address extends Horde_Form_Type_longtext { + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Address"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_addresslink extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + return true; + } + +} + +class Horde_Form_Type_file extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired()) { + $uploaded = Browser::wasFileUploaded($var->getVarName()); + if (is_a($uploaded, 'PEAR_Error')) { + $message = $uploaded->getMessage(); + return false; + } + } + + return true; + } + + function getInfo($vars, $var, &$info) + { + $name = $var->getVarName(); + $uploaded = Browser::wasFileUploaded($name); + if ($uploaded === true) { + $info['name'] = $_FILES[$name]['name']; + $info['type'] = $_FILES[$name]['type']; + $info['tmp_name'] = $_FILES[$name]['tmp_name']; + $info['file'] = $_FILES[$name]['tmp_name']; + $info['error'] = $_FILES[$name]['error']; + $info['size'] = $_FILES[$name]['size']; + } + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("File upload")); + } + +} + +class Horde_Form_Type_image extends Horde_Form_Type { + + /** + * Has a file been uploaded on this form submit? + * + * @var boolean + */ + var $_uploaded = null; + + /** + * Show the upload button? + * + * @var boolean + */ + var $_show_upload = true; + + /** + * Show the option to upload also original non-modified image? + * + * @var boolean + */ + var $_show_keeporig = false; + + /** + * Limit the file size? + * + * @var integer + */ + var $_max_filesize = null; + + /** + * Hash containing the previously uploaded image info. + * + * @var array + */ + var $_img = array(); + + function init($show_upload = true, $show_keeporig = false, $max_filesize = null) + { + $this->_show_upload = $show_upload; + $this->_show_keeporig = $show_keeporig; + $this->_max_filesize = $max_filesize; + } + + function onSubmit($var, $vars) + { + /* Get the upload. */ + $this->_getUpload($vars, $var); + + /* If this was done through the upload button override the submitted + * value of the form. */ + if ($vars->get('_do_' . $var->getVarName())) { + $var->form->setSubmitted(false); + } + } + + function isValid($var, $vars, $value, &$message) + { + $field = $vars->get($var->getVarName()); + + /* Get the upload. */ + $this->_getUpload($vars, $var); + + /* The upload generated a PEAR Error. */ + if (is_a($this->_uploaded, 'PEAR_Error')) { + /* Not required and no image upload attempted. */ + if (!$var->isRequired() && empty($field['img']) && + $this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) { + return true; + } + + if (($this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) && + empty($field['img'])) { + /* Nothing uploaded and no older upload. */ + $message = _("This field is required."); + return false; + } elseif (!empty($field['img'])) { + /* Nothing uploaded but older upload present. */ + return true; + } else { + /* Some other error message. */ + $message = $this->_uploaded->getMessage(); + return false; + } + } elseif ($this->_max_filesize && + $this->_img['size'] > $this->_max_filesize) { + $message = sprintf(_("The image file was larger than the maximum allowed size (%d bytes)."), $this->_max_filesize); + return false; + } + + return true; + } + + function getInfo($vars, $var, &$info) + { + /* Get the upload. */ + $this->_getUpload($vars, $var); + + /* Get image params stored in the hidden field. */ + $value = $var->getValue($vars); + $info = $this->_img; + if (empty($info['file'])) { + unset($info['file']); + return; + } + if ($this->_show_keeporig) { + $info['keep_orig'] = !empty($value['keep_orig']); + } + + /* Set the uploaded value (either true or PEAR_Error). */ + $info['uploaded'] = &$this->_uploaded; + + /* If a modified file exists move it over the original. */ + if ($this->_show_keeporig && $info['keep_orig']) { + /* Requested the saving of original file also. */ + $info['orig_file'] = Horde::getTempDir() . '/' . $info['file']; + $info['file'] = Horde::getTempDir() . '/mod_' . $info['file']; + /* Check if a modified file actually exists. */ + if (!file_exists($info['file'])) { + $info['file'] = $info['orig_file']; + unset($info['orig_file']); + } + } else { + /* Saving of original not required. */ + $mod_file = Horde::getTempDir() . '/mod_' . $info['file']; + $info['file'] = Horde::getTempDir() . '/' . $info['file']; + + if (file_exists($mod_file)) { + /* Unlink first (has to be done on Windows machines?) */ + unlink($info['file']); + rename($mod_file, $info['file']); + } + } + } + + /** + * Gets the upload and sets up the upload data array. Either + * fetches an upload done with this submit or retries stored + * upload info. + */ + function _getUpload($vars, $var) + { + /* Don't bother with this function if already called and set + * up vars. */ + if (!empty($this->_img)) { + return true; + } + + /* Check if file has been uploaded. */ + $varname = $var->getVarName(); + $this->_uploaded = Browser::wasFileUploaded($varname . '[new]'); + + if ($this->_uploaded === true) { + /* A file has been uploaded on this submit. Save to temp dir for + * preview work. */ + $this->_img['type'] = $this->getUploadedFileType($varname . '[new]'); + + /* Get the other parts of the upload. */ + Horde_Array::getArrayParts($varname . '[new]', $base, $keys); + + /* Get the temporary file name. */ + $keys_path = array_merge(array($base, 'tmp_name'), $keys); + $this->_img['file'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get the actual file name. */ + $keys_path= array_merge(array($base, 'name'), $keys); + $this->_img['name'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get the file size. */ + $keys_path= array_merge(array($base, 'size'), $keys); + $this->_img['size'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get any existing values for the image upload field. */ + $upload = $vars->get($var->getVarName()); + $upload['img'] = @unserialize($upload['img']); + + /* Get the temp file if already one uploaded, otherwise create a + * new temporary file. */ + if (!empty($upload['img']['file'])) { + $tmp_file = Horde::getTempDir() . '/' . $upload['img']['file']; + } else { + $tmp_file = Horde::getTempFile('Horde', false); + } + + /* Move the browser created temp file to the new temp file. */ + move_uploaded_file($this->_img['file'], $tmp_file); + $this->_img['file'] = basename($tmp_file); + + /* Store the uploaded image file data to the hidden field. */ + $upload['img'] = serialize($this->_img); + $vars->set($var->getVarName(), $upload); + } elseif ($this->_uploaded) { + /* File has not been uploaded. */ + $upload = $vars->get($var->getVarName()); + if ($this->_uploaded->getCode() == 4 && !empty($upload['img'])) { + $this->_img = @unserialize($upload['img']); + } + } + } + + function getUploadedFileType($field) + { + /* Get any index on the field name. */ + $index = Horde_Array::getArrayParts($field, $base, $keys); + + if ($index) { + /* Index present, fetch the mime type var to check. */ + $keys_path = array_merge(array($base, 'type'), $keys); + $type = Horde_Array::getElement($_FILES, $keys_path); + $keys_path= array_merge(array($base, 'tmp_name'), $keys); + $tmp_name = Horde_Array::getElement($_FILES, $keys_path); + } else { + /* No index, simple set up of vars to check. */ + $type = $_FILES[$field]['type']; + $tmp_name = $_FILES[$field]['tmp_name']; + } + + if (empty($type) || ($type == 'application/octet-stream')) { + /* Type wasn't set on upload, try analising the upload. */ + global $conf; + require_once 'Horde/MIME/Magic.php'; + if (!($type = MIME_Magic::analyzeFile($tmp_name, isset($conf['mime']['magic_db']) ? $conf['mime']['magic_db'] : null))) { + if ($index) { + /* Get the name value. */ + $keys_path = array_merge(array($base, 'name'), $keys); + $name = Horde_Array::getElement($_FILES, $keys_path); + + /* Work out the type from the file name. */ + $type = MIME_Magic::filenameToMIME($name); + + /* Set the type. */ + $keys_path = array_merge(array($base, 'type'), $keys); + Horde_Array::getElement($_FILES, $keys_path, $type); + } else { + /* Work out the type from the file name. */ + $type = MIME_Magic::filenameToMIME($_FILES[$field]['name']); + + /* Set the type. */ + $_FILES[$field]['type'] = MIME_Magic::filenameToMIME($_FILES[$field]['name']); + } + } + } + + return $type; + } + + /** + * Loads any existing image data into the image field. Requires that the + * array $image passed to it contains the structure: + * $image['load']['file'] - the filename of the image; + * $image['load']['data'] - the raw image data. + * + * @param array $image The image array. + */ + function loadImageData(&$image) + { + /* No existing image data to load. */ + if (!isset($image['load'])) { + return; + } + + /* Save the data to the temp dir. */ + $tmp_file = Horde::getTempDir() . '/' . $image['load']['file']; + if ($fd = fopen($tmp_file, 'w')) { + fwrite($fd, $image['load']['data']); + fclose($fd); + } + + $image['img'] = serialize(array('file' => $image['load']['file'])); + unset($image['load']); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Image upload"), + 'params' => array( + 'show_upload' => array('label' => _("Show upload?"), + 'type' => 'boolean'), + 'show_keeporig' => array('label' => _("Show option to keep original?"), + 'type' => 'boolean'), + 'max_filesize' => array('label' => _("Maximum file size in bytes"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_boolean extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + function getInfo($vars, $var, &$info) + { + $info = String::lower($vars->get($var->getVarName())) == 'on'; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("True or false")); + } + +} + +class Horde_Form_Type_link extends Horde_Form_Type { + + /** + * List of hashes containing link parameters. Possible keys: 'url', 'text', + * 'target', 'onclick', 'title', 'accesskey'. + * + * @var array + */ + var $values; + + function init($values) + { + $this->values = $values; + } + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Link"), + 'params' => array( + 'url' => array( + 'label' => _("Link URL"), + 'type' => 'text'), + 'text' => array( + 'label' => _("Link text"), + 'type' => 'text'), + 'target' => array( + 'label' => _("Link target"), + 'type' => 'text'), + 'onclick' => array( + 'label' => _("Onclick event"), + 'type' => 'text'), + 'title' => array( + 'label' => _("Link title attribute"), + 'type' => 'text'), + 'accesskey' => array( + 'label' => _("Link access key"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_email extends Horde_Form_Type { + + var $_allow_multi = false; + var $_strip_domain = false; + var $_link_compose = false; + var $_check_smtp = false; + var $_link_name; + + /** + * A string containing valid delimiters (default is just comma). + * + * @var string + */ + var $_delimiters = ','; + + function init($allow_multi = false, $strip_domain = false, + $link_compose = false, $link_name = null, + $delimiters = ',') + { + $this->_allow_multi = $allow_multi; + $this->_strip_domain = $strip_domain; + $this->_link_compose = $link_compose; + $this->_link_name = $link_name; + $this->_delimiters = $delimiters; + } + + /** + */ + function isValid($var, $vars, $value, &$message) + { + // Split into individual addresses. + $emails = $this->splitEmailAddresses($value); + + // Check for too many. + if (!$this->_allow_multi && count($emails) > 1) { + $message = _("Only one email address is allowed."); + return false; + } + + // Check for all valid and at least one non-empty. + $nonEmpty = 0; + foreach ($emails as $email) { + if (!strlen($email)) { + continue; + } + if (!$this->validateEmailAddress($email)) { + $message = sprintf(_("\"%s\" is not a valid email address."), $email); + return false; + } + ++$nonEmpty; + } + + if (!$nonEmpty && $var->isRequired()) { + if ($this->_allow_multi) { + $message = _("You must enter at least one email address."); + } else { + $message = _("You must enter an email address."); + } + return false; + } + + return true; + } + + /** + * Explodes an RFC 2822 string, ignoring a delimiter if preceded + * by a "\" character, or if the delimiter is inside single or + * double quotes. + * + * @param string $string The RFC 822 string. + * + * @return array The exploded string in an array. + */ + function splitEmailAddresses($string) + { + $quotes = array('"', "'"); + $emails = array(); + $pos = 0; + $in_quote = null; + $in_group = false; + $prev = null; + + if (!strlen($string)) { + return array(); + } + + $char = $string[0]; + if (in_array($char, $quotes)) { + $in_quote = $char; + } elseif ($char == ':') { + $in_group = true; + } elseif (strpos($this->_delimiters, $char) !== false) { + $emails[] = ''; + $pos = 1; + } + + for ($i = 1, $iMax = strlen($string); $i < $iMax; ++$i) { + $char = $string[$i]; + if (in_array($char, $quotes)) { + if ($prev !== '\\') { + if ($in_quote === $char) { + $in_quote = null; + } elseif (is_null($in_quote)) { + $in_quote = $char; + } + } + } elseif ($in_group) { + if ($char == ';') { + $emails[] = substr($string, $pos, $i - $pos + 1); + $pos = $i + 1; + $in_group = false; + } + } elseif ($char == ':') { + $in_group = true; + } elseif (strpos($this->_delimiters, $char) !== false && + $prev !== '\\' && + is_null($in_quote)) { + $emails[] = substr($string, $pos, $i - $pos); + $pos = $i + 1; + } + $prev = $char; + } + + if ($pos != $i) { + /* The string ended without a delimiter. */ + $emails[] = substr($string, $pos, $i - $pos); + } + + return $emails; + } + + /** + * RFC(2)822 Email Parser. + * + * By Cal Henderson + * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License + * http://creativecommons.org/licenses/by-sa/2.5/ + * + * http://code.iamcal.com/php/rfc822/ + * + * http://iamcal.com/publish/articles/php/parsing_email + * + * Revision 4 + * + * @param string $email An individual email address to validate. + * + * @return boolean + */ + function validateEmailAddress($email) + { + static $comment_regexp, $email_regexp; + if ($comment_regexp === null) { + $this->_defineValidationRegexps($comment_regexp, $email_regexp); + } + + // We need to strip comments first (repeat until we can't find + // any more). + while (true) { + $new = preg_replace("!$comment_regexp!", '', $email); + if (strlen($new) == strlen($email)){ + break; + } + $email = $new; + } + + // Now match what's left. + $result = (bool)preg_match("!^$email_regexp$!", $email); + if ($result && $this->_check_smtp) { + $result = $this->validateEmailAddressSmtp($email); + } + + return $result; + } + + /** + * Attempt partial delivery of mail to an address to validate it. + * + * @param string $email An individual email address to validate. + * + * @return boolean + */ + function validateEmailAddressSmtp($email) + { + list(, $maildomain) = explode('@', $email, 2); + + // Try to get the real mailserver from MX records. + if (function_exists('getmxrr') && + @getmxrr($maildomain, $mxhosts, $mxpriorities)) { + // MX record found. + array_multisort($mxpriorities, $mxhosts); + $mailhost = $mxhosts[0]; + } else { + // No MX record found, try the root domain as the mail + // server. + $mailhost = $maildomain; + } + + $fp = @fsockopen($mailhost, 25, $errno, $errstr, 5); + if (!$fp) { + return false; + } + + // Read initial response. + fgets($fp, 4096); + + // HELO + fputs($fp, "HELO $mailhost\r\n"); + fgets($fp, 4096); + + // MAIL FROM + fputs($fp, "MAIL FROM: \r\n"); + fgets($fp, 4096); + + // RCPT TO - gets the result we want. + fputs($fp, "RCPT TO: <$email>\r\n"); + $result = trim(fgets($fp, 4096)); + + // QUIT + fputs($fp, "QUIT\r\n"); + fgets($fp, 4096); + fclose($fp); + + return substr($result, 0, 1) == '2'; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Email"), + 'params' => array( + 'allow_multi' => array('label' => _("Allow multiple addresses?"), + 'type' => 'boolean'), + 'strip_domain' => array('label' => _("Protect address from spammers?"), + 'type' => 'boolean'), + 'link_compose' => array('label' => _("Link the email address to the compose page when displaying?"), + 'type' => 'boolean'), + 'link_name' => array('label' => _("The name to use when linking to the compose page"), + 'type' => 'text'), + 'delimiters' => array('label' => _("Character to split multiple addresses with"), + 'type' => 'text'), + ), + ); + } + + /** + * RFC(2)822 Email Parser. + * + * By Cal Henderson + * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License + * http://creativecommons.org/licenses/by-sa/2.5/ + * + * http://code.iamcal.com/php/rfc822/ + * + * http://iamcal.com/publish/articles/php/parsing_email + * + * Revision 4 + * + * @param string &$comment The regexp for comments. + * @param string &$addr_spec The regexp for email addresses. + */ + function _defineValidationRegexps(&$comment, &$addr_spec) + { + /** + * NO-WS-CTL = %d1-8 / ; US-ASCII control characters + * %d11 / ; that do not include the + * %d12 / ; carriage return, line feed, + * %d14-31 / ; and white space characters + * %d127 + * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + * DIGIT = %x30-39 + */ + $no_ws_ctl = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]"; + $alpha = "[\\x41-\\x5a\\x61-\\x7a]"; + $digit = "[\\x30-\\x39]"; + $cr = "\\x0d"; + $lf = "\\x0a"; + $crlf = "($cr$lf)"; + + /** + * obs-char = %d0-9 / %d11 / ; %d0-127 except CR and + * %d12 / %d14-127 ; LF + * obs-text = *LF *CR *(obs-char *LF *CR) + * text = %d1-9 / ; Characters excluding CR and LF + * %d11 / + * %d12 / + * %d14-127 / + * obs-text + * obs-qp = "\" (%d0-127) + * quoted-pair = ("\" text) / obs-qp + */ + $obs_char = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]"; + $obs_text = "($lf*$cr*($obs_char$lf*$cr*)*)"; + $text = "([\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|$obs_text)"; + $obs_qp = "(\\x5c[\\x00-\\x7f])"; + $quoted_pair = "(\\x5c$text|$obs_qp)"; + + /** + * obs-FWS = 1*WSP *(CRLF 1*WSP) + * FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space + * obs-FWS + * ctext = NO-WS-CTL / ; Non white space controls + * %d33-39 / ; The rest of the US-ASCII + * %d42-91 / ; characters not including "(", + * %d93-126 ; ")", or "\" + * ccontent = ctext / quoted-pair / comment + * comment = "(" *([FWS] ccontent) [FWS] ")" + * CFWS = *([FWS] comment) (([FWS] comment) / FWS) + * + * @note: We translate ccontent only partially to avoid an + * infinite loop. Instead, we'll recursively strip comments + * before processing the input. + */ + $wsp = "[\\x20\\x09]"; + $obs_fws = "($wsp+($crlf$wsp+)*)"; + $fws = "((($wsp*$crlf)?$wsp+)|$obs_fws)"; + $ctext = "($no_ws_ctl|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])"; + $ccontent = "($ctext|$quoted_pair)"; + $comment = "(\\x28($fws?$ccontent)*$fws?\\x29)"; + $cfws = "(($fws?$comment)*($fws?$comment|$fws))"; + $cfws = "$fws*"; + + /** + * atext = ALPHA / DIGIT / ; Any character except controls, + * "!" / "#" / ; SP, and specials. + * "$" / "%" / ; Used for atoms + * "&" / "'" / + * "*" / "+" / + * "-" / "/" / + * "=" / "?" / + * "^" / "_" / + * "`" / "{" / + * "|" / "}" / + * "~" + * atom = [CFWS] 1*atext [CFWS] + */ + $atext = "($alpha|$digit|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2e\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])"; + $atom = "($cfws?$atext+$cfws?)"; + + /** + * qtext = NO-WS-CTL / ; Non white space controls + * %d33 / ; The rest of the US-ASCII + * %d35-91 / ; characters not including "\" + * %d93-126 ; or the quote character + * qcontent = qtext / quoted-pair + * quoted-string = [CFWS] + * DQUOTE *([FWS] qcontent) [FWS] DQUOTE + * [CFWS] + * word = atom / quoted-string + */ + $qtext = "($no_ws_ctl|[\\x21\\x23-\\x5b\\x5d-\\x7e])"; + $qcontent = "($qtext|$quoted_pair)"; + $quoted_string = "($cfws?\\x22($fws?$qcontent)*$fws?\\x22$cfws?)"; + $word = "($atom|$quoted_string)"; + + /** + * obs-local-part = word *("." word) + * obs-domain = atom *("." atom) + */ + $obs_local_part = "($word(\\x2e$word)*)"; + $obs_domain = "($atom(\\x2e$atom)*)"; + + /** + * dot-atom-text = 1*atext *("." 1*atext) + * dot-atom = [CFWS] dot-atom-text [CFWS] + */ + $dot_atom_text = "($atext+(\\x2e$atext+)*)"; + $dot_atom = "($cfws?$dot_atom_text$cfws?)"; + + /** + * domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS] + * dcontent = dtext / quoted-pair + * dtext = NO-WS-CTL / ; Non white space controls + * + * %d33-90 / ; The rest of the US-ASCII + * %d94-126 ; characters not including "[", + * ; "]", or "\" + */ + $dtext = "($no_ws_ctl|[\\x21-\\x5a\\x5e-\\x7e])"; + $dcontent = "($dtext|$quoted_pair)"; + $domain_literal = "($cfws?\\x5b($fws?$dcontent)*$fws?\\x5d$cfws?)"; + + /** + * local-part = dot-atom / quoted-string / obs-local-part + * domain = dot-atom / domain-literal / obs-domain + * addr-spec = local-part "@" domain + */ + $local_part = "($dot_atom|$quoted_string|$obs_local_part)"; + $domain = "($dot_atom|$domain_literal|$obs_domain)"; + $addr_spec = "($local_part\\x40$domain)"; + } + +} + +class Horde_Form_Type_matrix extends Horde_Form_Type { + + var $_cols; + var $_rows; + var $_matrix; + var $_new_input; + + /** + * Initializes the variable. + * + * @example + * init(array('Column A', 'Column B'), + * array(1 => 'Row One', 2 => 'Row 2', 3 => 'Row 3'), + * array(array(true, true, false), + * array(true, false, true), + * array(fasle, true, false)), + * array('Row 4', 'Row 5')); + * + * @param array $cols A list of column headers. + * @param array $rows A hash with row IDs as the keys and row + * labels as the values. + * @param array $matrix A two dimensional hash with the field + * values. + * @param boolean|array $new_input If true, a free text field to add a new + * row is displayed on the top, a select + * box if this parameter is a value. + */ + function init($cols, $rows = array(), $matrix = array(), $new_input = false) + { + $this->_cols = $cols; + $this->_rows = $rows; + $this->_matrix = $matrix; + $this->_new_input = $new_input; + } + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + function getInfo($vars, $var, &$info) + { + $values = $vars->get($var->getVarName()); + if (!empty($values['n']['r']) && isset($values['n']['v'])) { + $new_row = $values['n']['r']; + $values['r'][$new_row] = $values['n']['v']; + unset($values['n']); + } + + $info = (isset($values['r']) ? $values['r'] : array()); + } + + function about() + { + return array( + 'name' => _("Field matrix"), + 'params' => array( + 'cols' => array('label' => _("Column titles"), + 'type' => 'stringlist'))); + } + +} + +class Horde_Form_Type_emailConfirm extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value['original'])) { + $message = _("This field is required."); + return false; + } + + if ($value['original'] != $value['confirm']) { + $message = _("Email addresses must match."); + return false; + } else { + $parser = new Mail_RFC822(); + $parsed_email = $parser->parseAddressList($value['original'], false, true); + + if (count($parsed_email) > 1) { + $message = _("Only one email address allowed."); + return false; + } + if (empty($parsed_email[0]->mailbox)) { + $message = _("You did not enter a valid email address."); + return false; + } + } + + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Email with confirmation")); + } + +} + +class Horde_Form_Type_password extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + $valid = true; + + if ($var->isRequired()) { + $valid = strlen(trim($value)) > 0; + + if (!$valid) { + $message = _("This field is required."); + } + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Password")); + } + +} + +class Horde_Form_Type_passwordconfirm extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value['original'])) { + $message = _("This field is required."); + return false; + } + + if ($value['original'] != $value['confirm']) { + $message = _("Passwords must match."); + return false; + } + + return true; + } + + function getInfo($vars, $var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = $value['original']; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Password with confirmation")); + } + +} + +class Horde_Form_Type_enum extends Horde_Form_Type { + + var $_values; + var $_prompt; + + function init($values, $prompt = null) + { + $this->_values = $values; + + if ($prompt === true) { + $this->_prompt = _("-- select --"); + } else { + $this->_prompt = $prompt; + } + } + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && $value == '' && !isset($this->_values[$value])) { + $message = _("This field is required."); + return false; + } + + if (count($this->_values) == 0 || isset($this->_values[$value]) || + ($this->_prompt && empty($value))) { + return true; + } + + $message = _("Invalid data."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Drop down list"), + 'params' => array( + 'values' => array('label' => _("Values to select from"), + 'type' => 'stringlist'), + 'prompt' => array('label' => _("Prompt text"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_mlenum extends Horde_Form_Type { + + var $_values; + var $_prompts; + + function init(&$values, $prompts = null) + { + $this->_values = &$values; + + if ($prompts === true) { + $this->_prompts = array(_("-- select --"), _("-- select --")); + } elseif (!is_array($prompts)) { + $this->_prompts = array($prompts, $prompts); + } else { + $this->_prompts = $prompts; + } + } + + function onSubmit($var, $vars) + { + $varname = $var->getVarName(); + $value = $vars->get($varname); + + if ($value['1'] != $value['old']) { + $var->form->setSubmitted(false); + } + } + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && (empty($value['1']) || empty($value['2']))) { + $message = _("This field is required."); + return false; + } + + if (!count($this->_values) || isset($this->_values[$value['1']]) || + (!empty($this->_prompts) && empty($value['1']))) { + return true; + } + + $message = _("Invalid data."); + return false; + } + + function getInfo($vars, &$var, &$info) + { + $info = $vars->get($var->getVarName()); + return $info['2']; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Multi-level drop down lists"), + 'params' => array( + 'values' => array('label' => _("Values to select from"), + 'type' => 'stringlist'), + 'prompt' => array('label' => _("Prompt text"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_multienum extends Horde_Form_Type_enum { + + var $size = 5; + + function init($values, $size = null) + { + if (!is_null($size)) { + $this->size = (int)$size; + } + + parent::init($values); + } + + function isValid($var, $vars, $value, &$message) + { + if (is_array($value)) { + foreach ($value as $val) { + if (!$this->isValid($var, $vars, $val, $message)) { + return false; + } + } + return true; + } + + if (empty($value) && ((string)(int)$value !== $value)) { + if ($var->isRequired()) { + $message = _("This field is required."); + return false; + } else { + return true; + } + } + + if (count($this->_values) == 0 || isset($this->_values[$value])) { + return true; + } + + $message = _("Invalid data."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Multiple selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringlist'), + 'size' => array('label' => _("Size"), + 'type' => 'int')) + ); + } + +} + +class Horde_Form_Type_keyval_multienum extends Horde_Form_Type_multienum { + + function getInfo($vars, $var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = array(); + foreach ($value as $key) { + $info[$key] = $this->_values[$key]; + } + } + +} + +class Horde_Form_Type_radio extends Horde_Form_Type_enum { + + /* Entirely implemented by Horde_Form_Type_enum; just a different + * view. */ + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Radio selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringlist'))); + } + +} + +class Horde_Form_Type_set extends Horde_Form_Type { + + var $_values; + var $_checkAll = false; + + function init(&$values, $checkAll = false) + { + $this->_values = $values; + $this->_checkAll = $checkAll; + } + + function isValid($var, $vars, $value, &$message) + { + if (count($this->_values) == 0 || count($value) == 0) { + return true; + } + foreach ($value as $item) { + if (!isset($this->_values[$item])) { + $error = true; + break; + } + } + if (!isset($error)) { + return true; + } + + $message = _("Invalid data."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Set"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringlist'))); + } + +} + +class Horde_Form_Type_date extends Horde_Form_Type { + + var $_format; + + function init($format = '%a %d %B') + { + $this->_format = $format; + } + + function isValid($var, $vars, $value, &$message) + { + $valid = true; + + if ($var->isRequired()) { + $valid = strlen(trim($value)) > 0; + + if (!$valid) { + $message = sprintf(_("%s is required"), $var->getHumanName()); + } + } + + return $valid; + } + + /** + * @static + */ + function getAgo($timestamp) + { + if ($timestamp === null) { + return ''; + } + + $diffdays = Date_Calc::dateDiff(date('j', $timestamp), + date('n', $timestamp), + date('Y', $timestamp), + date('j'), date('n'), date('Y')); + + /* An error occured. */ + if ($diffdays == -1) { + return; + } + + $ago = $diffdays * Date_Calc::compareDates(date('j', $timestamp), + date('n', $timestamp), + date('Y', $timestamp), + date('j'), date('n'), + date('Y')); + if ($ago < -1) { + return sprintf(_(" (%s days ago)"), $diffdays); + } elseif ($ago == -1) { + return _(" (yesterday)"); + } elseif ($ago == 0) { + return _(" (today)"); + } elseif ($ago == 1) { + return _(" (tomorrow)"); + } else { + return sprintf(_(" (in %s days)"), $diffdays); + } + } + + function getFormattedTime($timestamp, $format = null, $showago = true) + { + if (empty($format)) { + $format = $this->_format; + } + if (!empty($timestamp)) { + return strftime($format, $timestamp) . ($showago ? Horde_Form_Type_date::getAgo($timestamp) : ''); + } else { + return ''; + } + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Date")); + } + +} + +class Horde_Form_Type_time extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-2]?[0-9]:[0-5][0-9]$/', $value)) { + return true; + } + + $message = _("This field may only contain numbers and the colon."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Time")); + } + +} + +class Horde_Form_Type_hourminutesecond extends Horde_Form_Type { + + var $_show_seconds; + + function init($show_seconds = false) + { + $this->_show_seconds = $show_seconds; + } + + function isValid($var, $vars, $value, &$message) + { + $time = $vars->get($var->getVarName()); + if (!$this->_show_seconds && !isset($time['second'])) { + $time['second'] = 0; + } + + if (!$this->emptyTimeArray($time) && !$this->checktime($time['hour'], $time['minute'], $time['second'])) { + $message = _("Please enter a valid time."); + return false; + } elseif ($this->emptyTimeArray($time) && $var->isRequired()) { + $message = _("This field is required."); + return false; + } + + return true; + } + + function checktime($hour, $minute, $second) + { + if (!isset($hour) || $hour == '' || ($hour < 0 || $hour > 23)) { + return false; + } + if (!isset($minute) || $minute == '' || ($minute < 0 || $minute > 60)) { + return false; + } + if (!isset($second) || $second === '' || ($second < 0 || $second > 60)) { + return false; + } + + return true; + } + + /** + * Return the time supplied as a Horde_Date object. + * + * @param string $time_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch). + * + * @return Date The time object. + */ + function getTimeOb($time_in) + { + if (is_array($time_in)) { + if (!$this->emptyTimeArray($time_in)) { + $time_in = sprintf('1970-01-01 %02d:%02d:%02d', $time_in['hour'], $time_in['minute'], $this->_show_seconds ? $time_in['second'] : 0); + } + } + + return new Horde_Date($time_in); + } + + /** + * Return the time supplied split up into an array. + * + * @param string $time_in Time in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch). + * + * @return array Array with three elements - hour, minute and seconds. + */ + function getTimeParts($time_in) + { + if (is_array($time_in)) { + /* This is probably a failed isValid input so just return the + * parts as they are. */ + return $time_in; + } elseif (empty($time_in)) { + /* This is just an empty field so return empty parts. */ + return array('hour' => '', 'minute' => '', 'second' => ''); + } + $time = $this->getTimeOb($time_in); + return array('hour' => $time->hour, + 'minute' => $time->min, + 'second' => $time->sec); + } + + function emptyTimeArray($time) + { + return (is_array($time) && empty($time['hour']) && empty($time['minute']) && empty($time['second'])); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Time selection"), + 'params' => array( + 'seconds' => array('label' => _("Show seconds?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_monthyear extends Horde_Form_Type { + + var $_start_year; + var $_end_year; + + function init($start_year = null, $end_year = null) + { + if (empty($start_year)) { + $start_year = 1920; + } + if (empty($end_year)) { + $end_year = date('Y'); + } + + $this->_start_year = $start_year; + $this->_end_year = $end_year; + } + + function isValid($var, $vars, $value, &$message) + { + if (!$var->isRequired()) { + return true; + } + + if (!$vars->get($this->getMonthVar($var)) || + !$vars->get($this->getYearVar($var))) { + $message = _("Please enter a month and a year."); + return false; + } + + return true; + } + + function getMonthVar($var) + { + return $var->getVarName() . '[month]'; + } + + function getYearVar($var) + { + return $var->getVarName() . '[year]'; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Month and year"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_monthdayyear extends Horde_Form_Type { + + var $_start_year; + var $_end_year; + var $_picker; + var $_format_in = null; + var $_format_out = '%x'; + + /** + * Return the date supplied as a Horde_Date object. + * + * @param integer $start_year The first available year for input. + * @param integer $end_year The last available year for input. + * @param boolean $picker Do we show the DHTML calendar? + * @param integer $format_in The format to use when sending the date + * for storage. Defaults to Unix epoch. + * Similar to the strftime() function. + * @param integer $format_out The format to use when displaying the + * date. Similar to the strftime() function. + */ + function init($start_year = '', $end_year = '', $picker = true, + $format_in = null, $format_out = '%x') + { + if (empty($start_year)) { + $start_year = date('Y'); + } + if (empty($end_year)) { + $end_year = date('Y') + 10; + } + + $this->_start_year = $start_year; + $this->_end_year = $end_year; + $this->_picker = $picker; + $this->_format_in = $format_in; + $this->_format_out = $format_out; + } + + function isValid($var, $vars, $value, &$message) + { + $date = $vars->get($var->getVarName()); + $empty = $this->emptyDateArray($date); + + if ($empty == 1 && $var->isRequired()) { + $message = _("This field is required."); + return false; + } elseif ($empty == 0 && !checkdate($date['month'], $date['day'], $date['year'])) { + $message = _("Please enter a valid date, check the number of days in the month."); + return false; + } elseif ($empty == -1) { + $message = _("Select all date components."); + return false; + } + + return true; + } + + function emptyDateArray($date) + { + if (!is_array($date)) { + return empty($date); + } + + $empty = 0; + /* Check each date array component. */ + foreach ($date as $key => $val) { + if (empty($val)) { + $empty++; + } + } + + /* Check state of empty. */ + if ($empty == 0) { + /* If no empty parts return 0. */ + return 0; + } elseif ($empty == count($date)) { + /* If all empty parts return 1. */ + return 1; + } else { + /* If some empty parts return -1. */ + return -1; + } + } + + /** + * Return the date supplied split up into an array. + * + * @param string $date_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS + * and UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return array Array with three elements - year, month and day. + */ + function getDateParts($date_in) + { + if (is_array($date_in)) { + /* This is probably a failed isValid input so just return + * the parts as they are. */ + return $date_in; + } elseif (empty($date_in)) { + /* This is just an empty field so return empty parts. */ + return array('year' => '', 'month' => '', 'day' => ''); + } + + $date = $this->getDateOb($date_in); + return array('year' => $date->year, + 'month' => $date->month, + 'day' => $date->mday); + } + + /** + * Return the date supplied as a Horde_Date object. + * + * @param string $date_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS + * and UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return Date The date object. + */ + function getDateOb($date_in) + { + if (is_array($date_in)) { + /* If passed an array change it to the ISO format. */ + if ($this->emptyDateArray($date_in) == 0) { + $date_in = sprintf('%04d-%02d-%02d 00:00:00', + $date_in['year'], + $date_in['month'], + $date_in['day']); + } + } elseif (preg_match('/^\d{4}-?\d{2}-?\d{2}$/', $date_in)) { + /* Fix the date if it is the shortened ISO. */ + $date_in = $date_in . ' 00:00:00'; + } + + return new Horde_Date($date_in); + } + + /** + * Return the date supplied as a Horde_Date object. + * + * @param string $date Either an already set up Horde_Date object or a + * string date in one of the three formats supported + * by Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return string The date formatted according to the $format_out + * parameter when setting up the monthdayyear field. + */ + function formatDate($date) + { + if (!is_a($date, 'Date')) { + $date = $this->getDateOb($date); + } + + return $date->strftime($this->_format_out); + } + + /** + * Insert the date input through the form into $info array, in the format + * specified by the $format_in parameter when setting up monthdayyear + * field. + */ + function getInfo($vars, &$var, &$info) + { + $info = $this->_validateAndFormat($var->getValue($vars), $var); + } + + /** + * Validate/format a date submission. + */ + function _validateAndFormat($value, $var) + { + /* If any component is empty consider it a bad date and return the + * default. */ + if ($this->emptyDateArray($value) == 1) { + return $var->getDefault(); + } else { + $date = $this->getDateOb($value); + if ($this->_format_in === null) { + return $date->timestamp(); + } else { + return $date->strftime($this->_format_in); + } + } + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Date selection"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'), + 'picker' => array('label' => _("Show picker?"), + 'type' => 'boolean'), + 'format_in' => array('label' => _("Storage format"), + 'type' => 'text'), + 'format_out' => array('label' => _("Display format"), + 'type' => 'text'))); + } + +} + +/** + * @since Horde 3.2 + */ +class Horde_Form_Type_datetime extends Horde_Form_Type { + + var $_mdy; + var $_hms; + + /** + * Return the date supplied as a Horde_Date object. + * + * @param integer $start_year The first available year for input. + * @param integer $end_year The last available year for input. + * @param boolean $picker Do we show the DHTML calendar? + * @param integer $format_in The format to use when sending the date + * for storage. Defaults to Unix epoch. + * Similar to the strftime() function. + * @param integer $format_out The format to use when displaying the + * date. Similar to the strftime() function. + * @param boolean $show_seconds Include a form input for seconds. + */ + function init($start_year = '', $end_year = '', $picker = true, + $format_in = null, $format_out = '%x', $show_seconds = false) + { + $this->_mdy = new Horde_Form_Type_monthdayyear(); + $this->_mdy->init($start_year, $end_year, $picker, $format_in, $format_out); + + $this->_hms = new Horde_Form_Type_hourminutesecond(); + $this->_hms->init($show_seconds); + } + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired()) { + return $this->_mdy->isValid($var, $vars, $value, $message) && + $this->_hms->isValid($var, $vars, $value, $message); + } + return true; + } + + function getInfo(&$vars, &$var, &$info) + { + /* If any component is empty consider it a bad date and return the + * default. */ + $value = $var->getValue($vars); + if ($this->emptyDateArray($value) == 1 || $this->emptyTimeArray($value)) { + $info = $var->getDefault(); + return; + } + + $date = $this->getDateOb($value); + $time = $this->getTimeOb($value); + $date->hour = $time->hour; + $date->min = $time->min; + $date->sec = $time->sec; + if (is_null($this->format_in)) { + $info = $date->timestamp(); + } else { + $info = $date->strftime($this->format_in); + } + } + + function __get($property) + { + if ($property == 'show_seconds') { + return $this->_hms->$property; + } else { + return $this->_mdy->$property; + } + } + + function __set($property, $value) + { + if ($property == 'show_seconds') { + $this->_hms->$property = $value; + } else { + $this->_mdy->$property = $value; + } + } + + function checktime($hour, $minute, $second) + { + return $this->_hms->checktime($hour, $minute, $second); + } + + function getTimeOb($time_in) + { + return $this->_hms->getTimeOb($time_in); + } + + function getTimeParts($time_in) + { + return $this->_hms->getTimeParts($time_in); + } + + function emptyTimeArray($time) + { + return $this->_hms->emptyTimeArray($time); + } + + function emptyDateArray($date) + { + return $this->_mdy->emptyDateArray($date); + } + + function getDateParts($date_in) + { + return $this->_mdy->getDateParts($date_in); + } + + function getDateOb($date_in) + { + return $this->_mdy->getDateOb($date_in); + } + + function formatDate($date) + { + if ($date === null) { + return ''; + } + return $this->_mdy->formatDate($date); + } + + function about() + { + return array( + 'name' => _("Date and time selection"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'), + 'picker' => array('label' => _("Show picker?"), + 'type' => 'boolean'), + 'format_in' => array('label' => _("Storage format"), + 'type' => 'text'), + 'format_out' => array('label' => _("Display format"), + 'type' => 'text'), + 'seconds' => array('label' => _("Show seconds?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_colorpicker extends Horde_Form_Type { + + function isValid($var, $vars, $value, &$message) + { + if ($var->isRequired() && empty($value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^#([0-9a-z]){6}$/i', $value)) { + return true; + } + + $message = _("This field must contain a color code in the RGB Hex format, for example '#1234af'."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Colour selection")); + } + +} + +class Horde_Form_Type_sorter extends Horde_Form_Type { + + var $_instance; + var $_values; + var $_size; + var $_header; + + function init($values, $size = 8, $header = '') + { + static $horde_sorter_instance = 0; + + /* Get the next progressive instance count for the horde + * sorter so that multiple sorters can be used on one page. */ + $horde_sorter_instance++; + $this->_instance = 'horde_sorter_' . $horde_sorter_instance; + $this->_values = $values; + $this->_size = $size; + $this->_header = $header; + } + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + function getOptions($keys = null) + { + $html = ''; + if ($this->_header) { + $html .= ''; + } + + if (empty($keys)) { + $keys = array_keys($this->_values); + } else { + $keys = explode("\t", $keys['array']); + } + foreach ($keys as $sl_key) { + $html .= ''; + } + + return $html; + } + + function getInfo($vars, &$var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = explode("\t", $value['array']); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Sort order selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringlist'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'header' => array('label' => _("Header"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_selectfiles extends Horde_Form_Type { + + /** + * The text to use in the link. + * + * @var string + */ + var $_link_text; + + /** + * The style to use for the link. + * + * @var string + */ + var $_link_style; + + /** + * Create the link with an icon instead of text? + * + * @var boolean + */ + var $_icon; + + /** + * Contains gollem selectfile selectionID + * + * @var string + */ + var $_selectid; + + function init($selectid, $link_text = null, $link_style = '', + $icon = false) + { + $this->_selectid = $selectid; + if (is_null($link_text)) { + $link_text = _("Select Files"); + } + $this->_link_text = $link_text; + $this->_link_style = $link_style; + $this->_icon = $icon; + } + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + function getInfo($var, &$vars, &$info) + { + $value = $vars->getValue($var); + $info = $GLOBALS['registry']->call('files/selectlistResults', array($value)); + } + + function about() + { + return array( + 'name' => _("File selection"), + 'params' => array( + 'selectid' => array('label' => _("Id"), + 'type' => 'text'), + 'link_text' => array('label' => _("Link text"), + 'type' => 'text'), + 'link_style' => array('label' => _("Link style"), + 'type' => 'text'), + 'icon' => array('label' => _("Show icon?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_assign extends Horde_Form_Type { + + var $_leftValues; + var $_rightValues; + var $_leftHeader; + var $_rightHeader; + var $_size; + var $_width; + + function init($leftValues, $rightValues, $leftHeader = '', + $rightHeader = '', $size = 8, $width = '200px') + { + $this->_leftValues = $leftValues; + $this->_rightValues = $rightValues; + $this->_leftHeader = $leftHeader; + $this->_rightHeader = $rightHeader; + $this->_size = $size; + $this->_width = $width; + } + + function isValid($var, $vars, $value, &$message) + { + return true; + } + + function setValues($side, $values) + { + if ($side) { + $this->_rightValues = $values; + } else { + $this->_leftValues = $values; + } + } + + function getHeader($side) + { + return $side ? $this->_rightHeader : $this->_leftHeader; + } + + function getOptions($side, $formname, $varname) + { + $html = ''; + $headers = false; + if ($side) { + $values = $this->_rightValues; + if (!empty($this->_rightHeader)) { + $values = array('' => $this->_rightHeader) + $values; + $headers = true; + } + } else { + $values = $this->_leftValues; + if (!empty($this->_leftHeader)) { + $values = array('' => $this->_leftHeader) + $values; + $headers = true; + } + } + + foreach ($values as $key => $val) { + $html .= '