Pastey std/datetime || source-code

--****
-- == Date and Time
--
-- <>
--
-- **GMT** is "Greenwich Mean Time."
-- **UTC** is "Universal Coordinated Time."
--
-- GMT is for nostalgic sailors that remember square rigged vessels. UTC is the modern
-- standard. (GMT and UTC are //not// exactly equivalent.)

namespace datetime

integer yydiff = 80

include std/dll.e
include std/get.e
include std/machine.e
include std/types.e

ifdef LINUX then
	constant gmtime_ = dll:define_c_func(dll:open_dll(""), "gmtime", {dll:C_POINTER}, dll:C_POINTER)
	constant time_ = dll:define_c_func(dll:open_dll(""), "time", {dll:C_POINTER}, dll:C_POINTER)
elsifdef OSX then
	constant gmtime_ = dll:define_c_func(dll:open_dll("libc.dylib"), "gmtime", {dll:C_POINTER}, dll:C_POINTER)
	constant time_ = dll:define_c_func(dll:open_dll("libc.dylib"), "time", {dll:C_POINTER}, dll:C_INT)
elsifdef WINDOWS then
	constant gmtime_ = dll:define_c_func(dll:open_dll("msvcrt.dll"), "+gmtime", {dll:C_POINTER}, dll:C_POINTER)
	constant time_ = dll:define_c_proc(dll:open_dll("kernel32.dll"), "GetSystemTimeAsFileTime", {dll:C_POINTER})
elsifdef UNIX then
	constant gmtime_ = dll:define_c_func(dll:open_dll("libc.so"), "gmtime", {dll:C_POINTER}, dll:C_POINTER)
	constant time_ = dll:define_c_func(dll:open_dll("libc.so"), "time", {dll:C_POINTER}, dll:C_INT)
end ifdef

enum TM_SEC, TM_MIN, TM_HOUR, TM_MDAY, TM_MON, TM_YEAR --, TM_WDAY, TM_YDAY, TM_ISDST

function time()
	ifdef WINDOWS then
		atom ptra, valhi, vallow, deltahi, deltalow
		deltahi = 27111902
		deltalow = 3577643008
		ptra = machine:allocate(8)
		c_proc(time_, {ptra})
		vallow = peek4u(ptra)
		valhi = peek4u(ptra+4)
		machine:free(ptra)
		vallow -= deltalow
		valhi -= deltahi
		if vallow < 0 then
			vallow += power(2, 32)
			valhi -= 1
		end if
		return floor(((valhi * power(2,32)) + vallow) / 10000000)
	elsedef
		return c_func(time_, {dll:NULL})
	end ifdef
end function

function gmtime(atom time)
	sequence ret
	atom timep, tm_p
	integer n

	timep = machine:allocate( sizeof( C_POINTER ) )
	poke_pointer(timep, time)
	
	tm_p = c_func(gmtime_, {timep})
	
	machine:free(timep)
	if tm_p != 0 then
		return peek4s(tm_p & 9 )
	else
		return repeat( 0, 9 )
	end if
end function

constant
	Gregorian_Reformation = 1752,
	Gregorian_Reformation00 = 1700,
	DaysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
	EPOCH_1970 = 62135856000,
	DayLengthInSeconds = 86400

-- Helpers ------------------------------------------------------------------

function tolower(object x)
	return x + (x >= 'A' and x <= 'Z') * ('a' - 'A')
end function

-- Date Handling ------------------------------------------------------------

function isLeap(integer year) -- returns integer (0 or 1)
	sequence ly

		ly = (remainder(year, {4, 100, 400, 3200, 80000})=0)

		if not ly[1] then return 0 end if

		if year <= Gregorian_Reformation then
				return 1 -- ly[1] can't possibly be 0 here so set shortcut as '1'.
		else
				return ly[1] - ly[2] + ly[3] - ly[4] + ly[5]
		end if
end function

function daysInMonth(integer year, integer month) -- returns a month_
	if year = Gregorian_Reformation and month = 9 then
		return 19
	elsif month != 2 then
		return DaysPerMonth[month]
	else
		return DaysPerMonth[month] + isLeap(year)
	end if
end function

function daysInYear(integer year) -- returns a jday_ (355, 365 or 366)
	if year = Gregorian_Reformation then
		return 355
	end if
	
	return 365 + isLeap(year)
end function

-- Functions using the new data-types

function julianDayOfYear(object ymd) -- returns an integer
	integer year, month, day
	integer d

	year = ymd[1]
	month = ymd[2]
	day = ymd[3]

	if month = 1 then return day end if

	d = 0
	for i = 1 to month - 1 do
		d += daysInMonth(year, i)
	end for

	d += day

	if year = Gregorian_Reformation and month = 9 then
		if day > 13 then
			d -= 11
		elsif day > 2 then
			return 0
		end if
	end if

	return d
end function

function julianDay(object ymd) -- returns an integer
	integer year
	integer j, greg00

	year = ymd[1]
	j = julianDayOfYear(ymd)

	year  -= 1
	greg00 = year - Gregorian_Reformation00

	j += (
		365 * year
		+ floor(year/4)
		+ (greg00 > 0)
			* (
				- floor(greg00/100)
				+ floor(greg00/400+.25)
			)
		- 11 * (year >= Gregorian_Reformation)
	)

	if year >= 3200 then
		j -= floor(year/ 3200)
		if year >= 80000 then
			j += floor(year/80000)
		end if
	end if

	return j
end function

function julianDate(integer j) -- returns a Date
	integer year, doy

	-- Take a guesstimate at the year -- this is usually v.close
	if j >= 0 then
		year = floor(j / (12 * 30.43687604)) + 1
	else
		year = -floor(-j / 365.25) + 1
	end if

	-- Calculate the day in the guessed year
	doy = j - (julianDay({year, 1, 1}) - 1) -- = j - last day of prev year

	-- Correct any errors

	-- The guesstimate is usually so close that these whiles could probably
	-- be made into ifs, but I haven't checked all possible dates yet... ;)

	while doy <= 0 do -- we guessed too high for the year
		year -= 1
		doy += daysInYear(year)
	end while

	while doy > daysInYear(year) do -- we guessed too low
		doy -= daysInYear(year)
		year += 1
	end while

	-- guess month
	if doy <= daysInMonth(year, 1) then
		return {year, 1, doy}
	end if
	for month = 2 to 12 do
		doy -= daysInMonth(year, month-1)
		if doy <= daysInMonth(year, month) then
			return {year, month, doy}
		end if
	end for

	-- Skip to the next year on overflow
	-- The alternative is a crash, listed below
	return {year+1, 1, doy-31}
end function

-- Conversions to and from seconds

function datetimeToSeconds(object dt) -- returns an atom
	return julianDay(dt) * DayLengthInSeconds + (dt[4] * 60 + dt[5]) * 60 + dt[6]
end function

function secondsToDateTime(atom seconds) -- returns a DateTime
	integer days, minutes, hours

	days = floor(seconds / DayLengthInSeconds)
	seconds = remainder(seconds, DayLengthInSeconds)

		hours = floor( seconds / 3600 )
		seconds -= hours * 3600

		minutes = floor( seconds / 60 )
		seconds -= minutes* 60
	return julianDate(days) & {hours, minutes, seconds}
end function

--****
-- === Localized Variables

--**
-- Month Names

public sequence month_names = { 
	"January", "February", "March", "April", "May", "June", 
	"July", "August", "September", "October", "November", "December" 
}

--**
-- Abbreviations of Month Names

public sequence month_abbrs = { 
	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 
}

--**
-- Day Names

public sequence day_names = { 
	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
	"Saturday" 
}

--**
-- Abbreviations of Day Names

public sequence day_abbrs = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }

