IMPLEMENTATION MODULE DateTime;

FROM String      IMPORT String;
FROM SYSTEM      IMPORT SYSTEM;
FROM Conversions IMPORT IntConversions;

INLINE
  %#include <time.h>
  END;

  CLASS Date;
    INHERITS Comparable;
    VAR
      TheDay,
      TheMonth,
      TheYear: INTEGER;

    REDEFINE METHOD CREATE(Day, Month, Year: INTEGER);
      BEGIN
      VOID := Set(Day, Month, Year);
      END CREATE;

    METHOD Day: INTEGER;
      BEGIN
      RESULT := TheDay;
      END Day;

    METHOD Month: INTEGER;
      BEGIN
      RESULT := TheMonth;
      END Month;

    METHOD Year: INTEGER;
      BEGIN
      RESULT := TheYear;
      END Year;
                                
    METHOD IsLeap(Year: INTEGER): BOOLEAN;
      BEGIN
      RESULT := ((Year MOD 4 = 0) AND (Year MOD 100 <> 0))
                  OR (Year MOD 400 = 0);
      END IsLeap;                  
      
    METHOD LeapYear: BOOLEAN;
      BEGIN
      RESULT := IsLeap (Year);
      END LeapYear;                  
      
    METHOD GetYearLength(Year: INTEGER): INTEGER;
      BEGIN
      IF IsLeap (Year) THEN
        RESULT := 366;
       ELSE
        RESULT := 365;      
        END;
      END GetYearLength;
      
    METHOD YearLength: INTEGER;
      BEGIN
      RESULT := GetYearLength (TheYear);
      END YearLength;
       
    METHOD GetMonthLength(Year, 
                          Month: INTEGER): INTEGER;
      BEGIN
      CASE Month OF
        0:
          RESULT := 0;
          END;
        1, 3, 5, 7, 8, 10, 12: 
          RESULT := 31; 
          END;
        2:  
          IF IsLeap(Year) THEN
            RESULT := 29;
           ELSE
            RESULT := 28;
            END;
          END;
        4, 6, 9, 11: 
          RESULT := 30; 
          END;
        END; 
      END GetMonthLength;
      
    METHOD MonthLength: INTEGER;
      BEGIN
      RESULT := GetMonthLength (TheYear, TheMonth);
      END MonthLength;
      
    METHOD Set (Day, Month, Year: INTEGER) : BOOLEAN;
      BEGIN
      IF ((Day = 0) AND (Month = 0) AND (Year = 0)) OR
         ((Month >= 1) AND (Month <= 12) AND (Day >= 1) AND
         (Day <= GetMonthLength(Year, Month))) THEN
        TheDay := Day;
        TheMonth := Month;
        TheYear := Year;
        RESULT := TRUE; 
        END;
      END Set;      
    
    METHOD SetFromJulian (JulDate: INTEGER): BOOLEAN;
      BEGIN
      IF JulDate > 0 THEN
        VOID := Set (1, 1, 1900);
        IncDate (JulDate - 1);
        ASSERT Julian = JulDate;
        RESULT := TRUE;
        END;
      END SetFromJulian;
    
    METHOD IncDate (Days : INTEGER);
    VAR
      d : INTEGER;
    BEGIN
    ASSERT TheDay <> 0;
    IF Days < 0 THEN
      DecDate (-Days);
     ELSIF TheDay + Days <= MonthLength THEN
      TheDay := TheDay + Days;
     ELSE
      d := Days + TheDay - 1;
      TheDay := 1;
      WHILE TheMonth > 1 DO
        TheMonth := TheMonth - 1;
        d := d + GetMonthLength (TheYear, TheMonth);
        END;
      ---------------------------
      -- At this stage, THIS is set to the first of january,
      -- and d has been modified accordingly.
      ---------------------------
      WHILE d >= GetYearLength(TheYear) DO
        d := d - GetYearLength(TheYear);
        TheYear := TheYear + 1;
        END;
      WHILE d >= GetMonthLength (TheYear, TheMonth) DO
        d := d - GetMonthLength (TheYear, TheMonth);
        TheMonth := TheMonth + 1;
        IF TheMonth > 12 THEN
          TheYear := TheYear + 1;
          TheMonth := 1;
          END;
        END;
      FOR i := 1 TO d DO
        TheDay := TheDay + 1;
        IF TheDay > MonthLength THEN
          TheDay := 1;
          TheMonth := TheMonth + 1;
          IF TheMonth = 13 THEN
            TheMonth := 1;
            TheYear := TheYear + 1;
            END;
          END;
        END;
      END;
    END IncDate;  
      
    METHOD DecDate (Days : INTEGER);
    VAR
      d: INTEGER;
    BEGIN
    ASSERT TheDay <> 0;
    IF Days < 0 THEN
      IncDate (-Days);
     ELSIF Days < TheDay THEN
      TheDay := TheDay - Days;
     ELSE 
      d := Days - TheDay + 1;
      TheDay := 1;
      WHILE (TheMonth > 1) AND (d >= GetMonthLength(TheYear, TheMonth-1)) DO
        TheMonth := TheMonth - 1;
        d := d - GetMonthLength(TheYear, TheMonth);
        END;
      WHILE d >= GetYearLength(TheYear - 1) DO
        d := d - GetYearLength(TheYear - 1);
        TheYear := TheYear - 1;
        END;
      WHILE d > 31 DO
        WHILE (TheMonth > 1) AND (d >= GetMonthLength(TheYear, TheMonth-1)) DO
          TheMonth := TheMonth - 1;
          d := d - GetMonthLength (TheYear, TheMonth);
          END;  
        IF d > 31 THEN
          ASSERT TheMonth = 1;
          d := d - 31;
          TheMonth := 12;
          TheYear := TheYear - 1;
          END;
        END;
      FOR i := 1 TO d DO
        TheDay := TheDay - 1;
        IF TheDay = 0 THEN
          IF TheMonth = 1 THEN
            TheYear := TheYear - 1;
            TheMonth := 12;
           ELSE
            TheMonth := TheMonth - 1; 
            END;
          TheDay := MonthLength;  
          END;
        END;
      END;
    END DecDate;  

    METHOD Compare (Other : Date) : INTEGER;
    VAR
      d1, d2 : INTEGER;
    BEGIN
    d1 := (Year * 400) +       (Month * 31) + Day;
    d2 := (Other.Year * 400) + (Other.Month * 31) + Other.Day;
    IF d1 < d2 THEN
      RESULT := Smaller;
     ELSIF d1 > d2 THEN 
      RESULT := Greater;
     ELSE 
      RESULT := Equal;
      END;
    ASSERT (RESULT = Smaller) OR (RESULT = Equal) OR (RESULT = Greater);
    END Compare;
    
    REDEFINE METHOD IsGreater (Other: Comparable): BOOLEAN;
      BEGIN
      WHAT Other OF
        IN Date:
          RESULT := Compare(TAG) = Greater;
          END;
       ELSE
        END;
      END IsGreater;
    
    METHOD Julian : INTEGER;
    BEGIN
    IF (TheDay = 0) AND (TheMonth = 0) AND (TheYear = 0) THEN
      RESULT := 0;
     ELSE
      ASSERT TheDay <> 0;
      FOR y := 1900 TO TheYear - 1 DO
        RESULT := RESULT + GetYearLength(y);
        END;  
      FOR m := 1 TO TheMonth - 1 DO
        RESULT := RESULT + GetMonthLength(TheYear, m);
        END;
      RESULT := RESULT + TheDay;  
      END;
    END Julian;
    
  METHOD DayOfWeek: INTEGER;
    CONST
      Bias = 4;
    BEGIN
    -- RESULT := (Julian + 7 + Bias) MOD 7;
    RESULT := (Julian - 1) MOD 7;
    END DayOfWeek;

  METHOD ToString : ARRAY OF CHAR;
    BEGIN
    RESULT := Converter.GetDateFormat.GetFormat.CLONE;
    Converter.GetDateFormat.Replace (RESULT, Format.DayCH,   Day);
    Converter.GetDateFormat.Replace (RESULT, Format.MonthCH, Month);
    Converter.GetDateFormat.Replace (RESULT, Format.YearCH,  Year);
    END ToString;
   
  METHOD SetFromString (StringDate : ARRAY OF CHAR) : BOOLEAN;
    VAR
      d, m, y : INTEGER;
      Words   : ARRAY OF ARRAY OF CHAR;
      Model   : ARRAY OF ARRAY OF CHAR;
      a       : ARRAY OF CHAR;
    BEGIN
    IF (StringDate = VOID) OR (StringDate.SIZE = 0) THEN
      RESULT := Set (0, 0, 0);
      ASSERT RESULT;
     ELSE
      Words := String.BreakInWords (StringDate, " :/.,-+", VOID);
      IF (Words <> VOID) AND (Words.SIZE > 0) THEN
        IF (Words.SIZE = 1) AND 
          ((StringDate.SIZE = 6) OR (StringDate.SIZE = 8)) THEN
          --  200865                20081965
          Words.CREATE (3);
          Words[0] := StringDate.SLICE (0, 2);
          Words[1] := StringDate.SLICE (2, 2);
          IF (StringDate.SIZE = 6) THEN
            Words[2] := StringDate.SLICE (4, 2);
           ELSE 
            Words[2] := StringDate.SLICE (4, 2);
            END;
          END;
        IF Words.SIZE = 3 THEN
          RESULT := TRUE;
          a.CREATE (1);
          a [0] := Converter.GetDateFormat.GetSeparator;
          Model := String.BreakInWords (
                      Converter.GetDateFormat.GetFormat, a, VOID);
          FOR i := 0 TO 2 WHILE RESULT DO
            CASE Model [i][0] OF
              -- Format.DayCH:
              'D':
                d := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                END;
              -- Format.MonthCH:
              'M':
                m := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                END;
              -- Format.YearCH:
              'Y':
                y := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                -- IF the user entered only 2 digits for the year,
                -- let's assume it was 19xx
                IF RESULT THEN
                  IF (y < 100) AND (y <> 0) THEN
                    y := y + 1900;
                    END;
                  END;
                END;
              END;  
            END; -- FOR 3 WORDS
          IF RESULT THEN
            RESULT := Set (d, m, y);
            END;  
          END;  
        END;
      END;
    END SetFromString;
    
              
  END Date;
