19928/jj_temporal/jj_t/classes/ymd_time.e

672 lines
16 KiB
Plaintext

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