Dates and Clock

By R. S. Doiel, 2020-11-27

The Oakwood guidelines specified a common set of modules for Oberon-2 for writing programs outside of an Oberon System. A missing module from the Oakwood guidelines is modules for working with dates and the system clock. Fortunately the A2 Oberon System1 provides a template for that functionality. In this article I am exploring implementing the Dates and Clock modules for Oberon-07. I also plan to go beyond the A2 implementations and provide additional functionality such as parsing procedures and the ability to work with either the date or time related attributes separately in the Dates.DateTime record.

Divergences

One of the noticeable differences between Oberon-07 and Active Oberon is the types that functional procedures can return. We cannot return an Object in Oberon-07. This is not much of a handicap as we have variable procedure parameters. Likewise Active Oberon provides a large variety of integer number types. In Oberon-07 we have only INTEGER. Where I’ve create new procedures I’ve used the Oberon idiom of read only input parameters followed by variable parameters with side effects and finally parameters for the target record or values to be updated.

Similarities

In spite of the divergence I have split the module into two. The Dates module is the one you would include in your program, it provides a DateTime record type which holds the integer values for year, month, day, hour, minute and second. It provides the means of parsing a date or time string, comparison, difference and addition of dates. The second module Clock provides a mechanism to retrieve the real time clock value from the host system and map the C based time object into our own DateTime record. Clock is specific to OBNC method of interfacing to the C standard libraries of the host system. If you were to use a different Oberon compiled such as the Oxford Oberon Compiler you would need to re-implement Clock. Dates itself should be system independent and work with Oberon-07 compilers generally.

Clock

The Clock module is built from a skeleton in Oberon-07 describing the signatures of the procedure and an implementation in C that is built using the technique for discussed in my post Combining Oberon-07 and C with OBNC. In that article I outline Karl’s three step process to create a module that will be an interface to C code. In Step one I create the Oberon module. Normally I’d leave all procedures empty and develop them in C. In this specific case I went ahead and wrote the procedure called Get in Oberon and left the procedure GetRtcTime blank. This allowed OBNC to generate the C code for Get saving me some time and create the skeleton for GetRtcTime which does the work interfacing with the system clock via C library calls.

The interface Oberon module looked like this:

  1. MODULE Clock;
  2. PROCEDURE GetRtcTime*(VAR second, minute, hour, day, month, year : INTEGER);
  3. BEGIN
  4. END GetRtcTime;
  5. PROCEDURE Get*(VAR time, date : INTEGER);
  6. VAR
  7. second, minute, hour, day, month, year : INTEGER;
  8. BEGIN
  9. GetRtcTime(second, minute, hour, day, month, year);
  10. time = ((hour * 4096) + (minute * 64)) + second;
  11. date = ((year * 512) + (month * 32)) + day;
  12. END Get;
  13. END Clock.

I wrote the Get procedure code in Oberon-07 is the OBNC compiler will render the Oberon as C during the compilation process. I save myself writing some C code in by leveraging OBNC.

Step two was to write ClockTest.Mod in Oberon-07.

  1. MODULE ClockTest;
  2. IMPORT Tests, Chars, Clock; (* , Out; *)
  3. CONST
  4. MAXSTR = Chars.MAXSTR;
  5. VAR
  6. title : ARRAY MAXSTR OF CHAR;
  7. success, errors : INTEGER;
  8. PROCEDURE TestGetRtcTime() : BOOLEAN;
  9. VAR second, minute, hour, day, month, year : INTEGER;
  10. test, expected, result: BOOLEAN;
  11. BEGIN
  12. test := TRUE;
  13. second := 0; minute := 0; hour := 0;
  14. day := 0; month := 0; year := 0;
  15. expected := TRUE;
  16. Clock.GetRtcTime(second, minute, hour, day, month, year);
  17. result := (year > 1900);
  18. Tests.ExpectedBool(expected, result,
  19. "year should be greater than 1900", test);
  20. result := (month >= 0) & (month <= 11);
  21. Tests.ExpectedBool(expected, result,
  22. "month should be [0, 11]", test);
  23. result := (day >= 1) & (day <= 31);
  24. Tests.ExpectedBool(expected, result,
  25. "day should be non-zero", test);
  26. result := (hour >= 0) & (hour <= 23);
  27. Tests.ExpectedBool(expected, result,
  28. "hour should be [0, 23]", test);
  29. result := (minute >= 0) & (minute <= 59);
  30. Tests.ExpectedBool(expected, result,
  31. "minute should be [0, 59]", test);
  32. result := (second >= 0) & (second <= 60);
  33. Tests.ExpectedBool(expected, result,
  34. "second year should be [0,60]", test);
  35. RETURN test
  36. END TestGetRtcTime;
  37. PROCEDURE TestGet() : BOOLEAN;
  38. VAR time, date : INTEGER;
  39. test, expected, result : BOOLEAN;
  40. BEGIN
  41. test := TRUE;
  42. time := 0;
  43. date := 0;
  44. Clock.Get(time, date);
  45. expected := TRUE;
  46. result := (time > 0);
  47. Tests.ExpectedBool(expected, result,
  48. "time should not be zero", test);
  49. result := (date > 0);
  50. Tests.ExpectedBool(expected, result,
  51. "date should not be zero", test);
  52. RETURN test
  53. END TestGet;
  54. BEGIN
  55. Chars.Set("Clock module test", title);
  56. success := 0; errors := 0;
  57. Tests.Test(TestGetRtcTime, success, errors);
  58. Tests.Test(TestGet, success, errors);
  59. Tests.Summarize(title, success, errors);
  60. END ClockTest.