----------------------------------------------------
  CLASS Time;
    INHERITS Comparable;
    VAR
      TheHour, TheMinute, TheSecond: INTEGER;

    REDEFINE METHOD CREATE (Hour, Minute, Second: INTEGER);
      BEGIN
      TheHour := Hour;
      TheMinute := Minute;
      TheSecond := Second;
      END CREATE;
      
    REDEFINE METHOD IsGreater (Other: Comparable): BOOLEAN;
      VAR
        h1, h2, m1, m2: INTEGER;
      BEGIN
      WHAT Other OF
        IN Time:
          h1 := Hour;
          h2 := TAG.Hour;
          m1 := Minute;
          m2 := TAG.Minute;
          RESULT := (h1 > h2) OR ((h1 = h2) AND ((m1 > m2) OR 
                    ((m1 = m2) AND (Second > TAG.Second))));
          END;
       ELSE
        END;
      END IsGreater;

    METHOD Hour: INTEGER;
      BEGIN
      RESULT := TheHour;
      END Hour;

    METHOD Minute: INTEGER;
      BEGIN
      RESULT := TheMinute;
      END Minute;

    METHOD Second: INTEGER;
      BEGIN
      RESULT := TheSecond;
      END Second;
    
    METHOD Between (Low, TheInt, High : INTEGER) : BOOLEAN;
    BEGIN
    RESULT := ( (Low <= TheInt) AND (TheInt <= High) );
    END Between;    

    METHOD Set (Hour, Minute, Second: INTEGER) : BOOLEAN;
      BEGIN
      IF Between (0, Hour, 23) AND Between (0, Minute, 59)  AND
         Between (0, Second, 59) THEN
        TheHour := Hour;
        TheMinute := Minute;
        TheSecond := Second;
        RESULT := TRUE;
        END;
      END Set;
      
    METHOD ToSeconds : INTEGER;
    BEGIN
    RESULT := Canon;
    END ToSeconds;
    
    METHOD SetFromSeconds (Seconds : INTEGER);
    VAR
      h, m, s : INTEGER;
    BEGIN
    IF Seconds < (24 * 60 * 60) THEN
      h := (Seconds / (60 * 60) );
      s := (Seconds MOD 60);
      m := (Seconds / 60) MOD 60;
      VOID := Set (h, m, s);
      END;
    END SetFromSeconds;
    
    METHOD Canon : INTEGER;
      BEGIN
      RESULT := TheHour * 3600 + TheMinute * 60 + TheSecond;
      END Canon;

    METHOD Interval (To: Time): INTEGER;
      BEGIN
      RESULT := To.Canon - Canon;
      END Interval;
    
  METHOD ToString : ARRAY OF CHAR;
  BEGIN
  RESULT := Converter.GetTimeFormat.GetFormat.CLONE;
  Converter.GetTimeFormat.Replace (RESULT, Format.HourCH,    Hour);
  Converter.GetTimeFormat.Replace (RESULT, Format.MinCH,   Minute);
  Converter.GetTimeFormat.Replace (RESULT, Format.SecCH,   Second);
  END ToString;
  
  METHOD SetFromString (StringTime : ARRAY OF CHAR) : BOOLEAN;
    VAR
      h, m, s : INTEGER;
      Words   : ARRAY OF ARRAY OF CHAR;
      Model   : ARRAY OF ARRAY OF CHAR;
      a       : ARRAY OF CHAR;
    BEGIN
    IF StringTime = VOID THEN
      VOID := Set(0,0,0);
      RESULT := TRUE;
     ELSE
      Words := String.BreakInWords (StringTime, " :/.,-+", VOID);
      IF (Words <> VOID) OR (Words.SIZE > 0) THEN
        IF (Words.SIZE = 1) AND (StringTime.SIZE = 6) THEN
          Words.CREATE (3);
          Words[0] := StringTime.SLICE (0, 2);
          Words[1] := StringTime.SLICE (2, 2);
          Words[2] := StringTime.SLICE (4, 2);
          END;
        IF Words.SIZE = 3 THEN
          RESULT := TRUE;
          a.CREATE (1);
          a [0] := Converter.GetTimeFormat.GetSeparator;
          Model := String.BreakInWords (
                      Converter.GetTimeFormat.GetFormat, a, VOID);
          FOR i := 0 TO 2 WHILE RESULT DO
            CASE Model [i][0] OF
              -- Format.HourCH:
              'H':
                h := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                END;
              -- Format.MinCH:
              'M':
                m := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                END;
              -- Format.SecCH:
              'S':
                s := IntConversions.StringToInt (Words [i]);
                RESULT := (IntConversions.ErrorCode = IntConversions.NoError);
                END;
              END;  
            END; -- FOR 3 WORDS
          IF RESULT THEN
            RESULT := Set (h, m, s);
            END;  
          END;  
        END;
      END;
    END SetFromString;
  
      
  END Time;