--**
-- AM and PM

public sequence ampm = { "AM", "PM" }

--****
-- === Date and Time Type Accessors
--
-- These accessors can be used with the [[:datetime]] type.

public enum	
	--**
	-- Year (full year, i.e. 2010, 1922, )
	
	YEAR, 
	
	--**
	-- Month (1-12)
	
	MONTH, 
	
	--**
	-- Day (1-31)
	
	DAY, 
	
	--**
	-- Hour (0-23)
	
	HOUR,
		
	--**
	-- Minute (0-59)
	
	MINUTE,
		
	--**
	-- Second (0-59)
	
	SECOND

--****
-- === Intervals
--
-- These constant enums are to be used with the [[:add]] and [[:subtract]] routines.
--

public enum 
	--**
	-- Years
	YEARS,
		
	--**
	-- Months
	MONTHS, 

	--**
	-- Weeks
	WEEKS, 

	--**
	-- Days
	DAYS,
	
	--**
	-- Hours
	HOURS,
	
	--**
	-- Minutes
	MINUTES, 

	--**
	-- Seconds
	SECONDS, 

	--**
	-- Date
	DATE

--****
-- === Types

--**
-- datetime type
--
-- Parameters:
--   # ##obj## : any object, so no crash takes place.
--
-- Comments:
-- A datetime type is a sequence of length six in the form
-- ##{year, month, day_of_month, hour, minute, second}##. Checks are made to guarantee
-- those values are in range. 
--
-- datetime can be used to declare a variable as a datetime type, or used to check if a variable is a datetime type.
--
-- Note:
-- All items must be integers except for
-- seconds which could either integer or atom values.
--
-- Example 1:
-- 
-- include std/datetime.e  
-- datetime x = now() -- x is a datetime variable, and now() returns a datetime 
-- ? x
-- --> {2019,9,8,3,23,23}
--
-- ? datetime(x) -- is it a datetime variable?  
-- --> 1         -- True 
-- 
--
-- Example 2:
-- 
-- include std/datetime.e  
-- object y = {2019,1,10} -- y is not a datetime variable, just a sequence containing a date {year,month,day}
-- ? y
-- --> {2019,1,10}  
--
-- ? datetime(y) -- is it a datetime variable?  
-- --> 0         -- False  
-- 
--
-- Example 3:
-- 
-- include std/datetime.e  
-- datetime z = {2019,1,10} -- z is declared as a datetime type; it is validated on assignment
-- -- Since what we assigned was not a valid datetime variable, we get an error.
--
-- Result:
-- type_check failure, z is {2019,1,1}
-- 

