module strings_m !! summary: Routines for string handling !! author: George Benthien & others !! !! Many of these routines were originally written by !! [George Benthien](https://gbenthien.net/strings/str-index.html), some have !! been written by Sarit Dutta or gathered/modified from other authors (appropriately credited). !! !! These routines were developed primarily to aid in the reading and manipulation !! of input data from an ASCII text file. Accordingly, it is assumed that all !! characters to be processed are ASCII characters. use constants_m implicit none private :: str_from_inum, str_from_ilnum, str_from_dnum interface str_from_num !! Generic interface for writing a number to a string. The calling syntax is !! `str_from_num(num, frmt)` where `number` is a real number or an integer, !! `format` is the format desired, e.g., *e15.6*, *i5*, etc. module procedure str_from_inum module procedure str_from_ilnum module procedure str_from_dnum end interface str_from_num contains !****************************************************************************** pure function str_is_letter(str) result(res) !! Returns `.true.` if `str` contains only letters (*a--z* or *A--Z*) and !! `.false.` otherwise. character(len=*), intent(in) :: str logical :: res character(len=1) :: ch integer :: i res = .true. do i = 1, len(str) ch = str(i:i) select case(ch) case ('A':'Z') cycle case ('a':'z') cycle case default res = .false. return end select end do end function !****************************************************************************** pure function str_is_digit(str) result(res) !! Returns `.true.` if `str` contains only digits (0,1,...,9) and !!`.false.` otherwise. character(len=*), intent(in) :: str logical :: res character(len=1) :: ch integer :: i res = .true. do i = 1, len(str) ch = str(i:i) select case(ch) case('0':'9') cycle case default res = .false. return end select end do end function !******************************************************************************* pure function str_is_space(str) result(res) !! Returns `.true.` if `str` is non-empty and contains only whitespace !! characters (tab or blankspace). Otherwise `.false.` is returned. !! !! *Note*: This function will return `.false.` for an empty string. character(len=*), intent(in) :: str logical :: res integer :: lenstr integer :: ich integer :: i lenstr = len(str) if (lenstr == 0) then res = .false. return end if res = .true. do i = 1, lenstr ich = iachar(str(i:i)) select case(ich) case (9,32) res = .true. case default res = .false. return end select end do end function !******************************************************************************* pure function str_is_comment(line, comment_str) result(res) !! Returns `.true.` if `line` is a comment, `.false.` other wise. !! !! `line` is a comment if `comment_str` is its first non-blank character !! sequence. If `line` is an empty string or contains only blankspaces, the !! return value is `.false.` If `comment_str` is empty, the return value is !! `.true.`. character(len=*), intent (in) :: line !! Input string character(len=*), intent (in) :: comment_str !! String marking the beginning of a comment. logical :: res res = .false. if ( index(adjustl(line),comment_str) == 1 ) then res = .true. end if end function !******************************************************************************* pure function str_compact(str) result(ostr) !! Returns a copy of `str` with multiple spaces and tabs converted to !! single spaces, control characters deleted, and leading and trailing !! spaces removed. character(len=*), intent(in) :: str character(len=:), allocatable :: ostr character(len=len(str)) :: buf character(len=1) :: ch integer :: lenstr integer :: isp, ich integer :: i, k lenstr = len(str) isp = 0; k = 0 buf = '' do i = 1, lenstr ch = str(i:i) ich = iachar(ch) select case(ich) case(9,32) ! space or tab character if (isp==0) then k = k+1 buf(k:k) = ' ' end if isp = 1 case(33:) ! not a space, quote, or control character k = k + 1 buf(k:k) = ch isp = 0 end select end do ostr = trim(adjustl(buf)) end function !****************************************************************************** pure function str_remove_stcc(str) result(ostr) !! Returns a copy of the string `str` with spaces, tabs, and !! control characters removed. character(len=*), intent(in) :: str character(len=:), allocatable :: ostr character(len=len(str)) :: buf character(len=1) :: ch integer :: lenstr integer :: ich integer :: k integer :: i lenstr = len(str) k = 0 do i = 1, lenstr ch = str(i:i) ich = iachar(ch) select case(ich) case(0:32) ! space, tab, or control character cycle case(33:) k = k+1 buf(k:k) = ch end select end do ostr = trim(adjustl(buf)) end function !****************************************************************************** pure function str_to_upper(str) result(ucstr) !! This function returns a string that is like the string `str` with all characters !! that are not between a pair of quotes ("..." or '...') converted to uppercase. character (len=*), intent(in) :: str character (len=len(str)) :: ucstr integer :: ilen integer :: ioffset integer :: iquote integer :: iav integer :: iqc integer :: i ilen = len(str) ioffset = iachar('A') - iachar('a') iquote = 0 ucstr = str do i = 1, ilen iav = iachar(str(i:i)) if ( (iquote==0) .and. ((iav==34) .or. (iav==39)) ) then iquote = 1 iqc = iav cycle end if if ( (iquote==1) .and. (iav==iqc) ) then iquote = 0 cycle end if if (iquote==1) cycle if ( (iav >= iachar('a')) .and. (iav <= iachar('z')) ) then ucstr(i:i) = achar(iav+ioffset) end if end do end function !********************************************************************** pure function str_to_lower(str) result(lcstr) !! This function returns a string that is like the string `str` with all characters !! that are not between a pair of quotes ("..." or '...') converted to lowercase. character (len=*), intent(in):: str character (len=len(str)) :: lcstr integer :: ilen integer :: ioffset integer :: iquote integer :: iav integer :: iqc integer :: i ilen = len(str) ioffset = iachar('A')-iachar('a') iquote = 0 lcstr = str do i = 1, ilen iav = iachar(str(i:i)) if ( (iquote==0) .and. ((iav==34) .or. (iav==39)) ) then iquote = 1 iqc = iav cycle end if if ( (iquote==1) .and. (iav==iqc) ) then iquote=0 cycle end if if (iquote==1) cycle if ( (iav >= iachar('A')) .and. (iav <= iachar('Z')) ) then lcstr(i:i) = achar(iav-ioffset) end if end do end function !****************************************************************************** subroutine str_shift(str, n) !! Shifts characters in `str` by `n` positions (positive values !! denote a right shift and negative values denote a left shift). Characters !! that are shifted off the end are lost. Positions opened up by the shift !! are replaced by spaces. character(len=*), intent(in out):: str integer, intent(in) :: n integer :: lenstr integer :: nabs lenstr = len(str) nabs = iabs(n) if (nabs >= lenstr) then str = repeat(' ', lenstr) return end if if (n < 0) str = str(nabs+1:)//repeat(' ',nabs) ! shift left if (n > 0) str = repeat(' ',nabs)//str(:lenstr-nabs) ! shift right end subroutine !****************************************************************************** subroutine str_insert(str, substr, loc) !! Inserts the string `substr` into the string `str` at position `loc`. !! Characters in `str` starting at position `loc` are shifted right to !! make room for the inserted string. Trailing spaces of `substr` are !! removed prior to insertion. character(len=*), intent(in out) :: str character(len=*), intent(in) :: substr integer, intent(in) :: loc character(len=len(str)) ::tempstr integer :: len_substr len_substr = len_trim(substr) tempstr = str(loc:) call str_shift(tempstr, len_substr) tempstr(1:len_substr) = substr(1:len_substr) str(loc:) = tempstr end subroutine !****************************************************************************** subroutine str_del(str, substr, n) !! Deletes first `n` occurrences of substring `substr` from string `str` and !! shifts characters left to fill hole. If `n < 0`, all occurances are !! deleted. If `n` is not explicitly provided, it defaults to removing the !! first occurrence. Trailing spaces or blanks are not considered part of !! `substr`. character(len=*), intent(in out) :: str character(len=*), intent(in) :: substr integer, optional, intent(in) :: n integer :: n_ integer :: lensubstr integer :: ipos integer :: cntr n_ = 1 if (present(n)) n_ = n lensubstr = len_trim(substr) cntr = 0 do if ((n_ > 0) .and. (cntr > n_)) exit ipos = index(str,substr) if (ipos == 0) exit str = str(:ipos-1)//str(ipos+lensubstr:) cntr = cntr + 1 end do end subroutine !********************************************************************** subroutine str_strip_comment(str, comment_str) !! Strips trailing comment from a string. !! !! The comment is assumed to begin with the sequence of characters in !! `comment_str`. If the sequence `comment_str` is not found within `str`, !! no changes are made. character(len=*), intent (in out) :: str !! Input string character(len=*), intent (in) :: comment_str !! String indicating beginning of a comment. integer :: ipos ipos = index(adjustl(str),comment_str) if (ipos /= 0) then str = str(1:(ipos-1)) end if end subroutine !********************************************************************** subroutine str_get_keyval(str, key, val, delimiter) !! Split a string `str` into two strings, `key` and `val` based on space !! delimiter. !! !! A non-empty non-comment string should be passed to this subroutine. !! Keys can have corresponding empty values, but keys must always be present character (len=*), intent (in) :: str character (len=:), allocatable, intent (out) :: key character (len=:), allocatable, intent (out) :: val character (len=*), intent (in), optional :: delimiter character (len=:), allocatable :: delimiter_ character (len=:), allocatable :: str_just integer :: m integer :: n !blankspace is represented as the integer 32 in ascii chart. delimiter_ = achar(32) if (present(delimiter)) delimiter_ = delimiter str_just = trim(adjustl(str)) n = len(str_just) m = index(str_just, delimiter_) if (m == 0) then key = str_just val = '' else key = trim(str_just(1:m-1)) val = str_just(m+len_trim(delimiter_):n) end if val = trim(adjustl(val)) end subroutine !****************************************************************************** subroutine str_match(str, ipos, imatch) !! This routine finds the delimiter in string `str` that matches the delimiter !! in position `ipos` of `str`. The argument `imatch` contains the position of !! the matching delimiter. Allowable delimiters are (), [], {}, <>. character(len=*), intent(in) :: str integer, intent(in) :: ipos integer, intent(out) :: imatch character(len=1) :: delim1 character(len=1) :: delim2 character(len=1) :: ch integer :: lenstr integer :: istart integer :: iend integer :: inc integer :: idelim2 integer :: isum integer :: i lenstr = len_trim(str) delim1 = str(ipos:ipos) select case(delim1) case('(') idelim2=iachar(delim1)+1 istart=ipos+1 iend=lenstr inc=1 case(')') idelim2=iachar(delim1)-1 istart=ipos-1 iend=1 inc=-1 case('[','{','<') idelim2=iachar(delim1)+2 istart=ipos+1 iend=lenstr inc=1 case(']','}','>') idelim2=iachar(delim1)-2 istart=ipos-1 iend=1 inc=-1 case default write(*,*) delim1,' is not a valid delimiter' return end select if (istart < 1 .or. istart > lenstr) then write(*,*) delim1,' has no matching delimiter' return end if delim2=achar(idelim2) ! matching delimiter isum = 1 do i = istart, iend, inc ch=str(i:i) if (ch /= delim1 .and. ch /= delim2) cycle if (ch == delim1) isum = isum+1 if (ch == delim2) isum = isum-1 if (isum == 0) exit end do if(isum /= 0) then write(*,*) delim1,' has no matching delimiter' return end if imatch = i end subroutine !********************************************************************** pure function str_from_inum(num, frmt) result(str) integer, intent(in) :: num character(len=:), allocatable :: str character(len=*), optional, intent(in) :: frmt character(len=:), allocatable :: frmt_ character(len=24) :: buf frmt_ = '(i0)' if (present(frmt)) frmt_ = frmt write(buf, frmt_) num str = trim(adjustl(buf)) end function !********************************************************************** pure function str_from_ilnum(num, frmt) result(str) integer(ip_long), intent(in) :: num character(len=:), allocatable :: str character(len=*), optional, intent(in) :: frmt character(len=:), allocatable :: frmt_ character(len=24) :: buf frmt_ = '(i0)' if (present(frmt)) frmt_ = frmt write(buf, frmt_) num str = trim(adjustl(buf)) end function !********************************************************************** pure function str_from_dnum(num, frmt) result(str) real(rp), intent(in) :: num character(len=:), allocatable :: str character(len=*), optional, intent(in) :: frmt character(len=:), allocatable :: frmt_ character(len=32) :: buf frmt_ = '(g0.15)' if (present(frmt)) frmt_ = frmt write(buf, frmt_) num str = str_trimzero(buf) end function !****************************************************************************** subroutine str_compact_rlstr(str) !! author: Izaak Beekman !! date: 02/24/2015 !! !! Compact a string representing a real number, so that the same value is !! displayed with fewer characters. character(len=*),intent(in out) :: str !! string representation of a real number. character(len=len(str)) :: significand character(len=len(str)) :: expnt character(len=2) :: separator integer :: exp_start integer :: decimal_pos integer :: sig_trim integer :: exp_trim integer :: i !! counter str = adjustl(str) exp_start = scan(str, 'eEdD') if (exp_start == 0) exp_start = scan(str, '-+', back=.true.) decimal_pos = scan(str, '.') if (exp_start /= 0) separator = str(exp_start:exp_start) if ( exp_start < decimal_pos ) then !possibly signed, exponent-less float significand = str sig_trim = len_trim(significand) do i = len_trim(significand),decimal_pos+2,-1 !look from right to left at 0s !but save one after the decimal place if (significand(i:i) == '0') then sig_trim = i-1 else exit end if end do str = trim(significand(1:sig_trim)) else if (exp_start > decimal_pos) then !float has exponent significand = str(1:exp_start-1) sig_trim = len_trim(significand) do i = len_trim(significand),decimal_pos+2,-1 !look from right to left at 0s if (significand(i:i) == '0') then sig_trim = i-1 else exit end if end do expnt = adjustl(str(exp_start+1:)) if (expnt(1:1) == '+' .or. expnt(1:1) == '-') then separator = trim(adjustl(separator))//expnt(1:1) exp_start = exp_start + 1 expnt = adjustl(str(exp_start+1:)) end if exp_trim = 1 do i = 1,(len_trim(expnt)-1) !look at exponent leading zeros saving last if (expnt(i:i) == '0') then exp_trim = i+1 else exit end if end do str = trim(adjustl(significand(1:sig_trim)))// & trim(adjustl(separator))// & trim(adjustl(expnt(exp_trim:))) !else ! mal-formed real, BUT this code should be unreachable end if end subroutine !******************************************************************************* pure function str_trimzero(str) result(res) !! Deletes nonsignificant trailing zeroes from number string str. If number !! string ends in a decimal point, one trailing zero is added. character(len=*), intent(in) :: str character(len=:), allocatable :: res character(len=len(str)) :: buf character(len=10) :: sexp character(len=1) :: ch integer :: ipos integer :: lbuf integer :: i buf = str ipos = scan(str,'eE') if (ipos > 0) then sexp = buf(ipos:) buf = buf(1:ipos-1) endif lbuf = len_trim(buf) do i = lbuf, 1, -1 ch = buf(i:i) if (ch == '0') cycle if (ch == '.') then buf = buf(1:i)//'0' if (ipos > 0) buf = trim(buf)//trim(sexp) exit endif buf = buf(1:i) exit end do if(ipos > 0) buf = trim(buf)//trim(sexp) res = trim(adjustl(buf)) end function !*********************************************************************** pure function str_to_d(str) result(res) character(len=*), intent(in) :: str real(rp) :: res read(str,*) res end function !*********************************************************************** pure function str_to_i(str) result(res) character(len=*), intent(in) :: str integer :: res read(str,*) res end function !*********************************************************************** pure function str_strip(str, chars, ends) result (ostr) !! Returns a copy of string `str` with the leading and trailing characters !! removed. The `chars` argument is a string specifying the set of characters to !! be removed. The `chars` argument is not a prefix or suffix; rather, all !! combinations of its values are stripped. If `ends = 'l'`, only leading !! characters are removed, if `ends = 'r'`, only trailing characters are !! removed, and if `ends = 'b'` both leading and trailing characters are !! removed. character(len=*), intent(in) :: str character(len=*), intent(in) :: chars character(len=1), intent(in) :: ends !! {'l', 'r', 'b'} character(len=:), allocatable :: ostr integer :: lenstr integer :: ibeg integer :: iend lenstr = len(str) select case (ends) case ('l') ibeg = verify(str, chars) iend = lenstr case ('r') ibeg = 1 iend = verify(str, chars, .true.) case ('b') ibeg = verify(str, chars) iend = verify(str, chars, .true.) case default ibeg = 1 iend = lenstr end select if ((ibeg==0) .or. (iend==0)) then ostr = '' else ostr = str(ibeg:iend) end if end function !********************************************************************** subroutine str_split(str, delimiter, before) !! Routine finds the first instance of a character from `delims` in the the !! string `str`. The characters before the found delimiter are output in !! `before`. The characters after the found delimiter are output in `str`. !! Repeated applications of this routine can be used to parse a string into its !! component parts. Multiple whitespaces of `str` are compacted into a single !! whitespace before splitting begins. If either `str` or `delimiter` is !! empty, an empty string is retured in `before` and `str` remains !! unchanged. character(len=*), intent(in out) :: str character(len=*), intent(in) :: delimiter character(len=:), allocatable, intent(out) :: before integer :: lenstr integer :: lendelim integer :: ipos str = str_compact(str) lenstr = len(str) lendelim = len(delimiter) if ( (lenstr == 0) .or. (lendelim == 0) ) then ! `str` or `delimiter` is empty before = '' return end if ipos = index(str, delimiter) if (ipos == 0) then ! string does not contain any delimiter before = '' return else before = str(1:(ipos-1)) str = str((ipos+lendelim-1):) end if end subroutine !********************************************************************** subroutine str_append(dest, source, sep) !! Appends a copy of the `source` string to the `dest` string, with !! optional string `sep` in between. It is assumed that `dest` is long !! enough to hold the result, otherwise an error will be generated. character(len=*), intent(in out) :: dest character(len=*), intent(in) :: source character(len=*), optional, intent(in) :: sep character(len=:), allocatable :: sep_ integer :: len_dest integer :: len_source integer :: len_sep_ integer :: ipos sep_ = '' if (present(sep)) sep_ = sep len_dest = len_trim(dest) len_source = len_trim(adjustl(source)) len_sep_ = len(sep_) ipos = len_dest + 1 if (len_sep_ > 0) then dest( ipos:(ipos+len_sep_) ) = sep_ ipos = ipos + len_sep_ + 1 end if dest( ipos:(ipos+len_source) ) = trim(adjustl(source)) end subroutine !********************************************************************** pure function str_startswith(str, prefix, start, finish) result(res) !! Returns `.true.` if the string `str` starts with `prefix`, otherwise !! returns `.false.`. With optional `start`, test beginning at that position. !! With optional `finish`, stop comparing beyond that position. character(len=*), intent(in) :: str character(len=*), intent(in) :: prefix integer, optional, intent(in) :: start integer, optional, intent(in) :: finish logical :: res integer :: ibeg integer :: iend ibeg = 1; iend = len(str) if (present(start)) ibeg = start if (present(finish)) iend = finish !Return .false. if prefix is longer than str(ibeg:iend) if (len(prefix) > (iend-ibeg+1)) then res = .false. return end if if (index(str(ibeg:iend), prefix) == 1) then res = .true. else res = .false. end if end function !********************************************************************** pure function str_endswith(str, suffix, start, finish) result(res) !! Returns `.true.` if the string `str` ends with `suffix`, otherwise !! return `.false.`. With optional `start`, test beginning at that position. !! With optional `finish`, stop comparing beyond that position. character(len=*), intent(in) :: str character(len=*), intent(in) :: suffix integer, optional, intent(in) :: start integer, optional, intent(in) :: finish logical :: res integer :: ibeg integer :: iend integer :: iloc ibeg = 1; iend = len(str) if (present(start)) ibeg = start if (present(finish)) iend = finish !Return .false. if suffix is longer than str(ibeg:iend) if (len(suffix) > (iend-ibeg+1)) then res = .false. return end if ! Getting last occurrance of suffix iloc = index(str(ibeg:iend), suffix, back=.true.) if ( (iloc+len(suffix)-1) == iend) then res = .true. else res = .false. end if end function !****************************************************************************** subroutine readline(nunitr, line, comment_str, ios) !! Reads a line from unit=nunitr, ignoring blank lines !! and deleting comments integer, intent(in) :: nunitr character(len=*), intent(in out):: line character(len=*), intent(in) :: comment_str integer, intent(out) :: ios do read(nunitr,'(a)', iostat=ios) line ! read input line if(ios /= 0) return if ((len_trim(line) /= 0) .and. (.not. str_is_comment(line, comment_str))) then call str_strip_comment(line, comment_str) exit end if end do end subroutine !****************************************************************************** end module strings_m