class Nickel::ZDate

TODO: get methods should accept dayname or dayindex

Constants

FRI
MON
SAT
SUN
THU
TUE
WED

Attributes

days_in_common_year_months[R]
days_in_leap_year_months[R]
days_of_week[R]
full_days_of_week[R]
full_months_of_year[R]
months_of_year[R]

Public Class Methods

days_in_month(month, year) click to toggle source
# File lib/nickel/zdate.rb, line 289
def days_in_month(month, year)
  if year % 400 == 0 || year % 4 == 0 && year % 100 != 0
    ZDate.days_in_leap_year_months[month - 1]
  else
    ZDate.days_in_common_year_months[month - 1]
  end
end
diff_in_months(month1, year1, month2, year2) click to toggle source

Gets the difference FROM month1, year1 TO month2, year2 don't use it the other way around, it won't work

# File lib/nickel/zdate.rb, line 299
def diff_in_months(month1, year1, month2, year2)
  # first get the difference in months
  if month2 >= month1
    diff_in_months = month2 - month1
  else
    diff_in_months = 12 - (month1 - month2)
    year2 -= 1  # this makes the next line nice
  end
  diff_in_months + (year2 - year1) * 12
end
format_date(year, month = 1, day = 1) click to toggle source

formats the year, month, day into the format expected by the ZDate constructor

# File lib/nickel/zdate.rb, line 324
def format_date(year, month = 1, day = 1)
  format_year(year) + format_month(month) + format_day(day)
end
format_day(d) click to toggle source
# File lib/nickel/zdate.rb, line 319
def format_day(d)
  d.to_s.rjust(2, '0')
end
format_month(m) click to toggle source
# File lib/nickel/zdate.rb, line 315
def format_month(m)
  m.to_s.rjust(2, '0')
end
format_year(y) click to toggle source
# File lib/nickel/zdate.rb, line 310
def format_year(y)
  # if there were only two digits, prepend 20 (e.g. "08" should be "2008")
  y.to_s.rjust(4, '20')
end
interpret(str, current_date) click to toggle source

Interpret Date is equally as important, our goals: First off, convention of the NLP is to not allow month names to the construct finder (unless it is implying date span), so we will not be interpreting anything such as january 2nd, 2008. Instead all dates will be represented in this form month/day/year. However it may not be as nice as that. We need to match things like '5', if someone just typed in “the 5th.” Because of this, there will be overlap between interpret_date and interpret_time in matching; interpret_date should ALWAYS be found after interpret_time in the construct finder. If the construct finder happens upon a digit on it's own, e.g. “5”, it will not run interpret_time because there is no “at” preceeding it. Therefore it will fall through to the finder with interpret_date and we will assume the user meant the 5th. If interpret_date is before interpret_time, then .… wait… does the order actually matter? Even if this is before interpret_time, it shouldn't get hit because the time should be picked up at the “at” construct. This may be a bunch of useless rambling.

2/08 <—— This is not A date 2/2008 <—— Neither is this, but I can see people using these as wrappers, must support this in next version 11/08 <—— same 11/2008 <—— same 2/1/08, 2/12/08, 2/1/2008, 2/12/2008 11/1/08, 11/12/08, 11/1/2008, 11/12/2008 2/1 feb first 2/12 feb twelfth 11/1 nov first 11/12 nov twelfth 11 the 11th 2 the 2nd

Match all of the following:

a.) 1   10
b.) 1/1  1/12  10/1  10/12
c.) 1/1/08 1/12/08 1/1/2008 1/12/2008 10/1/08 10/12/08 10/12/2008 10/12/2008
d.) 1st 10th
# File lib/nickel/zdate.rb, line 358
def interpret(str, current_date)
  day_str, month_str, year_str = nil, nil, nil
  ambiguous = { month: false, year: false }   # assume false, we use this flag if we aren't certain about the year

  # appropriate matches
  a_d = /^(\d{1,2})(rd|st|nd|th)?$/     # handles cases a and d
  b = /^(\d{1,2})\/(\d{1,2})$/          # handles case b
  c = /^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/   # handles case c

  if mdata = str.match(a_d)
    ambiguous[:month] = true
    day_str = mdata[1]
  elsif mdata = str.match(b)
    ambiguous[:year] = true
    month_str = mdata[1]
    day_str = mdata[2]
  elsif mdata = str.match(c)
    month_str = mdata[1]
    day_str = mdata[2]
    year_str = mdata[3]
  else
    return nil
  end

  inst_str = ZDate.format_date(year_str || current_date.year_str, month_str || current_date.month_str, day_str || current_date.day_str)
  # in this case we do not care if date fails validation, if it does, it just means we haven't found a valid date, return nil
  date = ZDate.new(inst_str) rescue nil
  if date
    if ambiguous[:year]
      # say the date is 11/1 and someone enters 2/1, they probably mean next year, I pick 4 months as a threshold but that is totally arbitrary
      current_date.diff_in_months(date) < -4 && date = date.add_years(1)
    elsif ambiguous[:month]
      current_date.day > date.day && date = date.add_months(1)
    end
  end
  date