public type datetime(object o)
	if atom(o) then return 0 end if

	if length(o) != 6 then return 0 end if

	if not integer(o[YEAR]) then return 0 end if

	if not integer(o[MONTH]) then return 0 end if

	if not integer(o[DAY]) then return 0 end if

	if not integer(o[HOUR]) then return 0 end if

	if not integer(o[MINUTE]) then return 0 end if

	if not atom(o[SECOND]) then return 0 end if

	if not equal(o[1..3], {0,0,0}) then
		-- Special case of all zeros is allowed; used when the data is a time only.
		if o[MONTH] < 1 then return 0 end if

		if o[MONTH] > 12 then return 0 end if

		if o[DAY] < 1 then return 0 end if

		if o[DAY] > daysInMonth(o[YEAR],o[MONTH]) then return 0 end if
	end if

	if o[HOUR] < 0 then return 0 end if

	if o[HOUR] > 23 then return 0 end if

	if o[MINUTE] < 0 then return 0 end if

	if o[MINUTE] > 59 then return 0 end if

	if o[SECOND] < 0 then return 0 end if

	if o[SECOND] >= 60 then return 0 end if

	return 1
end type

--****
-- === Routines

--****
-- Signature:
--  function time()
--
-- Description:
--   returns the number of seconds since some fixed point in the past.
--
-- Returns:
--   An **atom**, which represents an absolute number of seconds.
--
-- Comments: 
--   Take the difference between two readings of ##time()## to measure, for example, how long 
--   a section of code takes to execute.
--
--   On some machines, ##time()## can return a negative number. However, you can still use the
--   difference in calls to ##time()## to measure elapsed time.
--
-- Example 1:
-- 
-- constant ITERATIONS = 1000000
-- integer p
-- atom t0, loop_overhead
-- 
-- t0 = time()
-- for i = 1 to ITERATIONS do
--     -- time an empty loop
-- end for
-- loop_overhead = time() - t0
-- 
-- t0 = time()
-- for i = 1 to ITERATIONS do
--     p = power(2, 20)
-- end for
-- ? (time() - t0 - loop_overhead)/ITERATIONS
-- -- calculates time (in seconds) for one call to power
-- 
--
-- See Also: 
-- [[:date]], [[:now]]
 
--****
-- Signature:
--  function date()
--
-- Description:
-- returns a sequence with information on the current date.
--
-- Returns:
-- A **sequence** of length 8, laid out as follows~:
-- # year  ~-- since 1900
-- # month ~-- January = 1
-- # day   ~-- day of month, starting at 1
-- # hour  ~-- 0 to 23
-- # minute ~-- 0 to 59
-- # second ~-- 0 to 59
-- # day of the week ~-- Sunday = 1
-- # day of the year ~-- January 1st = 1
--
-- Comments:
-- The value returned for the year is actually the number of years since 1900 (not the last 2 digits of the year). 
-- In the year 2000 this value was 100. In 2001 it was 101, and so on.
--  
-- Example 1:
--
-- 
-- include std/datetime.e
-- sequence now = date()
-- ? now
-- --> {95,3,24,23,47,38,6,83}
-- -- Friday March 24, 1995 at 11:47:38pm, day 83 of the year
-- 
--
-- See Also:
--  [[:time]], [[:now]]

