note description: "[ An exact point of time as on a gregorian callendar. Has a `Year', `Month', and `Day' (i.e. a date). ]" names: "date, time" date: "1 Jan 99" author: "Jimmy J. Johnson" copyright: "Copyright 2009, Jimmy J. Johnson" license: "Eiffel Forum License v2 (see forum.txt)" URL: "$URL:$" date: "$Date: 2009-06-25 21:37:23 -0400 (Thu, 25 Jun 2009) $" revision: "$Revision: 7 $" class YMD_TIME inherit ABSTRACT_TIME rename as_integer as as_days, from_integer as from_days redefine default_create, is_valid, duration_anchor, interval_anchor end create default_create, set_now, set_now_utc, set, from_days, from_string feature {NONE} -- Initialization default_create -- Create an instance based on todays date. do set_now end feature -- Access year: INTEGER -- Year part of the date. month: INTEGER -- Month part of the date. day: INTEGER -- Day part of the date. do Result := internal_day if Result > last_day_of_month then Result := last_day_of_month end end week_number: INTEGER -- Week of the year containing this date. local d: YMD_TIME first_d: INTEGER -- Jan 1st is on what day? do create d d.set (year, 1, 1) first_d := d.weekday Result := (((julian + first_d - 1 - 1) // 7) + 1) ensure result_large_enough: Result >= 1 result_small_enough: Result <= 54 -- 53 ? 54 if leapyear falls just right. end last_day_of_month: INTEGER -- Date of last day for current month do inspect month when 2 then if is_leapyear then Result := 29 else Result := 28 end when 4, 6, 9, 11 then Result := 30 else Result := 31 end ensure day_in_range: Result >= 28 and Result <= 31 good_not_leap: Result = 28 implies (month = 2 and not is_leapyear) good_in_leap: Result = 29 implies (month = 2 and is_leapyear) good_30s: Result = 30 implies (month = 4 or month = 6 or month = 9 or month = 11) good_31s: Result = 31 implies (month=1 or month=3 or month=5 or month=7 or month=8 or month=10 or month=12) end days_remaining_this_month: INTEGER -- Number of days from current until end of month. -- Used in some calculations. do Result := last_day_of_month - day ensure valid_result: Result >= 0 and Result < last_day_of_month end julian: INTEGER -- Day of the year between 1 and 366 local n,i : INTEGER do from i := 1 until i >= month loop inspect i when 2 then if is_leapyear then n := n + 29 else n := n + 28 end when 4,6,9,11 then n := n + 30 else n := n + 31 end i := i + 1 end result := n + day ensure valid_leapyear_result: is_leapyear implies (1 <= Result and Result <= 366) valid_result: not is_leapyear implies (1 <= Result and Result <= 365) end weekday: INTEGER -- 1 for Sunday, 2 for Monday, etc -- Only works as far back as ~2 Mar 0001. ??? local x : INTEGER do x := internal\\7 + 1 + 1 if x > 7 then -- it can only be 8 x := 1 end result := x ensure valid_weekday: 1 <= Result and Result <= 7 end as_string: STRING -- The date represented as a string with no spaces. -- 18 Jan 2005 would be "20050118". do create Result.make (10) if is_bc then Result.append ("BC") end if not (year.abs >= 1000) then Result.append ("0") end if not (year.abs >= 100) then Result.append ("0") end if not (year.abs >= 10) then Result.append ("0") end Result.append (year.abs.out) if not (month >= 10) then Result.append ("0") end Result.append (month.out) if not (day >= 10) then Result.append ("0") end Result.append (day.out) end as_days: INTEGER -- The number of days from midnight (00:00:00) -- on 1 Jan 1970 to the beginning Current's `day'. local t: YMD_TIME do create t t.set (1970, 1, 1) Result := days_between (t) end feature -- Element Change from_days (a_days: INTEGER) -- Change Current to the time represented by `a_days'. -- `A_days' is assumed to be the number of days since 1 Jan 1970. -- `A_days' must represent a date that is not BC do set (1970, 1, 1) add_days (a_days) end from_string (a_string: STRING) -- Change Current to the time represented by `a_string'. -- It must be in the format as provided by `as_string'. local d, m, y: INTEGER do y := a_string.substring (1, 4).to_integer m := a_string.substring (5, 6).to_integer d := a_string.substring (7, 8).to_integer set (y, m, d) end set_now -- Set the current object to today's date. -- This was copied from ISE's DATE class with the one minor change. do C_date.update set (C_date.year_now, C_date.month_now, C_date.day_now) end set_now_utc -- Set the current object to today's date in utc format. -- This was copied from ISE's DATE class with the one minor change. do C_date.update set (C_date.year_now, C_date.month_now, C_date.day_now) end set_now_utc_fine -- Set the current object to today's date in utc format. -- This was copied from ISE's TIME class with minor changes. do C_date.update set (C_date.year_now, C_date.month_now, C_date.day_now) end set (a_year, a_month, a_day: INTEGER) -- Give date new year, month, and day. -- If day > num days in month then day will return last day in the month. require realistic_year: a_year /= 0 realistic_month: a_month >= 1 and a_month <= 12 realistic_day: a_day >= 1 and a_day <= 31 do year := a_year month := a_month internal_day := a_day ensure year_assigned: year = a_year month_assigned: month = a_month day_assigned: day = a_day end set_year (a_year: INTEGER) -- Change the year. require realistic_year: a_year /= 0 do year := a_year ensure year_assigned: year = a_year end set_month (a_month: INTEGER) -- Change the month. require realistic_month: a_month >= 1 and a_month <= 12 do month := a_month ensure month_assigned: month = a_month end set_day (a_day: INTEGER) -- Change the day. -- If a_day > number of days in the month then -- 'day' will be the last day of month. require realistic_day: a_day >= 1 and a_day <= 31 do internal_day := a_day ensure day_assigned: day = a_day end truncate_to_years -- Set the day to first day of month 1. -- Use when all but the `year' is to be ignored. do set_day (1) set_month (1) ensure year_unchanged: year = old year month_one: month = 1 day_one: day = 1 end truncate_to_months -- Set day to first day of current month. -- Use when the `day' portion of date is to be ignored. do set_day (1) ensure year_unchanged: year = old year month_unchanged: month = old month day_one: day = 1 end feature -- Status report is_leapyear: BOOLEAN -- Is this a leapyear? do if is_bc then Result := (year + 1) \\ 4 = 0 and not ((year + 1) \\ 400 = 0) else Result := year \\ 4 = 0 and (not (year \\ 100 = 0) or else year \\ 400 = 0) end end is_bc: BOOLEAN -- Does the date represent a date B.C. (ie year < 1) do Result := year <= -1 ensure definition: Result implies year <= -1 end is_representable_as_integer: BOOLEAN -- Can Current be represented as an integer? do Result := not is_bc and then (Current >= Minimum_representable_date and Current <= Maximum_representable_date) end feature -- Querry days_between (other: like Current): INTEGER -- Days between this date and 'other'. -- Only works back to ~2 Mar 0001. require other_exists : other /= Void do Result := (other.internal - internal).abs ensure definition: Result = (other.internal - internal).abs end time_between (other: like Current): like Duration_anchor -- The difference between two dates as a duration local larger, smaller: like Current y, m, d: INTEGER do larger := max (other) smaller := min (other) y := larger.year - smaller.year m := larger.month - smaller.month d := larger.day - smaller.day if d < 0 then d := d + smaller.last_day_of_month m := m - 1 end if m < 0 then m := m + 12 y := y - 1 end create Result Result.set (y, m, d) if Current < other then Result.negate end end is_valid_integer_representation (a_integer: INTEGER): BOOLEAN -- Is `a_integer' in range to be converted to a time? -- Dependent on the `internal' representation of dates. do -- These values were found by trial and error. This will give a -- date from 1 Jan 0001 to 18 Oct 1,469,902, which, I believe, is -- far enough into the future. Result := a_integer >= 1721426 and a_integer <= 538592032 ensure then definition: Result implies (a_integer >= 1721426) and then (a_integer <= 538592032) -- dependent on `internal' end is_valid_string_representation (a_string: STRING): BOOLEAN -- Is `a_string' in a format that can be used to initialize Current? local bcs: detachable STRING ys, ms, ds: STRING y, m, d: INTEGER pad: INTEGER -- add 2 if is "BC" do if a_string /= Void and then (a_string.count = 8 or a_string.count = 10) then if a_string.count = 10 then pad := 2 bcs := a_string.substring (1, 2) end ys := a_string.substring (1 + pad, 4 + pad) ms := a_string.substring (5 + pad, 6 + pad) ds := a_string.substring (7 + pad, 8 + pad) if ys.is_integer and then ms.is_integer and then ds.is_integer then y := ys.to_integer m := ms.to_integer d := ds.to_integer if (y /= 0) and then (m >= 0 and m < 12)and then (d >= 0 and d <= 31) then Result := True if bcs /= Void and then not equal (bcs, "BC") then Result := False end end end end end feature -- Basic operations add_years (a_num: INTEGER) -- Add 'a_num' number of years to the date. Works for negative numbers also. local y: INTEGER do y := year year := year + a_num if year = 0 then -- Must preserve invarient: year can not be 0. if y < 0 then -- year was less than 0 and increased to 0. year := 1 else -- year was greater than 0 and decreased to 0. year := -1 end end ensure valid_date: is_valid end add_months (a_num: INTEGER) -- Add 'a_num' number of months to the date. Works for negative numbers also. local m: INTEGER -- store month prior making month valid to call 'add_years'. do month := month + a_num m := month month := month \\ 12 -- preserve invarient if month < 1 then month := month + 12 -- preserve invarient add_years (-1) end add_years (m // 12) -- add a year for every multiple of 12. ensure valid_date: is_valid end add_days (a_num: INTEGER) -- Add 'a_num' number of days to the date. Works for negative numbers also. local i: INTEGER do if a_num > 0 then from i := a_num until i <= days_remaining_this_month loop i := i - (days_remaining_this_month + 1) set_day (1) add_months (1) end set_day (day + i) elseif a_num < 0 then from i := a_num.abs until i < day loop i := (day - i).abs add_months (-1) set_day (last_day_of_month) end set_day (day - i) else -- do nothing if a_num = 0 end ensure valid_date: is_valid end add_duration (a_duration: like Duration_anchor) -- Add a length of time (in years, months, and days) to the date. do add_days (a_duration.days) add_months (a_duration.months) add_years (a_duration.years) end feature -- Comparison is_less alias "<" (other: like Current): BOOLEAN -- Does this date come before 'other'? require else other_not_void: other /= Void do Result := year < other.year or else (year = other.year) and (month < other.month) or else (year = other.year) and (month = other.month) and (day < other.day) ensure then -- definition: year < other.year or else -- (year = other.year) and (month < other.month) or else -- (year = other.year) and (month = other.month) and (day < other.day) end feature {YMD_TIME} -- Implementation frozen internal: INTEGER -- Internal representation of YMD_TIME -- Used internally by some features. -- Does not work for BC dates; only works back to 1 January 0001, -- at which time the result is 1,721,426. -- Will work up to a date of 18 Oct 1,469,902 (found by trial). require not_bc: not is_bc local c, ya : INTEGER; d,m,y : INTEGER; do d := day; m := month; y := year; if m > 2 then m := m - 3; else m := m + 9; y := y - 1; end c := y // 100; ya := y - 100 * c; result := (146097 * c) // 4 + (1461 * ya) // 4 + (153 * m + 2) // 5 + d + 1721119; ensure result_large_enough: Result >= 1721426 result_small_enough: Result <= 538592032 end frozen from_internal (num: INTEGER) -- Create a YMD_TIME from an internal representation. local y,m,d,j : INTEGER do j := num; j := j - 1721119 y := (4 * j - 1) // 146097; j := 4 * j - 1 - 146097 * y; d := j // 4; j := (4 * d + 3) // 1461; d := 4 * d + 3 - 1461 * j; d := (d + 4) // 4; m := (5 * d - 3) // 153; d := 5 * d - 3 - 153 * m; d := (d + 5) // 5; y := 100 * y + j; if m < 10 then m := m + 3; else m := m - 9; y := y + 1; end; internal_day := d; month := m; year := y; end feature {NONE} -- Implementation internal_day: INTEGER -- Used to save last day of month if day is greater than 28, 30, or 31. -- Actual day is calculated from this value. is_valid: BOOLEAN -- Is the date logical? do Result := is_valid_year and is_valid_month and is_valid_day end is_valid_year: BOOLEAN -- Is the year logical? -- Only invalid year is year "0". do Result := year /= 0 ensure definition: year /= 0 end is_valid_month: BOOLEAN -- Is the month logical? do Result := 1 <= month and month <= 12 ensure definition: 1 <= month and month <= 12 end is_valid_day: BOOLEAN -- Is the day logical based on month and year? do Result := day >= 1 and then ( (day <= 28) or else ((month=4 or month=6 or month=9 or month=11) and then day <= 30) or else ((month=1 or month=3 or month=5 or month=7 or month=8 or month=10 or month=12) and then day <= 31) or else (month=2 and is_leapyear and day <= 29) ) end feature {NONE} -- Implementation Minimum_representable_date: like Current -- The earliest date that can be represented as an integer. -- This value is dependent on the implementation of `internal' and -- was found by trial and error to be 1 Jan 0001. do create Result Result.set_year (1) Result.set_month (1) Result.set_day (1) end Maximum_representable_date: like Current -- The latest date that can be represented as an integer. -- This value is dependent on the implementation of `internal' and -- was found by trial and error to be 18 Oct 1,469,902. do create Result Result.set_year (1_469_902) Result.set_month (10) Result.set_day (18) end feature {NONE} -- Anchors (for covariant redefinitions) duration_anchor: YMD_DURATION -- Anchor for features using durations. -- Not to be called; just used to anchor types. -- Declared as a feature to avoid adding an attribute. require else not_callable: False do check do_not_call: False then -- Because give no info; simply used as anchor. end end interval_anchor: YMD_INTERVAL -- Anchor for features using intervals. -- Not to be called; just used to anchor types. -- Declared as a feature to avoid adding an attribute. require else not_callable: False do check do_not_call: False then -- Because give no info; simply used as anchor. end end invariant is_valid: is_valid end -- class YMD_TIME