require 'date'
require 'facets/time/stamp'

# = Date
#
# This new version of Date extension has been largely improved by
# porting some of the methods used by ActiveSupport. The old version
# already had much in common with the Active Support library, so it
# was decided to take it a step further in that direction for the
# sake of interoparability.
#
# Hopefully most of these methods will find there way into Ruby's
# own standard library eventually.
#
# The biggest difference with ActiveSupport is the lack of many
# of the "English-esque" methods, and that we use #stamp with
# Date::FORMAT, instead of #to_formmated_s with Date::DATE_FORMATS.
# We do not override the standard #to_s method like ActiveSupport does.

class Date

  FORMAT = {
    :short        => "%e %b",
    :long         => "%B %e, %Y",
    :db           => "%Y-%m-%d",
    :number       => "%Y%m%d",
    :rfc822       => "%e %b %Y",
    :default      => "%Y-%m-%d",
    nil           => "%Y-%m-%d"
  }

  # Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
  def self.yesterday
    ::Date.today.yesterday
  end
  
  # Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
  def self.tomorrow
    ::Date.today.tomorrow
  end
  
  # Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today.
  def self.current
    ::Time.zone_default ? ::Time.zone.today : ::Date.today
  end

  # A method to keep Time, Date and DateTime instances interchangeable
  # on conversions. In this case, it simply returns +self+.
  def to_date
    self
  end unless method_defined?(:to_date) # 1.9+ ?

  # Converts a Date instance to a DateTime, where the time is set to the beginning of the day
  # and UTC offset is set to 0.
  #
  #   date = Date.new(2007, 11, 10)  # Sat, 10 Nov 2007
  #   date.to_datetime               # Sat, 10 Nov 2007 00:00:00 0000
  #
  def to_datetime
    ::DateTime.civil(year, month, day, 0, 0, 0, 0)
  end unless method_defined?(:to_datetime) # 1.9+ ?

  # Converts a Date instance to a Time, where the time is set to the beginning of the day.
  # The timezone can be either :local or :utc (default :local).
  #
  #   date = Date.new(2007, 11, 10)  # Sat, 10 Nov 2007
  #
  #   date.to_time                   # Sat Nov 10 00:00:00 0800 2007
  #   date.to_time(:local)           # Sat Nov 10 00:00:00 0800 2007
  #
  #   date.to_time(:utc)             # Sat Nov 10 00:00:00 UTC 2007
  #
  def to_time(form=:local)
    ::Time.send(form, year, month, day)
    ##::Time.send("#{form}_time", year, month, day)
  end

  #
  def xmlschema
    to_time.xmlschema
  end

  # Returns the number of days in the date's month.
  #
  #   Date.new(2004,2).days_in_month  #=> 29
  #
  # CREDIT: Ken Kunz.
  def days_in_month
     Date.civil(year, month, -1).day
  end

  def days_of_month
    (1..days_in_month).to_a
  end

  # Get the month name for this date object
  #
  # CREDIT: Benjamin Oakes
  def month_name
    MONTHNAMES[self.month]
  end

  # Convert to a formatted string. See DATE_FORMATS for predefined formats.
  #
  # This method is aliased to <tt>to_s</tt>.
  #
  #   date = Date.new(2007, 11, 10)   # Sat, 10 Nov 2007
  #
  #   date.stamp(:db)                 # => "2007-11-10"
  #   date.stamp(:short)              # => "10 Nov"
  #   date.stamp(:long)               # => "November 10, 2007"
  #   date.stamp(:rfc822)             # => "10 Nov 2007"
  #
  # == Adding your own formats to stamp
  # You can add your own formats to the Date::FORMAT hash.
  # Use the format name as the hash key and a strftime string
  # as the value. Eg.
  #
  #   Date::FORMAT[:month_and_year] = "%B %Y"
  #
  def stamp(format=:default)
    if formatter = FORMAT[format]
      strftime(formatter)
    else
      to_s
    end
  end

  ## Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
  ##def inspect
  ##  strftime("%a, %d %b %Y")
  ##end

  # Provides precise Date calculations for years, months, and days.
  # The +options+ parameter takes a hash with  any of these keys:
  # <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
  def advance(options)
    d = self
    d = d >> options.delete(:years) * 12 if options[:years]
    d = d >> options.delete(:months)     if options[:months]
    d = d +  options.delete(:weeks) * 7  if options[:weeks]
    d = d +  options.delete(:days)       if options[:days]
    d
  end

  # Returns a new Date where one or more of the elements have been changed
  # according to the +options+ parameter.
  #
  #   Date.new(2007, 5, 12).change(:day=>1)                 # Date.new(2007, 5, 1)
  #   Date.new(2007, 5, 12).change(:year=>2005, :month=>1)  # Date.new(2005, 1, 12)
  #
  def change(options)
    ::Date.new(
      options[:year]  || self.year,
      options[:month] || self.month,
      options[:day]   || self.day
    )
  end

  # Converts Date to a Time (or DateTime if necessary) with the time portion
  # set to the beginning of the day (0:00) and then subtracts the specified
  # number of seconds
  def ago(seconds)
    to_time.since(-seconds)
  end

  # Converts Date to a Time (or DateTime if necessary) with the time portion
  # set to the beginning of the day (0:00) and then adds the specified number
  # of seconds.
  def since(seconds)
    to_time.since(seconds)
  end
  alias :in :since

  # Converts Date to a Time (or DateTime if necessary) with the time portion
  # set to the beginning of the day (0:00).
  def beginning_of_day
    to_time
  end
  alias :midnight :beginning_of_day

  # Convenience method which returns a new Date/DateTime representing the
  # time 1 day ago.
  def yesterday
    self - 1
  end

  # Convenience method which returns a new Date/DateTime representing
  # the time 1 day since the instance time.
  def tomorrow
    self + 1
  end