----------------------------------------------------
  CLASS Instant;  
    INHERITS Comparable;
    
    VAR
      TheDate: Date;
      TheTime: Time;
    
    REDEFINE METHOD CREATE (TheDate: Date;
                            TheTime: Time);
      BEGIN
      ASSERT TheDate <> VOID;
      ASSERT TheTime <> VOID;
      THIS.TheDate := TheDate;
      THIS.TheTime := TheTime;
      END CREATE;
                                  
    METHOD GetDate: Date;
      BEGIN
      RESULT := TheDate;
      END GetDate;
      
    METHOD GetTime: Time;
      BEGIN
      RESULT := TheTime;
      END GetTime;  
      
    REDEFINE METHOD IsGreater (Other: Comparable): BOOLEAN;
      VAR
        i: INTEGER;
      BEGIN
      WHAT Other OF
        IN Instant:
          i := TheDate.Compare (TAG.TheDate);
          IF i = TheDate.Greater THEN
            RESULT := TRUE;
           ELSIF i = TheDate.Equal THEN
            RESULT := TheTime.IsGreater (TAG.TheTime);
            END;
          END;
        END;
      END IsGreater;
      
    METHOD Interval (Other: Instant): INTEGER;
      CONST
        PerDay = 60 * 60 * 24;
      BEGIN        
      IF IsGreater (Other) THEN
        RESULT := -Other.Interval (THIS);
        ASSERT RESULT < 0;
       ELSE         
        IF Other.TheDate.Compare (TheDate) = 0 THEN
          RESULT := TheTime.Interval (Other.TheTime);
         ELSE                                                      
          RESULT := PerDay * (Other.TheDate.Julian - TheDate.Julian - 1);
          RESULT := RESULT + (PerDay - TheTime.Canon) + Other.TheTime.Canon;
          END;            
        ASSERT RESULT >= 0;
        END;
      END Interval;
      
  END Instant;                            