--**
-- converts a sequence formatted according to the built-in ##date## function to a valid datetime
-- sequence.
--
-- Parameters:
--   # ##src## : a sequence which ##date## might have returned
--
-- Returns:
--   A **sequence**, more precisely a [[:datetime]], corresponding to the same moment 
--   in time.
--
-- Example 1:
-- 
-- include std/datetime.e
--
-- sequence a_date
-- datetime today
--
-- a_date = date()
-- today = from_date(a_date)
--
-- ? a_date
-- --> {119,9,20,21,0,57,6,263}
--
-- ? today
-- --> {2019,9,20,21,0,57}
-- 
--
-- See Also:
--     [[:date]], [[:from_unix]], [[:now]], [[:new]]

public function from_date(sequence src)
	return {src[YEAR]+1900, src[MONTH], src[DAY], src[HOUR], src[MINUTE], src[SECOND]}
end function

--**
-- creates a new datetime value initialized with the current date and time.
--
-- Returns:
--   A **sequence**, more precisely a [[:datetime]], corresponding to the current 
--   moment in time.
--
-- Example:
-- 
-- include std/datetime.e
-- datetime dt = now()
-- ? dt
-- --> {2019,09,19,8,47,13}
-- 
--
-- See Also:
--     [[:from_date]], [[:from_unix]], [[:new]], [[:new_time]], [[:now_gmt]]

public function now()
	return from_date(date())
end function

--**
-- create a new datetime value that falls into the Greenwich Mean Time (GMT) timezone.
--
-- Comments:
-- This function will return a datetime that is GMT no matter what timezone the system
-- is running under.
--
-- Example 1:
-- 
-- include std/datetime.e
-- datetime lt = now() -- local time
-- datetime gmt = now_gmt()
-- ? lt
-- --> {2019,9,23,11,3,42}
--  
-- ? gmt
-- --> {2019,9,23,16,3,42}
--
-- Note: that this example was run by someone
-- in the Central Standard Timezone.
-- 
--
-- See Also:
-- [[:now]]

public function now_gmt()
	sequence t1 = gmtime(time())

	return { 
		t1[TM_YEAR] + 1900, t1[TM_MON] + 1, t1[TM_MDAY], 
		t1[TM_HOUR], t1[TM_MIN], t1[TM_SEC]
	}
end function

--**
-- creates a new datetime value.
--
-- !! TODO: test default parameter usage
--
-- Parameters:
--   # ##year##   ~-- the full year.
--   # ##month##  ~-- the month (1-12).
--   # ##day##    ~-- the day of the month (1-31).
--   # ##hour##   ~-- the hour (0-23) (defaults to 0)
--   # ##minute## ~-- the minute (0-59) (defaults to 0)
--   # ##second## ~-- the second (0-59) (defaults to 0)
--
-- Example 1:
-- 
-- dt = new(2010, 1, 1, 0, 0, 0)
-- -- dt is Jan 1st, 2010
-- 
--
-- See Also:
--     [[:from_date]], [[:from_unix]], [[:now]], [[:new_time]]

public function new(integer year=0, integer month=0, integer day=0,
			integer hour=0, integer minute=0, atom second=0)
	datetime d
	d = {year, month, day, hour, minute, second}
	if equal(d, {0,0,0,0,0,0}) then
		return now()
	else
		return d
	end if
end function

--**
-- creates a new datetime value with a date of zeros.
--
-- Parameters:
--   # ##hour## : is the hour (0-23)
--   # ##minute## : is the minute (0-59)
--   # ##second## : is the second (0-59)
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence dt = new_time(10, 30, 55)
-- -- dt is 10:30:55 AM
-- 
--
-- See Also:
--     [[:from_date]], [[:from_unix]], [[:now]], [[:new]]

public function new_time(integer hour, integer minute, atom second)
	return new(0, 0, 0, hour, minute, second)
end function

--**
-- gets the day of week of the datetime ##dt##.
--
-- Parameters:
--    # ##dt## : a datetime to be queried.
--
-- Returns:
--    An **integer**, between 1 (Sunday) and 7 (Saturday).
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 5, 2, 0, 0, 0)
-- atom day = weeks_day(d) 
-- ? day
-- -- day is 6 because May 2, 2008 is a Friday.
-- 

public function weeks_day(datetime dt)
	return remainder(julianDay(dt)-1+4094, 7) + 1
end function

--**
-- gets the Julian day of year of the supplied date.
--
-- Parameters:
--   # ##dt## : a datetime to be queried.
--
-- Returns:
--   An **integer**, between 1 and 366.
--
-- Comments:
--   For dates earlier than 1800, this routine may give inaccurate results if the date
--   applies to a country other than United Kingdom or a former colony thereof. The 
--   change from Julian to Gregorian calendar took place much earlier in some other 
--   European countries.
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 5, 2, 0, 0, 0)
-- atom day = years_day(d)
-- -- day is 123
-- 

public function years_day(datetime dt)
	return julianDayOfYear({dt[YEAR], dt[MONTH], dt[DAY]})
