(********************************************************************)
(*                                                                  *)
(*  time.s7i      Time and date library                             *)
(*  Copyright (C) 1991 - 1994, 2005 - 2013 Thomas Mertes            *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


include "enable_io.s7i";


(**
 *  Describes times and dates.
 *  For dates the proleptic Gregorian calendar is used (which assumes
 *  that the Gregorian calendar was even in effect at dates preceding
 *  its official introduction). This convention is used according to
 *  ISO 8601 which also defines that positive and negative years exist
 *  and that the year preceding 1 is 0. Time is measured in hours,
 *  minutes, seconds and micro seconds. Additionally information about
 *  the difference to UTC and a flag indicating daylight saving time
 *  is maintained also.
 *)
const type: time is new object struct
    var integer: year is 0;
    var integer: month is 1;
    var integer: day is 1;
    var integer: hour is 0;
    var integer: minute is 0;
    var integer: second is 0;
    var integer: micro_second is 0;
    var integer: timeZone is 0;
    var boolean: daylightSavingTime is FALSE;
  end struct;


const proc: GET_TIME (inout integer: year, inout integer: month, inout integer: day,
    inout integer: hour, inout integer: minute, inout integer: second,
    inout integer: micro_second, inout integer: timeZone,
    inout boolean: daylightSavingTime)                           is action "TIM_NOW";

const proc: AWAIT_TIME (in integer: year, in integer: month, in integer: day,
    in integer: hour, in integer: minute, in integer: second,
    in integer: micro_second, in integer: timeZone)              is action "TIM_AWAIT";

const proc: FROM_TIMESTAMP (in integer: timestamp, inout integer: year,
    inout integer: month, inout integer: day, inout integer: hour,
    inout integer: minute, inout integer: second, inout integer: micro_second,
    inout integer: timeZone, inout boolean: daylightSavingTime)  is action "TIM_FROM_TIMESTAMP";

const proc: SET_LOCAL_TZ (in integer: year, in integer: month, in integer: day,
    in integer: hour, in integer: minute, in integer: second,
    inout integer: timeZone, inout boolean: daylightSavingTime)  is action "TIM_SET_LOCAL_TZ";


(**
 *  Determine if a year is a leap year in the Gregorian calendar.
 *  @return TRUE if the year is a leap year, FALSE otherwise.
 *)
const func boolean: isLeapYear (in integer: year) is
  return (year rem 4 = 0 and year rem 100 <> 0) or year rem 400 = 0;


(**
 *  Determine the number of days in a 'year'.
 *  @return the number of days in the year.
 *)
const func integer: daysInYear (in integer: year) is func
  result
    var integer: days is 0;
  begin
    if isLeapYear(year) then
      days := 366;
    else
      days := 365;
    end if;
  end func;


(**
 *  Determine the number of days in the 'month' of the given 'year'.
 *  @return the number of days in the month.
 *)
const func integer: daysInMonth (in integer: year, in integer: month) is func
  result
    var integer: leng is 0;
  local
    const set of integer: monthsOfLength31 is {1, 3, 5, 7, 8, 10, 12};
  begin
    if month in monthsOfLength31 then
      leng := 31;
    else
      if month = 2 then
        if isLeapYear(year) then
          leng := 29;
        else
          leng := 28;
        end if;
      else
        leng := 30;
      end if;
    end if;
  end func;


(**
 *  Determine the number of days in the month of a given date.
 *  @return the number of days in the month.
 *)
const func integer: daysInMonth (in time: date) is
  return daysInMonth(date.year, date.month);


(**
 *  Convert a time to a string with ISO 8601 YYYY-MM-DD date format.
 *  @return the string result of the conversion.
 *)
const func string: strDate (in time: aTime) is
  return aTime.year  lpad0 4 <& "-" <&
         aTime.month lpad0 2 <& "-" <&
         aTime.day   lpad0 2;


(**
 *  Convert a time to a string with ISO 8601 hh:mm:ss time format.
 *  @return the string result of the conversion.
 *)
const func string: strTime (in time: aTime) is func
  result
    var string: isoTime is "";
  begin
    isoTime := aTime.hour   lpad0 2 <& ":" <&
               aTime.minute lpad0 2 <& ":" <&
               aTime.second lpad0 2;
    if aTime.micro_second <> 0 then
      isoTime &:= "." <& aTime.micro_second lpad0 6;
    end if;
  end func;


const func string: strTimeZone (in time: aTime) is func
  result
    var string: timeZone is "UTC";
  begin
    if aTime.timeZone <> 0 then
      if aTime.timeZone > 0 then
        timeZone &:= "+";
      end if;
      timeZone &:= str(aTime.timeZone div 60);
      if aTime.timeZone rem 60 <> 0 then
        timeZone &:= ":" & str(abs(aTime.timeZone) rem 60);
      end if;
    end if;
    if aTime.daylightSavingTime then
      timeZone &:= " (DST)";
    end if;
  end func;


const func string: str_yyyy_mm_dd (in time: aTime, in string: delimiter) is
  return aTime.year  lpad0 4 <& delimiter <&
         aTime.month lpad0 2 <& delimiter <&
         aTime.day   lpad0 2;


const func string: str_yy_mm_dd (in time: aTime, in string: delimiter) is
  return aTime.year rem 100 lpad0 2 <& delimiter <&
         aTime.month        lpad0 2 <& delimiter <&
         aTime.day          lpad0 2;


const func string: str_mm_dd_yyyy (in time: aTime, in string: delimiter) is
  return aTime.month lpad0 2 <& delimiter <&
         aTime.day   lpad0 2 <& delimiter <&
         aTime.year  lpad0 4;


const func string: str_mm_dd_yy (in time: aTime, in string: delimiter) is
  return aTime.month        lpad0 2 <& delimiter <&
         aTime.day          lpad0 2 <& delimiter <&
         aTime.year rem 100 lpad0 2;


const func string: str_dd_mm_yyyy (in time: aTime, in string: delimiter) is
  return aTime.day   lpad0 2 <& delimiter <&
         aTime.month lpad0 2 <& delimiter <&
         aTime.year  lpad0 4;


const func string: str_dd_mm_yy (in time: aTime, in string: delimiter) is
  return aTime.day   lpad0 2 <& delimiter <&
         aTime.month lpad0 2 <& delimiter <&
         aTime.year  rem 100 lpad0 2;


const func string: str_d_m_yyyy (in time: aTime, in string: delimiter) is
  return str(aTime.day)   <& delimiter <&
         str(aTime.month) <& delimiter <&
         aTime.year  lpad0 4;


const func string: str_d_m_yy (in time: aTime, in string: delimiter) is
  return str(aTime.day)   <& delimiter <&
         str(aTime.month) <& delimiter <&
         aTime.year  rem 100 lpad0 2;


const func string: str_hh_mm (in time: aTime, in string: delimiter) is
  return aTime.hour   lpad0 2 <& delimiter <&
         aTime.minute lpad0 2;


const func string: str_hh_mm_ss (in time: aTime, in string: delimiter) is
  return aTime.hour   lpad0 2 <& delimiter <&
         aTime.minute lpad0 2 <& delimiter <&
         aTime.second lpad0 2;


(**
 *  Convert a time to a string with ISO 8601 date and time format.
 *  The time is converted to the YYYY-MM-DD hh:mm:ss format.
 *  Microseconds, time zone and information about the daylight
 *  saving time are omitted.
 *  @return the string result of the conversion.
 *)
const func string: strDateTime (in time: aTime) is
  return aTime.year   lpad0 4 <& "-" <&
         aTime.month  lpad0 2 <& "-" <&
         aTime.day    lpad0 2 <& " " <&
         aTime.hour   lpad0 2 <& ":" <&
         aTime.minute lpad0 2 <& ":" <&
         aTime.second lpad0 2;


(**
 *  Convert a time to a string with ISO 8601 date and time format.
 *  The string has the format YYYY-MM-DD hh:mm:ss.uuuuuu followed by a
 *  time zone in the format UTC+n and (DST), if it is a daylight
 *  saving time. The microseconds (uuuuuu) are omitted, if they are
 *  zero. A time zone of UTC+0 is also omitted.
 *  @return the string result of the conversion.
 *)
const func string: str (in time: aTime) is
  return strDate(aTime) <& " " <&
         strTime(aTime) <& " " <&
         strTimeZone(aTime);


(**
 *  Convert a time to a time literal.
 *  @return the time literal.
 *)
const func string: literal (in time: aTime) is
  return "time(" & literal(str(aTime)) & ")";


(**
 *  Convert a string in the ISO 8601 date format to a time.
 *  The accepted ISO 8601 date formats are YYYY-MM, YYYY-MM-DD,
 *  YYYY-MM-DDTHH, YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS
 *  Additionally a space is also allowed to separate the date and the
 *  time representations: YYYY-MM-DD HH, YYYY-MM-DD HH:MM,
 *  YYYY-MM-DD HH:MM:SS
 *  @return the time result of the conversion.
 *  @exception RANGE_ERROR If stri contains not a valid time value.
 *)
const func time: time (in var string: stri) is func
  result
    var time: aTime is time.value;
  local
    var boolean: checkForTimeZone is FALSE;
  begin
    if stri[1] = '-' then
      stri := stri[2 ..];
      aTime.year := -integer(getint(stri));
    else
      aTime.year := integer(getint(stri));
    end if;
    if stri <> "" and stri[1] = '-' then
      stri := stri[2 ..];
      aTime.month := integer(getint(stri));
      if stri <> "" and stri[1] = '-' then
        stri := stri[2 ..];
        aTime.day := integer(getint(stri));
        if stri <> "" and (stri[1] = ' ' or stri[1] = 'T') then
          stri := stri[2 ..];
          aTime.hour := integer(getint(stri));
          if stri <> "" and stri[1] = ':' then
            stri := stri[2 ..];
            aTime.minute := integer(getint(stri));
            if stri <> "" and stri[1] = ':' then
              stri := stri[2 ..];
              aTime.second := integer(getint(stri));
              if stri <> "" and stri[1] = '.' then
                stri := stri[2 ..];
                aTime.micro_second := integer((getint(stri) & "00000X")[.. 6]);
              end if;
            end if;
          end if;
          checkForTimeZone := TRUE;
        end if;
      end if;
    elsif stri <> "" and stri[1] = ':' then
      stri := stri[2 ..];
      aTime.hour := aTime.year;
      aTime.year := 0;
      aTime.minute := integer(getint(stri));
      if stri <> "" and stri[1] = ':' then
        stri := stri[2 ..];
        aTime.second := integer(getint(stri));
        if stri <> "" and stri[1] = '.' then
          stri := stri[2 ..];
          aTime.micro_second := integer((getint(stri) & "00000X")[.. 6]);
        end if;
      end if;
      checkForTimeZone := TRUE;
    end if;
    if checkForTimeZone then
      if startsWith(stri, " UTC") then
        stri := stri[5 ..];
      end if;
      if stri <> "" then
        if stri[1] = '+' then
          stri := stri[2 ..];
          aTime.timeZone := integer(getint(stri)) * 60;
          if stri <> "" and stri[1] = ':' then
            stri := stri[2 ..];
            aTime.timeZone +:= integer(getint(stri));
          end if;
        elsif stri[1] = '-' then
          stri := stri[2 ..];
          aTime.timeZone := -integer(getint(stri)) * 60;
          if stri <> "" and stri[1] = ':' then
            stri := stri[2 ..];
            aTime.timeZone -:= integer(getint(stri));
          end if;
        end if;
      end if;
      if stri = " (DST)" then
        aTime.daylightSavingTime := TRUE;
        stri := "";
      end if;
    end if;
    if stri <> "" then
      raise RANGE_ERROR;
    end if;
    if aTime.month < 1 or aTime.month > 12 or
        aTime.day < 1 or aTime.day > daysInMonth(aTime.year, aTime.month) or
        aTime.hour < 0 or aTime.hour > 23 or
        aTime.minute < 0 or aTime.minute > 59 or
        aTime.second < 0 or aTime.second > 59 then
      raise RANGE_ERROR;
    end if;
  end func;


(**
 *  Convert a string in the ISO 8601 date format to a time.
 *  The accepted ISO 8601 date formats are YYYY-MM, YYYY-MM-DD,
 *  YYYY-MM-DDTHH, YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS
 *  Additionally a space is also allowed to separate the date and the
 *  time representations: YYYY-MM-DD HH, YYYY-MM-DD HH:MM,
 *  YYYY-MM-DD HH:MM:SS
 *  @return the time result of the conversion.
 *  @exception RANGE_ERROR If stri contains not a valid time value.
 *)
const func time: (attr time) parse (in string: stri) is
    return time(stri);


enable_io(time);


(**
 *  Check if two ''time'' values are equal.
 *  @return TRUE if both times are equal, FALSE otherwise.
 *)
const func boolean: (in time: aTime1) = (in time: aTime2) is
  return
    aTime1.year = aTime2.year and
    aTime1.month = aTime2.month and
    aTime1.day = aTime2.day and
    aTime1.hour = aTime2.hour and
    aTime1.minute = aTime2.minute and
    aTime1.second = aTime2.second and
    aTime1.micro_second = aTime2.micro_second;


(**
 *  Check if two ''time'' values are not equal.
 *  @return FALSE if both times are equal, TRUE otherwise.
 *)
const func boolean: (in time: aTime1) <> (in time: aTime2) is
  return
    aTime1.year <> aTime2.year or
    aTime1.month <> aTime2.month or
    aTime1.day <> aTime2.day or
    aTime1.hour <> aTime2.hour or
    aTime1.minute <> aTime2.minute or
    aTime1.second <> aTime2.second or
    aTime1.micro_second <> aTime2.micro_second;


(**
 *  Check if ''aTime1'' is less than or equal to ''aTime2''.
 *  @return TRUE if ''aTime1'' is less than or equal to ''aTime2'',
 *          FALSE otherwise.
 *)
const func boolean: (in time: aTime1) <= (in time: aTime2) is func
  result
    var boolean: isLessEqual is FALSE;
  begin
    if aTime1.year < aTime2.year then
      isLessEqual := TRUE;
    elsif aTime1.year = aTime2.year then
      if aTime1.month < aTime2.month then
        isLessEqual := TRUE;
      elsif aTime1.month = aTime2.month then
        if aTime1.day < aTime2.day then
          isLessEqual := TRUE;
        elsif aTime1.day = aTime2.day then
          if aTime1.hour < aTime2.hour then
            isLessEqual := TRUE;
          elsif aTime1.hour = aTime2.hour then
            if aTime1.minute < aTime2.minute then
              isLessEqual := TRUE;
            elsif aTime1.minute = aTime2.minute then
              if aTime1.second < aTime2.second then
                isLessEqual := TRUE;
              elsif aTime1.second = aTime2.second then
                if aTime1.micro_second <= aTime2.micro_second then
                  isLessEqual := TRUE;
                end if;
              end if;
            end if;
          end if;
        end if;
      end if;
    end if;
  end func;


(**
 *  Check if ''aTime1'' is greater than or equal to ''aTime2''.
 *  @return TRUE if ''aTime1'' is greater than or equal to ''aTime2'',
 *          FALSE otherwise.
 *)
const func boolean: (in time: aTime1) >= (in time: aTime2) is func
  result
    var boolean: isGreaterEqual is FALSE;
  begin
    if aTime1.year > aTime2.year then
      isGreaterEqual := TRUE;
    elsif aTime1.year = aTime2.year then
      if aTime1.month > aTime2.month then
        isGreaterEqual := TRUE;
      elsif aTime1.month = aTime2.month then
        if aTime1.day > aTime2.day then
          isGreaterEqual := TRUE;
        elsif aTime1.day = aTime2.day then
          if aTime1.hour > aTime2.hour then
            isGreaterEqual := TRUE;
          elsif aTime1.hour = aTime2.hour then
            if aTime1.minute > aTime2.minute then
              isGreaterEqual := TRUE;
            elsif aTime1.minute = aTime2.minute then
              if aTime1.second > aTime2.second then
                isGreaterEqual := TRUE;
              elsif aTime1.second = aTime2.second then
                if aTime1.micro_second >= aTime2.micro_second then
                  isGreaterEqual := TRUE;
                end if;
              end if;
            end if;
          end if;
        end if;
      end if;
    end if;
  end func;


(**
 *  Check if ''aTime1'' is less than ''aTime2''.
 *  @return TRUE if ''aTime1'' is less than ''aTime2'',
 *          FALSE otherwise.
 *)
const func boolean: (in time: aTime1) < (in time: aTime2) is
  return not aTime1 >= aTime2;


(**
 *  Check if ''aTime1'' is greater than ''aTime2''.
 *  @return TRUE if ''aTime1'' is greater than ''aTime2'',
 *          FALSE otherwise.
 *)
const func boolean: (in time: aTime1) > (in time: aTime2) is
  return not aTime1 <= aTime2;


(**
 *  Compares two times.
 *  @return -1, 0 or 1 if the first argument is considered to be
 *          respectively less than, equal to, or greater than the
 *          second.
 *)
const func integer: compare (in time: aTime1, in time: aTime2) is func
  result
    var integer: signumValue is 0;
  begin
    if aTime1.year < aTime2.year then
      signumValue := -1;
    elsif aTime1.year > aTime2.year then
      signumValue := 1;
    elsif aTime1.month < aTime2.month then
      signumValue := -1;
    elsif aTime1.month > aTime2.month then
      signumValue := 1;
    elsif aTime1.day < aTime2.day then
      signumValue := -1;
    elsif aTime1.day > aTime2.day then
      signumValue := 1;
    elsif aTime1.hour < aTime2.hour then
      signumValue := -1;
    elsif aTime1.hour > aTime2.hour then
      signumValue := 1;
    elsif aTime1.minute < aTime2.minute then
      signumValue := -1;
    elsif aTime1.minute > aTime2.minute then
      signumValue := 1;
    elsif aTime1.second < aTime2.second then
      signumValue := -1;
    elsif aTime1.second > aTime2.second then
      signumValue := 1;
    elsif aTime1.micro_second < aTime2.micro_second then
      signumValue := -1;
    elsif aTime1.micro_second > aTime2.micro_second then
      signumValue := 1;
    end if;
  end func;


(**
 *  Compute the hash value of a time.
 *  @return the hash value.
 *)
const func integer: hashCode (in time: aTime) is
  return (aTime.year << 6) + (aTime.month << 5) + (aTime.day << 4) +
         (aTime.hour << 3) + (aTime.minute << 2) + (aTime.second << 1) +
         aTime.micro_second;


(**
 *  Truncate 'aTime' to the beginning of a second.
 *  @return the truncated time.
 *)
const func time: truncToSecond (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Truncate 'aTime' to the beginning of a minute.
 *  @return the truncated time.
 *)
const func time: truncToMinute (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.second := 0;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Truncate 'aTime' to the beginning of a hour.
 *  @return the truncated time.
 *)
const func time: truncToHour (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.minute := 0;
    truncatedTime.second := 0;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Truncate 'aTime' to the beginning of a day.
 *  @return the truncated time.
 *)
const func time: truncToDay (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.hour := 0;
    truncatedTime.minute := 0;
    truncatedTime.second := 0;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Truncate 'aTime' to the beginning of the first day in the month.
 *  @return the truncated time.
 *)
const func time: truncToMonth (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.day := 1;
    truncatedTime.hour := 0;
    truncatedTime.minute := 0;
    truncatedTime.second := 0;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Truncate 'aTime' to the beginning of the first day in the year.
 *  @return the truncated time.
 *)
const func time: truncToYear (in time: aTime) is func
  result
    var time: truncatedTime is time.value;
  begin
    truncatedTime := aTime;
    truncatedTime.month := 1;
    truncatedTime.day := 1;
    truncatedTime.hour := 0;
    truncatedTime.minute := 0;
    truncatedTime.second := 0;
    truncatedTime.micro_second := 0;
  end func;


(**
 *  Return the weekday number of 'aDate'.
 *  @return 1 for monday, 2 for tuesday, and so on up to 7 for sunday.
 *)
const func integer: dayOfWeek (in time: aDate) is func
  result
    var integer: weekday is 0;
  local
    var integer: year is 0;
    var integer: month is 0;
  begin
    year := aDate.year;
    month := aDate.month;
    if month <= 2 then
      decr(year);
      month +:= 12;
    end if;
    weekday := succ(pred(year + year mdiv 4 - year mdiv 100 + year mdiv 400 +
        (31 * (month - 2)) mdiv 12 + aDate.day) mod 7);
  end func;


(**
 *  Return the day number in the year of 'aDate'.
 *  @return 1 for the 1. of january and successive values up to 366.
 *)
const func integer: dayOfYear (in time: aDate) is func
  result
    var integer: dayOfYear is 0;
  begin
    if isLeapYear(aDate.year) then
      dayOfYear := [](0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335)[aDate.month] + aDate.day;
    else
      dayOfYear := [](0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)[aDate.month] + aDate.day;
    end if;
  end func;


(**
 *  Return the week number of the 'year' for the 'dayOfYear'.
 *  According to ISO 8601: Week number 1 of every year contains the
 *  4. of january. Since 1st to 3rd of january might be in the
 *  previous week there can be also a week number 0.
 *  @return a week number from 0 to 53 for weeks belonging to the year
 *          of the given date.
 *)
const func integer: weekOfYear (in var integer: year, in integer: dayOfYear) is func
  result
    var integer: weekNumber is 0;
  local
    var integer: weekDayOfJan4 is 0;
  begin
    year := pred(year);
    weekDayOfJan4 := succ(pred(year + year mdiv 4 - year mdiv 100 + year mdiv 400 + 32) mod 7);
    weekNumber := (dayOfYear + weekDayOfJan4 - 5) mdiv 7 + 1;
  end func;


(**
 *  Return the week number of the year for 'aDate'.
 *  According to ISO 8601: Week number 1 of every year contains the
 *  4. of january. Since 1st to 3rd of january might be in the
 *  previous week there can be also a week number 0.
 *  @return a week number from 0 to 53 for weeks belonging to the year
 *          of the given date.
 *)
const func integer: weekOfYear (in time: aDate) is
  return weekOfYear(aDate.year, dayOfYear(aDate));


(**
 *  Return the year of the ISO 8601 week date for 'aDate'.
 *  At the beginning and the end of an Gregorian calendar year there
 *  might be days which belong to a week of the previous or next year.
 *  For example 2005-01-01 is 2004-W53-6 and 2007-12-31 is 2008-W01-1.
 *  @return the year in the range 'pred(aDate.year)' to
 *          'succ(aDate.year)'.
 *)
const func integer: weekDateYear (in time: aDate) is func
  result
    var integer: weekDateYear is 0;
  local
    var integer: weekNumber is 0;
  begin
    weekNumber := weekOfYear(aDate.year, dayOfYear(aDate));
    if weekNumber <= 0 then
      weekDateYear := pred(aDate.year);
    elsif weekNumber >= 53 and aDate.day >= 29 and
        weekOfYear(succ(aDate.year), 31 - aDate.day) = 1 then
      weekDateYear := succ(aDate.year);
    else
      weekDateYear := aDate.year;
    end if;
  end func;


(**
 *  Return the week number of the ISO 8601 week date for 'aDate'.
 *  At the beginning and the end of an Gregorian calendar year there
 *  might be days which belong to a week of the previous or next year.
 *  For example 2005-01-01 is 2004-W53-6 and 2007-12-31 is 2008-W01-1.
 *  @return the week number in the range 1 to 53.
 *)
const func integer: weekDateWeek (in time: aDate) is func
  result
    var integer: weekNumber is 0;
  begin
    weekNumber := weekOfYear(aDate.year, dayOfYear(aDate));
    if weekNumber <= 0 then
      weekNumber := weekOfYear(pred(aDate.year), 366);
    elsif weekNumber >= 53 and aDate.day >= 29 and
        weekOfYear(succ(aDate.year), 31 - aDate.day) = 1 then
      weekNumber := 1;
    end if;
  end func;


const proc: NORMALIZE (inout time: tim) is func
  local
    var integer: monthLength is 0;
  begin
    tim.second       +:= tim.micro_second mdiv 1000000;
    tim.micro_second  := tim.micro_second mod  1000000;
    tim.minute       +:= tim.second       mdiv      60;
    tim.second        := tim.second       mod       60;
    tim.hour         +:= tim.minute       mdiv      60;
    tim.minute        := tim.minute       mod       60;
    tim.day          +:= tim.hour         mdiv      24;
    tim.hour          := tim.hour         mod       24;
    tim.year         +:= pred(tim.month)  mdiv      12;
    tim.month    := succ(pred(tim.month)  mod       12);
    monthLength := daysInMonth(tim.year, tim.month);
    while tim.day > monthLength do
      tim.day := tim.day - monthLength;
      if tim.month < 12 then
        incr(tim.month);
      else
        tim.month := 1;
        incr(tim.year);
      end if;
      monthLength := daysInMonth(tim.year, tim.month);
    end while;
    while tim.day < 1 do
      if tim.month > 1 then
        decr(tim.month);
      else
        tim.month := 12;
        decr(tim.year);
      end if;
      tim.day := tim.day + daysInMonth(tim.year, tim.month);
    end while;
  end func;


(**
 *  Convert a time to Coordinated Universal Time (UTC).
 *  @return the time in UTC.
 *)
const func time: toUTC (in time: aTime) is func
  result
    var time: timeInUTC is time.value;
  begin
    timeInUTC := aTime;
    timeInUTC.minute -:= aTime.timeZone;
    timeInUTC.timeZone := 0;
    timeInUTC.daylightSavingTime := FALSE;
    NORMALIZE(timeInUTC);
  end func;


(**
 *  Convert the time ''aTime'' to a time in the local time zone.
 *  In the result year, month, day, hour, minute, secand and micro_second
 *  from ''aTime'' are adjusted to the local time zone.
 *  In the time zone of Central European Time (CET) the following holds:
 *   str(toLocalTZ(time("2000-1-1")))  returns "2000-01-01 01:00:00 UTC+1"
 *  @return the time in the local time zone.
 *)
const func time: toLocalTZ (in time: aTime) is func
  result
    var time: localTime is time.value;
  begin
    if aTime.timeZone = 0 then
      localTime := aTime;
    else
      localTime := toUTC(aTime);
    end if;
    SET_LOCAL_TZ(localTime.year, localTime.month, localTime.day,
        localTime.hour, localTime.minute, localTime.second,
        localTime.timeZone, localTime.daylightSavingTime);
    localTime.minute +:= localTime.timeZone;
    NORMALIZE(localTime);
  end func;


(**
 *  Sets timeZone and daylightSavingTime for a given time.
 *  In the result year, month, day, hour, minute, secand and micro_second
 *  from ''aTime'' are returned unchanged.
 *  In the time zone of Central European Time (CET) the following holds:
 *   str(setLocalTZ(time("2000-1-1")))  returns "2000-01-01 00:00:00 UTC+1"
 *  @return the time where the local time zone has been set.
 *)
const func time: setLocalTZ (in time: aTime) is func
  result
    var time: localTime is time.value;
  begin
    localTime := aTime;
    SET_LOCAL_TZ(localTime.year, localTime.month, localTime.day,
        localTime.hour, localTime.minute, localTime.second,
        localTime.timeZone, localTime.daylightSavingTime);
  end func;


(**
 *  Compute the julian day number of 'aDate'.
 *  The julian day number is the number of days that have elapsed
 *  since January 1, 4713 BC in the proleptic Julian calendar.
 *  @return the julian day number.
 *)
const func integer: julianDayNumber (in time: aDate) is func
  result
    var integer: julianDayNumber is 0;
  local
    var integer: year is 0;
    var integer: month is 0;
  begin
    year := aDate.year;
    month := aDate.month;
    if month <= 2 then
      decr(year);
      month +:= 12;
    end if;
    julianDayNumber := (1461 * (year + 4800)) mdiv 4 +
                       (367 * (month - 2)) mdiv 12 -
                       (3 * ((year + 4900) mdiv 100)) mdiv 4 +
                       aDate.day - 32075;
  end func;


(**
 *  Return the time of a 'julianDayNumber'.
 *  The julian day number is the number of days that have elapsed
 *  since January 1, 4713 BC in the proleptic Julian calendar.
 *  @return the time 0:00:00 at the day with the 'julianDayNumber'.
 *)
const func time: julianDayNumToTime (in integer: julianDayNumber) is func
  result
    var time: aTime is time.value;
  local
    var integer: l is 0;
    var integer: n is 0;
    var integer: i is 0;
    var integer: j is 0;
  begin
    l := julianDayNumber + 68569;
    n := (4 * l) mdiv 146097;
    l := l - (146097 * n + 3) mdiv 4;
    i := (4000 * (l + 1)) mdiv 1461001;
    l := l - (1461 * i) mdiv 4 + 31;
    j := (80 * l) mdiv 2447;
    aTime.day := l - (2447 * j) mdiv 80;
    l := j mdiv 11;
    aTime.month := j + 2 - (12 * l);
    aTime.year := 100 * (n - 49) + i + l;
  end func;


(**
 *  Return a time expressed in seconds since the Unix epoch.
 *  The Unix epoch (1970-01-01 00:00:00 UTC) corresponds to 0.
 *  @return the seconds since the Unix epoch.
 *)
const func integer: timestamp1970 (in time: aTime) is func
  result
    var integer: seconds is 0;
  local
    const integer: DAYS_FROM_0_TO_1970 is 719162; # Days between 0-01-01 and 1970-01-01
    var integer: yearBefore is 0;
  begin
    yearBefore := pred(aTime.year);
    seconds := (((yearBefore * 365 +
                  yearBefore mdiv 4 -
                  yearBefore mdiv 100 +
                  yearBefore mdiv 400 -
               DAYS_FROM_0_TO_1970 +
               pred(dayOfYear(aTime))) * 24 +
               aTime.hour) * 60 +
               aTime.minute) * 60 +
               aTime.second -
               aTime.timeZone * 60;
  end func;


(**
 *  Convert a Unix timestamp into a time from the local time zone.
 *  The timestamp is expressed in seconds since the Unix epoch.
 *  The Unix epoch (1970-01-01 00:00:00 UTC) corresponds to 0.
 *  @return the local time that corresponds to the timestamp.
 *)
const func time: timestamp1970ToTime (in integer: timestamp) is func
  result
    var time: aTime is time.value;
  begin
    FROM_TIMESTAMP(timestamp, aTime.year, aTime.month, aTime.day,
        aTime.hour, aTime.minute, aTime.second,
        aTime.micro_second, aTime.timeZone,
        aTime.daylightSavingTime);
  end func;


(**
 *  Return a time expressed as Windows FILETIME.
 *  A FILETIME is expressed in 100-nanosecond intervals since the Windows epoch.
 *  The Windows epoch (1601-01-01 00:00:00 UTC) corresponds to 0.
 *  @return the number of 100-nanosecond intervals since the Windows epoch.
 *)
const func integer: timestamp1601 (in time: aTime) is func
  result
    var integer: nanosecs100 is 0;
  local
    const integer: SECONDS_1601_1970 is 11644473600;
    const integer: NANOSECS100_TICK is 10000000;
  begin
    nanosecs100 := (timestamp1970(aTime) + SECONDS_1601_1970) * NANOSECS100_TICK +
                   aTime.micro_second * 10;
  end func;


(**
 *  Convert a Windows FILETIME into a time from the local time zone.
 *  A FILETIME is expressed in 100-nanosecond intervals since the Windows epoch.
 *  The Windows epoch (1601-01-01 00:00:00 UTC) corresponds to 0.
 *  @return the local time that corresponds to the FILETIME.
 *)
const func time: timestamp1601ToTime (in integer: timestamp) is func
  result
    var time: aTime is time.value;
  local
    const integer: SECONDS_1601_1970 is 11644473600;
    const integer: NANOSECS100_TICK is 10000000;
    var integer: utcSeconds is 0;
  begin
    utcSeconds := timestamp mdiv NANOSECS100_TICK - SECONDS_1601_1970;
    aTime := timestamp1970ToTime(utcSeconds);
    aTime.micro_second := (timestamp mod NANOSECS100_TICK) div 10
  end func;


(*
(*####
 *  Convert a timestamp into Coordinated Universal Time (UTC).
 *)
const func time: timestamp1970ToUTC (in integer: timestamp) is func
  result
    var time: aTime is time.value;
  local
    const integer: SECS_DAY is 24 * 60 * 60;
    var integer: dayclock is 0;
    var integer: dayno is 0;
    var integer: year is 1970;
  begin
    dayclock := timestamp mod SECS_DAY;
    dayno    := timestamp mdiv SECS_DAY;
    aTime.second := dayclock rem 60;
    aTime.minute := (dayclock rem 3600) div 60;
    aTime.hour   := dayclock div 3600;
    if dayno >= 0 then
      while dayno >= daysInYear(year) do
        dayno -:= daysInYear(year);
        incr(year);
      end while;
      aTime.month := 1;
      while dayno >= daysInMonth(year, aTime.month) do
        dayno -:= daysInMonth(year, aTime.month);
        incr(aTime.month);
      end while;
      aTime.day := succ(dayno);
    else
      decr(year);
      dayno := pred(-dayno);
      while dayno >= daysInYear(year) do
        dayno -:= daysInYear(year);
        decr(year);
      end while;
      aTime.month := 12;
      while dayno >= daysInMonth(year, aTime.month) do
        dayno -:= daysInMonth(year, aTime.month);
        decr(aTime.month);
      end while;
      aTime.day := daysInMonth(year, aTime.month) - dayno;
    end if;
    aTime.year := year;
  end func;
*)


(**
 *  Compute pseudo-random time in the range [low, high].
 *  The random values are uniform distributed.
 *  @return a random time such that low <= rand(low, high) and
 *          rand(low, high) <= high holds.
 *  @exception RANGE_ERROR The range is empty (low > high holds).
 *)
const func time: rand (in time: low, in time: high) is func
  result
    var time: randomTime is time.value;
  local
    var integer: timestampLow is 0;
    var integer: timestampHigh is 0;
  begin
    timestampLow := timestamp1970(low);
    timestampHigh := timestamp1970(high);
    randomTime := low;
    randomTime.second +:= rand(timestampLow, timestampHigh) - timestampLow;
    if randomTime.second = low.second then
      if randomTime.second = high.second then
        randomTime.micro_second := rand(low.micro_second, high.micro_second);
      else
        randomTime.micro_second := rand(low.micro_second, 999999);
      end if;
    elsif randomTime.second = high.second then
      randomTime.micro_second := rand(0, high.micro_second);
    else
      randomTime.micro_second := rand(0, 999999);
    end if;
    NORMALIZE(randomTime);
  end func;


(**
 *  Determine the current local time.
 *  @return the current local time.
 *)
const func time: time (NOW) is func
  result
    var time: localTime is time.value;
  begin
    GET_TIME(localTime.year, localTime.month, localTime.day,
        localTime.hour, localTime.minute, localTime.second,
        localTime.micro_second, localTime.timeZone,
        localTime.daylightSavingTime);
  end func;


(**
 *  Return the specified UTC time.
 *)
const func time: time (in integer: year, in integer: month, in integer: day,
    in integer: hour, in integer: minute, in integer: second) is func
  result
    var time: utcTime is time.value;
  begin
    utcTime.year   := year;
    utcTime.month  := month;
    utcTime.day    := day;
    utcTime.hour   := hour;
    utcTime.minute := minute;
    utcTime.second := second;
  end func;


(**
 *  Return the specified UTC time.
 *)
const func time: time (in integer: year, in integer: month, in integer: day,
    in integer: hour, in integer: minute, in integer: second,
    in integer: micro_second) is func
  result
    var time: utcTime is time.value;
  begin
    utcTime.year         := year;
    utcTime.month        := month;
    utcTime.day          := day;
    utcTime.hour         := hour;
    utcTime.minute       := minute;
    utcTime.second       := second;
    utcTime.micro_second := micro_second;
  end func;


(**
 *  Return the specified time in the specified timeZone.
 *)
const func time: timeInTimeZone (in integer: year, in integer: month, in integer: day,
    in integer: hour, in integer: minute, in integer: second, in integer: timeZone) is func
  result
    var time: aTime is time.value;
  begin
    aTime.year     := year;
    aTime.month    := month;
    aTime.day      := day;
    aTime.hour     := hour;
    aTime.minute   := minute;
    aTime.second   := second;
    aTime.timeZone := timeZone;
  end func;


(**
 *  Return the specified UTC date.
 *)
const func time: date (in integer: year, in integer: month, in integer: day) is func
  result
    var time: aDate is time.value;
  begin
    aDate.year  := year;
    aDate.month := month;
    aDate.day   := day;
  end func;


(**
 *  Wait until 'aTime' is reached
 *)
const proc: await (ref time: aTime) is func
  begin
    AWAIT_TIME(aTime.year, aTime.month, aTime.day,
        aTime.hour, aTime.minute, aTime.second,
        aTime.micro_second, aTime.timeZone);
  end func;


DECLARE_TERNARY(time);