@@ -7,11 +7,13 @@ module string_core_utils
77 public :: core_int_date_to_yyyymmdd ! Convert encoded date integer to "yyyy-mm-dd" format
88 public :: core_int_seconds_to_hhmmss ! Convert integer seconds past midnight to "hh:mm:ss" format
99 public :: split ! Parse a string into tokens, one at a time
10- public :: stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing
10+ public :: stringify ! Convert one or more values of any intrinsic data types to a character string.
1111 public :: tokenize ! Parse a string into tokens
1212 public :: increment_string ! Increment a string whose ending characters are digits.
1313 public :: last_non_digit ! Get position of last non-digit in the input string.
1414 public :: get_last_significant_char ! Get position of last significant (non-blank, non-null) character in string.
15+ public :: core_glob_match ! Match a string against a '*'-wildcard glob pattern.
16+ public :: core_glob_list_excluded ! First-match-wins exclusion decision over an ordered glob pattern list ('!' keeps).
1517
1618 interface tokenize
1719 module procedure tokenize_into_first_last
@@ -20,16 +22,16 @@ module string_core_utils
2022
2123contains
2224
23- character (len= 10 ) pure function core_to_str(n)
25+ character (len= 10 ) pure function core_to_str(n) result(str)
2426 ! return default integer as a left justified string
2527
2628 integer , intent (in ) :: n
27-
28- write (core_to_str ,' (i0)' ) n
29-
29+
30+ write (str ,' (i0)' ) n
31+
3032 end function core_to_str
3133
32- character (len= 10 ) pure function core_int_date_to_yyyymmdd (date)
34+ character (len= 10 ) pure function core_int_date_to_yyyymmdd (date) result(date_str)
3335 ! Undefined behavior if date <= 0
3436
3537 ! Input arguments
@@ -44,12 +46,12 @@ character(len=10) pure function core_int_date_to_yyyymmdd (date)
4446 month = (date - year* 10000 ) / 100
4547 day = date - year* 10000 - month* 100
4648
47- write (core_int_date_to_yyyymmdd , ' (i4.4,A,i2.2,A,i2.2)' ) &
48- year,' -' ,month,' -' ,day
49+ write (date_str , ' (i4.4,A,i2.2,A,i2.2)' ) &
50+ year,' -' ,month,' -' ,day
4951
5052 end function core_int_date_to_yyyymmdd
5153
52- character (len= 8 ) pure function core_int_seconds_to_hhmmss (seconds)
54+ character (len= 8 ) pure function core_int_seconds_to_hhmmss (seconds) result(time_str)
5355 ! Undefined behavior if seconds outside [0, 86400]
5456
5557 ! Input arguments
@@ -64,8 +66,8 @@ character(len=8) pure function core_int_seconds_to_hhmmss (seconds)
6466 minutes = (seconds - hours* 3600 ) / 60
6567 secs = (seconds - hours* 3600 - minutes* 60 )
6668
67- write (core_int_seconds_to_hhmmss ,' (i2.2,A,i2.2,A,i2.2)' ) &
68- hours,' :' ,minutes,' :' ,secs
69+ write (time_str ,' (i2.2,A,i2.2,A,i2.2)' ) &
70+ hours,' :' ,minutes,' :' ,secs
6971
7072 end function core_int_seconds_to_hhmmss
7173
@@ -116,12 +118,12 @@ end subroutine split
116118 ! > If `value` contains zero element or is of unsupported data types, an empty character string is produced.
117119 ! > If `separator` is not supplied, it defaults to ", " (i.e., a comma and a space).
118120 ! > (KCW, 2024-02-04)
119- pure function stringify (value , separator )
121+ pure function stringify (value , separator ) result(str)
120122 use , intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
121123
122124 class(* ), intent (in ) :: value(:)
123125 character (* ), optional , intent (in ) :: separator
124- character (:), allocatable :: stringify
126+ character (:), allocatable :: str
125127
126128 integer , parameter :: sizelimit = 1024
127129
@@ -138,7 +140,7 @@ pure function stringify(value, separator)
138140 n = min (size (value), sizelimit)
139141
140142 if (n == 0 ) then
141- stringify = ' '
143+ str = ' '
142144
143145 return
144146 end if
@@ -220,12 +222,12 @@ pure function stringify(value, separator)
220222
221223 write (buffer, format ) value
222224 class default
223- stringify = ' '
225+ str = ' '
224226
225227 return
226228 end select
227229
228- stringify = trim (buffer)
230+ str = trim (buffer)
229231 end function stringify
230232
231233 ! > Parse a string into tokens. Each character in `set` is a token delimiter.
@@ -305,7 +307,7 @@ end subroutine tokenize_into_tokens_separator
305307 ! 0 success
306308 ! -1 error: no trailing digits in string
307309 ! -2 error: incremented integer is out of range
308- integer function increment_string (s , inc )
310+ integer function increment_string (s , inc ) result(status)
309311 integer , intent (in ) :: inc ! value to increment string (may be negative)
310312 character (len=* ), intent (inout ) :: s ! string with trailing digits
311313
@@ -323,7 +325,7 @@ integer function increment_string(s, inc)
323325 ndigit = lstr - lnd
324326
325327 if (ndigit == 0 ) then
326- increment_string = - 1
328+ status = - 1
327329 return
328330 end if
329331
@@ -338,46 +340,46 @@ integer function increment_string(s, inc)
338340
339341 ! Increment the integer
340342 ival = ival + inc
341- if ( ival < 0 .or. ival > 10 ** ndigit-1 ) then
342- increment_string = - 2
343+ if (ival < 0 .or. ival > 10 ** ndigit-1 ) then
344+ status = - 2
343345 return
344346 end if
345347
346348 ! Overwrite trailing digits
347349 pow = ndigit
348350 do i = lnd+1 ,lstr
349- digit = MOD ( ival,10 ** pow ) / 10 ** (pow-1 )
350- s(i:i) = CHAR ( ICHAR (' 0' ) + digit )
351+ digit = MOD (ival,10 ** pow) / 10 ** (pow-1 )
352+ s(i:i) = CHAR (ICHAR (' 0' ) + digit)
351353 pow = pow - 1
352354 end do
353355
354- increment_string = 0
356+ status = 0
355357
356358 end function increment_string
357359
358360 ! Get position of last non-digit in the input string.
359361 ! Return values:
360362 ! > 0 => position of last non-digit
361363 ! = 0 => token is all digits (or empty)
362- integer pure function last_non_digit(s)
364+ integer pure function last_non_digit(s) result(pos)
363365 character (len=* ), intent (in ) :: s
364366 integer :: n, nn, digit
365367
366368 n = get_last_significant_char(s)
367369 if (n == 0 ) then ! empty string
368- last_non_digit = 0
370+ pos = 0
369371 return
370372 end if
371373
372374 do nn = n,1 ,- 1
373375 digit = ICHAR (s(nn:nn)) - ICHAR (' 0' )
374376 if ( digit < 0 .or. digit > 9 ) then
375- last_non_digit = nn
377+ pos = nn
376378 return
377379 end if
378380 end do
379381
380- last_non_digit = 0 ! all characters are digits
382+ pos = 0 ! all characters are digits
381383
382384 end function last_non_digit
383385
@@ -386,13 +388,13 @@ end function last_non_digit
386388 ! Return values:
387389 ! > 0 => position of last significant character
388390 ! = 0 => no significant characters in string
389- integer pure function get_last_significant_char(cs)
391+ integer pure function get_last_significant_char(cs) result(pos)
390392 character (len=* ), intent (in ) :: cs ! Input character string
391393 integer :: l, n
392394
393395 l = LEN (cs)
394396 if ( l == 0 ) then
395- get_last_significant_char = 0
397+ pos = 0
396398 return
397399 end if
398400
@@ -401,8 +403,97 @@ integer pure function get_last_significant_char(cs)
401403 exit
402404 end if
403405 end do
404- get_last_significant_char = n
406+ pos = n
405407
406408 end function get_last_significant_char
407409
410+ ! > Match `string` against a glob `pattern` in which `*` matches any run of
411+ ! > characters, including an empty one; every other character, including `?`,
412+ ! > matches only itself. Trailing blanks in both arguments are not significant
413+ ! > (leading and embedded blanks are). An empty pattern matches only an empty
414+ ! > string.
415+ pure logical function core_glob_match(string, pattern) result(is_match)
416+ character (len=* ), intent (in ) :: string
417+ character (len=* ), intent (in ) :: pattern
418+
419+ integer :: ls, lp ! significant lengths of string/pattern
420+ integer :: s, p ! current positions in string/pattern
421+ integer :: star_p ! position of the most recent '*' in pattern (0 = none seen)
422+ integer :: star_s ! string position currently tried as that star's first unmatched character
423+
424+ ls = len_trim (string)
425+ lp = len_trim (pattern)
426+
427+ s = 1
428+ p = 1
429+ star_p = 0
430+ star_s = 0
431+
432+ do while (s <= ls)
433+ if (p <= lp) then
434+ if (pattern(p:p) == ' *' ) then
435+ ! Record the star and first try matching it to nothing
436+ star_p = p
437+ star_s = s
438+ p = p + 1
439+ cycle
440+ else if (pattern(p:p) == string (s:s)) then
441+ p = p + 1
442+ s = s + 1
443+ cycle
444+ end if
445+ end if
446+ ! Mismatch: backtrack to the most recent star and extend its match
447+ ! by one character; with no star to extend, the match fails.
448+ if (star_p > 0 ) then
449+ star_s = star_s + 1
450+ s = star_s
451+ p = star_p + 1
452+ else
453+ is_match = .false.
454+ return
455+ end if
456+ end do
457+
458+ ! String fully consumed; the pattern matches if only stars remain
459+ do while (p <= lp)
460+ if (pattern(p:p) /= ' *' ) exit
461+ p = p + 1
462+ end do
463+ is_match = (p > lp)
464+
465+ end function core_glob_match
466+
467+ ! > Decide whether `name` is excluded by an ordered list of glob `patterns`
468+ ! > (see `core_glob_match`). Patterns are evaluated in order and the FIRST
469+ ! > pattern whose glob matches decides: a pattern with a leading `!` keeps
470+ ! > the name (not excluded), any other pattern excludes it. A name matching
471+ ! > no pattern is not excluded; blank patterns are skipped, so fixed-size
472+ ! > namelist arrays can be passed directly. This enables gitignore-style
473+ ! > lists such as ['!aero_post*', 'aero_*'], which excludes the `aero_`
474+ ! > names except those beginning with `aero_post`.
475+ pure logical function core_glob_list_excluded(name, patterns) result(excluded)
476+ character (len=* ), intent (in ) :: name
477+ character (len=* ), intent (in ) :: patterns(:)
478+
479+ integer :: i
480+
481+ excluded = .false.
482+ do i = 1 , size (patterns)
483+ if (len_trim (patterns(i)) == 0 ) cycle
484+ if (patterns(i)(1 :1 ) == ' !' ) then
485+ if (core_glob_match(name, patterns(i)(2 :))) then
486+ ! Keep-verb: a match means the name stays compared
487+ return
488+ end if
489+ else
490+ if (core_glob_match(name, patterns(i))) then
491+ excluded = .true.
492+ return
493+ end if
494+ end if
495+ end do
496+
497+ end function core_glob_list_excluded
498+
408499end module string_core_utils
0 commit comments