Loading...
Searching...
No Matches
date.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_date Date
3!! Lightweight date and time utilities used by the fpx preprocessor.
4!!
5!! This module provides a compact `datetime` type together with a small set
6!! of date/time operations required by fpx. Its primary purpose is to support
7!! expansion of the predefined macros:
8!!
9!! - `__DATE__`
10!! - `__TIME__`
11!! - `__TIMESTAMP__`
12!!
13!! Rather than providing a complete calendaring framework, this module focuses
14!! on the functionality required by preprocessing tasks:
15!!
16!! - Retrieval of the current local date and time using `date_and_time()`
17!! - Construction of datetime objects from numeric components or strings
18!! - Parsing of commonly encountered date/time representations
19!! - Flexible formatting through `to_string(fmt)`
20!! - Day-of-week computation using Zeller's congruence
21!!
22!! The implementation deliberately remains lightweight and dependency-free.
23!! It is not intended to replace dedicated date/time libraries, but instead
24!! provides exactly the capabilities required by fpx while remaining portable
25!! across standard-conforming Fortran compilers.
26!!
27!! @section date_examples Examples
28!!
29!! 1. Expanding predefined macros:
30!! @code{.f90}
31!! type(datetime) :: dt
32!!
33!! dt = now()
34!!
35!! print *, '__DATE__ -> ', dt%to_string('MMM-dd-yyyy')
36!! print *, '__TIME__ -> ', dt%to_string('HH:mm:ss')
37!! print *, '__TIMESTAMP__ -> ', dt%to_string('ddd-MMM-yyyy HH:mm:ss')
38!! ...
39!! @endcode
40!!
41!! 2. Constructing a datetime from a string:
42!! @code{.f90}
43!! type(datetime) :: build_time
44!!
45!! build_time = datetime('2025-08-12 09:30:00')
46!!
47!! print *, build_time%to_string('ddd-MMM-yyyy')
48!! ...
49!! @endcode
50!!
51!! 3. Constructing a datetime from components:
52!! @code{.f90}
53!! type(datetime) :: epoch
54!!
55!! epoch = datetime(1970, 1, 1)
56!!
57!! print *, epoch%to_string()
58!! ...
59!! @endcode
60!!
61!! 4. Timestamping preprocessing operations:
62!! @code{.f90}
63!! type(datetime) :: dt
64!!
65!! dt = now()
66!! print *, 'Preprocessing started at ', dt%to_string('HH:mm:ss')
67!! ...
68!! @endcode
69module fpx_date
70 use, intrinsic :: iso_fortran_env, only: i1 => int8, i2 => int16
71 implicit none; private
72
73 public :: now
74
75 !> Compact representation of date and time
76 !! Stores all components in minimal integer kinds to reduce memory usage.
77 !! All fields are public for easy access.
78 !! <h2 class="groupheader">Examples</h2>
79 !! @code{.f90}
80 !! type(datetime) :: bt
81 !! bt = datetime('2025-08-12 09:30:00')
82 !! print *, 'build on: ', bt%to_string('ddd-MMM-yyyy')
83 !! ...
84 !! @endcode
85 !! @section datetime_type_remarks Remarks
86 !! This type intentionally provides only the functionality required by fpx.
87 !! It is designed to be compact, portable, and efficient rather than serving
88 !! as a comprehensive date/time framework.
89 !!
90 !! @section datetime_type_constructors Constructors
91 !! Initializes a new instance of the @ref datetime class.
92 !! Two constructor forms are available:
93 !!
94 !! - Construction from numeric components
95 !! - Construction from a character representation
96 !!
97 !! @b Constructor
98 !! @code{.f90}
99 !! type(datetime) function datetime(character(*) string, (optional) character(*) fmt)
100 !! @endcode
101 !!
102 !! @param[in] string
103 !! date as string
104 !! @param[in] fmt
105 !! (optional) date format
106 !!
107 !! @b Examples
108 !! @code{.f90}
109 !! type(datetime) :: d
110 !! d = datetime('2025-08-12 09:30:00')
111 !! ...
112 !! @endcode
113 !!
114 !! @b Constructor
115 !! @code{.f90}
116 !! type(datetime) function datetime((optional) integer year, (optional) integer month, ...)
117 !! @endcode
118 !!
119 !! @param[in] year
120 !! (optional)
121 !! @param[in] month
122 !! (optional)
123 !! @param[in] day
124 !! (optional)
125 !! @param[in] hour
126 !! (optional)
127 !! @param[in] minute
128 !! (optional)
129 !! @param[in] second
130 !! (optional)
131 !! @param[in] millisecond
132 !! (optional)
133 !!
134 !! @b Examples
135 !! @code{.f90}
136 !! type(datetime) :: d
137 !! d = datetime(1970, 1, 1)
138 !! ...
139 !! @endcode
140 !!
141 !! @return The constructed datetime object.
142 !!
143 !! @ingroup group_date
144 type, public :: datetime
145 private
146 integer(i2), public :: year !< Year
147 integer(i1), public :: month !< Month
148 integer(i1), public :: day !< Day
149 integer(i1), public :: hour !< Hour
150 integer(i1), public :: minute !< Minute
151 integer(i1), public :: second !< Second
152 integer(i2), public :: millisecond !< Millisecond
153 contains
154 procedure, pass(this), public :: to_string => datetime_to_string
155 procedure, pass(this), public :: parse => datetime_parse
156 end type
157
158 !> Constructor interface for @ref datetime type
159 !!
160 !! @ingroup group_date
161 interface datetime
162 !! @cond
163 module procedure :: datetime_new, datetime_new_from_string
164 !! @endcond
165 end interface
166
167contains
168
169 !> Constructor
170 elemental function datetime_new(year, month, day, hour, minute, second, millisecond) result(that)
171 integer, intent(in), optional :: year
172 integer, intent(in), optional :: month
173 integer, intent(in), optional :: day
174 integer, intent(in), optional :: hour
175 integer, intent(in), optional :: minute
176 integer, intent(in), optional :: second
177 integer, intent(in), optional :: millisecond
178 type(datetime) :: that
179
180 that%year = 0_i2; if (present(year)) that%year = int(year, kind=i2)
181 that%month = 0_i1; if (present(month)) that%month = int(month, kind=i1)
182 that%day = 0_i1; if (present(day)) that%day = int(day, kind=i1)
183 that%hour = 0_i1; if (present(hour)) that%hour = int(hour, kind=i1)
184 that%minute = 0_i1; if (present(minute)) that%minute = int(minute, kind=i1)
185 that%second = 0_i1; if (present(second)) that%second = int(second, kind=i1)
186 that%millisecond = 0_i2; if (present(millisecond)) that%millisecond = int(millisecond, kind=i2)
187 end function
188
189 elemental function datetime_new_from_string(string, fmt) result(that)
190 character(*), intent(in) :: string
191 character(*), intent(in), optional :: fmt
192 type(datetime) :: that
193
194 if (present(fmt)) then
195 call that%parse(string, fmt)
196 else
197 call that%parse(string)
198 end if
199 end function
200
201 !> Return current local date and time
202 !! Uses intrinsic `date_and_time()` and populates all fields including milliseconds.
203 !! @return the datetime object corresponding to the current time
204 !!
205
206 !! @ingroup group_date
207 function now() result(res)
208 type(datetime) :: res
209 !private
210 integer :: values(9)
211
212 call date_and_time(values=values)
213
214 res%year = int(values(1), kind=i2)
215 res%month = int(values(2), kind=i1)
216 res%day = int(values(3), kind=i1)
217 res%hour = int(values(5), kind=i1)
218 res%minute = int(values(6), kind=i1)
219 res%second = int(values(7), kind=i1)
220 res%millisecond = int(values(8), kind=i2)
221 end function
222
223 !> Returns the day of the week calculated using Zeller's congruence.
224 !! Returned value is an integer scalar in the range [0-6], such that:
225 !! - 0: Sunday
226 !! - 1: Monday
227 !! - 2: Tuesday
228 !! - 3: Wednesday
229 !! - 4: Thursday
230 !! - 5: Friday
231 !! - 6: Saturday
232 !!
233 !! @ingroup group_date
234 pure elemental integer function weekday(this)
235 class(datetime), intent(in) :: this
236 !private
237 integer :: year, month, j, k
238
239 year = this%year
240 month = this%month
241
242 if (month <= 2) then
243 month = month + 12
244 year = year - 1
245 end if
246
247 j = year / 100
248 k = mod(year, 100)
249
250 weekday = mod(this%day + ((month + 1) * 26) / 10 + k + k / 4 + j / 4 + 5 * j, 7) - 1
251
252 if (weekday < 0) weekday = 6
253 end function
254
255 !> Parse date/time from string using common formats
256 !!
257 !! Supports ISO, US, and abbreviated month formats.
258 !! On error, defaults to Unix epoch (1970-01-01 00:00:00)
259 !! Perform conversion to ISO string
260 !! - d: Represents the day of the month as a number from 1 through 31.
261 !! - dd: Represents the day of the month as a number from 01 through 31.
262 !! - ddd: Represents the abbreviated name of the day (Mon, Tues, Wed, etc).
263 !! - dddd: Represents the full name of the day (Monday, Tuesday, etc).
264 !! - h: 12-hour clock hour (e.g. 4).
265 !! - hh: 12-hour clock, with a leading 0 (e.g. 06)
266 !! - H: 24-hour clock hour (e.g. 15)
267 !! - HH: 24-hour clock hour, with a leading 0 (e.g. 22)
268 !! - m: Minutes
269 !! - mm: Minutes with a leading zero
270 !! - M: Month number(eg.3)
271 !! - MM: Month number with leading zero(eg.04)
272 !! - MMM: Abbreviated Month Name (e.g. Dec)
273 !! - MMMM: Full month name (e.g. December)
274 !! - s: Seconds
275 !! - ss: Seconds with leading zero
276 !! - t: Abbreviated AM / PM (e.g. A or P)
277 !! - tt: AM / PM (e.g. AM or PM
278 !! - y: Year, no leading zero (e.g. 2015 would be 15)
279 !! - yy: Year, leading zero (e.g. 2015 would be 015)
280 !! - yyy: Year, (e.g. 2015)
281 !! - yyyy: Year, (e.g. 2015)
282 !!
283 !! @ingroup group_date
284 elemental subroutine datetime_parse(this, string, fmt)
285 class(datetime), intent(inout) :: this
286 character(*), intent(in) :: string
287 character(*), intent(in), optional :: fmt
288 !private
289 integer :: ierr
290 logical :: valid
291 character(256) :: errmsg
292 character(len(string)) :: tmp
293 character(:), allocatable :: dftfmt
294
295 if (present(fmt)) then
296 dftfmt = fmt
297 else
298 if (len_trim(string) == 10) then
299 dftfmt = 'yyyy-MM-dd'
300 else
301 dftfmt = 'yyyy-MM-dd HH:mm:ss'
302 end if
303 end if
304
305 tmp = string
306
307 this%year = 0_i2; this%month = 0_i1; this%day = 0_i1
308 this%hour = 0_i1; this%minute = 0_i1; this%second = 0_i1; this%millisecond = 0_i2
309
310 select case (dftfmt)
311 case ('MMM-dd-yyyy')
312 select case (tmp(:3))
313 case ('Jan'); tmp(:3) = ' 01'
314 case ('Feb'); tmp(:3) = ' 02'
315 case ('Mar'); tmp(:3) = ' 03'
316 case ('Apr'); tmp(:3) = ' 04'
317 case ('May'); tmp(:3) = ' 05'
318 case ('Jun'); tmp(:3) = ' 06'
319 case ('Jul'); tmp(:3) = ' 07'
320 case ('Aug'); tmp(:3) = ' 08'
321 case ('Sep'); tmp(:3) = ' 09'
322 case ('Oct'); tmp(:3) = ' 10'
323 case ('Nov'); tmp(:3) = ' 11'
324 case ('Dec'); tmp(:3) = ' 12'
325 end select
326 read(tmp(2:), '(i2.2,1x,i2.2,1x,i4.4)', iostat=ierr, iomsg=errmsg) &
327 this%month, &
328 this%day, &
329 this%year
330 case ('MMM-dd-yyyy HH:mm:ss', 'MMM-dd-yyyyTHH:mm:ss')
331 select case (tmp(:3))
332 case ('Jan'); tmp(:3) = ' 01'
333 case ('Feb'); tmp(:3) = ' 02'
334 case ('Mar'); tmp(:3) = ' 03'
335 case ('Apr'); tmp(:3) = ' 04'
336 case ('May'); tmp(:3) = ' 05'
337 case ('Jun'); tmp(:3) = ' 06'
338 case ('Jul'); tmp(:3) = ' 07'
339 case ('Aug'); tmp(:3) = ' 08'
340 case ('Sep'); tmp(:3) = ' 09'
341 case ('Oct'); tmp(:3) = ' 10'
342 case ('Nov'); tmp(:3) = ' 11'
343 case ('Dec'); tmp(:3) = ' 12'
344 end select
345 read(tmp(2:), '(i2.2,1x,i2.2,1x,i4.4,1x,i2.2,2(1x,i2.2))', iostat=ierr, iomsg=errmsg) &
346 this%month, &
347 this%day, &
348 this%year, &
349 this%hour, &
350 this%minute, &
351 this%second
352 case ('yyyy-MM')
353 read(tmp, '(i4.4,1x,i2.2)', iostat=ierr, iomsg=errmsg) &
354 this%year, &
355 this%month
356 case ('yyyy-MM-dd')
357 read(tmp, '(i4.4,2(1x,i2.2))', iostat=ierr, iomsg=errmsg) &
358 this%year, &
359 this%month, &
360 this%day
361 case ('dd-MM-yyyy')
362 read(tmp, '(i2.2,1x,i2.2,1x, i4.4)', iostat=ierr, iomsg=errmsg) &
363 this%day, &
364 this%month, &
365 this%year
366 case ('MM-dd-yyyy')
367 read(tmp, '(i2.2,1x,i2.2,1x,i4.4)', iostat=ierr, iomsg=errmsg) &
368 this%month, &
369 this%day, &
370 this%year
371 case ('yyyy-MM-ddTHH:mm:ss', 'yyyy-MM-dd HH:mm:ss')
372 read(tmp, '(i4.4,2(1x,i2.2),1x,i2.2,2(1x,i2.2))', iostat=ierr, iomsg=errmsg) &
373 this%year, &
374 this%month, &
375 this%day, &
376 this%hour, &
377 this%minute, &
378 this%second
379 case ('HH:mm:ss')
380 read(tmp, '(i2.2,2(1x,i2.2))', iostat=ierr, iomsg=errmsg) &
381 this%hour, &
382 this%minute, &
383 this%second
384 end select
385
386 if (ierr > 0) then
387 this%year = 1970_i2; this%month = 1_i1; this%day = 1_i1
388 this%hour = 0_i1; this%minute = 0_i1; this%second = 0_i1; this%millisecond = 0_i2
389 end if
390 end subroutine
391
392 !> Format datetime as string using flexible format codes
393 !! Supports many common patterns including those required for `__DATE__` and `__TIMESTAMP__`.
394 !! Default format: 'yyyy-MM-ddTHH:mm:ss'
395 !!
396 !! @ingroup group_date
397 function datetime_to_string(this, fmt) result(res)
398 class(datetime), intent(in) :: this
399 character(*), intent(in), optional :: fmt
400 character(:), allocatable :: res
401 !private
402 character :: sep, dash
403 character(:), allocatable :: dftfmt, tmp, tmp2
404 integer :: ierr
405 character(256) :: errmsg
406
407 if (present(fmt)) then
408 dftfmt = fmt
409 else
410 dftfmt = 'yyyy-MM-ddTHH:mm:ss'
411 end if
412 ! Manager optional parameters
413 sep = merge('T', ' ', index(dftfmt, 'T') > 0)
414 dash = merge('-', ' ', index(dftfmt, '-') > 0)
415
416 allocate(character(25) :: res)
417 ! Perform conversion to ISO string
418
419 select case (this%month)
420 case (1); tmp = 'Jan'
421 case (2); tmp = 'Feb'
422 case (3); tmp = 'Mar'
423 case (4); tmp = 'Apr'
424 case (5); tmp = 'May'
425 case (6); tmp = 'Jun'
426 case (7); tmp = 'Jul'
427 case (8); tmp = 'Aug'
428 case (9); tmp = 'Sep'
429 case (10); tmp = 'Oct'
430 case (11); tmp = 'Nov'
431 case (12); tmp = 'Dec'
432 end select
433 select case (weekday(this))
434 case (0); tmp2 = 'Sun'
435 case (1); tmp2 = 'Mon'
436 case (2); tmp2 = 'Tue'
437 case (3); tmp2 = 'Wed'
438 case (4); tmp2 = 'Thu'
439 case (5); tmp2 = 'Fri'
440 case (6); tmp2 = 'Sat'
441 end select
442
443 select case (dftfmt)
444 case ('MMM-dd-yyyy', 'MMM dd yyyy')
445 write(res, '(a3,a1,i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
446 tmp, &
447 dash, &
448 this%day, &
449 dash, &
450 this%year
451 case ('MMM-ddd-yyyy', 'MMM ddd yyyy')
452 write(res, '(a3,a1,a3," ",i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
453 tmp, &
454 dash, &
455 tmp2, &
456 this%day, &
457 dash, &
458 this%year
459 case ('MMM-dd-yyyy HH:mm:ss', 'MMM-dd-yyyyTHH:mm:ss', 'MMM dd yyyy HH:mm:ss', 'MMM dd yyyyTHH:mm:ss')
460 write(res, '(a3,a1,i2.2,a1,i4.4,a1,i2.2,2(":",i2.2))', iostat=ierr, iomsg=errmsg) &
461 tmp, &
462 dash, &
463 this%day, &
464 dash, &
465 this%year, &
466 this%hour, &
467 this%minute, &
468 this%second
469 case ('MMM-ddd-yyyy HH:mm:ss', 'MMM-ddd-yyyyTHH:mm:ss', 'MMM ddd yyyy HH:mm:ss', 'MMM ddd yyyyTHH:mm:ss')
470 write(res, '(a3,a1,a3," ",i2.2,a1,i4.4,a1,i2.2,2(":",i2.2))', iostat=ierr, iomsg=errmsg) &
471 tmp, &
472 dash, &
473 tmp2, &
474 this%day, &
475 dash, &
476 this%year, &
477 this%hour, &
478 this%minute, &
479 this%second
480 case ('yyyy-MM', 'yyyy MM')
481 write(res, '(i4.4,a1,i2.2)', iostat=ierr, iomsg=errmsg) &
482 this%year, &
483 dash, &
484 this%month
485 case ('yyyy-MM-dd', 'yyyy MM dd')
486 write(res, '(i4.4,2(a1,i2.2))', iostat=ierr, iomsg=errmsg) &
487 this%year, &
488 dash, &
489 this%month, &
490 dash, &
491 this%day
492 case ('yyyy-MM-ddd', 'yyyy MM ddd')
493 write(res, '(i4.4,a1,i2.2,a1,a3," ",i2.2)', iostat=ierr, iomsg=errmsg) &
494 this%year, &
495 dash, &
496 this%month, &
497 dash, &
498 tmp2, &
499 this%day
500 case ('dd-MM-yyyy', 'dd MM yyyy')
501 write(res, '(i2.2,a1,i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
502 this%day, &
503 dash, &
504 this%month, &
505 dash, &
506 this%year
507 case ('ddd-MM-yyyy', 'ddd MM yyyy')
508 write(res, '(a3,a1,i2.2," ",i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
509 tmp2, &
510 dash, &
511 this%month, &
512 this%day, &
513 dash, &
514 this%year
515 case ('MM-dd-yyyy', 'MM dd yyyy')
516 write(res, '(i2.2,a1,i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
517 this%month, &
518 dash, &
519 this%day, &
520 dash, &
521 this%year
522 case ('MM-ddd-yyyy', 'MM ddd yyyy')
523 write(res, '(i2.2,a1,a3," ",i2.2,a1,i4.4)', iostat=ierr, iomsg=errmsg) &
524 this%month, &
525 dash, &
526 tmp2, &
527 this%day, &
528 dash, &
529 this%year
530 case ('yyyy-MM-ddTHH:mm:ss', 'yyyy-MM-dd HH:mm:ss', 'yyyy MM ddTHH:mm:ss', 'yyyy MM dd HH:mm:ss')
531 write(res, '(i4.4,2(a1,i2.2),a1,i2.2,2(":",i2.2))', iostat=ierr, iomsg=errmsg) &
532 this%year, &
533 dash, &
534 this%month, &
535 dash, &
536 this%day, &
537 sep, &
538 this%hour, &
539 this%minute, &
540 this%second
541 case ('yyyy-MM-dddTHH:mm:ss', 'yyyy-MM-ddd HH:mm:ss', 'yyyy MM dddTHH:mm:ss', 'yyyy MM ddd HH:mm:ss')
542 write(res, '(i4.4,a1,i2.2,a1,a3," ",i2.2,a1,i2.2,2(":",i2.2))', iostat=ierr, iomsg=errmsg) &
543 this%year, &
544 dash, &
545 this%month, &
546 dash, &
547 tmp2, &
548 this%day, &
549 sep, &
550 this%hour, &
551 this%minute, &
552 this%second
553 case ('HH:mm:ss')
554 write(res, '(i2.2,2(":",i2.2))', iostat=ierr, iomsg=errmsg) &
555 this%hour, &
556 this%minute, &
557 this%second
558 end select
559 res = trim(res)
560 end function
561end module
Compact representation of date and time Stores all components in minimal integer kinds to reduce memo...
Definition date.f90:144