############################################################################
#
#	File:     datetime.icn
#
#	Subject:  Procedures for date and time operations
#
#	Author:   Robert J. Alexander and Ralph E. Griswold
#
#	Date:     August 9, 2000
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	Notes:
#		- the default value for function parameters named
#		  "hoursFromGmt" is the value of global variable
#		  "HoursFromGmt" if nonnull, or environment variable
#		  "HoursFromGmt" if set, or 0.
#		- The base year from which the "seconds" representation
#		  of a date is calculated is by default 1970 (the ad hoc
#		  standard used by both Unix and MS-Windows), but can be
#		  changed by either setting the global variable
#		  "DateBaseYear" or environment variable "DateBaseYear".
#		- There are some procedures not mentioned in this summary
#		  that are useful: DateRecToSec(), SecToDateRec(). See the
#		  source code for details.
#
#	ClockToSec(seconds)
#		converts a time in the format of &clock to seconds past
#		midnight.
#
#	DateLineToSec(dateline,hoursFromGmt)
#		converts a date in &dateline format to seconds since start of
#		dateBaseYear.
#
#	DateToSec(date,hoursFromGmt)
#		converts a date string in Icon &date format (yyyy/mm/dd)
#		to seconds past DateBaseYear.
#
#	SecToClock(seconds)
#		converts seconds past midnight to a string in the format of
#		&clock.
#
#	SecToDate(seconds,hoursFromGmt)
#		converts seconds past DateBaseYear to a string in Icon
#		&date format (yyyy/mm/dd).
#
#	SecToDateLine(seconds,hoursFromGmt)
#		produces a date in the same format as Icon's &dateline.
#
#	SecToUnixDate(seconds,hoursFromGmt)
#		returns a date and time in typical UNIX format:
#		Jan 14 10:24 1991.
#
#	IsLeapYear(year)
#		succeeds if year is a leap year, otherwise fails.
#
#	calendat(j)
#		returns a record with the month, day, and year corresponding
#		to the Julian Date Number j.
#
#	date()	natural date in English.
#
#	dayoweek(day, month, year)
#		produces the day of the week for the given date.
#		Note carefully the parameter order.
#
#	full13th(year1, year2)
#		generates records giving the days on which a full moon occurs
#		on Friday the 13th in the range from year1 though year2.
#
#	julian(m, d, y)
#		returns the Julian Day Number for the specified
#		month, day, and year.
#
#	pom(n, phase)
#		returns record with the Julian Day number of fractional
#		part of the day for which the nth such phase since
#		January, 1900.  Phases are encoded as:
#
#			0 - new moon
#			1 - first quarter
#			2 - full moon
#			3 - last quarter#
#
#		GMT is assumed.
#
#	saytime()
#		computes the time in natural English.  If an argument is
#		supplied it is used as a test value to check the operation
#		 the program.
#
#	walltime()
#		produces the number of seconds since midnight.  Beware
#		wrap-around when used in programs that span midnight.
#
############################################################################
#
#  See also:  datefns.icn
#
############################################################################
#
#  Acknowledgement:  Some of these procedures are based on an algorithm
#  given in "Numerical Recipes; The Art of Scientific Computing";
#  William H. Press, Brian P. Flannery, Saul A. Teukolsky, and William
#  T. Vetterling;#  Cambridge University Press, 1986.
#
############################################################################

record date1(month, day, year)
record date2(month, year, fraction)
record jdate(number, fraction)
record DateRec(year,month,day,hour,min,sec,weekday)

global Months,Days,DateBaseYear,HoursFromGmt

procedure ClockToSec(seconds)			#: convert &date to seconds
#
#  Converts a time in the format of &clock to seconds past midnight.
#
    seconds ? return (
	 (1(tab(many(&digits)),move(1)) * 60 +
	 1(tab(many(&digits)),move(1) | &null)) * 60 +
	 (tab(many(&digits)) | 0)
	 )
