Dates

This module provides minimal date time records and procedures for working with dates in YYYY-MM-DD and MM/DD/YYYY format and times in H:MM, HH:MM and HH:MM:SS formats.

Set
Set a DateTime record providing year, month, day, hour, minute and second as integers and the DateTime record to be populated.
SetDate
Set the date portion of a DateTime record, leaves the hour, minute and second attributes unmodified.
SetTime
Set the time portion of a DateTime record, leaves the year, month, date attributes unmodified.
Copy
Copy the attributes of one DateTime record into another DateTime record
ToChars
Given a DateTime record and a format constant, render the DateTime record to an array of CHAR.
LeapYear
Given a DateTime record check to see if it is a leap year.
NumOfDays
Given a year and monoth return the number of days in the month.
IsValid
Check to see if the all attributes in a DateTime record are valid.
OberonToDateTime
Convert oberon date and time integer values into a DateTime record
DateTimeToOberon
Convert a DateTime record into Oberon date and time integer values.
Now
Set a DateTime record’s attributes to the current time, depends of on the implementation of Clock.Mod.
WeekDate
Given a DateTime record calculates the year, week and weekday as integers values.
Equal
Checks to DateTime records to see if they have equivalent attribute values.
Compare
Compare two DateTime records, if t1 < t2 then return -1, if t1 = t2 return 0 else if t1 > t2 return 1.
CompareDate
Compare the year, month, day attributes of two DateTime records following the approach used in Compare.
CompareTime
Compare the hour, minute, second attributes of two DateTime records following the approach used in Compare.
TimeDifference
Take the differ of two DateTime records setting the difference in integer values for days, hours, minutes and seconds.
AddYears
Add years to a DateTime record. Years is a positive or negative integer.
AddMonths
Add months to a DateTime record. Months is either a positive or negative integer. Months will propogate to year in the DataTime record.
AddDays
Add days to a DateTime record. Days can be either a positive or negative integer. Days will propogate to month and year attributes of the DateTime record.
AddHours
Add hours to a DateTime record. Hours can be either a positive or negative integer. Hours will propogate to day, month and year attributes of the DateTime record.
AddMinutes
Add minutes to a DateTime record. Minutes can be either a positive or negative integer. Minutes will propogate to hour, day, month and year attributes of the DateTime record.
AddSeconds
Add seconds to a DateTime record. Seconds can be either a positive or negatice integer. Seconds will propogate to minute, hour, day, month, year attributes of the DateTime record.
IsValidDate
IsValidDate checks the day, month, year attributes of a DateTime record and validates the values. Returns TRUE if everthing is ok, FALSE otherwise.
IsValidTime
IsValidTime checks the hour, minute, second attributes of a DateTime record and validates the values. Returns TRUE if everthing is ok, FALSE otherwise.
IsDateString
Checks to see if an ARRAY OF CHAR is a parsiable date string (e.g. in 2020-11-26 or 11/26/2020). Returns TRUE if the string is parsable, FALSE otherwise. NOTE: It does NOT check to see if the day, month or year values are valid. It only checks the format of the string.
IsTimeString
Checks to see if an ARRAY OF CHAR is a parsible time string (e.g. 3:32, 14:55, 09:19:22). NOTE: It only checks the format and does not check the hour, minute and second values.
ParseDate
Parse an ARRAY OF CHAR setting the values if year, month and day. Return TRUE on successful parse and FALSE otherwise.
ParseTime
Parse an ARRAY OF CHAR setting the values of hour, minute and second. Return TRUE on succesful parse and FALSE otherwise.
Parse
Parse an ARRAY OF CHAR setting the attributes of a DateTime record. Return TURE on success, FALSE otherwise.

Limitations

Dates are presumed to be in the YYYY-DD-MM or MM/DD/YYYY formats. Does not handle dates with spelled out months or weekdays.

Time portion of the date object doesn’t include time zone. This will need to be rectified at some point.