end
new(yyyymmdd = nil) click to toggle source

Don't use attr_accessor for date, year, month, day; we want to validate on change.

# File lib/nickel/zdate.rb, line 28
def initialize(yyyymmdd = nil)
  d = yyyymmdd ? yyyymmdd.dup : ::Time.new.strftime('%Y%m%d')
  d.gsub!(/-/, '') # remove any hyphens, so a user can initialize with something like "2008-10-23"
  self.date = d
end
new_first_day_in_month(month, year) click to toggle source
# File lib/nickel/zdate.rb, line 280
def new_first_day_in_month(month, year)
  ZDate.new(ZDate.format_date(year, month))
end
new_last_day_in_month(month, year) click to toggle source
# File lib/nickel/zdate.rb, line 284
def new_last_day_in_month(month, year)
  day = days_in_month(month, year)
  ZDate.new(ZDate.format_date(year, month, day))
end

Public Instance Methods

<=>(other) click to toggle source
# File lib/nickel/zdate.rb, line 77
def <=>(other)
  return nil unless [:year, :month, :day].all? { |m| other.respond_to?(m) }

  if before?(other)
    -1
  elsif after?(other)
    1
  else
    0
  end
end
add_days(number) click to toggle source

add_ methods return new ZDate object, they DO NOT modify self

# File lib/nickel/zdate.rb, line 146
def add_days(number)
  if number < 0
    return sub_days(number.abs)
  end
  o = dup  # new ZDate object
  # Let's see what month we are going to end in
  while number > 0
    if o.days_left_in_month >= number
      o.date = ZDate.format_date(o.year_str, o.month_str, o.day + number)
      number = 0
    else
      number = number - 1 - o.days_left_in_month  # it costs 1 day to increment the month
      o.increment_month!
    end
  end
  o
end
add_months(number) click to toggle source
# File lib/nickel/zdate.rb, line 168
def add_months(number)
  new_month = 1 + ((month - 1 + number) % 12)
  if number > months_left_in_year            # are we going to change year?
    years_to_increment = 1 + ((number - months_left_in_year) / 12)    # second term adds years if user entered a large number of months (e.g. date.add_months(50))
  else
    years_to_increment = 0
  end
  new_year = year + years_to_increment
  new_day = get_day_or_max_day_in_month(day, new_month, new_year)
  ZDate.new(ZDate.format_date(new_year, new_month, new_day))
end
add_weeks(number) click to toggle source
# File lib/nickel/zdate.rb, line 164
def add_weeks(number)
  add_days(7 * number)
end
add_years(number) click to toggle source
# File lib/nickel/zdate.rb, line 180
def add_years(number)
  new_year = year + number
  new_day = get_day_or_max_day_in_month(day, month, new_year)
  ZDate.new(ZDate.format_date(new_year, month_str, new_day))
end
beginning_of_month() click to toggle source

beginning and end of month both return new ZDate objects

# File lib/nickel/zdate.rb, line 209
def beginning_of_month
  ZDate.new(ZDate.format_date(year_str, month_str))
end
beginning_of_next_month() click to toggle source
# File lib/nickel/zdate.rb, line 217
def beginning_of_next_month
  o = dup
  o.increment_month!
  o
end
date() click to toggle source
# File lib/nickel/zdate.rb, line 34
def date
  @date
end
date=(yyyymmdd) click to toggle source
# File lib/nickel/zdate.rb, line 38
def date=(yyyymmdd)
  @date = yyyymmdd
  validate
end
day() click to toggle source
# File lib/nickel/zdate.rb, line 63
def day
  day_str.to_i
end
day_of_year() click to toggle source
# File lib/nickel/zdate.rb, line 446
def day_of_year
  doy = day
  # iterate through days in months arrays, summing up the days
  if leap_year?
    doy = (1...month).to_a.reduce(doy) { |sum, n| sum + ZDate.days_in_leap_year_months[n - 1] }
  else
    doy = (1...month).to_a.reduce(doy) { |sum, n| sum + ZDate.days_in_common_year_months[n - 1] }
  end
  doy
