MODULE; IMPORT Date, FloatMode, Fmt, Lex, Scan, Text, Time, TextSeq, TextUtils; IMPORT MxConfig, SMsg AS Msg; TYPE TokScan = OBJECT t : TextSeq.T; i : INTEGER; d : TEXT; METHODS getInt(token := "unknown") : INTEGER RAISES {Error} := GetInt; getEnd() RAISES {Error} := GetEnd; END; PROCEDURE RCS_Date GetInt (ts : TokScan; token := "unknown") : INTEGER RAISES {Error} = VAR tok : TEXT; BEGIN IF ts.i >= ts.t.size() THEN RAISE Error("no more integers in date " & ts.d & " while looking for " & token); END; tok := ts.t.get(ts.i); INC(ts.i); TRY RETURN Scan.Int(tok); EXCEPT Lex.Error, FloatMode.Trap => RAISE Error("invalid integer " & tok & " in date " & ts.d & " while looking for " & token); END; END GetInt; PROCEDUREGetEnd (ts : TokScan) RAISES {Error} = BEGIN IF ts.i < ts.t.size() THEN RAISE Error("too many elements in date " & ts.d); END; END GetEnd; PROCEDURETokScanNew (d : TEXT) : TokScan = VAR ts := NEW(TokScan); BEGIN ts.t := TextUtils.Split(d, "."); ts.i := 0; ts.d := d; RETURN ts; END TokScanNew; PROCEDURECompare (a, b: T): [-1..1] = BEGIN WITH aLen = Text.Length(a), bLen = Text.Length(b) DO IF aLen = bLen THEN RETURN Text.Compare(a, b); ELSE IF aLen < bLen THEN RETURN -1 ELSE RETURN 1 END; END; END; END Compare; PROCEDUREEqual (a, b: T): BOOLEAN = BEGIN RETURN Text.Equal(a, b); END Equal; PROCEDUREFromTime (t: Time.T): T = VAR date := Date.FromTime(t, Date.UTC); BEGIN IF 1900 <= date.year AND date.year < 2000 THEN DEC(date.year, 1900); END; RETURN Fmt.Pad(Fmt.Int(date.year), 2, '0') & "." & Fmt.Pad(Fmt.Int(ORD(date.month)+1), 2, '0') & "." & Fmt.Pad(Fmt.Int(date.day), 2, '0') & "." & Fmt.Pad(Fmt.Int(date.hour), 2, '0') & "." & Fmt.Pad(Fmt.Int(date.minute), 2, '0') & "." & Fmt.Pad(Fmt.Int(date.second), 2, '0'); END FromTime; PROCEDUREToTime (d: T): Time.T RAISES {Error} = BEGIN IF Text.Equal(MxConfig.HOST_OS_TYPE, "WIN32") THEN RETURN ToTimeWin32(d); ELSE RETURN ToTimePOSIX(d); END; END ToTime; PROCEDUREToTimeApprox (d: T): Time.T = BEGIN IF Text.Equal(MxConfig.HOST_OS_TYPE, "WIN32") THEN RETURN ToTimeWin32(d, exceptions := FALSE); <* NOWARN *> ELSE RETURN ToTimePOSIX(d, exceptions := FALSE); <* NOWARN *> END; END ToTimeApprox; CONST DaysNormal = ARRAY [1..12] OF CARDINAL{ (* To beginning of month. *) 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; DaysLeap = ARRAY [1..12] OF CARDINAL{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 }; Epoch = 1970; EndOfTime = 2100; PROCEDUREDaysOfMonth (year, month: CARDINAL) : CARDINAL = CONST DaysOfMonthNormal = ARRAY [1..12] OF CARDINAL{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; BEGIN IF year MOD 4 = 0 AND month = 2 THEN RETURN 29; END; RETURN DaysOfMonthNormal[month]; END DaysOfMonth; PROCEDURECheckDate (VAR year, month, day, hour, minute, second: CARDINAL; exceptions : BOOLEAN) RAISES {Error} = VAR daymax : CARDINAL; BEGIN IF exceptions THEN IF NOT (Epoch <= year AND year < EndOfTime) THEN RAISE Error("Year " & Fmt.Int(year) & " out of range in RCS date"); END; IF month < 1 OR month > 12 THEN RAISE Error("Month " & Fmt.Int(month) & " out of range in RCS date"); END; daymax := DaysOfMonth(year, month); IF day < 1 OR day > daymax THEN RAISE Error("Day " & Fmt.Int(day) & " out of range in RCS date"); END; IF hour > 23 THEN RAISE Error("Hour " & Fmt.Int(hour) & " out of range in RCS date"); END; IF minute > 59 THEN RAISE Error("Minute " & Fmt.Int(minute) & " out of range in RCS date"); END; IF second > 59 THEN RAISE Error("Second " & Fmt.Int(second) & " out of range in RCS date"); END; ELSE (* no exceptions *) IF NOT (Epoch <= year AND year < EndOfTime) THEN Msg.Error("Year " & Fmt.Int(year) & " out of range in RCS date"); IF year < Epoch THEN year := Epoch; ELSE year := EndOfTime - 1; END; END; IF month < 1 OR month > 12 THEN Msg.Error("Month " & Fmt.Int(month) & " out of range in RCS date"); IF month < 1 THEN month := 1; ELSE month := 12; END; END; daymax := DaysOfMonth(year, month); IF day < 1 OR day > daymax THEN Msg.Error("Day " & Fmt.Int(day) & " out of range in RCS date"); IF day < 1 THEN day := 1; ELSE day := daymax; END; END; IF hour > 23 THEN Msg.Error("Hour " & Fmt.Int(hour) & " out of range in RCS date"); hour := 23; END; IF minute > 59 THEN Msg.Error("Minute " & Fmt.Int(minute) & " out of range in RCS date"); minute := 59; END; IF second > 59 THEN Msg.Error("Second " & Fmt.Int(second) & " out of range in RCS date"); second := 59; END; END; END CheckDate; PROCEDUREToTimeWin32 (d: T; exceptions := TRUE): Time.T RAISES {Error} = VAR ts: TokScan; year, month, day, hour, minute, second: CARDINAL; BEGIN ts := TokScanNew(d); year := ts.getInt("year"); month := ts.getInt("month"); day := ts.getInt("day"); hour := ts.getInt("hour"); minute := ts.getInt("minute"); second := ts.getInt("second"); ts.getEnd(); IF year < 100 THEN INC(year, 1900) END; (* Be feature-compatible with POSIX. *) CheckDate(year, month, day, hour, minute, second, exceptions); TRY RETURN Date.ToTime( Date.T {year, VAL(month - 1, Date.Month), day, hour, minute, second, 0, "UTC", Date.WeekDay.Sun}); EXCEPT Date.Error => IF exceptions THEN RAISE Error("Bad date: " & d); ELSE Msg.Fatal("Bad date: " & d); (* cannot happen :-) *) RETURN Time.Now(); END; END; END ToTimeWin32; PROCEDUREToTimePOSIX (d: T; exceptions := TRUE): Time.T
Date.ToTime
is badly broken on POSIX.
RAISES {Error} = VAR ts: TokScan; year, month, day, hour, minute, second: CARDINAL; FirstLeap := (Epoch + 3) DIV 4 * 4; numLeap: CARDINAL; (* Leap years to beginning of specified year. *) t: CARDINAL; BEGIN ts := TokScanNew(d); year := ts.getInt("year"); month := ts.getInt("month"); day := ts.getInt("day"); hour := ts.getInt("hour"); minute := ts.getInt("minute"); second := ts.getInt("second"); ts.getEnd(); IF year < 100 THEN INC(year, 1900) END; CheckDate(year, month, day, hour, minute, second, exceptions); numLeap := (year - FirstLeap + 3) DIV 4; t := (year - Epoch)*365 + numLeap; (* Days to beginning of year. *) IF year MOD 4 = 0 THEN INC(t, DaysLeap[month]); ELSE INC(t, DaysNormal[month]); END; INC(t, day - 1); t := ((t*24 + hour)*60 + minute)*60 + second; RETURN FLOAT(t, Time.T); END ToTimePOSIX; PROCEDUREValid (d: T): BOOLEAN = CONST Digits = SET OF CHAR{'0'..'9'}; VAR length: CARDINAL; firstDot: CARDINAL; ts: TokScan; year, month, day, hour, minute, second: CARDINAL; BEGIN length := Text.Length(d); CASE length OF | 17 => firstDot := 2; | 19 => firstDot := 4; ELSE RETURN FALSE; END; FOR pos := 0 TO length-1 DO WITH ch = Text.GetChar(d, pos) DO IF pos >= firstDot AND (pos-firstDot) MOD 3 = 0 THEN IF ch # '.' THEN RETURN FALSE END; ELSE IF NOT ch IN Digits THEN RETURN FALSE END; END; END; END; ts := TokScanNew(d); TRY year := ts.getInt(); month := ts.getInt(); day := ts.getInt(); hour := ts.getInt(); minute := ts.getInt(); second := ts.getInt(); ts.getEnd(); EXCEPT ELSE RETURN FALSE; END; IF length = 17 THEN IF NOT 70 <= year THEN RETURN FALSE END; ELSE IF NOT (2000 <= year AND year < 2100) THEN RETURN FALSE END; END; IF NOT (1 <= month AND month <= 12) THEN RETURN FALSE END; IF NOT (1 <= day AND day <= 31) THEN RETURN FALSE END; IF NOT (0 <= hour AND hour <= 23) THEN RETURN FALSE END; IF NOT (0 <= minute AND minute <= 59) THEN RETURN FALSE END; IF NOT (0 <= second AND second <= 59) THEN RETURN FALSE END; RETURN TRUE; END Valid; BEGIN END RCS_Date.