Source code for Dates.Mod

  1. (* Dates -- this module was inspired by the A2's Dates module, adapted
  2. for Oberon-07 and a POSIX system. It provides an assortment of procedures
  3. for working with a simple datetime record.
  4. Copyright (C) 2020 R. S. Doiel
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU Affero General Public License as
  7. published by the Free Software Foundation, either version 3 of the
  8. License, or (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU Affero General Public License for more details.
  13. You should have received a copy of the GNU Affero General Public License
  14. along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. @Author R. S. Doiel, <rsdoiel@gmail.com>
  16. copyright (c) 2020, all rights reserved.
  17. This software is released under the GNU AGPL
  18. See http://www.gnu.org/licenses/agpl-3.0.html
  19. *)
  20. MODULE Dates;
  21. IMPORT Chars, Strings, Clock, Convert := extConvert; (*, Out; **)
  22. CONST
  23. MAXSTR = Chars.MAXSTR;
  24. SHORTSTR = Chars.SHORTSTR;
  25. YYYYMMDD* = 1; (* YYYY-MM-DD format *)
  26. MMDDYYYY* = 2; (* MM/DD/YYYY format *)
  27. YYYYMMDDHHMMSS* = 3; (* YYYY-MM-DD HH:MM:SS format *)
  28. TYPE
  29. DateTime* = RECORD
  30. year*, month*, day*, hour*, minute*, second* : INTEGER
  31. END;
  32. VAR
  33. (* Month names, January = 0, December = 11 *)
  34. Months*: ARRAY 23 OF ARRAY 10 OF CHAR;
  35. (* Days of week, Monday = 0, Sunday = 6 *)
  36. Days*: ARRAY 7 OF ARRAY 10 OF CHAR;
  37. DaysInMonth: ARRAY 12 OF INTEGER;
  38. (* Set -- initialize a date record year, month and day values *)
  39. PROCEDURE Set*(year, month, day, hour, minute, second : INTEGER; VAR dt: DateTime);
  40. BEGIN
  41. dt.year := year;
  42. dt.month := month;
  43. dt.day := day;
  44. dt.hour := hour;
  45. dt.minute := minute;
  46. dt.second := second;
  47. END Set;
  48. (* SetDate -- set a Date record's year, month and day attributes *)
  49. PROCEDURE SetDate*(year, month, day : INTEGER; VAR dt: DateTime);
  50. BEGIN
  51. dt.year := year;
  52. dt.month := month;
  53. dt.day := day;
  54. END SetDate;
  55. (* SetTime -- set a Date record's hour, minute, second attributes *)
  56. PROCEDURE SetTime*(hour, minute, second : INTEGER; VAR dt: DateTime);
  57. BEGIN
  58. dt.hour := hour;
  59. dt.minute := minute;
  60. dt.second := second;
  61. END SetTime;
  62. (* Copy -- copy the values from one date record to another *)
  63. PROCEDURE Copy*(src : DateTime; VAR dest : DateTime);
  64. BEGIN
  65. dest.year := src.year;
  66. dest.month := src.month;
  67. dest.day := src.day;
  68. dest.hour := src.hour;
  69. dest.minute := src.minute;
  70. dest.second := src.second;
  71. END Copy;
  72. (* ToChars -- converts a date record into an array of chars using
  73. the format constant. Formats supported are YYYY-MM-DD HH:MM:SS
  74. or MM/DD/YYYY HH:MM:SS. *)
  75. PROCEDURE ToChars*(dt: DateTime; fmt : INTEGER; VAR src : ARRAY OF CHAR);
  76. VAR ok : BOOLEAN;
  77. BEGIN
  78. Chars.Clear(src);
  79. IF fmt = YYYYMMDD THEN
  80. Chars.AppendInt(dt.year, 4, "0", src);
  81. ok := Chars.AppendChar("-", src);
  82. Chars.AppendInt(dt.month, 2, "0", src);
  83. ok := Chars.AppendChar("-", src);
  84. Chars.AppendInt(dt.day, 2, "0", src);
  85. ELSIF fmt = MMDDYYYY THEN
  86. Chars.AppendInt(dt.month, 2, "0", src);
  87. ok := Chars.AppendChar("/", src);
  88. Chars.AppendInt(dt.day, 2, "0", src);
  89. ok := Chars.AppendChar("/", src);
  90. Chars.AppendInt(dt.year, 4, "0", src);
  91. ELSIF fmt = YYYYMMDDHHMMSS THEN
  92. Chars.AppendInt(dt.year, 4, "0", src);
  93. ok := Chars.AppendChar("-", src);
  94. Chars.AppendInt(dt.month, 2, "0", src);
  95. ok := Chars.AppendChar("-", src);
  96. Chars.AppendInt(dt.day, 2, "0", src);
  97. ok := Chars.AppendChar(" ", src);
  98. Chars.AppendInt(dt.hour, 2, "0", src);
  99. ok := Chars.AppendChar(":", src);
  100. Chars.AppendInt(dt.minute, 2, "0", src);
  101. ok := Chars.AppendChar(":", src);
  102. Chars.AppendInt(dt.second, 2, "0", src);
  103. END;
  104. END ToChars;
  105. (*
  106. * Date and Time functions very much inspired by A2 but
  107. * adapted for use in Oberon-07 and OBNC compiler.
  108. *)
  109. (* LeapYear -- returns TRUE if 'year' is a leap year *)
  110. PROCEDURE LeapYear*(year: INTEGER): BOOLEAN;
  111. BEGIN
  112. RETURN (year > 0) & (year MOD 4 = 0) & (~(year MOD 100 = 0) OR (year MOD 400 = 0))
  113. END LeapYear;
  114. (* NumOfDays -- number of days, returns the number of days in that month *)
  115. PROCEDURE NumOfDays*(year, month: INTEGER): INTEGER;
  116. VAR result : INTEGER;
  117. BEGIN
  118. result := 0;
  119. DEC(month);
  120. IF ((month >= 0) & (month < 12)) THEN
  121. IF (month = 1) & LeapYear(year) THEN
  122. result := DaysInMonth[1]+1;
  123. ELSE
  124. result := DaysInMonth[month];
  125. END;
  126. END;
  127. RETURN result
  128. END NumOfDays;
  129. (* IsValid -- checks if the attributes set in a DateTime record are valid *)
  130. PROCEDURE IsValid*(dt: DateTime): BOOLEAN;
  131. BEGIN
  132. RETURN ((dt.year > 0) & (dt.month > 0) & (dt.month <= 12) & (dt.day > 0) & (dt.day <= NumOfDays(dt.year, dt.month)) & (dt.hour >= 0) & (dt.hour < 24) & (dt.minute >= 0) & (dt.minute < 60) & (dt.second >= 0) & (dt.second < 60))
  133. END IsValid;
  134. (* IsValidDate -- checks to see if a datetime record has valid day, month and year
  135. attributes *)
  136. PROCEDURE IsValidDate*(dt: DateTime) : BOOLEAN;
  137. BEGIN
  138. RETURN (dt.year > 0) & (dt.month > 0) & (dt.month <= 12) & (dt.day > 0) & (dt.day <= NumOfDays(dt.year, dt.month))
  139. END IsValidDate;
  140. (* IsValidTime -- checks if the hour, minute, second attributes set in a DateTime record are valid *)
  141. PROCEDURE IsValidTime*(dt: DateTime): BOOLEAN;
  142. BEGIN
  143. RETURN (dt.hour >= 0) & (dt.hour < 24) & (dt.minute >= 0) & (dt.minute < 60) & (dt.second >= 0) & (dt.second < 60)
  144. END IsValidTime;
  145. (* OberonToDateTime -- convert an Oberon date/time to a DateTime
  146. structure *)
  147. PROCEDURE OberonToDateTime*(Date, Time: INTEGER; VAR dt : DateTime);
  148. BEGIN
  149. dt.second := Time MOD 64; Time := Time DIV 64;
  150. dt.minute := Time MOD 64; Time := Time DIV 64;
  151. dt.hour := Time MOD 24;
  152. dt.day := Date MOD 32; Date := Date DIV 32;
  153. dt.month := (Date MOD 16) + 1; Date := Date DIV 16;
  154. dt.year := Date;
  155. END OberonToDateTime;
  156. (* DateTimeToOberon -- convert a DateTime structure to an Oberon
  157. date/time *)
  158. PROCEDURE DateTimeToOberon*(dt: DateTime; VAR date, time: INTEGER);
  159. BEGIN
  160. IF IsValid(dt) THEN
  161. date := (dt.year)*512 + dt.month*32 + dt.day;
  162. time := dt.hour*4096 + dt.minute*64 + dt.second
  163. ELSE
  164. date := 0;
  165. time := 0;
  166. END;
  167. END DateTimeToOberon;
  168. (* Now -- returns the current date and time as a DateTime record. *)
  169. PROCEDURE Now*(VAR dt: DateTime);
  170. VAR d, t: INTEGER;
  171. BEGIN
  172. Clock.Get(t, d);
  173. OberonToDateTime(d, t, dt);
  174. END Now;
  175. (* WeekDate -- returns the ISO 8601 year number, week number &
  176. week day (Monday=1, ....Sunday=7)
  177. Algorithm is by Rick McCarty, http://personal.ecu.edu/mccartyr/ISOwdALG.txt
  178. *)
  179. PROCEDURE WeekDate*(dt: DateTime; VAR year, week, weekday: INTEGER);
  180. VAR doy, i, yy, c, g, jan1: INTEGER; leap: BOOLEAN;
  181. BEGIN
  182. IF IsValid(dt) THEN
  183. leap := LeapYear(dt.year);
  184. doy := dt.day; i := 0;
  185. WHILE (i < (dt.month - 1)) DO
  186. doy := doy + DaysInMonth[i];
  187. INC(i);
  188. END;
  189. IF leap & (dt.month > 2) THEN
  190. INC(doy);
  191. END;
  192. yy := (dt.year - 1) MOD 100;
  193. c := (dt.year - 1) - yy;
  194. g := (yy + yy) DIV 4;
  195. jan1 := 1 + (((((c DIV 100) MOD 4) * 5) + g) MOD 7);
  196. weekday := 1 + (((doy + (jan1 - 1)) - 1) MOD 7);
  197. IF (doy <= (8 - jan1)) & (jan1 > 4) THEN (* falls in year-1 ? *)
  198. year := dt.year - 1;
  199. IF (jan1 = 5) OR ((jan1 = 6) & LeapYear(year)) THEN
  200. week := 53;
  201. ELSE
  202. week := 52;
  203. END;
  204. ELSE
  205. IF leap THEN
  206. i := 366;
  207. ELSE
  208. i := 365;
  209. END;
  210. IF ((i - doy) < (4 - weekday)) THEN
  211. year := dt.year + 1;
  212. week := 1;
  213. ELSE
  214. year := dt.year;
  215. i := doy + (7-weekday) + (jan1-1);
  216. week := i DIV 7;
  217. IF (jan1 > 4) THEN
  218. DEC(week);
  219. END;
  220. END;
  221. END;
  222. ELSE
  223. year := -1; week := -1; weekday := -1;
  224. END;
  225. END WeekDate;
  226. (* Equal -- compare to date records to see if they are equal values *)
  227. PROCEDURE Equal*(t1, t2: DateTime) : BOOLEAN;
  228. BEGIN
  229. RETURN ((t1.second = t2.second) & (t1.minute = t2.minute) & (t1.hour = t2.hour) & (t1.day = t2.day) & (t1.month = t2.month) & (t1.year = t2.year))
  230. END Equal;
  231. (* compare -- used in Compare only for comparing specific values,
  232. returning an appropriate -1, 0, 1 *)
  233. PROCEDURE compare(t1, t2 : INTEGER) : INTEGER;
  234. VAR result : INTEGER;
  235. BEGIN
  236. IF (t1 < t2) THEN
  237. result := -1;
  238. ELSIF (t1 > t2) THEN
  239. result := 1;
  240. ELSE
  241. result := 0;
  242. END;
  243. RETURN result
  244. END compare;
  245. (* Compare -- returns -1 if (t1 < t2), 0 if (t1 = t2) or 1 if (t1 > t2) *)
  246. PROCEDURE Compare*(t1, t2: DateTime) : INTEGER;
  247. VAR result : INTEGER;
  248. BEGIN
  249. result := compare(t1.year, t2.year);
  250. IF (result = 0) THEN
  251. result := compare(t1.month, t2.month);
  252. IF (result = 0) THEN
  253. result := compare(t1.day, t2.day);
  254. IF (result = 0) THEN
  255. result := compare(t1.hour, t2.hour);
  256. IF (result = 0) THEN
  257. result := compare(t1.minute, t2.minute);
  258. IF (result = 0) THEN
  259. result := compare(t1.second, t2.second);
  260. END;
  261. END;
  262. END;
  263. END;
  264. END;
  265. RETURN result
  266. END Compare;
  267. (* CompareDate -- compare day, month and year values only *)
  268. PROCEDURE CompareDate*(t1, t2: DateTime) : INTEGER;
  269. VAR result : INTEGER;
  270. BEGIN
  271. result := compare(t1.year, t2.year);
  272. IF (result = 0) THEN
  273. result := compare(t1.month, t2.month);
  274. IF (result = 0) THEN
  275. result := compare(t1.day, t2.day);
  276. END;
  277. END;
  278. RETURN result
  279. END CompareDate;
  280. (* CompareTime -- compare second, minute and hour values only *)
  281. PROCEDURE CompareTime*(t1, t2: DateTime) : INTEGER;
  282. VAR result : INTEGER;
  283. BEGIN
  284. result := compare(t1.hour, t2.hour);
  285. IF (result = 0) THEN
  286. result := compare(t1.minute, t2.minute);
  287. IF (result = 0) THEN
  288. result := compare(t1.second, t2.second);
  289. END;
  290. END;
  291. RETURN result
  292. END CompareTime;
  293. (* TimeDifferences -- returns the absolute time difference between
  294. t1 and t2.
  295. Note that leap seconds are not counted,
  296. see http://www.eecis.udel.edu/~mills/leap.html *)
  297. PROCEDURE TimeDifference*(t1, t2: DateTime; VAR days, hours, minutes, seconds : INTEGER);
  298. CONST SecondsPerMinute = 60; SecondsPerHour = 3600; SecondsPerDay = 86400;
  299. VAR start, end: DateTime; year, month, second : INTEGER;
  300. BEGIN
  301. IF (Compare(t1, t2) = -1) THEN
  302. start := t1;
  303. end := t2;
  304. ELSE
  305. start := t2;
  306. end := t1;
  307. END;
  308. IF (start.year = end.year) & (start.month = end.month) & (start.day = end.day) THEN
  309. second := end.second - start.second + ((end.minute - start.minute) * SecondsPerMinute) + ((end.hour - start.hour) * SecondsPerHour);
  310. days := 0;
  311. hours := 0;
  312. minutes := 0;
  313. ELSE
  314. (* use start date/time as reference point *)
  315. (* seconds until end of the start.day *)
  316. second := (SecondsPerDay - start.second) - (start.minute * SecondsPerMinute) - (start.hour * SecondsPerHour);
  317. IF (start.year = end.year) & (start.month = end.month) THEN
  318. (* days between start.day and end.day *)
  319. days := (end.day - start.day) - 1;
  320. ELSE
  321. (* days until start.month ends excluding start.day *)
  322. days := NumOfDays(start.year, start.month) - start.day;
  323. IF (start.year = end.year) THEN
  324. (* months between start.month and end.month *)
  325. FOR month := start.month + 1 TO end.month - 1 DO
  326. days := days + NumOfDays(start.year, month);
  327. END;
  328. ELSE
  329. (* days until start.year ends (excluding start.month) *)
  330. FOR month := start.month + 1 TO 12 DO
  331. days := days + NumOfDays(start.year, month);
  332. END;
  333. FOR year := start.year + 1 TO end.year - 1 DO (* days between start.years and end.year *)
  334. IF LeapYear(year) THEN days := days + 366; ELSE days := days + 365; END;
  335. END;
  336. FOR month := 1 TO end.month - 1 DO (* days until we reach end.month in end.year *)
  337. days := days + NumOfDays(end.year, month);
  338. END;
  339. END;
  340. (* days in end.month until reaching end.day excluding end.day *)
  341. days := (days + end.day) - 1;
  342. END;
  343. (* seconds in end.day *)
  344. second := second + end.second + (end.minute * SecondsPerMinute) + (end.hour * SecondsPerHour);
  345. END;
  346. days := days + (second DIV SecondsPerDay); second := (second MOD SecondsPerDay);
  347. hours := (second DIV SecondsPerHour); second := (second MOD SecondsPerHour);
  348. minutes := (second DIV SecondsPerMinute); second := (second MOD SecondsPerMinute);
  349. seconds := second;
  350. END TimeDifference;
  351. (* AddYear -- Add/Subtract a number of years to/from date *)
  352. PROCEDURE AddYears*(VAR dt: DateTime; years : INTEGER);
  353. BEGIN
  354. ASSERT(IsValid(dt));
  355. dt.year := dt.year + years;
  356. ASSERT(IsValid(dt));
  357. END AddYears;
  358. (* AddMonths -- Add/Subtract a number of months to/from date.
  359. This will adjust date.year if necessary *)
  360. PROCEDURE AddMonths*(VAR dt: DateTime; months : INTEGER);
  361. VAR years : INTEGER;
  362. BEGIN
  363. ASSERT(IsValid(dt));
  364. years := months DIV 12;
  365. dt.month := dt.month + (months MOD 12);
  366. IF (dt.month > 12) THEN
  367. dt.month := dt.month - 12;
  368. INC(years);
  369. ELSIF (dt.month < 1) THEN
  370. dt.month := dt.month + 12;
  371. DEC(years);
  372. END;
  373. IF (years # 0) THEN AddYears(dt, years); END;
  374. ASSERT(IsValid(dt));
  375. END AddMonths;
  376. (* AddDays -- Add/Subtract a number of days to/from date.
  377. This will adjust date.month and date.year if necessary *)
  378. PROCEDURE AddDays*(VAR dt: DateTime; days : INTEGER);
  379. VAR nofDaysLeft : INTEGER;
  380. BEGIN
  381. ASSERT(IsValid(dt));
  382. IF (days > 0) THEN
  383. WHILE (days > 0) DO
  384. nofDaysLeft := NumOfDays(dt.year, dt.month) - dt.day;
  385. IF (days > nofDaysLeft) THEN
  386. dt.day := 1;
  387. AddMonths(dt, 1);
  388. days := days - nofDaysLeft - 1; (* -1 because we consume the first day of the next month *)
  389. ELSE
  390. dt.day := dt.day + days;
  391. days := 0;
  392. END;
  393. END;
  394. ELSIF (days < 0) THEN
  395. days := -days;
  396. WHILE (days > 0) DO
  397. nofDaysLeft := dt.day - 1;
  398. IF (days > nofDaysLeft) THEN
  399. dt.day := 1; (* otherwise, dt could become an invalid date if the previous month has less days than dt.day *)
  400. AddMonths(dt, -1);
  401. dt.day := NumOfDays(dt.year, dt.month);
  402. days := days - nofDaysLeft - 1; (* -1 because we consume the last day of the previous month *)
  403. ELSE
  404. dt.day := dt.day - days;
  405. days := 0;
  406. END;
  407. END;
  408. END;
  409. ASSERT(IsValid(dt));
  410. END AddDays;
  411. (* AddHours -- Add/Subtract a number of hours to/from date.
  412. This will adjust date.day, date.month and date.year if necessary *)
  413. PROCEDURE AddHours*(VAR dt: DateTime; hours : INTEGER);
  414. VAR days : INTEGER;
  415. BEGIN
  416. ASSERT(IsValid(dt));
  417. dt.hour := dt.hour + hours;
  418. days := dt.hour DIV 24;
  419. dt.hour := dt.hour MOD 24;
  420. IF (dt.hour < 0) THEN
  421. dt.hour := dt.hour + 24;
  422. DEC(days);
  423. END;
  424. IF (days # 0) THEN AddDays(dt, days); END;
  425. ASSERT(IsValid(dt));
  426. END AddHours;
  427. (* AddMinutes -- Add/Subtract a number of minutes to/from date.
  428. This will adjust date.hour, date.day, date.month and date.year
  429. if necessary *)
  430. PROCEDURE AddMinutes*(VAR dt: DateTime; minutes : INTEGER);
  431. VAR hours : INTEGER;
  432. BEGIN
  433. ASSERT(IsValid(dt));
  434. dt.minute := dt.minute + minutes;
  435. hours := dt.minute DIV 60;
  436. dt.minute := dt.minute MOD 60;
  437. IF (dt.minute < 0) THEN
  438. dt.minute := dt.minute + 60;
  439. DEC(hours);
  440. END;
  441. IF (hours # 0) THEN AddHours(dt, hours); END;
  442. ASSERT(IsValid(dt));
  443. END AddMinutes;
  444. (* AddSeconds -- Add/Subtract a number of seconds to/from date.
  445. This will adjust date.minute, date.hour, date.day, date.month and
  446. date.year if necessary *)
  447. PROCEDURE AddSeconds*(VAR dt: DateTime; seconds : INTEGER);
  448. VAR minutes : INTEGER;
  449. BEGIN
  450. ASSERT(IsValid(dt));
  451. dt.second := dt.second + seconds;
  452. minutes := dt.second DIV 60;
  453. dt.second := dt.second MOD 60;
  454. IF (dt.second < 0) THEN
  455. dt.second := dt.second + 60;
  456. DEC(minutes);
  457. END;
  458. IF (minutes # 0) THEN AddMinutes(dt, minutes); END;
  459. ASSERT(IsValid(dt));
  460. END AddSeconds;
  461. (* IsDateString -- return TRUE if the ARRAY OF CHAR is 10 characters
  462. long and is either in the form of YYYY-MM-DD or MM/DD/YYYY where
  463. Y, M and D are digits.
  464. NOTE: is DOES NOT check the ranges of the digits. *)
  465. PROCEDURE IsDateString*(inline : ARRAY OF CHAR) : BOOLEAN;
  466. VAR
  467. test : BOOLEAN; i, pos : INTEGER;
  468. src : ARRAY MAXSTR OF CHAR;
  469. BEGIN
  470. Chars.Set(inline, src);
  471. Chars.TrimSpace(src);
  472. test := FALSE;
  473. IF Strings.Length(src) = 10 THEN
  474. pos := Strings.Pos("-", src, 0);
  475. IF pos > 0 THEN
  476. IF (src[4] = "-") & (src[7] = "-") THEN
  477. test := TRUE;
  478. FOR i := 0 TO 9 DO
  479. IF (i # 4) & (i # 7) THEN
  480. IF Chars.IsDigit(src[i]) = FALSE THEN
  481. test := FALSE;
  482. END;
  483. END;
  484. END;
  485. ELSE
  486. test := FALSE;
  487. END;
  488. END;
  489. pos := Strings.Pos("/", src, 0);
  490. IF pos > 0 THEN
  491. IF (src[2] = "/") & (src[5] = "/") THEN
  492. test := TRUE;
  493. FOR i := 0 TO 9 DO
  494. IF (i # 2) & (i # 5) THEN
  495. IF Chars.IsDigit(src[i]) = FALSE THEN
  496. test := FALSE;
  497. END;
  498. END;
  499. END;
  500. ELSE
  501. test := FALSE;
  502. END;
  503. END;
  504. END;
  505. RETURN test
  506. END IsDateString;
  507. (* IsTimeString -- return TRUE if the ARRAY OF CHAR has 4 to 8
  508. characters in the form of H:MM, HH:MM, HH:MM:SS where H, M and S
  509. are digits. *)
  510. PROCEDURE IsTimeString*(inline : ARRAY OF CHAR) : BOOLEAN;
  511. VAR
  512. test : BOOLEAN;
  513. l : INTEGER;
  514. src : ARRAY MAXSTR OF CHAR;
  515. BEGIN
  516. Chars.Set(inline, src);
  517. Chars.TrimSpace(src);
  518. (* remove any trailing am/pm suffixes *)
  519. IF Chars.EndsWith("m", src) THEN
  520. IF Chars.EndsWith("am", src) THEN
  521. Chars.TrimSuffix("am", src);
  522. ELSE
  523. Chars.TrimSuffix("pm", src);
  524. END;
  525. Chars.TrimSpace(src);
  526. ELSIF Chars.EndsWith("M", src) THEN
  527. Chars.TrimSuffix("AM", src);
  528. Chars.TrimSuffix("PM", src);
  529. Chars.TrimSpace(src);
  530. ELSIF Chars.EndsWith("p", src) THEN
  531. Chars.TrimSuffix("p", src);
  532. Chars.TrimSpace(src);
  533. ELSIF Chars.EndsWith("P", src) THEN
  534. Chars.TrimSuffix("P", src);
  535. Chars.TrimSpace(src);
  536. ELSIF Chars.EndsWith("a", src) THEN
  537. Chars.TrimSuffix("a", src);
  538. Chars.TrimSpace(src);
  539. ELSIF Chars.EndsWith("A", src) THEN
  540. Chars.TrimSuffix("A", src);
  541. Chars.TrimSpace(src);
  542. END;
  543. Strings.Extract(src, 0, 8, src);
  544. test := FALSE;
  545. l := Strings.Length(src);
  546. IF (l = 4) THEN
  547. IF Chars.IsDigit(src[0]) & (src[1] = ":") &
  548. Chars.IsDigit(src[2]) & Chars.IsDigit(src[3]) THEN
  549. test := TRUE;
  550. ELSE
  551. test := FALSE;
  552. END;
  553. ELSIF (l = 5) THEN
  554. IF Chars.IsDigit(src[0]) & Chars.IsDigit(src[1]) &
  555. (src[2] = ":") &
  556. Chars.IsDigit(src[3]) & Chars.IsDigit(src[4]) THEN
  557. test := TRUE;
  558. ELSE
  559. test := FALSE;
  560. END;
  561. ELSIF (l = 8) THEN
  562. IF Chars.IsDigit(src[0]) & Chars.IsDigit(src[1]) &
  563. (src[2] = ":") &
  564. Chars.IsDigit(src[3]) & Chars.IsDigit(src[4]) &
  565. (src[5] = ":") &
  566. Chars.IsDigit(src[6]) & Chars.IsDigit(src[7]) THEN
  567. test := TRUE;
  568. ELSE
  569. test := FALSE;
  570. END;
  571. ELSE
  572. test := FALSE;
  573. END;
  574. RETURN test
  575. END IsTimeString;
  576. (* ParseDate -- parses a date string in YYYY-MM-DD or
  577. MM/DD/YYYY format. *)
  578. PROCEDURE ParseDate*(inline : ARRAY OF CHAR; VAR year, month, day : INTEGER) : BOOLEAN;
  579. VAR src, tmp : ARRAY MAXSTR OF CHAR; ok, b : BOOLEAN;
  580. BEGIN
  581. Chars.Set(inline, src);
  582. Chars.Clear(tmp);
  583. ok := FALSE;
  584. IF IsDateString(src) THEN
  585. (* FIXME: Need to allow for more than 4 digit years! *)
  586. IF (src[2] = "/") & (src[5] = "/") THEN
  587. ok := TRUE;
  588. Strings.Extract(src, 0, 2, tmp);
  589. Convert.StringToInt(tmp, month, b);
  590. ok := ok & b;
  591. Strings.Extract(src, 4, 2, tmp);
  592. Convert.StringToInt(tmp, day, b);
  593. ok := ok & b;
  594. Strings.Extract(src, 6, 4, tmp);
  595. Convert.StringToInt(tmp, year, b);
  596. ok := ok & b;
  597. ELSIF (src[4] = "-") & (src[7] = "-") THEN
  598. ok := TRUE;
  599. Strings.Extract(src, 0, 4, tmp);
  600. Convert.StringToInt(tmp, year, b);
  601. ok := ok & b;
  602. Strings.Extract(src, 5, 2, tmp);
  603. Convert.StringToInt(tmp, month, b);
  604. ok := ok & b;
  605. Strings.Extract(src, 8, 2, tmp);
  606. Convert.StringToInt(tmp, day, b);
  607. ok := ok & b;
  608. ELSE
  609. ok := FALSE;
  610. END;
  611. END;
  612. RETURN ok
  613. END ParseDate;
  614. (* ParseTime -- procedure for parsing time strings into hour,
  615. minute, second. Returns TRUE on successful parse, FALSE otherwise *)
  616. PROCEDURE ParseTime*(inline : ARRAY OF CHAR; VAR hour, minute, second : INTEGER) : BOOLEAN;
  617. VAR src, tmp : ARRAY MAXSTR OF CHAR; ok : BOOLEAN; cur, pos, l : INTEGER;
  618. BEGIN
  619. Chars.Set(inline, src);
  620. Chars.Clear(tmp);
  621. IF IsTimeString(src) THEN
  622. ok := TRUE;
  623. cur := 0; pos := 0;
  624. pos := Strings.Pos(":", src, cur);
  625. IF pos > 0 THEN
  626. (* Get Hour *)
  627. Strings.Extract(src, cur, pos - cur, tmp);
  628. Convert.StringToInt(tmp, hour, ok);
  629. IF ok THEN
  630. (* Get Minute *)
  631. cur := pos + 1;
  632. Strings.Extract(src, cur, 2, tmp);
  633. Convert.StringToInt(tmp, minute, ok);
  634. IF ok THEN
  635. (* Get second, optional, default to zero *)
  636. pos := Strings.Pos(":", src, cur);
  637. IF pos > 0 THEN
  638. cur := pos + 1;
  639. Strings.Extract(src, cur, 2, tmp);
  640. Convert.StringToInt(tmp, second, ok);
  641. cur := cur + 2;
  642. ELSE
  643. second := 0;
  644. END;
  645. (* Get AM/PM, optional, adjust hour if PM *)
  646. l := Strings.Length(src);
  647. WHILE (cur < l) & Chars.IsSpace(src[cur]) DO
  648. cur := cur + 1;
  649. END;
  650. Strings.Extract(src, cur, 2, tmp);
  651. Chars.TrimSpace(tmp);
  652. IF Chars.Equal(tmp, "PM") OR Chars.Equal(tmp, "pm") THEN
  653. hour := hour + 12;
  654. END;
  655. ELSE
  656. ok := FALSE;
  657. END;
  658. END;
  659. ELSE
  660. ok := FALSE;
  661. END;
  662. ELSE
  663. ok := FALSE;
  664. END;
  665. IF ok THEN
  666. ok := ((hour >= 0) & (hour <= 23)) &
  667. ((minute >= 0) & (minute <= 59)) &
  668. ((second >= 0) & (second <= 59));
  669. END;
  670. RETURN ok
  671. END ParseTime;
  672. (* Parse accepts a date array of chars in either dates, times
  673. or dates and times separate by spaces. Date formats supported
  674. include YYYY-MM-DD, MM/DD/YYYY. Time formats include
  675. H:MM, HH:MM, H:MM:SS, HH:MM:SS with 'a', 'am', 'p', 'pm'
  676. suffixes. Dates and times can also be accepted as JSON
  677. expressions with the individual time compontents are specified
  678. as attributes, e.g. `{"year": 1998, "month": 12, "day": 10,
  679. "hour": 11, "minute": 4, "second": 3}.
  680. Parse returns TRUE on successful parse, FALSE otherwise.
  681. BUG: Assumes a 4 digit year.
  682. *)
  683. PROCEDURE Parse*(inline : ARRAY OF CHAR; VAR dt: DateTime) : BOOLEAN;
  684. VAR src, ds, ts, tmp : ARRAY SHORTSTR OF CHAR; ok, okDate, okTime : BOOLEAN;
  685. pos, year, month, day, hour, minute, second : INTEGER;
  686. BEGIN
  687. dt.year := 0;
  688. dt.month := 0;
  689. dt.day := 0;
  690. dt.hour := 0;
  691. dt.minute := 0;
  692. dt.second := 0;
  693. Chars.Clear(tmp);
  694. Chars.Set(inline, src);
  695. Chars.TrimSpace(src);
  696. (* Split into Date and Time components *)
  697. pos := Strings.Pos(" ", src, 0);
  698. IF pos >= 0 THEN
  699. Strings.Extract(src, 0, pos, ds);
  700. pos := pos + 1;
  701. Strings.Extract(src, pos, Strings.Length(src) - pos, ts);
  702. ELSE
  703. Chars.Set(src, ds);
  704. Chars.Set(src, ts);
  705. END;
  706. ok := FALSE;
  707. IF IsDateString(ds) THEN
  708. ok := TRUE;
  709. okDate := ParseDate(ds, year, month, day);
  710. SetDate(year, month, day, dt);
  711. ok := ok & okDate;
  712. END;
  713. IF IsTimeString(ts) THEN
  714. ok := ok OR okDate;
  715. okTime := ParseTime(ts, hour, minute, second);
  716. SetTime(hour, minute, second, dt);
  717. ok := ok & okTime;
  718. END;
  719. RETURN ok
  720. END Parse;
  721. BEGIN
  722. Chars.Set("January", Months[0]);
  723. Chars.Set("February", Months[1]);
  724. Chars.Set("March", Months[2]);
  725. Chars.Set("April", Months[3]);
  726. Chars.Set("May", Months[4]);
  727. Chars.Set("June", Months[5]);
  728. Chars.Set("July", Months[6]);
  729. Chars.Set("August", Months[7]);
  730. Chars.Set("September", Months[8]);
  731. Chars.Set("October", Months[9]);
  732. Chars.Set("November", Months[10]);
  733. Chars.Set("December", Months[11]);
  734. Chars.Set("Sunday", Days[0]);
  735. Chars.Set("Monday", Days[1]);
  736. Chars.Set("Tuesday", Days[2]);
  737. Chars.Set("Wednesday", Days[3]);
  738. Chars.Set("Thursday", Days[4]);
  739. Chars.Set("Friday", Days[5]);
  740. Chars.Set("Saturday", Days[6]);
  741. DaysInMonth[0] := 31; (* January *)
  742. DaysInMonth[1] := 28; (* February *)
  743. DaysInMonth[2] := 31; (* March *)
  744. DaysInMonth[3] := 30; (* April *)
  745. DaysInMonth[4] := 31; (* May *)
  746. DaysInMonth[5] := 30; (* June *)
  747. DaysInMonth[6] := 31; (* July *)
  748. DaysInMonth[7] := 31; (* August *)
  749. DaysInMonth[8] := 30; (* September *)
  750. DaysInMonth[9] := 31; (* October *)
  751. DaysInMonth[10] := 30; (* November *)
  752. DaysInMonth[11] := 31; (* December *)
  753. END Dates.