----------------------------------------------------
  ONCE CLASS Now;
    INHERITS Time;

    REDEFINE METHOD Interval(To: Time): INTEGER;
      BEGIN
      END Interval;

    REDEFINE METHOD CREATE;
      BEGIN
      BASE(0,0,0);
      END CREATE;

    REDEFINE METHOD Hour: INTEGER;
      BEGIN
      RESULT := Instant.Hour;
      END Hour;

    REDEFINE METHOD Minute: INTEGER;
      BEGIN
      RESULT := Instant.Minute;
      END Minute;

    REDEFINE METHOD Second: INTEGER;
      BEGIN
      RESULT := Instant.Second;
      END Second;

    METHOD Instant: Time;
      VAR
        h, m, s: INTEGER;
      BEGIN
      INLINE
        % {
        %   yint      l;
        %   struct tm *unixtime;
        %   time_t    t;
        %
        %   t = time (NULL);
        %   unixtime = localtime (&t);
        %   Y_s = unixtime->tm_sec;
        %   Y_m = unixtime->tm_min;
        %   Y_h = unixtime->tm_hour;
        % }
        END;
      RESULT.CREATE (h, m, s);
      END Instant;

  END Now;

  ONCE CLASS Today;
    INHERITS Date;

    REDEFINE METHOD CREATE;
    BEGIN
    END CREATE;
    
    REDEFINE METHOD Day: INTEGER;
    BEGIN
    RESULT := Instant.Day;
    END Day;
    
    REDEFINE METHOD Month: INTEGER;
    BEGIN
    RESULT := Instant.Month;
    END Month;
    
    REDEFINE METHOD Year: INTEGER;
    BEGIN
    RESULT := Instant.Year;
    END Year;
    
    METHOD Instant: Date;
      VAR
        d, m, y: INTEGER;
      BEGIN
      INLINE
        % {
        %   yint      l;
        %   struct tm *unixtime;
        %   time_t    t;
        %
        %   t = time (NULL);
        %   unixtime = localtime (&t);
        %   Y_d = unixtime->tm_mday;
        %   Y_m = unixtime->tm_mon+1    ;   /* Tm_Mon  goes from 0 to 11 */
        %   Y_y = unixtime->tm_year+1900;   /* Tm_Year goes from 1900    */
        % }
        END;
      RESULT.CREATE (d, m, y);
      END Instant;
    
  END Today;

  
