From c4002ea8bfa3f11ad263966be6bb6dfca668f1a4 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Wed, 29 Oct 2025 10:41:04 -0600 Subject: [PATCH 01/11] Add missing dates --- src/dynamics/mpas/dyn_procedures.F90 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dynamics/mpas/dyn_procedures.F90 b/src/dynamics/mpas/dyn_procedures.F90 index cc014fc04..2127da915 100644 --- a/src/dynamics/mpas/dyn_procedures.F90 +++ b/src/dynamics/mpas/dyn_procedures.F90 @@ -74,6 +74,7 @@ pure elemental function t_by_equation_of_state(constant_r, p, rho) result(t) end function t_by_equation_of_state !> Compute the Exner function `pi` from the pressure `p`. Essentially, \( \Pi = (\frac{P}{P_0})^{\frac{R_d}{C_{pd}}} \). + !> (KCW, 2025-07-10) pure elemental function exner_function_of_cpd_p0_rd_p(constant_cpd, constant_p0, constant_rd, p) result(pi) use, intrinsic :: iso_fortran_env, only: real64 @@ -84,6 +85,7 @@ pure elemental function exner_function_of_cpd_p0_rd_p(constant_cpd, constant_p0, end function exner_function_of_cpd_p0_rd_p !> Compute the Exner function `pi` from the pressure `p`. Essentially, \( \Pi = (\frac{P}{P_0})^{\kappa} \). + !> (KCW, 2025-08-16) pure elemental function exner_function_of_kappa_p0_p(constant_kappa, constant_p0, p) result(pi) use, intrinsic :: iso_fortran_env, only: real64 From b360c7b97e5deee666bb54e0edec35f94934a9ef Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 24 Jul 2025 14:37:04 -0600 Subject: [PATCH 02/11] Implement `split` and `tokenize` from Fortran 2023 --- .../mpas/driver/dyn_mpas_procedures.F90 | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 b/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 index 4c3178fdd..f236d90e8 100644 --- a/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 +++ b/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 @@ -21,7 +21,9 @@ module dyn_mpas_procedures public :: almost_equal public :: clamp public :: index_unique + public :: split public :: stringify + public :: tokenize interface almost_divisible module procedure almost_divisible_real32 @@ -39,6 +41,11 @@ module dyn_mpas_procedures module procedure clamp_real32 module procedure clamp_real64 end interface clamp + + interface tokenize + module procedure tokenize_into_first_last + module procedure tokenize_into_tokens_separator + end interface tokenize contains !> Test if `a` is divisible by `b`, where `a` and `b` are both reals. !> (KCW, 2024-05-25) @@ -297,6 +304,38 @@ pure function index_unique(array) index_unique = pack([(i, i = 1, n)], mask_unique) end function index_unique + !> Parse a string into tokens, one at a time. This subroutine implements the `split` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.196). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine split(string, set, pos, back) + character(*), intent(in) :: string, set + integer, intent(inout) :: pos + logical, optional, intent(in) :: back + + integer :: offset + + if (present(back)) then + if (back) then + offset = clamp(pos, 1, len(string) + 1) + pos = scan(string(1:offset - 1), set, back=.true.) + + return + end if + end if + + offset = clamp(pos, 0, len(string)) + pos = scan(string(offset + 1:), set) + + if (pos == 0) then + pos = len(string) + 1 + + return + end if + + pos = offset + pos + end subroutine split + !> Convert one or more values of any intrinsic data types to a character string for pretty printing. !> If `value` contains more than one element, the elements will be stringified, delimited by `separator`, then concatenated. !> If `value` contains exactly one element, the element will be stringified without using `separator`. @@ -407,4 +446,65 @@ pure function stringify(value, separator) stringify = trim(buffer) end function stringify + + !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine tokenize_into_first_last(string, set, first, last) + character(*), intent(in) :: string, set + integer, allocatable, intent(out) :: first(:), last(:) + + integer :: pos_start(len(string) + 1), pos_end(len(string) + 1) + integer :: l, n, pos + + l = len(string) + n = 0 + pos = 0 + + do while (pos < l + 1) + n = n + 1 + pos_start(n) = pos + 1 + + call split(string, set, pos) + + pos_end(n) = pos - 1 + end do + + allocate(first(n), last(n)) + + first(:) = pos_start(1:n) + last(:) = pos_end(1:n) + end subroutine tokenize_into_first_last + + !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine tokenize_into_tokens_separator(string, set, tokens, separator) + character(*), intent(in) :: string, set + character(:), allocatable, intent(out) :: tokens(:) + character(:), allocatable, optional, intent(out) :: separator(:) + + integer, allocatable :: first(:), last(:) + integer :: i, n + + call tokenize(string, set, first, last) + + n = size(first) + + allocate(character(maxval(last - first) + 1) :: tokens(n)) + + do i = 1, n + tokens(i) = string(first(i):last(i)) + end do + + if (present(separator)) then + allocate(character(1) :: separator(n - 1)) + + do i = 1, n - 1 + separator(i) = string(last(i) + 1:last(i) + 1) + end do + end if + end subroutine tokenize_into_tokens_separator end module dyn_mpas_procedures From 204442657f9d7d4d25e5583bf2fa527efe657b75 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 11 Sep 2025 02:34:59 -0600 Subject: [PATCH 03/11] Implement unit tests for `split` and `tokenize` --- .../tests/unit/test_dyn_mpas_procedures.pf | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf index ea8721113..53d1c4fed 100644 --- a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf +++ b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf @@ -38,6 +38,14 @@ module test_dyn_mpas_procedures public :: test_index_unique_by_logical_arrays public :: test_index_unique_by_real_arrays_real32 public :: test_index_unique_by_real_arrays_real64 + public :: test_split_by_empty_string_set + public :: test_split_by_forward_searches + public :: test_split_by_backward_searches + public :: test_split_by_out_of_bounds_searches + public :: test_tokenize_into_first_last_by_empty_string_set + public :: test_tokenize_into_first_last_by_known_string_set + public :: test_tokenize_into_tokens_separator_by_empty_string_set + public :: test_tokenize_into_tokens_separator_by_known_string_set contains @test subroutine test_almost_divisible_by_properties_real32() @@ -1052,4 +1060,336 @@ contains test_result = index_unique(-test_data - 10.0_real64) @assertEqual(expected_result, test_result) end subroutine test_index_unique_by_real_arrays_real64 + + @test + subroutine test_split_by_empty_string_set() + use dyn_mpas_procedures, only: split + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + + integer :: pos + + ! Test `split` by empty `string`. + + pos = 0 + call split('', test_set_comma, pos) + @assertEqual(1, pos) + + ! Test `split` by empty `string` and optional `back`. + + pos = 1 + call split('', test_set_comma, pos, back=.true.) + @assertEqual(0, pos) + + ! Test `split` by empty `set`. + + pos = 0 + call split(test_string_comma_separated, '', pos) + @assertEqual(66, pos) + + ! Test `split` by empty `set` and optional `back`. + + pos = 66 + call split(test_string_comma_separated, '', pos, back=.true.) + @assertEqual(0, pos) + end subroutine test_split_by_empty_string_set + + @test + subroutine test_split_by_forward_searches() + use dyn_mpas_procedures, only: split + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_pos_comma_separated(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + integer, parameter :: expected_pos_comma_space_separated(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + + integer :: i, pos + + pos = 0 + + do i = 1, size(expected_pos_comma_separated) + call split(test_string_comma_separated, test_set_comma, pos) + + @assertEqual(expected_pos_comma_separated(i), pos) + end do + + pos = 0 + + do i = 1, size(expected_pos_comma_space_separated) + call split(test_string_comma_space_separated, test_set_comma_space, pos) + + @assertEqual(expected_pos_comma_space_separated(i), pos) + end do + end subroutine test_split_by_forward_searches + + @test + subroutine test_split_by_backward_searches() + use dyn_mpas_procedures, only: split + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_pos_comma_separated(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + integer, parameter :: expected_pos_comma_space_separated(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + + integer :: i, pos + + pos = 66 + + do i = 1, size(expected_pos_comma_separated) + call split(test_string_comma_separated, test_set_comma, pos, back=.true.) + + @assertEqual(expected_pos_comma_separated(i), pos) + end do + + pos = 66 + + do i = 1, size(expected_pos_comma_space_separated) + call split(test_string_comma_space_separated, test_set_comma_space, pos, back=.true.) + + @assertEqual(expected_pos_comma_space_separated(i), pos) + end do + end subroutine test_split_by_backward_searches + + @test + subroutine test_split_by_out_of_bounds_searches() + use dyn_mpas_procedures, only: split + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + integer, parameter :: expected_pos_forward_searches(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + integer, parameter :: expected_pos_backward_searches(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + + integer :: i, pos + + pos = -huge(0) + call split('', test_set_comma, pos) + @assertEqual(1, pos) + + pos = huge(0) + call split('', test_set_comma, pos, back=.true.) + @assertEqual(0, pos) + + pos = -huge(0) + call split(test_string_comma_separated, '', pos) + @assertEqual(66, pos) + + pos = huge(0) + call split(test_string_comma_separated, '', pos, back=.true.) + @assertEqual(0, pos) + + pos = -huge(0) + + do i = 1, size(expected_pos_forward_searches) + call split(test_string_comma_separated, test_set_comma, pos) + + @assertEqual(expected_pos_forward_searches(i), pos) + end do + + pos = huge(0) + + do i = 1, size(expected_pos_backward_searches) + call split(test_string_comma_separated, test_set_comma, pos, back=.true.) + + @assertEqual(expected_pos_backward_searches(i), pos) + end do + end subroutine test_split_by_out_of_bounds_searches + + @test + subroutine test_tokenize_into_first_last_by_empty_string_set() + use dyn_mpas_procedures, only: tokenize + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + integer, parameter :: expected_first_empty_string(*) = [1] + integer, parameter :: expected_last_empty_string(*) = [0] + integer, parameter :: expected_first_empty_set(*) = [1] + integer, parameter :: expected_last_empty_set(*) = [65] + + integer, allocatable :: first(:), last(:) + + ! Test `tokenize` by empty `string`. + + call tokenize('', test_set_comma, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_empty_string, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_empty_string, last) + + ! Test `tokenize` by empty `set`. + + call tokenize(test_string_comma_separated, '', first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_empty_set, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_empty_set, last) + end subroutine test_tokenize_into_first_last_by_empty_string_set + + @test + subroutine test_tokenize_into_first_last_by_known_string_set() + use dyn_mpas_procedures, only: tokenize + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_first_comma_separated(*) = & + [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + integer, parameter :: expected_last_comma_separated(*) = & + [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + integer, parameter :: expected_first_comma_space_separated(*) = & + [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + integer, parameter :: expected_last_comma_space_separated(*) = & + [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + + integer, allocatable :: first(:), last(:) + + call tokenize(test_string_comma_separated, test_set_comma, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_comma_separated, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_comma_separated, last) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_comma_space_separated, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_comma_space_separated, last) + end subroutine test_tokenize_into_first_last_by_known_string_set + + @test + subroutine test_tokenize_into_tokens_separator_by_empty_string_set() + use dyn_mpas_procedures, only: tokenize + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + + character(:), allocatable :: expected_tokens(:), expected_separator(:) + character(:), allocatable :: tokens(:), separator(:) + + ! Test `tokenize` by empty `string`. + + allocate(character(0) :: expected_tokens(1)) + allocate(character(1) :: expected_separator(0)) + + call tokenize('', test_set_comma, tokens) + @assertTrue(allocated(tokens)) + @assertEqual(len(expected_tokens), len(tokens)) + @assertEqual(size(expected_tokens), size(tokens)) + + ! Test `tokenize` by empty `string` and optional `separator`. + + call tokenize('', test_set_comma, tokens, separator=separator) + @assertTrue(allocated(tokens)) + @assertEqual(len(expected_tokens), len(tokens)) + @assertEqual(size(expected_tokens), size(tokens)) + @assertTrue(allocated(separator)) + @assertEqual(len(expected_separator), len(separator)) + @assertEqual(size(expected_separator), size(separator)) + + ! Test `tokenize` by empty `set`. + + deallocate(expected_tokens) + deallocate(expected_separator) + allocate(character(len(test_string_comma_separated)) :: expected_tokens(1)) + expected_tokens(1) = test_string_comma_separated + allocate(character(1) :: expected_separator(0)) + + call tokenize(test_string_comma_separated, '', tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens, tokens, whitespace=keep_all) + + ! Test `tokenize` by empty `set` and optional `separator`. + + call tokenize(test_string_comma_separated, '', tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(len(expected_separator), len(separator)) + @assertEqual(size(expected_separator), size(separator)) + end subroutine test_tokenize_into_tokens_separator_by_empty_string_set + + @test + subroutine test_tokenize_into_tokens_separator_by_known_string_set() + use dyn_mpas_procedures, only: tokenize + use funit + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + character(*), parameter :: expected_tokens_comma_separated(*) = [character(7) :: & + '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & + 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + ] + character(*), parameter :: expected_tokens_comma_space_separated(*) = [character(7) :: & + '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & + 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + ] + character(*), parameter :: expected_separator_comma_separated(*) = [character(1) :: & + ',', ',', ',', ',', ',', ',', ',', ',', ',', ',', & + ',', ',', ',', ',', ',', ',', ',', ',' & + ] + character(*), parameter :: expected_separator_comma_space_separated(*) = [character(1) :: & + ',', ' ', ',', ' ', ',', ' ', ',', ' ', ',', ' ', & + ',', ' ', ',', ' ', ',', ' ', ',', ' ' & + ] + + character(:), allocatable :: tokens(:), separator(:) + + call tokenize(test_string_comma_separated, test_set_comma, tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_separated, tokens, whitespace=keep_all) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_space_separated, tokens, whitespace=keep_all) + + call tokenize(test_string_comma_separated, test_set_comma, tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_separated, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(expected_separator_comma_separated, separator, whitespace=keep_all) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_space_separated, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(expected_separator_comma_space_separated, separator, whitespace=keep_all) + end subroutine test_tokenize_into_tokens_separator_by_known_string_set end module test_dyn_mpas_procedures From cebb7684758b606b0b78f2d6d36b2630d0c5d6fb Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 24 Jul 2025 16:13:00 -0600 Subject: [PATCH 04/11] Rewrite `parse_stream_name` in terms of `tokenize` --- .../mpas/driver/dyn_mpas_subdriver.F90 | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/src/dynamics/mpas/driver/dyn_mpas_subdriver.F90 b/src/dynamics/mpas/driver/dyn_mpas_subdriver.F90 index 371a3af78..1362a659c 100644 --- a/src/dynamics/mpas/driver/dyn_mpas_subdriver.F90 +++ b/src/dynamics/mpas/driver/dyn_mpas_subdriver.F90 @@ -1669,60 +1669,27 @@ end subroutine dyn_mpas_init_stream_with_pool !> (KCW, 2024-06-01) pure function parse_stream_name(stream_name) result(var_info_list) ! Module(s) from MPAS. - use dyn_mpas_procedures, only: index_unique + use dyn_mpas_procedures, only: index_unique, tokenize character(*), intent(in) :: stream_name type(var_info_type), allocatable :: var_info_list(:) character(*), parameter :: supported_stream_name_operator = '+-' - character(1) :: stream_name_operator - character(:), allocatable :: stream_name_fragment + character(:), allocatable :: stream_name_fragment(:), stream_name_operator(:) character(len(invariant_var_info_list % name)), allocatable :: var_name_list(:) - integer :: i, j, n, offset + integer :: i, j type(var_info_type), allocatable :: var_info_list_buffer(:) - n = len_trim(stream_name) - - if (n == 0) then - ! Empty character string means empty list. - var_info_list = parse_stream_name_fragment('') - - return - end if - - i = scan(stream_name, supported_stream_name_operator) - - if (i == 0) then - ! No operators are present in the stream name. It is just a single stream name fragment. - stream_name_fragment = stream_name - var_info_list = parse_stream_name_fragment(stream_name_fragment) - - return - end if - - offset = 0 - var_info_list = parse_stream_name_fragment('') + call tokenize(stream_name, supported_stream_name_operator, stream_name_fragment, stream_name_operator) - do while (.true.) - ! Extract operator from the stream name. - if (offset > 0) then - stream_name_operator = stream_name(offset:offset) - else - stream_name_operator = '+' - end if - - ! Extract stream name fragment from the stream name. - if (i > 1) then - stream_name_fragment = stream_name(offset + 1:offset + i - 1) - else - stream_name_fragment = '' - end if + var_info_list = parse_stream_name_fragment(stream_name_fragment(1)) + do i = 2, size(stream_name_fragment) ! Process the stream name fragment according to the operator. - if (len_trim(stream_name_fragment) > 0) then - var_info_list_buffer = parse_stream_name_fragment(stream_name_fragment) + var_info_list_buffer = parse_stream_name_fragment(stream_name_fragment(i)) - select case (stream_name_operator) + if (size(var_info_list_buffer) > 0) then + select case (stream_name_operator(i - 1)) case ('+') var_info_list = [var_info_list, var_info_list_buffer] case ('-') @@ -1734,20 +1701,6 @@ pure function parse_stream_name(stream_name) result(var_info_list) ! Do nothing for unknown operators. Should not happen at all. end select end if - - offset = offset + i - - ! Terminate loop when everything in the stream name has been processed. - if (offset + 1 > n) then - exit - end if - - i = scan(stream_name(offset + 1:), supported_stream_name_operator) - - ! Run the loop one last time for the remaining stream name fragment. - if (i == 0) then - i = n - offset + 1 - end if end do ! Discard duplicate variable information by names. From 8e2052fca1eabfc3a534f58fe6d4fe50c47b92a1 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Mon, 10 Nov 2025 12:28:02 -0700 Subject: [PATCH 05/11] Avoid unnecessary renames from `stringify` to `core_stringify` --- src/core_utils/string_core_utils.F90 | 16 ++-- src/utils/string_utils.F90 | 5 +- .../src/core_utils/test_string_core_utils.pf | 88 +++++++++---------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/core_utils/string_core_utils.F90 b/src/core_utils/string_core_utils.F90 index 5f449098f..6412edea7 100644 --- a/src/core_utils/string_core_utils.F90 +++ b/src/core_utils/string_core_utils.F90 @@ -6,9 +6,9 @@ module string_core_utils public :: core_to_str ! Convert integer to left justified string public :: core_int_date_to_yyyymmdd ! Convert encoded date integer to "yyyy-mm-dd" format public :: core_int_seconds_to_hhmmss ! Convert integer seconds past midnight to "hh:mm:ss" format - public :: core_stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing + public :: stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing -CONTAINS +contains character(len=10) pure function core_to_str(n) ! return default integer as a left justified string @@ -65,12 +65,12 @@ end function core_int_seconds_to_hhmmss !> If `value` contains zero element or is of unsupported data types, an empty character string is produced. !> If `separator` is not supplied, it defaults to ", " (i.e., a comma and a space). !> (KCW, 2024-02-04) - pure function core_stringify(value, separator) + pure function stringify(value, separator) use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64 class(*), intent(in) :: value(:) character(*), optional, intent(in) :: separator - character(:), allocatable :: core_stringify + character(:), allocatable :: stringify integer, parameter :: sizelimit = 1024 @@ -87,7 +87,7 @@ pure function core_stringify(value, separator) n = min(size(value), sizelimit) if (n == 0) then - core_stringify = '' + stringify = '' return end if @@ -162,12 +162,12 @@ pure function core_stringify(value, separator) write(buffer, format) value class default - core_stringify = '' + stringify = '' return end select - core_stringify = trim(buffer) - end function core_stringify + stringify = trim(buffer) + end function stringify end module string_core_utils diff --git a/src/utils/string_utils.F90 b/src/utils/string_utils.F90 index 371e7dfc4..eba1f1a20 100644 --- a/src/utils/string_utils.F90 +++ b/src/utils/string_utils.F90 @@ -5,7 +5,8 @@ module string_utils use cam_logfile, only: iulog use cam_abortutils, only: endrun use string_core_utils, only: core_int_date_to_yyyymmdd, core_int_seconds_to_hhmmss - use string_core_utils, only: stringify=>core_stringify, to_str=>core_to_str + use string_core_utils, only: to_str => core_to_str + use string_core_utils, only: stringify implicit none private @@ -19,7 +20,7 @@ module string_utils public :: to_lower ! Convert all characters in string to lower case. public :: stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing -CONTAINS +contains subroutine strlist_get_ind(strlist, str, ind, abort) diff --git a/test/unit/fortran/src/core_utils/test_string_core_utils.pf b/test/unit/fortran/src/core_utils/test_string_core_utils.pf index 1498ed764..8f1eb7a31 100644 --- a/test/unit/fortran/src/core_utils/test_string_core_utils.pf +++ b/test/unit/fortran/src/core_utils/test_string_core_utils.pf @@ -70,7 +70,7 @@ end subroutine test_zero_int_seconds_to_hhmmss subroutine test_stringify_empty_arrays() use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=128), allocatable :: carr(:) integer(int32), allocatable :: i32arr(:) @@ -82,18 +82,18 @@ subroutine test_stringify_empty_arrays() allocate(carr(0), i32arr(0), i64arr(0), larr(0), r32arr(0), r64arr(0)) ! Empty arrays of any intrinsic data types should result in empty character strings. - @assertEqual('', core_stringify(carr)) - @assertEqual('', core_stringify(i32arr)) - @assertEqual('', core_stringify(i64arr)) - @assertEqual('', core_stringify(larr)) - @assertEqual('', core_stringify(r32arr)) - @assertEqual('', core_stringify(r64arr)) + @assertEqual('', stringify(carr)) + @assertEqual('', stringify(i32arr)) + @assertEqual('', stringify(i64arr)) + @assertEqual('', stringify(larr)) + @assertEqual('', stringify(r32arr)) + @assertEqual('', stringify(r64arr)) end subroutine test_stringify_empty_arrays @test subroutine test_stringify_character_array() use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator = '<' character(len=*), parameter :: custom_separator_with_spaces = ' < ' @@ -113,18 +113,18 @@ subroutine test_stringify_character_array() 'Orthoclase', 'Quartz', ' Topaz', 'Corundum', 'Diamond' ] ! Spaces around each value should be trimmed. The separator should default to ", ". - @assertEqual(expected_default_separator, core_stringify(carr)) + @assertEqual(expected_default_separator, stringify(carr)) ! Spaces around the separator should be preserved. - @assertEqual(expected_custom_separator, core_stringify(carr, separator=custom_separator)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(carr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator, stringify(carr, separator=custom_separator)) + @assertEqual(expected_custom_separator_with_spaces, stringify(carr, separator=custom_separator_with_spaces)) end subroutine test_stringify_character_array @test subroutine test_stringify_integer_array() use, intrinsic :: iso_fortran_env, only: int32, int64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator = '<' character(len=*), parameter :: custom_separator_with_spaces = ' < ' @@ -144,21 +144,21 @@ subroutine test_stringify_integer_array() i64arr(:) = int(i32arr, int64) ! Spaces around each value should be trimmed. The separator should default to ", ". - @assertEqual(expected_default_separator, core_stringify(i32arr)) - @assertEqual(expected_default_separator, core_stringify(i64arr)) + @assertEqual(expected_default_separator, stringify(i32arr)) + @assertEqual(expected_default_separator, stringify(i64arr)) ! Spaces around the separator should be preserved. - @assertEqual(expected_custom_separator, core_stringify(i32arr, separator=custom_separator)) - @assertEqual(expected_custom_separator, core_stringify(i64arr, separator=custom_separator)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(i32arr, separator=custom_separator_with_spaces)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(i64arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator, stringify(i32arr, separator=custom_separator)) + @assertEqual(expected_custom_separator, stringify(i64arr, separator=custom_separator)) + @assertEqual(expected_custom_separator_with_spaces, stringify(i32arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator_with_spaces, stringify(i64arr, separator=custom_separator_with_spaces)) end subroutine test_stringify_integer_array @test subroutine test_stringify_integer_extreme_values() use, intrinsic :: iso_fortran_env, only: int32, int64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator_with_spaces = ' < ' character(len=*), parameter :: expected_int32_default_separator = & @@ -181,17 +181,17 @@ subroutine test_stringify_integer_extreme_values() i64arr(10) = huge(i64arr) ! Extreme values should be handled properly. - @assertEqual(expected_int32_default_separator, core_stringify(i32arr)) - @assertEqual(expected_int64_default_separator, core_stringify(i64arr)) - @assertEqual(expected_int32_custom_separator_with_spaces, core_stringify(i32arr, separator=custom_separator_with_spaces)) - @assertEqual(expected_int64_custom_separator_with_spaces, core_stringify(i64arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_int32_default_separator, stringify(i32arr)) + @assertEqual(expected_int64_default_separator, stringify(i64arr)) + @assertEqual(expected_int32_custom_separator_with_spaces, stringify(i32arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_int64_custom_separator_with_spaces, stringify(i64arr, separator=custom_separator_with_spaces)) end subroutine test_stringify_integer_extreme_values @test subroutine test_stringify_floating_point_array() use, intrinsic :: iso_fortran_env, only: real32, real64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator = '<' character(len=*), parameter :: custom_separator_with_spaces = ' < ' @@ -215,21 +215,21 @@ subroutine test_stringify_floating_point_array() ! Each value should have a fixed width of 13. Only negative values should have signs. ! The separator should default to ", ". - @assertEqual(expected_default_separator, core_stringify(r32arr)) - @assertEqual(expected_default_separator, core_stringify(r64arr)) + @assertEqual(expected_default_separator, stringify(r32arr)) + @assertEqual(expected_default_separator, stringify(r64arr)) ! Spaces around the separator should be preserved. - @assertEqual(expected_custom_separator, core_stringify(r32arr, separator=custom_separator)) - @assertEqual(expected_custom_separator, core_stringify(r64arr, separator=custom_separator)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(r32arr, separator=custom_separator_with_spaces)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(r64arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator, stringify(r32arr, separator=custom_separator)) + @assertEqual(expected_custom_separator, stringify(r64arr, separator=custom_separator)) + @assertEqual(expected_custom_separator_with_spaces, stringify(r32arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator_with_spaces, stringify(r64arr, separator=custom_separator_with_spaces)) end subroutine test_stringify_floating_point_array @test subroutine test_stringify_floating_point_scientific_notation() use, intrinsic :: iso_fortran_env, only: real32, real64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator_with_spaces = ' < ' character(len=*), parameter :: expected_default_separator = & @@ -248,17 +248,17 @@ subroutine test_stringify_floating_point_scientific_notation() r64arr(:) = real(r32arr, real64) ! Scientific notation should be enabled if there is any value with a magnitude >= 1.0E+5. - @assertEqual(expected_default_separator, core_stringify(r32arr)) - @assertEqual(expected_default_separator, core_stringify(r64arr)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(r32arr, separator=custom_separator_with_spaces)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(r64arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_default_separator, stringify(r32arr)) + @assertEqual(expected_default_separator, stringify(r64arr)) + @assertEqual(expected_custom_separator_with_spaces, stringify(r32arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator_with_spaces, stringify(r64arr, separator=custom_separator_with_spaces)) end subroutine test_stringify_floating_point_scientific_notation @test subroutine test_stringify_floating_point_extreme_values() use, intrinsic :: iso_fortran_env, only: real32, real64 use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator_with_spaces = ' < ' character(len=*), parameter :: expected_real32_default_separator = & @@ -285,16 +285,16 @@ subroutine test_stringify_floating_point_extreme_values() r64arr(10) = 9.999999E+99_real64 ! Arbitrarily limited by the "es13.6e2" format specification. ! Extreme values should be handled properly. - @assertEqual(expected_real32_default_separator, core_stringify(r32arr)) - @assertEqual(expected_real64_default_separator, core_stringify(r64arr)) - @assertEqual(expected_real32_custom_separator_with_spaces, core_stringify(r32arr, separator=custom_separator_with_spaces)) - @assertEqual(expected_real64_custom_separator_with_spaces, core_stringify(r64arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_real32_default_separator, stringify(r32arr)) + @assertEqual(expected_real64_default_separator, stringify(r64arr)) + @assertEqual(expected_real32_custom_separator_with_spaces, stringify(r32arr, separator=custom_separator_with_spaces)) + @assertEqual(expected_real64_custom_separator_with_spaces, stringify(r64arr, separator=custom_separator_with_spaces)) end subroutine test_stringify_floating_point_extreme_values @test subroutine test_stringify_logical_array() use funit - use string_core_utils, only: core_stringify + use string_core_utils, only: stringify character(len=*), parameter :: custom_separator = '.or.' character(len=*), parameter :: custom_separator_with_spaces = ' .or. ' @@ -313,9 +313,9 @@ subroutine test_stringify_logical_array() larr(2:10:2) = .false. ! Spaces around each value should be trimmed. The separator should default to ", ". - @assertEqual(expected_default_separator, core_stringify(larr)) + @assertEqual(expected_default_separator, stringify(larr)) ! Spaces around the separator should be preserved. - @assertEqual(expected_custom_separator, core_stringify(larr, separator=custom_separator)) - @assertEqual(expected_custom_separator_with_spaces, core_stringify(larr, separator=custom_separator_with_spaces)) + @assertEqual(expected_custom_separator, stringify(larr, separator=custom_separator)) + @assertEqual(expected_custom_separator_with_spaces, stringify(larr, separator=custom_separator_with_spaces)) end subroutine test_stringify_logical_array From 95658b978d1376d70468dcf0222dac70b5b92357 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Mon, 10 Nov 2025 12:33:22 -0700 Subject: [PATCH 06/11] Also make `split` and `tokenize` available in `string_utils` module --- src/core_utils/string_core_utils.F90 | 100 +++++++++++++++++++++++++++ src/utils/string_utils.F90 | 4 +- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/core_utils/string_core_utils.F90 b/src/core_utils/string_core_utils.F90 index 6412edea7..c018523ca 100644 --- a/src/core_utils/string_core_utils.F90 +++ b/src/core_utils/string_core_utils.F90 @@ -6,7 +6,14 @@ module string_core_utils public :: core_to_str ! Convert integer to left justified string public :: core_int_date_to_yyyymmdd ! Convert encoded date integer to "yyyy-mm-dd" format public :: core_int_seconds_to_hhmmss ! Convert integer seconds past midnight to "hh:mm:ss" format + public :: split ! Parse a string into tokens, one at a time public :: stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing + public :: tokenize ! Parse a string into tokens + + interface tokenize + module procedure tokenize_into_first_last + module procedure tokenize_into_tokens_separator + end interface tokenize contains @@ -59,6 +66,38 @@ character(len=8) pure function core_int_seconds_to_hhmmss (seconds) end function core_int_seconds_to_hhmmss + !> Parse a string into tokens, one at a time. This subroutine implements the `split` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.196). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine split(string, set, pos, back) + character(*), intent(in) :: string, set + integer, intent(inout) :: pos + logical, optional, intent(in) :: back + + integer :: offset + + if (present(back)) then + if (back) then + offset = max(min(pos, len(string) + 1), 1) + pos = scan(string(1:offset - 1), set, back=.true.) + + return + end if + end if + + offset = max(min(pos, len(string)), 0) + pos = scan(string(offset + 1:), set) + + if (pos == 0) then + pos = len(string) + 1 + + return + end if + + pos = offset + pos + end subroutine split + !> Convert one or more values of any intrinsic data types to a character string for pretty printing. !> If `value` contains more than one element, the elements will be stringified, delimited by `separator`, then concatenated. !> If `value` contains exactly one element, the element will be stringified without using `separator`. @@ -170,4 +209,65 @@ pure function stringify(value, separator) stringify = trim(buffer) end function stringify + !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine tokenize_into_first_last(string, set, first, last) + character(*), intent(in) :: string, set + integer, allocatable, intent(out) :: first(:), last(:) + + integer :: pos_start(len(string) + 1), pos_end(len(string) + 1) + integer :: l, n, pos + + l = len(string) + n = 0 + pos = 0 + + do while (pos < l + 1) + n = n + 1 + pos_start(n) = pos + 1 + + call split(string, set, pos) + + pos_end(n) = pos - 1 + end do + + allocate(first(n), last(n)) + + first(:) = pos_start(1:n) + last(:) = pos_end(1:n) + end subroutine tokenize_into_first_last + + !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in + !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may + !> take years to become widespread. + !> (KCW, 2025-10-29) + pure subroutine tokenize_into_tokens_separator(string, set, tokens, separator) + character(*), intent(in) :: string, set + character(:), allocatable, intent(out) :: tokens(:) + character(:), allocatable, optional, intent(out) :: separator(:) + + integer, allocatable :: first(:), last(:) + integer :: i, n + + call tokenize(string, set, first, last) + + n = size(first) + + allocate(character(maxval(last - first) + 1) :: tokens(n)) + + do i = 1, n + tokens(i) = string(first(i):last(i)) + end do + + if (present(separator)) then + allocate(character(1) :: separator(n - 1)) + + do i = 1, n - 1 + separator(i) = string(last(i) + 1:last(i) + 1) + end do + end if + end subroutine tokenize_into_tokens_separator + end module string_core_utils diff --git a/src/utils/string_utils.F90 b/src/utils/string_utils.F90 index eba1f1a20..dbf444d82 100644 --- a/src/utils/string_utils.F90 +++ b/src/utils/string_utils.F90 @@ -6,7 +6,7 @@ module string_utils use cam_abortutils, only: endrun use string_core_utils, only: core_int_date_to_yyyymmdd, core_int_seconds_to_hhmmss use string_core_utils, only: to_str => core_to_str - use string_core_utils, only: stringify + use string_core_utils, only: split, stringify, tokenize implicit none private @@ -18,7 +18,9 @@ module string_utils public :: to_str ! Convert integer to left justified string public :: to_upper ! Convert all characters in string to upper case. public :: to_lower ! Convert all characters in string to lower case. + public :: split ! Parse a string into tokens, one at a time public :: stringify ! Convert one or more values of any intrinsic data types to a character string for pretty printing + public :: tokenize ! Parse a string into tokens contains From cf4d469d0d6dcaa763cd1e6117f1adcbcf01372d Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 11 Dec 2025 12:45:55 -0700 Subject: [PATCH 07/11] Add more detailed descriptions for `split` and `tokenize` The text is adapted from the Fortran 2023 language standard. --- src/core_utils/string_core_utils.F90 | 35 ++++++++++++++----- .../mpas/driver/dyn_mpas_procedures.F90 | 35 ++++++++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/core_utils/string_core_utils.F90 b/src/core_utils/string_core_utils.F90 index c018523ca..053a9bf09 100644 --- a/src/core_utils/string_core_utils.F90 +++ b/src/core_utils/string_core_utils.F90 @@ -66,9 +66,18 @@ character(len=8) pure function core_int_seconds_to_hhmmss (seconds) end function core_int_seconds_to_hhmmss - !> Parse a string into tokens, one at a time. This subroutine implements the `split` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.196). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens, one at a time. Each character in `set` is a token delimiter. + !> If `back` is absent or is present with the value `.false.`, `pos` is assigned the position of the leftmost + !> token delimiter in `string` whose position is greater than `pos`, or if there is no such character, it + !> is assigned a value one greater than the length of `string`. This identifies a token with starting + !> position one greater than the value of `pos` on invocation, and ending position one less than the + !> value of `pos` on return. + !> If `back` is present with the value `.true.`, `pos` is assigned the position of the rightmost token delimiter + !> in `string` whose position is less than `pos`, or if there is no such character, it is assigned the value + !> zero. This identifies a token with ending position one less than the value of `pos` on invocation, and + !> starting position one greater than the value of `pos` on return. + !> This subroutine implements the `split` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.196). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine split(string, set, pos, back) character(*), intent(in) :: string, set @@ -209,9 +218,13 @@ pure function stringify(value, separator) stringify = trim(buffer) end function stringify - !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens. Each character in `set` is a token delimiter. + !> `first` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`. + !> Each element is assigned, in array element order, the starting position of each token in `string`, in the order found. + !> `last` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`. + !> Each element is assigned, in array element order, the ending position of each token in `string`, in the order found. + !> This subroutine implements the `tokenize` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.210). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine tokenize_into_first_last(string, set, first, last) character(*), intent(in) :: string, set @@ -239,9 +252,13 @@ pure subroutine tokenize_into_first_last(string, set, first, last) last(:) = pos_end(1:n) end subroutine tokenize_into_first_last - !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens. Each character in `set` is a token delimiter. + !> `tokens` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`, + !> and with character length equal to the length of the longest token. It contains the tokens in `string`. + !> `separator` is allocated with the lower bound equal to one and the upper bound equal to one less than the number of + !> tokens in `string`, and with character length equal to one. It contains the token delimiters in `string`. + !> This subroutine implements the `tokenize` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.210). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine tokenize_into_tokens_separator(string, set, tokens, separator) character(*), intent(in) :: string, set diff --git a/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 b/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 index f236d90e8..632dfbd74 100644 --- a/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 +++ b/src/dynamics/mpas/driver/dyn_mpas_procedures.F90 @@ -304,9 +304,18 @@ pure function index_unique(array) index_unique = pack([(i, i = 1, n)], mask_unique) end function index_unique - !> Parse a string into tokens, one at a time. This subroutine implements the `split` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.196). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens, one at a time. Each character in `set` is a token delimiter. + !> If `back` is absent or is present with the value `.false.`, `pos` is assigned the position of the leftmost + !> token delimiter in `string` whose position is greater than `pos`, or if there is no such character, it + !> is assigned a value one greater than the length of `string`. This identifies a token with starting + !> position one greater than the value of `pos` on invocation, and ending position one less than the + !> value of `pos` on return. + !> If `back` is present with the value `.true.`, `pos` is assigned the position of the rightmost token delimiter + !> in `string` whose position is less than `pos`, or if there is no such character, it is assigned the value + !> zero. This identifies a token with ending position one less than the value of `pos` on invocation, and + !> starting position one greater than the value of `pos` on return. + !> This subroutine implements the `split` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.196). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine split(string, set, pos, back) character(*), intent(in) :: string, set @@ -447,9 +456,13 @@ pure function stringify(value, separator) stringify = trim(buffer) end function stringify - !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens. Each character in `set` is a token delimiter. + !> `first` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`. + !> Each element is assigned, in array element order, the starting position of each token in `string`, in the order found. + !> `last` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`. + !> Each element is assigned, in array element order, the ending position of each token in `string`, in the order found. + !> This subroutine implements the `tokenize` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.210). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine tokenize_into_first_last(string, set, first, last) character(*), intent(in) :: string, set @@ -477,9 +490,13 @@ pure subroutine tokenize_into_first_last(string, set, first, last) last(:) = pos_end(1:n) end subroutine tokenize_into_first_last - !> Parse a string into tokens. This subroutine implements the `tokenize` intrinsic procedure as defined in - !> the Fortran 2023 language standard (Section 16.9.210). We implement it ourselves because the compiler support may - !> take years to become widespread. + !> Parse a string into tokens. Each character in `set` is a token delimiter. + !> `tokens` is allocated with the lower bound equal to one and the upper bound equal to the number of tokens in `string`, + !> and with character length equal to the length of the longest token. It contains the tokens in `string`. + !> `separator` is allocated with the lower bound equal to one and the upper bound equal to one less than the number of + !> tokens in `string`, and with character length equal to one. It contains the token delimiters in `string`. + !> This subroutine implements the `tokenize` intrinsic procedure as defined in the Fortran 2023 language standard + !> (Section 16.9.210). We implement it ourselves because the compiler support may take years to become widespread. !> (KCW, 2025-10-29) pure subroutine tokenize_into_tokens_separator(string, set, tokens, separator) character(*), intent(in) :: string, set From 51ac90d0a057e34f1c51d9e95b76aa5c1b77c0c3 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 11 Dec 2025 12:48:22 -0700 Subject: [PATCH 08/11] Adjust test strings to cover more permutations of token delimiters --- .../mpas/tests/unit/test_dyn_mpas_procedures.pf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf index 53d1c4fed..d0086aa32 100644 --- a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf +++ b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf @@ -1106,7 +1106,7 @@ contains ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] @@ -1141,7 +1141,7 @@ contains ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] @@ -1256,7 +1256,7 @@ contains ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_first_comma_separated(*) = & [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] @@ -1347,7 +1347,7 @@ contains ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ', Mercury,Venus ,Earth , Mars, Jupiter,Saturn ,Uranus , Neptune, ' + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' character(*), parameter :: test_set_comma_space = ', ' character(*), parameter :: expected_tokens_comma_separated(*) = [character(7) :: & '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & @@ -1362,8 +1362,8 @@ contains ',', ',', ',', ',', ',', ',', ',', ',' & ] character(*), parameter :: expected_separator_comma_space_separated(*) = [character(1) :: & - ',', ' ', ',', ' ', ',', ' ', ',', ' ', ',', ' ', & - ',', ' ', ',', ' ', ',', ' ', ',', ' ' & + ' ', ',', ' ', ',', ' ', ' ', ' ', ',', ' ', ' ', & + ',', ' ', ',', ',', ' ', ' ', ',', ' ' & ] character(:), allocatable :: tokens(:), separator(:) From 6facbe101437b7694b8a9594dc7c78591268b856 Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 11 Dec 2025 12:49:02 -0700 Subject: [PATCH 09/11] Also add unit tests to `string_core_utils` --- .../src/core_utils/test_string_core_utils.pf | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) diff --git a/test/unit/fortran/src/core_utils/test_string_core_utils.pf b/test/unit/fortran/src/core_utils/test_string_core_utils.pf index 8f1eb7a31..86cc6399a 100644 --- a/test/unit/fortran/src/core_utils/test_string_core_utils.pf +++ b/test/unit/fortran/src/core_utils/test_string_core_utils.pf @@ -319,3 +319,335 @@ subroutine test_stringify_logical_array() @assertEqual(expected_custom_separator, stringify(larr, separator=custom_separator)) @assertEqual(expected_custom_separator_with_spaces, stringify(larr, separator=custom_separator_with_spaces)) end subroutine test_stringify_logical_array + +@test +subroutine test_split_by_empty_string_set() + use funit + use string_core_utils, only: split + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + + integer :: pos + + ! Test `split` by empty `string`. + + pos = 0 + call split('', test_set_comma, pos) + @assertEqual(1, pos) + + ! Test `split` by empty `string` and optional `back`. + + pos = 1 + call split('', test_set_comma, pos, back=.true.) + @assertEqual(0, pos) + + ! Test `split` by empty `set`. + + pos = 0 + call split(test_string_comma_separated, '', pos) + @assertEqual(66, pos) + + ! Test `split` by empty `set` and optional `back`. + + pos = 66 + call split(test_string_comma_separated, '', pos, back=.true.) + @assertEqual(0, pos) +end subroutine test_split_by_empty_string_set + +@test +subroutine test_split_by_forward_searches() + use funit + use string_core_utils, only: split + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_pos_comma_separated(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + integer, parameter :: expected_pos_comma_space_separated(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + + integer :: i, pos + + pos = 0 + + do i = 1, size(expected_pos_comma_separated) + call split(test_string_comma_separated, test_set_comma, pos) + + @assertEqual(expected_pos_comma_separated(i), pos) + end do + + pos = 0 + + do i = 1, size(expected_pos_comma_space_separated) + call split(test_string_comma_space_separated, test_set_comma_space, pos) + + @assertEqual(expected_pos_comma_space_separated(i), pos) + end do +end subroutine test_split_by_forward_searches + +@test +subroutine test_split_by_backward_searches() + use funit + use string_core_utils, only: split + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_pos_comma_separated(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + integer, parameter :: expected_pos_comma_space_separated(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + + integer :: i, pos + + pos = 66 + + do i = 1, size(expected_pos_comma_separated) + call split(test_string_comma_separated, test_set_comma, pos, back=.true.) + + @assertEqual(expected_pos_comma_separated(i), pos) + end do + + pos = 66 + + do i = 1, size(expected_pos_comma_space_separated) + call split(test_string_comma_space_separated, test_set_comma_space, pos, back=.true.) + + @assertEqual(expected_pos_comma_space_separated(i), pos) + end do +end subroutine test_split_by_backward_searches + +@test +subroutine test_split_by_out_of_bounds_searches() + use funit + use string_core_utils, only: split + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + integer, parameter :: expected_pos_forward_searches(*) = & + [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + integer, parameter :: expected_pos_backward_searches(*) = & + [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + + integer :: i, pos + + pos = -huge(0) + call split('', test_set_comma, pos) + @assertEqual(1, pos) + + pos = huge(0) + call split('', test_set_comma, pos, back=.true.) + @assertEqual(0, pos) + + pos = -huge(0) + call split(test_string_comma_separated, '', pos) + @assertEqual(66, pos) + + pos = huge(0) + call split(test_string_comma_separated, '', pos, back=.true.) + @assertEqual(0, pos) + + pos = -huge(0) + + do i = 1, size(expected_pos_forward_searches) + call split(test_string_comma_separated, test_set_comma, pos) + + @assertEqual(expected_pos_forward_searches(i), pos) + end do + + pos = huge(0) + + do i = 1, size(expected_pos_backward_searches) + call split(test_string_comma_separated, test_set_comma, pos, back=.true.) + + @assertEqual(expected_pos_backward_searches(i), pos) + end do +end subroutine test_split_by_out_of_bounds_searches + +@test +subroutine test_tokenize_into_first_last_by_empty_string_set() + use funit + use string_core_utils, only: tokenize + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + integer, parameter :: expected_first_empty_string(*) = [1] + integer, parameter :: expected_last_empty_string(*) = [0] + integer, parameter :: expected_first_empty_set(*) = [1] + integer, parameter :: expected_last_empty_set(*) = [65] + + integer, allocatable :: first(:), last(:) + + ! Test `tokenize` by empty `string`. + + call tokenize('', test_set_comma, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_empty_string, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_empty_string, last) + + ! Test `tokenize` by empty `set`. + + call tokenize(test_string_comma_separated, '', first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_empty_set, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_empty_set, last) +end subroutine test_tokenize_into_first_last_by_empty_string_set + +@test +subroutine test_tokenize_into_first_last_by_known_string_set() + use funit + use string_core_utils, only: tokenize + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + integer, parameter :: expected_first_comma_separated(*) = & + [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + integer, parameter :: expected_last_comma_separated(*) = & + [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + integer, parameter :: expected_first_comma_space_separated(*) = & + [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + integer, parameter :: expected_last_comma_space_separated(*) = & + [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + + integer, allocatable :: first(:), last(:) + + call tokenize(test_string_comma_separated, test_set_comma, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_comma_separated, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_comma_separated, last) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, first, last) + @assertTrue(allocated(first)) + @assertEqual(expected_first_comma_space_separated, first) + @assertTrue(allocated(last)) + @assertEqual(expected_last_comma_space_separated, last) +end subroutine test_tokenize_into_first_last_by_known_string_set + +@test +subroutine test_tokenize_into_tokens_separator_by_empty_string_set() + use funit + use string_core_utils, only: tokenize + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + + character(:), allocatable :: expected_tokens(:), expected_separator(:) + character(:), allocatable :: tokens(:), separator(:) + + ! Test `tokenize` by empty `string`. + + allocate(character(0) :: expected_tokens(1)) + allocate(character(1) :: expected_separator(0)) + + call tokenize('', test_set_comma, tokens) + @assertTrue(allocated(tokens)) + @assertEqual(len(expected_tokens), len(tokens)) + @assertEqual(size(expected_tokens), size(tokens)) + + ! Test `tokenize` by empty `string` and optional `separator`. + + call tokenize('', test_set_comma, tokens, separator=separator) + @assertTrue(allocated(tokens)) + @assertEqual(len(expected_tokens), len(tokens)) + @assertEqual(size(expected_tokens), size(tokens)) + @assertTrue(allocated(separator)) + @assertEqual(len(expected_separator), len(separator)) + @assertEqual(size(expected_separator), size(separator)) + + ! Test `tokenize` by empty `set`. + + deallocate(expected_tokens) + deallocate(expected_separator) + allocate(character(len(test_string_comma_separated)) :: expected_tokens(1)) + expected_tokens(1) = test_string_comma_separated + allocate(character(1) :: expected_separator(0)) + + call tokenize(test_string_comma_separated, '', tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens, tokens, whitespace=keep_all) + + ! Test `tokenize` by empty `set` and optional `separator`. + + call tokenize(test_string_comma_separated, '', tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(len(expected_separator), len(separator)) + @assertEqual(size(expected_separator), size(separator)) +end subroutine test_tokenize_into_tokens_separator_by_empty_string_set + +@test +subroutine test_tokenize_into_tokens_separator_by_known_string_set() + use funit + use string_core_utils, only: tokenize + + character(*), parameter :: test_string_comma_separated = & + ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + character(*), parameter :: test_set_comma = ',' + character(*), parameter :: test_string_comma_space_separated = & + ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + character(*), parameter :: test_set_comma_space = ', ' + character(*), parameter :: expected_tokens_comma_separated(*) = [character(7) :: & + '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & + 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + ] + character(*), parameter :: expected_tokens_comma_space_separated(*) = [character(7) :: & + '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & + 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + ] + character(*), parameter :: expected_separator_comma_separated(*) = [character(1) :: & + ',', ',', ',', ',', ',', ',', ',', ',', ',', ',', & + ',', ',', ',', ',', ',', ',', ',', ',' & + ] + character(*), parameter :: expected_separator_comma_space_separated(*) = [character(1) :: & + ' ', ',', ' ', ',', ' ', ' ', ' ', ',', ' ', ' ', & + ',', ' ', ',', ',', ' ', ' ', ',', ' ' & + ] + + character(:), allocatable :: tokens(:), separator(:) + + call tokenize(test_string_comma_separated, test_set_comma, tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_separated, tokens, whitespace=keep_all) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, tokens) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_space_separated, tokens, whitespace=keep_all) + + call tokenize(test_string_comma_separated, test_set_comma, tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_separated, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(expected_separator_comma_separated, separator, whitespace=keep_all) + + call tokenize(test_string_comma_space_separated, test_set_comma_space, tokens, separator=separator) + @assertTrue(allocated(tokens)) + ! Treat whitespace differences as significant by specifying `whitespace=keep_all`. + @assertEqual(expected_tokens_comma_space_separated, tokens, whitespace=keep_all) + @assertTrue(allocated(separator)) + @assertEqual(expected_separator_comma_space_separated, separator, whitespace=keep_all) +end subroutine test_tokenize_into_tokens_separator_by_known_string_set From 9c6410be89ac6d76deb9d851799cf871694e12da Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Mon, 15 Dec 2025 12:09:22 -0700 Subject: [PATCH 10/11] Adjust test strings to cover more permutations of token delimiters --- .../tests/unit/test_dyn_mpas_procedures.pf | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf index d0086aa32..a01c6d4bd 100644 --- a/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf +++ b/src/dynamics/mpas/tests/unit/test_dyn_mpas_procedures.pf @@ -1067,7 +1067,7 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer :: pos @@ -1088,11 +1088,11 @@ contains pos = 0 call split(test_string_comma_separated, '', pos) - @assertEqual(66, pos) + @assertEqual(67, pos) ! Test `split` by empty `set` and optional `back`. - pos = 66 + pos = 67 call split(test_string_comma_separated, '', pos, back=.true.) @assertEqual(0, pos) end subroutine test_split_by_empty_string_set @@ -1103,15 +1103,15 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer, parameter :: expected_pos_comma_space_separated(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer :: i, pos @@ -1138,19 +1138,19 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer, parameter :: expected_pos_comma_space_separated(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer :: i, pos - pos = 66 + pos = 67 do i = 1, size(expected_pos_comma_separated) call split(test_string_comma_separated, test_set_comma, pos, back=.true.) @@ -1158,7 +1158,7 @@ contains @assertEqual(expected_pos_comma_separated(i), pos) end do - pos = 66 + pos = 67 do i = 1, size(expected_pos_comma_space_separated) call split(test_string_comma_space_separated, test_set_comma_space, pos, back=.true.) @@ -1173,12 +1173,12 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer, parameter :: expected_pos_forward_searches(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer, parameter :: expected_pos_backward_searches(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer :: i, pos @@ -1192,7 +1192,7 @@ contains pos = -huge(0) call split(test_string_comma_separated, '', pos) - @assertEqual(66, pos) + @assertEqual(67, pos) pos = huge(0) call split(test_string_comma_separated, '', pos, back=.true.) @@ -1221,12 +1221,12 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer, parameter :: expected_first_empty_string(*) = [1] integer, parameter :: expected_last_empty_string(*) = [0] integer, parameter :: expected_first_empty_set(*) = [1] - integer, parameter :: expected_last_empty_set(*) = [65] + integer, parameter :: expected_last_empty_set(*) = [66] integer, allocatable :: first(:), last(:) @@ -1253,19 +1253,19 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_first_comma_separated(*) = & - [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + [1, 2, 3, 4, 12, 13, 19, 25, 26, 31, 32, 33, 41, 42, 49, 56, 57, 65, 66, 67] integer, parameter :: expected_last_comma_separated(*) = & - [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + [0, 1, 2, 10, 11, 17, 23, 24, 29, 30, 31, 39, 40, 47, 54, 55, 63, 64, 65, 66] integer, parameter :: expected_first_comma_space_separated(*) = & - [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + [1, 2, 3, 4, 12, 13, 19, 25, 26, 31, 32, 33, 41, 42, 49, 56, 57, 65, 66, 67] integer, parameter :: expected_last_comma_space_separated(*) = & - [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + [0, 1, 2, 10, 11, 17, 23, 24, 29, 30, 31, 39, 40, 47, 54, 55, 63, 64, 65, 66] integer, allocatable :: first(:), last(:) @@ -1288,7 +1288,7 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(:), allocatable :: expected_tokens(:), expected_separator(:) @@ -1344,26 +1344,26 @@ contains use funit character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' character(*), parameter :: expected_tokens_comma_separated(*) = [character(7) :: & - '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & - 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + '', '', '', 'Mercury', '', 'Venus', 'Earth', '', 'Mars', '', & + '', 'Jupiter', '', 'Saturn', 'Uranus', '', 'Neptune', '', '', '' & ] character(*), parameter :: expected_tokens_comma_space_separated(*) = [character(7) :: & - '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & - 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + '', '', '', 'Mercury', '', 'Venus', 'Earth', '', 'Mars', '', & + '', 'Jupiter', '', 'Saturn', 'Uranus', '', 'Neptune', '', '', '' & ] character(*), parameter :: expected_separator_comma_separated(*) = [character(1) :: & ',', ',', ',', ',', ',', ',', ',', ',', ',', ',', & - ',', ',', ',', ',', ',', ',', ',', ',' & + ',', ',', ',', ',', ',', ',', ',', ',', ',' & ] character(*), parameter :: expected_separator_comma_space_separated(*) = [character(1) :: & - ' ', ',', ' ', ',', ' ', ' ', ' ', ',', ' ', ' ', & - ',', ' ', ',', ',', ' ', ' ', ',', ' ' & + ' ', ' ', ',', ' ', ' ', ' ', ' ', ',', ' ', ',', & + ' ', ',', ' ', ',', ',', ',', ',', ' ', ' ' & ] character(:), allocatable :: tokens(:), separator(:) From c6a1f4d11df7d9584af680d880a8d653c65c7b7b Mon Sep 17 00:00:00 2001 From: Kuan-Chih Wang Date: Thu, 18 Dec 2025 13:38:37 -0700 Subject: [PATCH 11/11] Bring the same changes to `string_core_utils` --- .../src/core_utils/test_string_core_utils.pf | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/test/unit/fortran/src/core_utils/test_string_core_utils.pf b/test/unit/fortran/src/core_utils/test_string_core_utils.pf index 86cc6399a..7db43ce60 100644 --- a/test/unit/fortran/src/core_utils/test_string_core_utils.pf +++ b/test/unit/fortran/src/core_utils/test_string_core_utils.pf @@ -326,7 +326,7 @@ subroutine test_split_by_empty_string_set() use string_core_utils, only: split character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer :: pos @@ -347,11 +347,11 @@ subroutine test_split_by_empty_string_set() pos = 0 call split(test_string_comma_separated, '', pos) - @assertEqual(66, pos) + @assertEqual(67, pos) ! Test `split` by empty `set` and optional `back`. - pos = 66 + pos = 67 call split(test_string_comma_separated, '', pos, back=.true.) @assertEqual(0, pos) end subroutine test_split_by_empty_string_set @@ -362,15 +362,15 @@ subroutine test_split_by_forward_searches() use string_core_utils, only: split character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer, parameter :: expected_pos_comma_space_separated(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer :: i, pos @@ -397,19 +397,19 @@ subroutine test_split_by_backward_searches() use string_core_utils, only: split character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_pos_comma_separated(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer, parameter :: expected_pos_comma_space_separated(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer :: i, pos - pos = 66 + pos = 67 do i = 1, size(expected_pos_comma_separated) call split(test_string_comma_separated, test_set_comma, pos, back=.true.) @@ -417,7 +417,7 @@ subroutine test_split_by_backward_searches() @assertEqual(expected_pos_comma_separated(i), pos) end do - pos = 66 + pos = 67 do i = 1, size(expected_pos_comma_space_separated) call split(test_string_comma_space_separated, test_set_comma_space, pos, back=.true.) @@ -432,12 +432,12 @@ subroutine test_split_by_out_of_bounds_searches() use string_core_utils, only: split character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer, parameter :: expected_pos_forward_searches(*) = & - [1, 2, 10, 16, 17, 23, 24, 25, 30, 31, 39, 46, 47, 54, 55, 56, 64, 65, 66] + [1, 2, 3, 11, 12, 18, 24, 25, 30, 31, 32, 40, 41, 48, 55, 56, 64, 65, 66, 67] integer, parameter :: expected_pos_backward_searches(*) = & - [65, 64, 56, 55, 54, 47, 46, 39, 31, 30, 25, 24, 23, 17, 16, 10, 2, 1, 0] + [66, 65, 64, 56, 55, 48, 41, 40, 32, 31, 30, 25, 24, 18, 12, 11, 3, 2, 1, 0] integer :: i, pos @@ -451,7 +451,7 @@ subroutine test_split_by_out_of_bounds_searches() pos = -huge(0) call split(test_string_comma_separated, '', pos) - @assertEqual(66, pos) + @assertEqual(67, pos) pos = huge(0) call split(test_string_comma_separated, '', pos, back=.true.) @@ -480,12 +480,12 @@ subroutine test_tokenize_into_first_last_by_empty_string_set() use string_core_utils, only: tokenize character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' integer, parameter :: expected_first_empty_string(*) = [1] integer, parameter :: expected_last_empty_string(*) = [0] integer, parameter :: expected_first_empty_set(*) = [1] - integer, parameter :: expected_last_empty_set(*) = [65] + integer, parameter :: expected_last_empty_set(*) = [66] integer, allocatable :: first(:), last(:) @@ -512,19 +512,19 @@ subroutine test_tokenize_into_first_last_by_known_string_set() use string_core_utils, only: tokenize character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' integer, parameter :: expected_first_comma_separated(*) = & - [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + [1, 2, 3, 4, 12, 13, 19, 25, 26, 31, 32, 33, 41, 42, 49, 56, 57, 65, 66, 67] integer, parameter :: expected_last_comma_separated(*) = & - [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + [0, 1, 2, 10, 11, 17, 23, 24, 29, 30, 31, 39, 40, 47, 54, 55, 63, 64, 65, 66] integer, parameter :: expected_first_comma_space_separated(*) = & - [1, 2, 3, 11, 17, 18, 24, 25, 26, 31, 32, 40, 47, 48, 55, 56, 57, 65, 66] + [1, 2, 3, 4, 12, 13, 19, 25, 26, 31, 32, 33, 41, 42, 49, 56, 57, 65, 66, 67] integer, parameter :: expected_last_comma_space_separated(*) = & - [0, 1, 9, 15, 16, 22, 23, 24, 29, 30, 38, 45, 46, 53, 54, 55, 63, 64, 65] + [0, 1, 2, 10, 11, 17, 23, 24, 29, 30, 31, 39, 40, 47, 54, 55, 63, 64, 65, 66] integer, allocatable :: first(:), last(:) @@ -547,7 +547,7 @@ subroutine test_tokenize_into_tokens_separator_by_empty_string_set() use string_core_utils, only: tokenize character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(:), allocatable :: expected_tokens(:), expected_separator(:) @@ -603,26 +603,26 @@ subroutine test_tokenize_into_tokens_separator_by_known_string_set() use string_core_utils, only: tokenize character(*), parameter :: test_string_comma_separated = & - ',,Mercury,Venus,,Earth,,,Mars,,Jupiter,Saturn,,Uranus,,,Neptune,,' + ',,,Mercury,,Venus,Earth,,Mars,,,Jupiter,,Saturn,Uranus,,Neptune,,,' character(*), parameter :: test_set_comma = ',' character(*), parameter :: test_string_comma_space_separated = & - ' ,Mercury Venus, Earth ,Mars Jupiter,Saturn ,Uranus, Neptune, ' + ' ,Mercury Venus Earth ,Mars , Jupiter, Saturn,Uranus,,Neptune, ' character(*), parameter :: test_set_comma_space = ', ' character(*), parameter :: expected_tokens_comma_separated(*) = [character(7) :: & - '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & - 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + '', '', '', 'Mercury', '', 'Venus', 'Earth', '', 'Mars', '', & + '', 'Jupiter', '', 'Saturn', 'Uranus', '', 'Neptune', '', '', '' & ] character(*), parameter :: expected_tokens_comma_space_separated(*) = [character(7) :: & - '', '', 'Mercury', 'Venus', '', 'Earth', '', '', 'Mars', '', & - 'Jupiter', 'Saturn', '', 'Uranus', '', '', 'Neptune', '', '' & + '', '', '', 'Mercury', '', 'Venus', 'Earth', '', 'Mars', '', & + '', 'Jupiter', '', 'Saturn', 'Uranus', '', 'Neptune', '', '', '' & ] character(*), parameter :: expected_separator_comma_separated(*) = [character(1) :: & ',', ',', ',', ',', ',', ',', ',', ',', ',', ',', & - ',', ',', ',', ',', ',', ',', ',', ',' & + ',', ',', ',', ',', ',', ',', ',', ',', ',' & ] character(*), parameter :: expected_separator_comma_space_separated(*) = [character(1) :: & - ' ', ',', ' ', ',', ' ', ' ', ' ', ',', ' ', ' ', & - ',', ' ', ',', ',', ' ', ' ', ',', ' ' & + ' ', ' ', ',', ' ', ' ', ' ', ' ', ',', ' ', ',', & + ' ', ',', ' ', ',', ',', ',', ',', ' ', ' ' & ] character(:), allocatable :: tokens(:), separator(:)