end


class DateTime

  #
  def self.local_offset
    ::Time.local(2007).utc_offset.to_r / 86400
  end

  def future?
    self > ::DateTime.current
  end

  def past?
    self < ::DateTime.current
  end

  # Converts self to a Ruby Date object; time portion is discarded
  def to_date
    ::Date.new(year, month, day)
  end

  # Attempts to convert self to a Ruby Time object; returns self if
  # out of range of Ruby Time class. If self has an offset other than 0,
  # self will just be returned unaltered, since there's no clean way
  # to map it to a Time.
  def to_time
    self.offset == 0 ? ::Time.utc_time(year, month, day, hour, min, sec) : self
  end

  # To be able to keep Times, Dates and DateTimes interchangeable on conversions
  def to_datetime
    self
  end

  # Convert to a formatted string. See Time::FORMAT for predefined formats.
  # 
  # This method is aliased to <tt>to_s</tt>.
  # 
  #   datetime = DateTime.civil(2007,12,4,0,0,0,0)   # Tue, 04 Dec 2007 00:00:00 +0000
  # 
  #   datetime.stamp(:db)            # => "2007-12-04 00:00:00"
  #   datetime.stamp(:db)            # => "2007-12-04 00:00:00"
  #   datetime.stamp(:number)        # => "20071204000000"
  #   datetime.stamp(:short)         # => "04 Dec 00:00"
  #   datetime.stamp(:long)          # => "December 04, 2007 00:00"
  #   datetime.stamp(:rfc822)        # => "Tue, 04 Dec 2007 00:00:00 +0000"
  #
  # == Adding your own datetime formats to stamp
  #
  # DateTime formats are shared with Time. You can add your own to the
  # Time::FORMAT hash. Use the format name as the hash key and
  # a strftime string as the value. Eg.
  #
  #   Time::FORMAT[:month_and_year] = "%B %Y"
  #
  def stamp(format=:default)
    if formatter = ::Time::FORMAT[format]
      strftime(formatter)
    else
      to_s
    end
  end

  # Seconds since midnight: DateTime.now.seconds_since_midnight
  def seconds_since_midnight
    self.sec + (self.min * 60) + (self.hour * 3600)
  end

  # Returns a new DateTime where one or more of the elements have been
  # changed according to the +options+ parameter. The time options
  # (hour, minute, sec) reset cascadingly, so if only the hour is 
  # passed, then minute and sec is set to 0. If the hour and
  # minute is passed, then sec is set to 0.
  def change(options)
    ::DateTime.civil(
      options[:year]  || self.year,
      options[:month] || self.month,
      options[:day]   || self.day,
      options[:hour]  || self.hour,
      options[:min]   || (options[:hour] ? 0 : self.min),
      options[:sec]   || ((options[:hour] || options[:min]) ? 0 : self.sec),
      options[:offset]  || self.offset,
      options[:start]  || self.start
    )
  end

  # Uses Date to provide precise Time calculations for years, months, and days.
  # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
  # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
  # <tt>:minutes</tt>, <tt>:seconds</tt>.
  def advance(options)
    d = to_date.advance(options)
    datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
    seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600
    seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date.since(seconds_to_advance)
  end

  # Returns a new DateTime representing the time a number of seconds ago
  # Do not use this method in combination with x.months, use months_ago instead!
  def ago(seconds)
    self.since(-seconds)
  end

  # Returns a new DateTime representing the time a number of seconds since the instance time
  # Do not use this method in combination with x.months, use months_since instead!
  def since(seconds)
    self + Rational(seconds.round, 86400)
  end
  alias :in :since

  # Returns a new DateTime representing the start of the day (0:00)
  def beginning_of_day
    change(:hour => 0)
  end
  alias :midnight :beginning_of_day

  # Returns a new DateTime representing the end of the day (23:59:59)
  def end_of_day
    change(:hour => 23, :min => 59, :sec => 59)
  end
  
  # Adjusts DateTime to UTC by adding its offset value; offset is set to 0
  #
  # Example:
  #
  #   DateTime.civil(2005,2,21,10,11,12,Rational(-6, 24))      # Mon, 21 Feb 2005 10:11:12 -0600
  #   DateTime.civil(2005,2,21,10,11,12,Rational(-6, 24)).utc  # Mon, 21 Feb 2005 16:11:12 +0000
  #
  def utc
    new_offset(0)
  end
  alias_method :getutc, :utc
  
  # Returns true if offset == 0
  def utc?
    offset == 0
  end
  
  # Returns the offset value in seconds
  def utc_offset
    (offset * 86400).to_i
  end

  # Converts datetime to an appropriate format for use in XML
  def xmlschema
    strftime("%Y-%m-%dT%H:%M:%S%Z")
  end unless method_defined?(:xmlschema) # 1.9+ ?

  # Converts self to a floating-point number of seconds since the Unix epoch 
  def to_f
    days_since_unix_epoch = self - ::DateTime.civil(1970)
    (days_since_unix_epoch * 86_400).to_f
  end