CLASS Format;
  VAR
    TheFormat    : ARRAY OF CHAR;
    TheSeparator : CHAR;
    
  CONST
    YearCH  = "Y";  
    MonthCH = "M";  
    DayCH   = "D";  
    
    HourCH  = "H";
    MinCH   = "M";
    SecCH   = "S";
    
  METHOD SetFormat (a : ARRAY OF CHAR);
    BEGIN
    TheFormat := a;
    END SetFormat;  

  METHOD SetSeparator (c : CHAR);
    BEGIN
    TheSeparator := c;
    END SetSeparator;  

  METHOD GetFormat : ARRAY OF CHAR;
    BEGIN
    RESULT := TheFormat;
    END GetFormat;  

  METHOD GetSeparator : CHAR;
    BEGIN
    RESULT := TheSeparator;
    END GetSeparator;  

  METHOD FindSep;
    VAR
      Sep      : CHAR;
      SepCount : INTEGER;
      BEGIN
    FOR i := 0 TO TheFormat.SIZE-1 DO
      IF NOT IsLetter (TheFormat [i]) THEN
        IF Sep = SYSTEM.CHR (0) THEN
          Sep := TheFormat[i];
------------------------------------------------------------
-- patched by bernard
------------------------------------------------------------
          SepCount := 1;