end
day_str() click to toggle source
# File lib/nickel/zdate.rb, line 51
def day_str
  @date[6..7]
end
dayindex() click to toggle source
# File lib/nickel/zdate.rb, line 430
def dayindex
  ZDate.days_of_week.index(dayname)
end
dayname() click to toggle source
# File lib/nickel/zdate.rb, line 423
def dayname
  # well this is going to be a hack, I need an algo for finding the day
  # Ruby's Time.local is the fastest way to create a Ruby Time object
  t = ::Time.local(year, ZDate.months_of_year[month - 1], day)
  t.strftime('%a').downcase
end
days_in_month() click to toggle source
# File lib/nickel/zdate.rb, line 407
def days_in_month
  if leap_year?
    ZDate.days_in_leap_year_months[month - 1]
  else
    ZDate.days_in_common_year_months[month - 1]
  end
end
days_left_in_month() click to toggle source
# File lib/nickel/zdate.rb, line 415
def days_left_in_month
  days_in_month - day
end
days_left_in_year() click to toggle source
# File lib/nickel/zdate.rb, line 457
def days_left_in_year
  leap_year? ? 366 - day_of_year : 365 - day_of_year
end
diff_in_days(date_to_compare) click to toggle source

Gets the absolute difference in days between self and date_to_compare, order is not important.

# File lib/nickel/zdate.rb, line 251
def diff_in_days(date_to_compare)
  # d1 will be the earlier date, d2 the later
  if date_to_compare > self
    d1, d2 = dup, date_to_compare.dup
  elsif self > date_to_compare
    d1, d2 = date_to_compare.dup, dup
  else
    return 0  # same date
  end

  total = 0
  while d1.year != d2.year
    total += d1.days_left_in_year + 1 # need one extra day to push us to jan 1
    d1 = ZDate.new(ZDate.format_date(d1.year + 1))
  end
  total += d2.day_of_year - d1.day_of_year
  total
end
diff_in_days_to_this(closest_day_index) click to toggle source
# File lib/nickel/zdate.rb, line 270
def diff_in_days_to_this(closest_day_index)
  if closest_day_index >= dayindex
    closest_day_index - dayindex  # could be 0
  else   # day_num < self.dayindex
    7 - (dayindex - closest_day_index)
  end
end
diff_in_months(date2) click to toggle source

difference in months FROM self TO date2, for instance, if self is oct 1 and date2 is nov 14, will return 1 if self is nov 14 and date2 is oct 1, will return -1

# File lib/nickel/zdate.rb, line 399
def diff_in_months(date2)
  if date2 > self
    ZDate.diff_in_months(month, year, date2.month, date2.year)
  else
    ZDate.diff_in_months(date2.month, date2.year, month, year) * -1
  end
end
end_of_month() click to toggle source
# File lib/nickel/zdate.rb, line 213
def end_of_month
  ZDate.new(ZDate.format_date(year_str, month_str, days_in_month))
end
fmt(txt) click to toggle source
# File lib/nickel/zdate.rb, line 71
def fmt(txt)
  txt.gsub!(/%Y/, year_str)
  txt.gsub!(/%m/, month_str)
  txt.gsub!(/%d/, day_str)
end
full_dayname() click to toggle source
# File lib/nickel/zdate.rb, line 434
def full_dayname
  ZDate.full_days_of_week[dayindex]
end
full_monthname() click to toggle source
# File lib/nickel/zdate.rb, line 438
def full_monthname
  Z.full_months_of_year[month - 1]
