// TimeConvB.bcpl -- conversion between packed and unpacked representations.
//			Requires companion module TimeConvA.asm
// Copyright Xerox Corporation 1979

//	Last modified December 31, 1977  7:05 PM

get "AltoDefs.d"
get "Time.d"

external
[
// Outgoing procedures
UNPACKDT; PACKDT; WEEKDAY

// Incoming procedures
TimeDiv; TimeMul
MoveBlock; ReadCalendar; DoubleAdd; SysErr

// Incoming statics
monthTable; OsVersion
]

//	The internal format for a calendar date and time
// (or simply "daytime") is a 32-bit integer (PTV, packed time vector)
// representing seconds relative to midnight, January 1, 1901 GMT.
// There is an unpacked format (UTV, unpacked time vector, defined in Time.d)
// more suitable for person-oriented I/O: a 7-word vector containing year,
// month (January=0), day (first day of month=1), hour, minute,
// second, day of week, daylight saving time flag, and time zone information.
// Conversion between the two forms is not trivial, since it must deal with
// leap years, daylight time, time zones, and lengths of months.

// Notes:
// (1) This implementation does not need to know the Gregorian calendar
// rule whereby years divisible by 100 aren't leap years unless also divisible
// by 400.  This is because the Alto calendar only spans the years
// 1901 to 2036, and the year 2000 is a leap year.

// (2) This version of the code is intended to work whether or not the 
// local time parameters are available.  If they aren't,
// we assume that the local time zone is 8 (Pacific Standard).  This
// compatibility code should be removed eventually.  It is marked by "****".


//----------------------------------------------------------------------------
structure ULT:  // Unpacked Local Time information
//----------------------------------------------------------------------------
// UNPACKDT and PACKDT know about the order of these words.  Beware!
[
beginDST word		// Day of year on or before which DST starts
endDST word		// Day of year on or before which DST ends
zone:			// Local time zone -- format same as in UTV structure
   [
   blank bit 5
   sign bit 1
   hour bit 4
   minute bit 6
   ]
compZone word		// Composite zone: minutes west (+) or east (-) of GMT
]


manifest ecOSVersion = 2900  // Error code for OS older than version 14

//----------------------------------------------------------------------------
let UNPACKDT(ptv, utv) = valof
//----------------------------------------------------------------------------
// Unpacks 32-bit time at ptv into UTV structure (see Time.d).
// If ptv is 0, uses current date and time.
[
// Set up local time parameters
let beginDST, endDST, zone, compZone = nil, nil, nil, nil
SetupULT(lv beginDST, 0)
utv>>UTV.word6 = 0
utv>>UTV.zone = zone

// Get or default packed time
let pv = vec 2
test ptv eq 0
   ifso ReadCalendar(pv)
   ifnot MoveBlock(pv, ptv, 2)

// Unpack second straightforwardly
utv>>UTV.second = TimeDiv(pv, 60, pv)

// pv now has number of minutes since Jan 1, 1901.
// At this point, apply the local time zone by subtracting it from pv.
// Since this may cause the result to underflow, also add 4 years' worth
// of minutes so as to make it positive again, thereby making day zero be
// Jan 1, 1897.  (We ignore the fact that 1900 was not a leap year).
// Note that compZone is in the range [-720..720]
let tv = vec 2
tv!0, tv!1 = 32, 6688-compZone  // 4 years = 2103840 = 32*2↑16 + 6688 minutes
DoubleAdd(pv, tv)

// Now unpack minute
utv>>UTV.minute = TimeDiv(pv, 60, pv)

// Go around this loop twice if daylight savings time must be applied.
for i = 0 to 1 do
   [
   // Now unpack hour in day
   let pv2 = vec 2
   utv>>UTV.hour = TimeDiv(pv, 24, pv2)

   // pv2!1 has days since Jan 1, 1897.  Compute day of week.
   // Jan 1, 1897 would have been a Thursday (=3) if 1900 were a leap year.
   tv!0, tv!1 = 0, pv2!1 + 3
   utv>>UTV.weekday = TimeDiv(tv, 7, tv)

   // Unpack 4-year cycles and day within cycle, and normalize day to
   // 366-day years.
   let day = TimeDiv(pv2, 3*365+366, pv2)
   day = day + (day ge 3*365+31+28? 3, (day+(365-31-28))/365)

   // pv2!1 now has number of 4-year cycles since Jan 1, 1897.
   // Compute real year and day in year.
   utv>>UTV.year = 1897 + 4*pv2!1 + day/366
   day = day rem 366

   // Compute month and day in month
   let month = 0
   while day ge monthTable!(month+1) do month = month+1
   utv>>UTV.month = month
   utv>>UTV.day = day - monthTable!month +1

   // If daylight savings time applies, add one hour and go around
   // the loop again.  Note that daylight savings time goes into effect at
   // 2 AM standard time and out of effect at 2 AM daylight time, which is
   // 1 AM standard time (standard time is what utv presently contains).
   if not CheckDateGE(utv, beginDST, 2) % CheckDateGE(utv, endDST, 1) break
   utv>>UTV.daylight = true
   DoubleAdd(pv, table [ 0; 1 ])
   ]

resultis utv
]