end

procedure DateInit()
#
#  Initialize the date globals -- done automatically by calls to date
#  procedures.
#
   initial {
      Months := ["January","February","March","April","May","June",
	    "July","August","September","October","November","December"]
      Days := ["Sunday","Monday","Tuesday","Wednesday","Thursday",
	    "Friday","Saturday"]
      /DateBaseYear := integer(getenv("DateBaseYear")) | 1970
      /HoursFromGmt := integer(getenv("HoursFromGmt")) | 0
      }
   return
end


procedure DateLineToSec(dateline,hoursFromGmt)	#: convert &dateline to seconds
#
#  Converts a date in long form to seconds since start of DateBaseYear.
#
   local day,halfday,hour,min,month,sec,year
   static months
   initial {
      DateInit()
      months := table()
      months["jan"] := 1
      months["feb"] := 2
      months["mar"] := 3
      months["apr"] := 4
      months["may"] := 5
      months["jun"] := 6
      months["jul"] := 7
      months["aug"] := 8
      months["sep"] := 9
      months["oct"] := 10
      months["nov"] := 11
      months["dec"] := 12
      }
   map(dateline) ? {
      tab(many(' \t'))
      =("sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat") &
	    tab(many(&letters)) | &null & tab(many(' \t,')) | &null
      month := 1(tab(many(&letters)),tab(many('  \t')) | &null)
      day <- integer(1(tab(many(&digits)),tab(many(' \t,')) | &null)) | &null &
      year <- integer(1(tab(many(&digits)),tab(many(' \t')) | &null)) | &null &
      (hour <- integer(tab(many(&digits))) &
	    ((=":" & min <- integer(tab(many(&digits)))) &
	    ((=":" & sec <- integer(tab(many(&digits)))) | &null) | &null) &
	    tab(many(' \t')) | &null & halfday := =("am" | "pm") | &null &
	    tab(many(' \t')) | &null) | &null & pos(0)
      }
   \month :=  \months[month[1+:3]] | fail
   if not /(halfday | hour) then {
      if hour = 12 then hour := 0
      if halfday == "pm" then
	    hour +:= 12
      }
   return DateRecToSec(DateRec(year,month,day,hour,min,sec),hoursFromGmt)
end

procedure DateRecToSec(dateRec,hoursFromGmt)
#
#  Converts a DateRec to seconds since start of DateBaseYear.
#
   local day,hour,min,month,sec,secs,year,yr
   static days
   initial {
      DateInit()
      days := [
	 0,
	 2678400,
	 5097600,
	 7776000,
	 10368000,
	 13046400,
	 15638400,
	 18316800,
	 20995200,
	 23587200,
	 26265600,
	 28857600
      ]
      }
   /hoursFromGmt := HoursFromGmt
   hoursFromGmt := integer(hoursFromGmt) | runerr(101,hoursFromGmt)
   year := \dateRec.year | +&date[1+:4]
   month := \dateRec.month | +&date[6+:2]
   day := \dateRec.day | +&date[9+:2]
   hour := \dateRec.hour | 0
   min := \dateRec.min | 0
   sec := \dateRec.sec | 0
   secs := 0
   every yr := DateBaseYear to year - 1  do {
      secs +:= if IsLeapYear(yr) then 31622400 else 31536000
      }
   if month > 2 & IsLeapYear(year) then secs +:= 86400
   return secs + days[month] + (day - 1) * 86400 +
	 (hour - hoursFromGmt) * 3600 + min * 60 + sec
end

procedure DateToSec(date,hoursFromGmt)	#: convert &date to seconds
#
#  Converts a date in Icon &date format (yyyy/mm/dd) do seconds
#  past DateBaseYear.
#
    date ? return DateRecToSec(DateRec(+1(tab(find("/")),move(1)),
	+1(tab(find("/")),move(1)),+tab(0)),hoursFromGmt)
end