----------------------------------------------------------                
         ELSIF (TheFormat [i] <> Sep) THEN
          SYSTEM.HALT;
         ELSE --  TheFormat [i] = Sep)
          SepCount := SepCount + 1;
          END;
        END;
      END;
    ASSERT (SepCount = 2);
    TheSeparator := Sep;
    END FindSep;
  
  METHOD Replace (TheValue  : ARRAY OF CHAR;
                  TheChar   : CHAR;
                  TheInt    : INTEGER);
    VAR
      Start, End   : INTEGER;    -- 
      SubFormatLen : INTEGER;
      a            : ARRAY OF CHAR;                  
      TheNewInt    : INTEGER;
    BEGIN
    Start := String.Pos (TheValue, TheChar);
    IF (Start <> TheValue.SIZE) THEN
      End := Start;
      WHILE (End+1 < TheValue.SIZE) AND (TheValue [End+1] = TheChar) DO
        End := End + 1;
        END;
      SubFormatLen := End - Start + 1;
-------------------------------------------------------------------------
-- patched by bernard to allow 2-digits year formats
-------------------------------------------------------------------------      
      IF (TheChar = Format.YearCH) AND (SubFormatLen = 2) THEN
        TheNewInt := TheInt MOD 100;
       ELSE
        TheNewInt := TheInt;
        END;
      a := IntConversions.IntToString (TheNewInt, SubFormatLen);
      IF a.SIZE = SubFormatLen THEN
        FOR i := 0 TO SubFormatLen - 1 DO 
          IF a [i] = ' ' THEN
            TheValue [Start+i] := '0';
           ELSE
            TheValue [Start+i] := a [i];
            END;
          END;
       ELSE   
        FOR i := Start TO End DO  -- if the sub format is too short to keep
          TheValue [i] := '?';    -- the converted integer, store an error
          END;                    -- char '?'
        END;  
      END;  
    END Replace;                  
  
  METHOD IsDigit (c : CHAR) : BOOLEAN;
    BEGIN
    RESULT :=  (c >= '0') AND (c <= '9');
    END IsDigit;

  METHOD IsLetter (c : CHAR) : BOOLEAN;
    BEGIN
    RESULT := (c = YearCH)   OR  (c = MonthCH)  OR  (c = DayCH)  OR
              (c = HourCH)   OR  (c = MinCH)    OR  (c = SecCH); 
    END IsLetter;
  
  END Format;
  
-------------------------------------------------------------------------
-- Converter
-------------------------------------------------------------------------  
  
ONCE CLASS Converter;
  VAR
    DateFormat : Format;
    TimeFormat : Format;

  REDEFINE METHOD CREATE;
    BEGIN
    DateFormat.CREATE;
    DateFormat.SetFormat    ("DD/MM/YYYY");
    DateFormat.SetSeparator ("/");
    TimeFormat.CREATE;
    TimeFormat.SetFormat    ("HH:MM:SS");
    TimeFormat.SetSeparator (":");
    END CREATE;  
  
  METHOD GetDateFormat : Format;
    BEGIN
    RESULT := DateFormat;
    END GetDateFormat;

  METHOD GetTimeFormat : Format; 
    BEGIN
    RESULT := TimeFormat;
    END GetTimeFormat;

  METHOD SetDateFormat (a : ARRAY OF CHAR);
    BEGIN
    DateFormat.SetFormat (a);
    DateFormat.FindSep;
    END SetDateFormat;
  
  METHOD SetTimeFormat (a : ARRAY OF CHAR);
    BEGIN
    TimeFormat.SetFormat (a);
    TimeFormat.FindSep;
    END SetTimeFormat;
  
  END Converter;
  
END DateTime;