ClockTest is a simple test module for Clock.Mod. It also serves the role when compiled with OBNC to create the template C code for Clock.c. Here’s the steps we take to generate Clock.c with OBNC:

  1. obnc ClockTest.Mod
  2. mv .obnc/Clock.c ./
  3. vi Clock.c

After compiling .obnc/Clock.c I then moved .obnc/Clock.c to my working directory. Filled in the C version of GetRtcTime function and modified my Clock.Mod to contain my empty procedure.

The finally version of Clock.c looks like (note how we need to include “Clock.h” in the head of the our C source file).

  1. /*GENERATED BY OBNC 0.16.1*/
  2. #include ".obnc/Clock.h"
  3. #include <obnc/OBNC.h>
  4. #include <time.h>
  5. #define OBERON_SOURCE_FILENAME "Clock.Mod"
  6. void Clock__GetRtcTime_(OBNC_INTEGER *second_, OBNC_INTEGER *minute_,
  7. OBNC_INTEGER *hour_, OBNC_INTEGER *day_,
  8. OBNC_INTEGER *month_, OBNC_INTEGER *year_)
  9. {
  10. time_t now;
  11. struct tm *time_info;
  12. now = time(NULL);
  13. time_info = localtime(&now);
  14. *second_ = time_info->tm_sec;
  15. *minute_ = time_info->tm_min;
  16. *hour_ = time_info->tm_hour;
  17. *day_ = time_info->tm_mday;
  18. *month_ = time_info->tm_mon;
  19. *year_ = (time_info->tm_year) + 1900;
  20. }
  21. void Clock__Get_(OBNC_INTEGER *time_, OBNC_INTEGER *date_)
  22. {
  23. OBNC_INTEGER second_, minute_, hour_, day_, month_, year_;
  24. Clock__GetRtcTime_(&second_, &minute_,
  25. &hour_, &day_, &month_, &year_);
  26. (*time_) = ((hour_ * 4096) + (minute_ * 64)) + second_;
  27. (*date_) = ((year_ * 512) + (month_ * 32)) + day_;
  28. }
  29. void Clock__Init(void)
  30. {
  31. }

The final version of Clock.Mod looks like

  1. MODULE Clock;
  2. PROCEDURE GetRtcTime*(VAR second, minute,
  3. hour, day, month, year : INTEGER);
  4. BEGIN
  5. END GetRtcTime;
  6. PROCEDURE Get*(VAR time, date : INTEGER);
  7. BEGIN
  8. END Get;
  9. END Clock.

Step three was to re-compile ClockTest.Mod and run the tests.

  1. obnc ClockTest.Mod
  2. ./ClockTest

Dates

The dates module provides a rich variety of procedures for working with dates. This includes parsing date strings into DateTime records, testing strings for supported date formats, setting dates or time in a DateTime record as well as comparison, difference and addition (both addition and subtraction) of dates. Tests for the Dates module is implemented in DatesTest.Mod.

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

Postscript: In this article I included a reference to the module Chars. This is a non-standard module I wrote for Oberon-07. Here is a link to Chars. RSD, 2021-05-06

Next, Previous


  1. A2 information can be found in the Oberon wikibook↩︎