procedure SecToClock(seconds)		#: convert seconds to &clock
#
#  Converts seconds past midnight to a string in the format of &clock.
#
    local sec
    sec := seconds % 60
    seconds /:= 60
    return right(seconds / 60,2,"0") || ":" || right(seconds % 60,2,"0") ||
	     ":" || right(sec,2,"0")
end

procedure SecToDate(seconds,hoursFromGmt) #: convert seconds to &date
#
#  Converts seconds past DateBaseYear to a &date in Icon date format
#  (yyyy,mm,dd).
#
    local r
    r := SecToDateRec(seconds,hoursFromGmt)
    return right(r.year,4,"0") || "/" || right(r.month,2,"0") || "/" ||
	     right(r.day,2,"0")
end

procedure SecToDateLine(seconds,hoursFromGmt) #: convert seconds to &dateline
#
#  Produces a date in the same format as Icon's &dateline.
#
   local d,hour,halfday
   d := SecToDateRec(seconds,hoursFromGmt)
   if (hour := d.hour) < 12 then {
      halfday := "am"
      }
   else {
      halfday := "pm"
      hour -:= 12
      }
   if hour = 0 then hour := 12
   return Days[d.weekday] || ", " || Months[d.month] || " " || d.day ||
	 ", " || d.year || "  " || hour || ":" || right(d.min,2,"0") || " " ||
	 halfday
end

procedure SecToDateRec(seconds,hoursFromGmt)
#
#  Produces a date record computed from the seconds since the start of
#  DateBaseYear.
#
   local day,hour,min,month,secs,weekday,year
   initial DateInit()
   seconds := integer(seconds) | runerr(101,seconds)
   /hoursFromGmt := HoursFromGmt
   hoursFromGmt := integer(hoursFromGmt) | runerr(101,hoursFromGmt)
   seconds +:= hoursFromGmt * 3600
   weekday := (seconds / 86400 % 7 + 4) % 7 + 1
   year := DateBaseYear
   repeat {
      secs := if IsLeapYear(year) then 31622400 else 31536000
      if seconds < secs then break
      year +:= 1
      seconds -:= secs
      }
   month := 1
   every secs :=
	 2678400 |
	 (if IsLeapYear(year) then 2505600 else 2419200) |
	 2678400 |
	 2592000 |
	 2678400 |
	 2592000 |
	 2678400 |
	 2678400 |
	 2592000 |
	 2678400 |
	 2592000 |
	 2678400 do {
      if seconds < secs then break
      month +:= 1
      seconds -:= secs
      }
   day := seconds / 86400 + 1
   seconds %:= 86400
   hour := seconds / 3600
   seconds %:= 3600
   min := seconds / 60
   seconds %:= 60
   return DateRec(year,month,day,hour,min,seconds,weekday)
end

procedure SecToUnixDate(seconds,hoursFromGmt)	#: convert seconds to UNIX time
#
#  Returns a date and time in UNIX format: Jan 14 10:24 1991
#
   local d
   d := SecToDateRec(seconds,hoursFromGmt)
   return Months[d.month][1+:3] || " " || d.day || " " ||
	 d.hour || ":" || right(d.min,2,"0") || " " || d.year
end

procedure IsLeapYear(year)			#: determine if year is leap
   #
   # Fails unless year is a leap year.
   #
   return year % 4 = 0 & (year % 100 ~= 0 | year % 400 = 0) & &null
end

procedure calendat(julian)			#: Julian date
   local ja, jalpha, jb, jc, jd, je, gregorian
   local month, day, year

   gregorian := 2299161

   if julian >= gregorian then {
      jalpha := integer(((julian - 1867216) - 0.25) / 36524.25)
      ja := julian + 1 + jalpha - integer(0.25 * jalpha)
      }
   else ja := julian

   jb := ja + 1524
   jc := integer(6680.0 + ((jb - 2439870) - 122.1) / 365.25)
   jd := 365 * jc + integer(0.25 * jc)
   je := integer((jb - jd) / 30.6001)
   day := jb - jd - integer(30.6001 * je)
   month := je - 1
   if month > 12 then month -:= 12
   year := jc - 4715
   if month > 2 then year -:= 1
   if year <= 0 then year -:= 1

   return date1(month, day, year)