end
get_date_from_day_and_week_of_month(day_num, week_num) click to toggle source
# File lib/nickel/zdate.rb, line 461
def get_date_from_day_and_week_of_month(day_num, week_num)
  # This method is extremely sloppy, clean it up
  # Get the index of the first day of this month
  first_day_of_month = beginning_of_month
  first_day_index = first_day_of_month.dayindex

  diff_in_days_to_first_occ = first_day_of_month.diff_in_days_to_this(day_num)

  # now find the number of days to the correct occurrence; REMEMBER TO CHECK FOR LAST MONTH
  if week_num == -1
    total_diff_in_days = diff_in_days_to_first_occ + 21      # 7 * 3 weeks; are already at the first ocurrence, so this is total diff in days to 4th occurrence; may not be the last!!
  else
    total_diff_in_days = diff_in_days_to_first_occ + 7 * (week_num - 1)
  end

  # there is a chance that the last occurrence is not the 4th week of the month; if that is the case, add an extra 7 days
  if (week_num == -1) && (month == beginning_of_month.add_days(total_diff_in_days + 7).month)
    total_diff_in_days += 7
  end

  # Now we have the number of days FROM THE START OF THE CURRENT MONTH; if we are not past that date, then we have found the first occurrence
  if (total_diff_in_days + 1) >= day
    return beginning_of_month.add_days(total_diff_in_days)
  else # We have already past the date; calculate the occurrence next month!
    # Get the index of the first day next month
    first_day_index = add_months(1).beginning_of_month.dayindex

    # Find the number of days away to the day of interest (NOT the week)
    if day_num > first_day_index
      diff_in_days_to_first_occ = day_num - first_day_index
    elsif day_num < first_day_index
      diff_in_days_to_first_occ = 7 - (first_day_index - day_num)
    else # first_day_index == day_num
      diff_in_days_to_first_occ = 0
    end

    # now find the number of days to the correct occurrence; REMEMBER TO CHECK FOR LAST MONTH
    if week_num == -1
      total_diff_in_days = diff_in_days_to_first_occ + 21      # 7 * 3 weeks
    else
      total_diff_in_days = diff_in_days_to_first_occ + 7 * (week_num - 1)
    end

    # there is a chance that the last occurrence is not the 4th week of the month; if that is the case, add an extra 7 days
    if (week_num == -1) && (add_months(1).month == add_months(1).beginning_of_month.add_days(total_diff_in_days + 7).month)
      total_diff_in_days += 7
    end

    return add_months(1).beginning_of_month.add_days(total_diff_in_days)
  end # END if (total_diff_in_days + 1) ...
end
get_next_date_from_date_of_month(date_of_month) click to toggle source

returns a new ZDate object, NOTE! this returns nil if that date does not exist (sept 31st)

# File lib/nickel/zdate.rb, line 514
def get_next_date_from_date_of_month(date_of_month)
  o = dup
  if day == date_of_month
    o
  else
    if day > date_of_month
      o.increment_month!
    end
    ZDate.new(ZDate.format_date(o.year_str, o.month_str, date_of_month)) rescue nil
  end
end
is_today?() click to toggle source
# File lib/nickel/zdate.rb, line 89
def is_today?
  warn '[DEPRECATION] `is_today?` is deprecated.  Please use `today?` instead.'
  today?
end
jump_to_month(month_number) click to toggle source

returns new ZDate object, note this is the MONTH NUMBER, not MONTH INDEX from ZDate.months_of_year returns the first day of the month

# File lib/nickel/zdate.rb, line 199
def jump_to_month(month_number)
  # find difference in months
  if month_number >= month
    ZDate.new(ZDate.format_date(year_str, month_number))
  else
    ZDate.new(ZDate.format_date(year + 1, month_number))
  end
end
leap_year?() click to toggle source
# File lib/nickel/zdate.rb, line 442
def leap_year?
  year % 400 == 0 || year % 4 == 0 && year % 100 != 0
end
month() click to toggle source
# File lib/nickel/zdate.rb, line 59
def month
  month_str.to_i
end
month_str() click to toggle source
# File lib/nickel/zdate.rb, line 47
def month_str
  @date[4..5]
end
months_left_in_year() click to toggle source
# File lib/nickel/zdate.rb, line 419
def months_left_in_year
  12 - month
end
next(day) click to toggle source

for example, “next friday”

# File lib/nickel/zdate.rb, line 123
def next(day)
  x_weeks_from_day(1, day)
end
ordinal_dayindex(num, day_index) click to toggle source

for example, “1st friday”, uses self as the reference month

# File lib/nickel/zdate.rb, line 100
def ordinal_dayindex(num, day_index)
  # create a date object at the first occurrence of day_index
  first_occ_date = ZDate.new(ZDate.format_date(year_str, month_str)).this(day_index)
  # if num is 1 through 4, we can just add (num-1) weeks
  if num <= 4
    d = first_occ_date.add_weeks(num - 1)
  else
    # we want the last occurrence of this month
    # add 4 weeks to first occurrence, see if we are in the same month, subtract 1 week if we are not
    d = first_occ_date.add_weeks(4)
    if d.month != month
      d = d.sub_weeks(1)
    end
  end
  d
end
prev(day) click to toggle source

for example, “previous friday”

# File lib/nickel/zdate.rb, line 128
def prev(day)
  (dayindex == day) ? dup : x_weeks_from_day(-1, day)