end function

--**
-- determines if ##dt## falls within leap year.
--
-- Parameters:
--   # ##dt## : a datetime to be queried.
--
-- Returns:
--   An **integer**, of 1 if leap year, otherwise 0.
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 1, 1, 0, 0, 0)
-- ? is_leap_year(d) -->  prints 1  -- True
--
-- d = new(2005, 1, 1, 0, 0, 0)
-- ? is_leap_year(d) -- prints 0    -- False
-- 
--
-- See Also:
--   [[:days_in_month]]

public function is_leap_year(datetime dt)
	return isLeap(dt[YEAR])
end function

--**
-- returns the number of days in the month of ##dt##.
--
-- Comments:
-- This takes into account leap year.
--
-- Parameters:
--   # ##dt## : a datetime to be queried.
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 1, 1, 0, 0, 0)
-- ? days_in_month(d) --> 31
--
-- d = new(2008, 2, 1, 0, 0, 0) -- a leap year
-- ? days_in_month(d) --> 29
-- 
--
-- See Also:
--   [[:is_leap_year]]

public function days_in_month(datetime dt)
	return daysInMonth(dt[YEAR], dt[MONTH])
end function

--**
-- returns the number of days in the year of ##dt##.
--
-- Comments:
-- This takes into account leap year.
--
-- Parameters:
--   # ##dt## : a datetime to be queried.
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2007, 1, 1, 0, 0, 0)
-- ? days_in_year(d) --> 365
--
-- d = new(2008, 1, 1, 0, 0, 0) -- a leap year
-- ? days_in_year(d) --> 366
-- 
--
-- See Also:
--   [[:is_leap_year]], [[:days_in_month]]

public function days_in_year(datetime dt)
	return daysInYear(dt[YEAR])
end function

--**
-- converts a datetime value to the //Unix// numeric format (seconds since ##EPOCH_1970##).
--
-- Parameters:
--   # ##dt## : a datetime to be queried.
--
-- Returns:
--   An **atom**, so this will not overflow during the winter 2038-2039.
--
--
-- Example 1:
-- 
-- include std/datetime.e
-- atom secs_since_epoch = to_unix(now())
-- ? secs_since_epoch
-- --> secs_since_epoch is equal to the current seconds since epoch
-- 
--
-- See Also:
--     [[:from_unix]], [[:format]]

public function to_unix(datetime dt)
	return datetimeToSeconds(dt) - EPOCH_1970
end function

--**
-- creates a datetime value from the //Unix// numeric format (seconds since EPOCH).
--
-- Parameters:
--   # ##unix## : an atom, counting seconds elapsed since EPOCH.
--
-- Returns:
--   A **sequence**, more precisely a **datetime** representing the same moment 
--   in time.
--
-- Example 1:
-- 
-- include std/datetime.e
-- atom d = from_unix(0)
-- ? d
-- --> d is 1970-01-01 00:00:00  (zero seconds since EPOCH)
-- 
--
-- See Also:
--     [[:to_unix]], [[:from_date]], [[:now]], [[:new]]

public function from_unix(atom unix)
	return secondsToDateTime(EPOCH_1970 + unix)
end function

--**
-- formats the date according to the format pattern string.
--
-- Parameters:
--   # ##d## : a datetime which is to be printed out
--   # ##pattern## : a format string, similar to the ones ##sprintf## uses, but with 
--     some Unicode encoding. The default is ##"%Y-%m-%d %H:%M:%S"##.
--
-- Returns:
--  A **string**, with the date ##d## formatted according to the specification in ##pattern##.
--
-- Comments:
-- Pattern string can include the following specifiers~:
--
-- * ##~%%## ~-- a literal %
-- * ##%a## ~-- locale's abbreviated weekday name (e.g., Sun)
-- * ##%A## ~-- locale's full weekday name (e.g., Sunday)
-- * ##%b## ~-- locale's abbreviated month name (e.g., Jan)
-- * ##%B## ~-- locale's full month name (e.g., January)
-- * ##%C## ~-- century; like %Y, except omit last two digits (e.g., 21)
-- * ##%d## ~-- day of month (e.g, 01)
-- * ##%H## ~-- hour (00..23)
-- * ##%I## ~-- hour (01..12)
-- * ##%j## ~-- day of year (001..366)
-- * ##%k## ~-- hour ( 0..23)
-- * ##%l## ~-- hour ( 1..12)
-- * ##%m## ~-- month (01..12)
-- * ##%M## ~-- minute (00..59)
-- * ##%p## ~-- locale's equivalent of either AM or PM; blank if not known
-- * ##%P## ~-- like %p, but lower case
-- * ##%s## ~-- seconds since 1970-01-01 00:00:00 UTC
-- * ##%S## ~-- second (00..60)
-- * ##%u## ~-- day of week (1..7); 1 is Monday
-- * ##%w## ~-- day of week (0..6); 0 is Sunday
-- * ##%y## ~-- last two digits of year (00..99)
-- * ##%Y## ~-- year
--
-- Example 1:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 5, 2, 12, 58, 32)
-- sequence s = format(d, "%Y-%m-%d %H:%M:%S")
-- --> s is "2008-05-02 12:58:32"
-- 
--
-- Example 2:
-- 
-- include std/datetime.e
-- sequence d = new(2008, 5, 2, 12, 58, 32)
-- sequence s = format(d, "%A, %B %d '%y %H:%M%p")
-- --> s is "Friday, May 2 '08 12:58PM"
-- 
--
-- See Also:
--     [[:to_unix]], [[:parse]]
--