end

procedure date()			#: date in natural English

   &dateline ? {
      tab(find(", ") + 2)
      return tab(find("  "))
      }

end

procedure dayoweek(day, month, year)	#: day of the week
#
#  The method used was adapted from a Web page by Mark Dettinger.
#  URL as of 7 August 2000 was:
#  http://www.informatik.uni-ulm.de/pm/mitarbeiter/mark/day_of_week.html
#
   static d_code, c_code, m_code, ml_code, y, C, M, Y

   initial {
      d_code := ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday",
         "Thursday", "Friday"]

      c_code := table()
      c_code[16] := c_code[20] := 0
      c_code[17] := c_code[21] := 6
      c_code[18] := c_code[22] := 4
      c_code[19] := c_code[23] := 2

      m_code := table()
      m_code[1] := m_code["January"] := 1
      m_code[2] := m_code["February"] := 4
      m_code[3] := m_code["March"] := 4
      m_code[4] := m_code["April"] := 0
      m_code[5] := m_code["May"] := 2
      m_code[6] := m_code["June"] := 5
      m_code[7] := m_code["July"] := 0
      m_code[8] := m_code["August"] := 3
      m_code[9] := m_code["September"] := 6
      m_code[10] := m_code["October"] := 1
      m_code[11] := m_code["November"] := 4
      m_code[12] := m_code["December"] := 6

      ml_code := copy(m_code)
      ml_code[1] := ml_code["January"] := 0
      ml_code[2] := ml_code["February"] := 3
      }

   #  This can be fixed to go back to October 15, 1582.

   if year < 1600 then stop("*** can't compute day of week that far back")

   #  This can be fixed to go indefinitely far into the future; the day of
   #  of the week repeats every 400 years.

   if year > 2299 then stop("*** can't compute day of week that far ahead")

   C := c_code[(year / 100) + 1]
   y := year % 100
   Y := (y / 12) + (y % 12) + ((y % 12) / 4)
   month := integer(month)
   M := if (year % 4) = 0 then ml_code[month] else m_code[month]

   return d_code[(C + Y + M + day) % 7 + 1] 
   
end

procedure full13th(year1, year2)	#: full moons on Friday 13ths
   local time_zone, jd, jday, fraction, jul
   local year, month, julday, n, icon, day_of_week, c

   time_zone :=  -5.0 / 24.0

   every year := year1 to year2 do {
      every month := 1 to 12 do {
         jday := julian(month, 13, year)
         day_of_week := (jday + 1) % 7
         if day_of_week = 5 then {
            n := integer(12.37 * (year - 1900 + integer((month - 0.5) / 12.0)))
            icon := 0
            repeat {
               jul := pom(n,2)
               jd := jul.number
               fraction := 24.0 * (jul.fraction + time_zone)
               if (fraction < 0.0) then {
                  jd  -:= 1
                  fraction  +:= 24.0
                  }
               if fraction > 12.0 then {
                  jd +:= 1
                  fraction -:= 12.0
                  }
               else fraction  +:= 12.0
               if jd = jday then {
                  suspend date2(month, year, fraction)
                  break
                  }
               else {
                  c := if jday >= jd then 1 else -1
                  if c = -icon then break
                  icon := c
                  n +:= c
                  }
               }
            }
         }
      }

end