end


class Time

  #
  def self.local_time(*args)
    time_with_datetime_fallback(:local, *args)
  end

  #
  def self.utc_time(*args)
    time_with_datetime_fallback(:utc, *args)
  end

  #
  def self.time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0)
    ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec)
  rescue
    offset = utc_or_local.to_sym == :local ? ::DateTime.local_offset : 0
    ::DateTime.civil(year, month, day, hour, min, sec, offset)
  end

  public :to_date
  public :to_datetime

  # Converts a Time object to a Date, dropping hour, minute, and second precision.
  #
  #   my_time = Time.now  # Mon Nov 12 22:59:51 -0500 2007
  #   my_time.to_date     # Mon, 12 Nov 2007
  #
  #   your_time = Time.parse("1/13/2009 1:13:03 P.M.")  # Tue Jan 13 13:13:03 -0500 2009
  #   your_time.to_date                                 # Tue, 13 Jan 2009
  #
  def to_date
    ::Date.new(year, month, day)
  end

  ##  # Convert a Time to a Date. Time is a superset of Date.
  ##  # It is the year, month and day that are carried over.
  ##
  ##  def to_date
  ##    require 'date' # just in case
  ##    jd = Date.__send__(:civil_to_jd, year, mon, mday, Date::ITALY)
  ##    Date.new!(Date.__send__(:jd_to_ajd, jd, 0, 0), 0, Date::ITALY)
  ##  end


  # Converts a Time instance to a Ruby DateTime instance, preserving UTC offset.
  #
  #   my_time = Time.now    # Mon Nov 12 23:04:21 -0500 2007
  #   my_time.to_datetime   # Mon, 12 Nov 2007 23:04:21 -0500
  #
  #   your_time = Time.parse("1/13/2009 1:13:03 P.M.")  # Tue Jan 13 13:13:03 -0500 2009
  #   your_time.to_datetime                             # Tue, 13 Jan 2009 13:13:03 -0500
  #
  def to_datetime
    ::DateTime.civil(year, month, day, hour, min, sec, Rational(utc_offset, 86400))
  end

end


class String

  #
  def to_time(form = :utc)
    ::Time.__send__("#{form}_time", *::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec).map{|arg| arg || 0 })
  end

  # Convert string to DateTime.
  def to_datetime
    date = ::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec).map { |arg| arg || 0 }
    ::DateTime.civil(*date)
  end

  # Parse data from string.
  def to_date
    #::Date::civil(*ParseDate.parsedate(self)[0..2])
    ::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday))
  end

end