public function format(datetime d, sequence pattern = "%Y-%m-%d %H:%M:%S")
	integer in_fmt, ch, tmp
	sequence res

	in_fmt = 0
	res = ""

	for i = 1 to length(pattern) do
		ch = pattern[i]

		if in_fmt then
			in_fmt = 0

			if ch = '%' then
				res &= '%'
			elsif ch = 'a' then
				res &= day_abbrs[weeks_day(d)]
			elsif ch = 'A' then
				res &= day_names[weeks_day(d)]
			elsif ch = 'b' then
				res &= month_abbrs[d[MONTH]]
			elsif ch = 'B' then
				res &= month_names[d[MONTH]]
			elsif ch = 'C' then
				res &= sprintf("%02d", d[YEAR] / 100)
			elsif ch = 'd' then
				res &= sprintf("%02d", d[DAY])
			elsif ch = 'H' then
				res &= sprintf("%02d", d[HOUR])
			elsif ch = 'I' then
				tmp = d[HOUR]
				if tmp > 12 then
					tmp -= 12
				elsif tmp = 0 then
					tmp = 12
				end if
				res &= sprintf("%02d", tmp)
			elsif ch = 'j' then
				res &= sprintf("%d", julianDayOfYear(d))
			elsif ch = 'k' then
				res &= sprintf("%d", d[HOUR])
			elsif ch = 'l' then
				tmp = d[HOUR]
				if tmp > 12 then
					tmp -= 12
				elsif tmp = 0 then
					tmp = 12
				end if
				res &= sprintf("%d", tmp)
			elsif ch = 'm' then
				res &= sprintf("%02d", d[MONTH])
			elsif ch = 'M' then
				res &= sprintf("%02d", d[MINUTE])
			elsif ch = 'p' then
				if d[HOUR] <= 12 then
					res &= ampm[1]
				else
					res &= ampm[2]
				end if
			elsif ch = 'P' then
				if d[HOUR] <= 12 then
					res &= tolower(ampm[1])
				else
					res &= tolower(ampm[2])
				end if
			elsif ch = 's' then
				res &= sprintf("%d", to_unix(d))
			elsif ch = 'S' then
				res &= sprintf("%02d", d[SECOND])
			elsif ch = 'u' then
				tmp = weeks_day(d)
				if tmp = 1 then
					res &= "7" -- Sunday
				else
					res &= sprintf("%d", weeks_day(d) - 1)
				end if
			elsif ch = 'w' then
				res &= sprintf("%d", weeks_day(d) - 1)
			elsif ch = 'y' then
			   tmp = floor(d[YEAR] / 100)
			   res &= sprintf("%02d", d[YEAR] - (tmp * 100))
			elsif ch = 'Y' then
				res &= sprintf("%04d", d[YEAR])
			else
				-- TODO: error or just add?
			end if
		elsif ch = '%' then
			in_fmt = 1
		else
			res &= ch
		end if
	end for
	return res
end function

--
-- Used to determine how to handle %y in parse()
--

constant date_now = now()

