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