procedure julian(month, day, year)	#: Julian date
   local jul, gregorian, ja, julian_year, julian_month

   gregorian := (15 + 31 * (10 + 12 * 1582))

   if year = 0 then fail
   if year < 0 then year +:= 1
   if month > 2 then  {
      julian_year := year
      julian_month := month + 1
      } else {
      julian_year := year - 1
      julian_month := month + 13
      }
   jul := (integer(365.25 * julian_year) + integer(30.6001 * julian_month) +
      day + 1720995)
   if day + 31 * (month + 12 * year) >= gregorian then  {
      ja := integer(0.01 * julian_year)
      jul +:= 2 - ja + integer(0.25 * ja)
      }

   return jul

end

procedure pom(n, nph)			#: phase of moon
   local i, jd, fraction, radians
   local am, as, c, t, t2, extra

   radians := &pi / 180

   c := n + nph / 4.0
   t := c / 1236.85
   t2 := t * t
   as := 359.2242 + 29.105356 * c
   am := 306.0253 + 385.816918 * c + 0.010730 * t2
   jd := 2415020 + 28 * n + 7 * nph
   extra := 0.75933 + 1.53058868 * c + ((1.178e-4) - (1.55e-7) * t) * t2

   if nph = (0 | 2) then
      extra +:=  (0.1734 - 3.93e-4 * t) * sin(radians * as) - 0.4068 *
          sin(radians * am)
   else if nph = (1 | 3) then
      extra +:= (0.1721 - 4.0e-4 * t) * sin(radians * as) - 0.6280 *
        sin(radians * am)
   else fail

   if extra >= 0 then i := integer(extra)
   else i := integer(extra - 1.0)
   jd  +:=  i
   fraction := extra - i

   return jdate(integer(jd), fraction)

end

procedure saytime(time)			#: time in natural English
   local hour,min,mod,near,numbers,out,sec
   #
   # Extract the hours, minutes, and seconds from the time.
   #
   /time := &clock
   time ? {
      hour := integer(tab(find(":") | 0)) | fail
      move(1)
      min := tab(find(":") | 0)
      move(1)
      sec := tab(0)
      }
   min := integer(min) | 0
   sec := integer(sec) | 0
   #
   # Now start the processing in earnest.
   #
   near := ["just gone","just after","nearly","almost"]
   if sec > 29 then min +:= 1    # round up minutes
   mod := min % 5                # where we are in 5 minute bracket
   out := near[mod] || " " | ""  # start building the result
   if min > 32 then hour +:= 1   # we are TO the hour
   min +:= 2             # shift minutes to straddle the 5-minute point
   #
   # Now special-case the result for Noon and Midnight hours.
   #
   if hour % 12 = 0 & min % 60 <= 4 then {
      return if hour = 12 then out || "noon"
				    else out || "midnight"
      }
   min -:= min % 5               # find the nearest 5 mins
   if hour > 12 then hour -:= 12 # get rid of 25-hour clock
   else if hour = 0 then hour := 12 # .. and allow for midnight
   #
   # Determine the phrase to use for each 5-minute segment.
   #
   case min of {
       0: {}                      # add "o'clock" later
      60: min=0                   # ditto
       5: out ||:= "five past"
      10: out ||:= "ten past"
      15: out ||:= "a quarter past"
      20: out ||:= "twenty past"
      25: out ||:= "twenty-five past"
      30: out ||:= "half past"
      35: out ||:= "twenty five to"
      40: out ||:= "twenty to"
      45: out ||:= "a quarter to"
      50: out ||:= "ten to"
      55: out ||:= "five to"
      }
   numbers := ["one","two","three","four","five","six",
		     "seven","eight","nine","ten","eleven","twelve"]
   out ||:= (if *out = 0 then "" else " ") || numbers[hour]
				 # add the hour number
   if min = 0 then out ||:= " o'clock" # .. and o'clock if exact
   return out                    # return the final result
end
 
procedure walltime()			#: time since midnight
   local seconds

   &clock ? {
      seconds := tab(upto(':')) * 3600		# seconds in a hour
      move(1)
      seconds +:= tab(upto(':')) * 60		# seconds in a minute
      move(1)
      return seconds + tab(0)
      }

end