--**
-- parses a datetime string according to the given format.
--
-- Parameters:
--   # ##val## : string datetime value
--   # ##fmt## : datetime format. Default is ##"%Y-%m-%d %H:%M:%S"##
--   # ##yysplit## : Set the maximum difference from the current year when parsing
--     a two digit year. Defaults to -80/+20.
--
-- Returns:
--	A **datetime**, value.
-- 
-- Comments:
--   Only a subset of the format specification is currently supported~:
--
--   * ##%d## ~--  day of month (e.g, 01)
--   * ##%H## ~--  hour (00..23)
--   * ##%m## ~--  month (01..12)
--   * ##%M## ~--  minute (00..59)
--   * ##%S## ~--  second (00..60)
--   * ##%y## ~--  2-digit year (YY)
--   * ##%Y## ~--  4-digit year (CCYY)
--
--   More format codes will be added in future versions.
--  
--   All non-format characters in the format string are ignored and are not
--   matched against the input string.
--
--   All non-digits in the input string are ignored.
--
-- Parsing Two Digit Years~:
--
--   When parsing a two digit year ##parse## has to make a decision if a given year
--   is in the past or future. For example, 10/18/44. Is that Oct 18, 1944 or
--   Oct 18, 2044. A common rule has come about for this purpose and that is the -80/+20 
--   rule. Based on research it was found that more historical events are recorded than
--   future events, thus it favors history rather than future. Some other applications may
--   require a different rule, thus the ##yylower## parameter can be supplied.
--
--   Assuming today is 12/22/2010 here is an example of the -80/+20 rule~:
--   || YY || Diff   || CCYY ||
--   | 18   | -92/+8  |  2018 |
--   | 95   | -15/+85 |  1995 |
--   | 33   | -77/+23 |  1933 |
--   | 29   | -81/+19 |  2029 |
--
--   Another rule in use is the -50/+50 rule. Therefore, if you supply -50 to the ##yylower## 
--   to set the lower bounds, some examples may be (given that today is 12/22/2010)~:
--   || YY || Diff   || CCYY ||
--   | 18   | -92/+8  |  2018 |
--   | 95   | -15/+85 |  1995 |
--   | 33   | -77/+23 |  2033 |
--   | 29   | -81/+19 |  2029 |
--
-- Note:
--   * Since 4.0.1 ~-- 2-digit year parsing and ##yylower## parameter.
--
-- Example 1:
-- 
-- include std/datetime.e
-- datetime d = parse("05/01/2009 10:20:30", "%m/%d/%Y %H:%M:%S")
-- --> d is { 2009, 5, 1, 10, 20, 30 }
-- 
--
-- Example 2:
-- 
-- include std/datetime.e
-- datetime d = parse("05/01/44", "%m/%d/%y", -50) -- -50/+50 rule
-- --> d is { 2044, 5, 14, 0, 0, 0 }
-- 
--
-- See Also:
--   [[:format]]
--

public function parse(sequence val, sequence fmt="%Y-%m-%d %H:%M:%S", integer yylower = -80)
	integer fpos = 1, spos = 1, maxlen, rpos 
	sequence res = {0,0,0,0,0,0}

	while fpos <= length(fmt) do
		if fmt[fpos] = '%' then
			fpos += 1

			switch fmt[fpos] do
				case 'Y' then
					rpos = 1
					maxlen = 4

				case 'y' then
					rpos = 1
					maxlen = 2

				case 'm' then
					rpos = 2
					maxlen = 2

				case 'd' then
					rpos = 3
					maxlen = 2

				case 'H' then
					rpos = 4
					maxlen = 2

				case 'M' then
					rpos = 5
					maxlen = 2

				case 'S' then
					rpos = 6
					maxlen = 2

				case else
					-- Ignore any invalid format character.
					rpos = 0
					
			end switch
			
			if rpos then
				sequence got
				integer epos
				while spos <= length(val) do
					if types:t_digit(val[spos]) then
						exit
					end if
					spos += 1
				end while
			    
				epos = spos + 1
				while epos <= length(val) and epos < spos + maxlen do
					if not types:t_digit(val[epos]) then
						exit
					end if
					epos += 1
				end while
				
				if spos > length(val) then
					return -1
				end if
				got = stdget:value(val[spos .. epos-1], , stdget:GET_LONG_ANSWER)
				if got[1] != stdget:GET_SUCCESS then
					return -1
				end if

				-- If this is a 2 digit year we have to do some special handling
				if fmt[fpos] = 'y' then
					-- Adjust the date to be not more than yysplit years ago
					integer century = floor(date_now[YEAR] / 100) * 100
					integer year = got[2] + (century - 100)
					if year < (date_now[YEAR] + yylower) then
						year = got[2] + century
					end if

					got[2] = year
				end if

				res[rpos] = got[2]
				spos = epos
			end if
		end if
		fpos += 1

	end while

	-- Ensure that what we got could be a date-time value.
	if not datetime(res) then
		return -1
	end if
	
	-- Ensure no remaining digits in string.
	while spos <= length(val) do
		if types:t_digit(val[spos]) then
			return -1
		end if
		spos += 1
	end while
	
	return new(res[1], res[2], res[3], res[4], res[5], res[6])
end function

