Class: Nickel::ZDate

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/nickel/zdate.rb

Overview

TODO: get methods should accept dayname or dayindex

Constant Summary collapse

MON =
0
TUE =
1
WED =
2
THU =
3
FRI =
4
SAT =
5
SUN =
6

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(yyyymmdd = nil) ⇒ ZDate

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


28
29
30
31
32
# 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

Class Attribute Details

.days_in_common_year_monthsObject (readonly)

Returns the value of attribute days_in_common_year_months


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def days_in_common_year_months
  @days_in_common_year_months
end

.days_in_leap_year_monthsObject (readonly)

Returns the value of attribute days_in_leap_year_months


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def days_in_leap_year_months
  @days_in_leap_year_months
end

.days_of_weekObject (readonly)

Returns the value of attribute days_of_week


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def days_of_week
  @days_of_week
end

.full_days_of_weekObject (readonly)

Returns the value of attribute full_days_of_week


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def full_days_of_week
  @full_days_of_week
end

.full_months_of_yearObject (readonly)

Returns the value of attribute full_months_of_year


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def full_months_of_year
  @full_months_of_year
end

.months_of_yearObject (readonly)

Returns the value of attribute months_of_year


24
25
26
# File 'lib/nickel/zdate.rb', line 24

def months_of_year
  @months_of_year
end

Class Method Details

.days_in_month(month, year) ⇒ Object


289
290
291
292
293
294
295
# 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) ⇒ Object

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


299
300
301
302
303
304
305
306
307
308
# 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) ⇒ Object

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


324
325
326
# 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) ⇒ Object


319
320
321
# File 'lib/nickel/zdate.rb', line 319

def format_day(d)
  d.to_s.rjust(2, '0')
end

.format_month(m) ⇒ Object


315
316
317
# File 'lib/nickel/zdate.rb', line 315

def format_month(m)
  m.to_s.rjust(2, '0')
end

.format_year(y) ⇒ Object


310
311
312
313
# 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) ⇒ Object

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


358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# 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_first_day_in_month(month, year) ⇒ Object


280
281
282
# 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) ⇒ Object


284
285
286
287
# 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

Instance Method Details

#<=>(other) ⇒ Object


77
78
79
80
81
82
83
84
85
86
87
# 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) ⇒ Object

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


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 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) ⇒ Object


168
169
170
171
172
173
174
175
176
177
178
# 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) ⇒ Object


164
165
166
# File 'lib/nickel/zdate.rb', line 164

def add_weeks(number)
  add_days(7 * number)
end

#add_years(number) ⇒ Object


180
181
182
183
184
# 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_monthObject

beginning and end of month both return new ZDate objects


209
210
211
# File 'lib/nickel/zdate.rb', line 209

def beginning_of_month
  ZDate.new(ZDate.format_date(year_str, month_str))
end

#beginning_of_next_monthObject


217
218
219
220
221
# File 'lib/nickel/zdate.rb', line 217

def beginning_of_next_month
  o = dup
  o.increment_month!
  o
end

#dateObject


34
35
36
# File 'lib/nickel/zdate.rb', line 34

def date
  @date
end

#date=(yyyymmdd) ⇒ Object


38
39
40
41
# File 'lib/nickel/zdate.rb', line 38

def date=(yyyymmdd)
  @date = yyyymmdd
  validate
end

#dayObject


63
64
65
# File 'lib/nickel/zdate.rb', line 63

def day
  day_str.to_i
end

#day_of_yearObject


446
447
448
449
450
451
452
453
454
455
# 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_strObject


51
52
53
# File 'lib/nickel/zdate.rb', line 51

def day_str
  @date[6..7]
end

#dayindexObject


430
431
432
# File 'lib/nickel/zdate.rb', line 430

def dayindex
  ZDate.days_of_week.index(dayname)
end

#daynameObject


423
424
425
426
427
428
# 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_monthObject


407
408
409
410
411
412
413
# 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_monthObject


415
416
417
# File 'lib/nickel/zdate.rb', line 415

def days_left_in_month
  days_in_month - day
end

#days_left_in_yearObject


457
458
459
# 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) ⇒ Object

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


251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# 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) ⇒ Object


270
271
272
273
274
275
276
# 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) ⇒ Object

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


399
400
401
402
403
404
405
# 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_monthObject


213
214
215
# 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) ⇒ Object


71
72
73
74
75
# 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_daynameObject


434
435
436
# File 'lib/nickel/zdate.rb', line 434

def full_dayname
  ZDate.full_days_of_week[dayindex]
end

#full_monthnameObject


438
439
440
# 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) ⇒ Object


461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# 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) ⇒ Object

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


514
515
516
517
518
519
520
521
522
523
524
# 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?Boolean

Returns:

  • (Boolean)

89
90
91
92
# 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) ⇒ Object

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


199
200
201
202
203
204
205
206
# 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?Boolean

Returns:

  • (Boolean)

442
443
444
# File 'lib/nickel/zdate.rb', line 442

def leap_year?
  year % 400 == 0 || year % 4 == 0 && year % 100 != 0
end

#monthObject


59
60
61
# File 'lib/nickel/zdate.rb', line 59

def month
  month_str.to_i
end

#month_strObject


47
48
49
# File 'lib/nickel/zdate.rb', line 47

def month_str
  @date[4..5]
end

#months_left_in_yearObject


419
420
421
# File 'lib/nickel/zdate.rb', line 419

def months_left_in_year
  12 - month
end

#next(day) ⇒ Object

for example, "next friday"


123
124
125
# File 'lib/nickel/zdate.rb', line 123

def next(day)
  x_weeks_from_day(1, day)
end

#ordinal_dayindex(num, day_index) ⇒ Object

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


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 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) ⇒ Object

for example, "previous friday"


128
129
130
# File 'lib/nickel/zdate.rb', line 128

def prev(day)
  (dayindex == day) ? dup : x_weeks_from_day(-1, day)
end

#readableObject


67
68
69
# File 'lib/nickel/zdate.rb', line 67

def readable
  month_str + '/' + day_str + '/' + year_str
end

#sub_days(number) ⇒ Object

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


224
225
226
227
228
229
230
231
232
233
234
235
236
# 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) ⇒ Object


242
243
244
245
246
247
248
# 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) ⇒ Object


238
239
240
# File 'lib/nickel/zdate.rb', line 238

def sub_weeks(number)
  sub_days(7 * number)
end

#this(day) ⇒ Object

for example, "this friday"


118
119
120
# File 'lib/nickel/zdate.rb', line 118

def this(day)
  x_weeks_from_day(0, day)
end

#to_dateObject


526
527
528
# File 'lib/nickel/zdate.rb', line 526

def to_date
  Date.new(year, month, day)
end

#to_sObject


530
531
532
# File 'lib/nickel/zdate.rb', line 530

def to_s
  date
end

#today?Boolean

returns true if self is today

Returns:

  • (Boolean)

95
96
97
# File 'lib/nickel/zdate.rb', line 95

def today?
  self == ZDate.new
end

#x_weeks_from_day(weeks_away, day2index) ⇒ Object

returns a new date object


133
134
135
136
137
138
139
140
141
142
143
# 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

#yearObject


55
56
57
# File 'lib/nickel/zdate.rb', line 55

def year
  year_str.to_i
end

#year_strObject


43
44
45
# File 'lib/nickel/zdate.rb', line 43

def year_str
  @date[0..3]
end