//----------------------------------------------------------------------------
and PACKDT(utv, ptv, useWord6; numargs na) = valof
//----------------------------------------------------------------------------
// Packs the time described by utv into a 32-bit time, returning zero if
// successful and the index of the incorrect UTV cell if unsuccessful.
// If useWord6 then uses the zone and daylight information in word 6 of utv;
// otherwise ignores it and uses the local time zone and computes DST on the
// basis of the date in utv.  Recomputes the weekday field unconditionally.
[
if na ls 3 then useWord6 = false

// Setup local time parameters
let beginDST, endDST, zone, compZone = nil, nil, nil, nil
SetupULT(lv beginDST, (useWord6? utv, 0))

// Test validity of unpacked form
let year, month, day = utv>>UTV.year-1901, utv>>UTV.month, utv>>UTV.day
let hour, minute, second = utv>>UTV.hour, utv>>UTV.minute, utv>>UTV.second
if year ls 0 % year ge 136 resultis iYear
let year4 = year & 3  // year rem 4 -- position in 4-year cycle
if month ls 0 % month ge 12 resultis iMonth
if day ls 1 % day gr monthTable!(month+1)-monthTable!month %
   (month eq 1 & day eq 29 & year4 ne 3) resultis iDay
if hour ls 0 % hour ge 24 resultis iHour
if minute ls 0 % minute ge 60 resultis iMinute
if second ls 0 % second ge 60 resultis iSecond

// Compute days since January 1, 1897, local time.  It will be corrected to
// January 1, 1901 later (see below for reason).
day = ((year+4)/4)*(3*365+366) +	// Days in previous 4-year cycles
      365*year4 +			// Days in this 4-year cycle
      monthTable!month +		// Days in previous months this year
      day -				// Days in this month
      (year4 ne 3 & month ge 2? 2, 1)	// Correction for leap year

// Compute weekday for DST check.
// Note that January 1, 1897 was a Thursday (=3).
ptv!0, ptv!1 = 0, day+3
let tv = vec 2
utv>>UTV.weekday = TimeDiv(ptv, 7, tv)

// Compute minutes since January 1, 1897, local time.
ptv!1 = day
TimeMul(hour, 24, ptv)
TimeMul(minute, 60, ptv)

// Apply time zone correction (convert to GMT) by adding local time zone.
// Simultaneously, subtract 4 years' worth of minutes to correct the origin
// from 1897 to 1901.  This forces the sign of the added quantity always
// to be negative.  Note that compZone is in the range [-720..720]
tv!0, tv!1 = -32-1, compZone-6688  // High part must be ones-complement

// If daylight savings time is now applicable, subtract one hour to convert
// to standard time.
if (useWord6? utv>>UTV.daylight,
 CheckDateGE(utv, beginDST, 2) & not CheckDateGE(utv, endDST, 2)) then
   tv!1 = tv!1 -60
DoubleAdd(ptv, tv)

// Pack seconds and we are finished
TimeMul(second, 60, ptv)
resultis 0
]

//----------------------------------------------------------------------------
and WEEKDAY(ptv) = valof
//----------------------------------------------------------------------------
// Returns the day of week of the packed daytime in ptv.
[
let utv = vec lenUTV
UNPACKDT(ptv, utv)
resultis utv>>UTV.weekday
]


//----------------------------------------------------------------------------
and CheckDateGE(utv, dstDay, dstHour) = valof
//----------------------------------------------------------------------------
// Returns true if the current time is greater than or equal to the
// specified DST time and false otherwise.
// utv = unpacked time vector describing current date and time
//  (month, day, hour, and weekday fields must be valid)
// dstDay = day on or before which DST switch occurs
// dstHour = hour at which DST switch occurs
[
let day = monthTable!(utv>>UTV.month) + utv>>UTV.day
let weekday = utv>>UTV.weekday
resultis (day ls dstDay-6? false,
   day gr dstDay? true,
   weekday eq 6? utv>>UTV.hour ge dstHour,
   day-weekday gr dstDay-6)
]


//----------------------------------------------------------------------------
and SetupULT(ult, utv) be
//----------------------------------------------------------------------------
// Sets up unpacked local time information for use by UNPACKDT and PACKDT.
// Stores results in ult (see ULT structure for contents).
// If utv is nonzero, uses it (rather than 570) to supply the local time zone.
// **** Defaults time information to Pacific if unavailable from system.
[
if OsVersion ls 14 then SysErr(0, ecOSVersion)

// Extract or default time parameters
test timeParams!0 eq 0 % timeParams!1 eq 0
   ifnot
      [
      ult>>ULT.beginDST = timeParams>>LTP.beginDST
      ult>>ULT.endDST = timeParams>>LTP.endDST
      ult>>ULT.zone.sign = timeParams>>LTP.sign
      ult>>ULT.zone.hour = timeParams>>LTP.zoneH
      ult>>ULT.zone.minute = timeParams>>LTP.zoneM
      ]
   ifso
      [ // **** Default them if not present
      ult>>ULT.beginDST, ult>>ULT.endDST = 121, 305
      ult>>ULT.zone = 8 lshift size ULT.zone.minute
      ] // ****
if utv ne 0 then ult>>ULT.zone = utv>>UTV.zone

// Compute composite zone -- minutes west (+) or east (-) of GMT
ult>>ULT.compZone = 60*(ult>>ULT.zone.hour) + ult>>ULT.zone.minute
if ult>>ULT.zone.sign ne 0 then ult>>ULT.compZone = -(ult>>ULT.compZone)
]