--**
-- adds a number of //intervals// to a datetime.
--
-- Parameters:
--   # ##dt## : the base datetime
--   # ##qty## : the number of //intervals// to add. It should be positive.
--   # ##interval## : which kind of interval to add.
--
-- Returns:
--   A **sequence**, more precisely a **datetime** representing the new moment in time.
--
-- Comments:
--   Please see Constants for Date and Time for a reference of valid intervals.
--
--   Do not confuse the item access constants (such as ##YEAR##, ##MONTH##, ##DAY## ) with the
--   interval constants (##YEARS##, ##MONTHS##, ##DAYS## ).
--
--   When adding ##MONTHS##, it is a calendar based addition. For instance, a date of
--   5/2/2008 with 5 ##MONTHS## added will become 10/2/2008. ##MONTHS## does not compute the number
--   of days per each month and the average number of days per month.
--
--   When adding ##YEARS##, leap year is taken into account. Adding 4 ##YEARS## to a date may result
--   in a different day of month number due to leap year.
--
-- Example 1:
-- 
-- include std/datetime.e
-- datetime d1, d2
-- d1 = now()
-- d2 = add(d1, 35, SECONDS) -- add 35 seconds to d1
-- d2 = add(d1, 7, WEEKS)    -- add 7 weeks to d1
-- d2 = add(d1, 19, YEARS)   -- add 19 years to d1
-- 
--
-- See Also:
--     [[:subtract]], [[:diff]]

public function add(datetime dt, object qty, integer interval)
	integer inc

	if interval = SECONDS then
	elsif interval = MINUTES then
		qty *= 60
	elsif interval = HOURS then
		qty *= 3600
	elsif interval = DAYS then
		qty *= 86400
	elsif interval = WEEKS then
		qty *= 604800
	elsif interval = MONTHS then
		if qty > 0 then
			inc = 1
		else
			inc = -1
			qty = -(qty)
		end if

		for i = 1 to qty do
			if inc = 1 and dt[MONTH] = 12 then
				dt[MONTH] = 1
				dt[YEAR] += 1
			elsif inc = -1 and dt[MONTH] = 1 then
				dt[MONTH] = 12
				dt[YEAR] -= 1
			else
				dt[MONTH] += inc
			end if
		end for

		return dt
	elsif interval = YEARS then
		dt[YEAR] += qty
		if isLeap(dt[YEAR]) = 0 and dt[MONTH] = 2 and dt[DAY] = 29 then
			dt[MONTH] = 3
			dt[DAY] = 1
		end if

		return dt
	elsif interval = DATE then
		qty = datetimeToSeconds(qty)
	end if

	return secondsToDateTime(datetimeToSeconds(dt) + qty)
end function

--**
-- subtracts a number of //intervals// to a base datetime.
--
-- Parameters:
--   # ##dt## : the base datetime
--   # ##qty## : the number of //intervals// to subtract. It should be positive.
--   # ##interval## : which kind of interval to subtract.
--
-- Returns:
--   A **sequence**, more precisely a **datetime** representing the new moment
--   in time.
--
-- Comments:
--   Please see Constants for Date and Time for a reference of valid intervals.
--
--   See the function ##add## for more information on adding and subtracting date
--   intervals
--
-- Example 1:
-- 
-- include std/datetime.e
-- datetime dt1, dt2
-- dt1 = now()
-- dt2 = subtract(dt1, 18, MINUTES) -- subtract 18 minutes from dt1
-- dt2 = subtract(dt1, 7, MONTHS)   -- subtract 7 months from dt1
-- dt2 = subtract(dt1, 12, HOURS)   -- subtract 12 hours from dt1
-- 
--
-- See Also:
--     [[:add]], [[:diff]]

public function subtract(datetime dt, atom qty, integer interval)
	return add(dt, -(qty), interval)
end function

--**
-- computes the difference, in seconds, between two dates.
--
-- Parameters:
--   # ##dt1## : the end datetime
--   # ##dt2## : the start datetime
--
-- Returns:
--   An **atom**, the number of seconds elapsed from ##dt2## to ##dt1##.
--
-- Comments:
--   ##dt2## is subtracted from ##dt1##, therefore, you can come up with a negative 
--   value.
--
-- Example 1:
-- 
-- include std/datetime.e
-- datetime d1 = now()
-- sleep(15)  -- sleep for 15 seconds
-- datetime d2 = now()
--
-- integer i = diff(d1, d2) -- i is 15
-- 
--
-- See Also:
--    [[:add]], [[:subtract]]

public function diff(datetime dt1, datetime dt2)
	return datetimeToSeconds(dt2) - datetimeToSeconds(dt1)
end function

1. Comment by _tom Sep 27, 2019

Abandoning Pastey as a technique to edit docs.

_tom