end
readable() click to toggle source
# File lib/nickel/zdate.rb, line 67
def readable
  month_str + '/' + day_str + '/' + year_str
end
sub_days(number) click to toggle source

sub_ methods return new ZDate object, they do not modify self.

# File lib/nickel/zdate.rb, line 224
def sub_days(number)
  o = dup
  while number > 0
    if (o.day - 1) >= number
      o.date = ZDate.format_date(o.year_str, o.month_str, o.day - number)
      number = 0
    else
      number -= o.day
      o.decrement_month!
    end
  end
  o
end
sub_months(number) click to toggle source
# File lib/nickel/zdate.rb, line 242
def sub_months(number)
  o = dup
  number.times do
    o.decrement_month!
  end
  o
end
sub_weeks(number) click to toggle source
# File lib/nickel/zdate.rb, line 238
def sub_weeks(number)
  sub_days(7 * number)
end
this(day) click to toggle source

for example, “this friday”

# File lib/nickel/zdate.rb, line 118
def this(day)
  x_weeks_from_day(0, day)
end
to_date() click to toggle source
# File lib/nickel/zdate.rb, line 526
def to_date
  Date.new(year, month, day)
end
to_s() click to toggle source
# File lib/nickel/zdate.rb, line 530
def to_s
  date
end
today?() click to toggle source

returns true if self is today

# File lib/nickel/zdate.rb, line 95
def today?
  self == ZDate.new
end
x_weeks_from_day(weeks_away, day2index) click to toggle source

returns a new date object

# File lib/nickel/zdate.rb, line 133
def x_weeks_from_day(weeks_away, day2index)
  day1index = dayindex
  if day1index > day2index
    days_away = 7 * (weeks_away + 1) - (day1index - day2index)
  elsif day1index < day2index
    days_away = (weeks_away * 7) + (day2index - day1index)
  elsif day1index == day2index
    days_away = 7 * weeks_away
  end
  add_days(days_away)  # returns a new date object
end
year() click to toggle source
# File lib/nickel/zdate.rb, line 55
def year
  year_str.to_i
end
year_str() click to toggle source
# File lib/nickel/zdate.rb, line 43
def year_str
  @date[0..3]
end

Protected Instance Methods

decrement_month!() click to toggle source
# File lib/nickel/zdate.rb, line 547
def decrement_month!
  if month != 1
    # just bump down a number and set days to the last day in the month
    self.date = ZDate.format_date(year_str, month - 1, ZDate.days_in_month(month - 1, year))
  else
    self.date = ZDate.format_date(year - 1, 12, 31)    # dec has 31 days
  end
end
increment_month!() click to toggle source

Modifies self. bumps self to first day of next month

# File lib/nickel/zdate.rb, line 538
def increment_month!
  if month != 12
    # just bump up a number
    self.date = ZDate.format_date(year_str, month + 1)
  else
    self.date = ZDate.format_date(year + 1)
  end
end

Private Instance Methods

after?(other) click to toggle source
# File lib/nickel/zdate.rb, line 562
def after?(other)
  (year > other.year) || (year == other.year && (month > other.month || (month == other.month && day > other.day)))
end
before?(other) click to toggle source
# File lib/nickel/zdate.rb, line 558
def before?(other)
  (year < other.year) || (year == other.year && (month < other.month || (month == other.month && day < other.day)))
end
get_day_or_max_day_in_month(day, month, year) click to toggle source
# File lib/nickel/zdate.rb, line 587
def get_day_or_max_day_in_month(day, month, year)
  # if day exists in month/year then use it, if it is not then use the last day of the month
  dm = ZDate.days_in_month(month, year)
  dm >= day ? day : dm
end
valid() click to toggle source
# File lib/nickel/zdate.rb, line 570
def valid
  # It is important that valid_day is last because we have to do the days_in_month calculation!!!
  @date.length == 8 && @date !~ /\D/ && valid_year && valid_month && valid_day
end
valid_day() click to toggle source
# File lib/nickel/zdate.rb, line 583
def valid_day
  day >= 1 && day <= days_in_month
end
valid_month() click to toggle source
# File lib/nickel/zdate.rb, line 579
def valid_month
  month >= 1 && month <= 12
end
valid_year() click to toggle source
# File lib/nickel/zdate.rb, line 575
def valid_year
  year >= 1900
end
validate() click to toggle source
# File lib/nickel/zdate.rb, line 566
def validate
  fail 'ZDate says: invalid date' unless valid
end