ZetCode

Fortran string processing

last modified November 14, 2026

Most text processing comes down to the same two moves: look at a string one character at a time, and build a new string from the pieces you keep. This article uses those moves to write a small toolkit of functions and subroutines: changing case, splitting a sentence into words, reversing text, recognising palindromes, replacing one substring with another, squeezing repeated blanks, sorting words and converting text to title case. Each example is a complete program with the reusable part in a module, so it can be compiled and tested on its own.

Changing case

A case conversion visits every character and, when the character is a letter, replaces it with the letter 32 codes away. That distance is not magic: in ASCII the capital letters occupy the codes 65 to 90, the lowercase letters 97 to 122, and the two blocks line up, so a minus A is always 32. Characters outside the letter ranges, such as digits and punctuation, are copied unchanged. The pure attribute on both functions promises that they change no global state, which lets the compiler use them more freely.

p1.f90
module casemod
    implicit none
contains
    pure function to_upper(s) result(r)
        character(len=*), intent(in) :: s
        character(len=len(s)) :: r
        integer :: i, c

        r = s
        do i = 1, len(s)
            c = iachar(s(i:i))
            if (c >= iachar('a') .and. c <= iachar('z')) r(i:i) = achar(c - 32)
        end do
    end function to_upper

    pure function to_lower(s) result(r)
        character(len=*), intent(in) :: s
        character(len=len(s)) :: r
        integer :: i, c

        r = s
        do i = 1, len(s)
            c = iachar(s(i:i))
            if (c >= iachar('A') .and. c <= iachar('Z')) r(i:i) = achar(c + 32)
        end do
    end function to_lower
end module casemod

program p1
    use casemod
    implicit none

    print *, to_upper('Hello, Fortran!')
    print *, to_lower('Hello, Fortran!')
    print *, to_upper('mixed CASE 42')
end program p1

The result of each function has the same length as its argument, because character(len=len(s)) is declared in terms of the dummy argument. The first call raises the letters and leaves the comma and the exclamation mark alone, the second lowers them, and the third shows that only letters are touched: CASE becomes CASE in upper case and the digits stay as they are. Using iachar and achar instead of arithmetic on characters makes the comparison explicit and portable to any ASCII-based system.

$ gfortran p1.f90 -o p1
$ ./p1
 HELLO, FORTRAN!
 hello, fortran!
 MIXED CASE 42

Splitting a sentence into words

A word here means a run of characters between spaces, and the task of cutting a string into such runs is called tokenizing; each piece is a token. The subroutine below keeps a position in the input, skips the spaces at that position, finds the next space with index, and copies the characters in between into the next element of an array. The subroutine returns the pieces and a count, because the rest of the array is still empty when there are fewer words than slots.

p2.f90
module splitmod
    implicit none
contains
    subroutine split(s, words, n)
        character(len=*), intent(in) :: s
        character(len=*), intent(out) :: words(:)
        integer, intent(out) :: n
        integer :: pos, sp

        n = 0
        pos = 1
        do
            do while (pos <= len(s) .and. s(pos:pos) == ' ')
                pos = pos + 1
            end do
            if (pos > len(s)) exit
            sp = index(s(pos:), ' ')
            if (sp == 0) then
                n = n + 1
                words(n) = s(pos:)
                exit
            end if
            n = n + 1
            words(n) = s(pos:pos + sp - 2)
            pos = pos + sp
        end do
    end subroutine split
end module splitmod

program p2
    use splitmod
    implicit none
    character(len=20) :: words(12)
    character(len=*), parameter :: text = &
        'The quick brown fox jumps over the lazy dog'
    integer :: i, n

    call split(text, words, n)
    print *, 'words:', n
    do i = 1, n
        print *, i, '[', trim(words(i)), ']'
    end do
end program p2

The sentence has nine words, so the count is 9 and the first nine elements of the array are filled. The do while loop inside the outer one is what allows several spaces in a row: each pass moves past one blank, and the loop stops at the first character that is not a blank. When index returns zero there is no further space, so the rest of the string is the last word and exit ends the loop. The array elements are twenty characters long, hence the trimming before printing between brackets. The subroutine trusts that the array has room for every word; a library version would compare n with size(words) before storing.

$ gfortran p2.f90 -o p2
$ ./p2
 words:           9
           1 [The]
           2 [quick]
           3 [brown]
           4 [fox]
           5 [jumps]
           6 [over]
           7 [the]
           8 [lazy]
           9 [dog]

Reversing and palindromes

To reverse a string, copy its last character to the first position, its second-to-last to the second, and so on. A palindrome is a word or sentence that reads the same in both directions once case and punctuation are ignored: Radar and A man, a plan, a canal: Panama are the usual examples. The module below adds a function that keeps only the letters and lowercases them, and a predicate, a function that returns true or false, which compares the cleaned string with its reverse.

p3.f90
module palmod
    implicit none
contains
    pure function reverse(s) result(r)
        character(len=*), intent(in) :: s
        character(len=len(s)) :: r
        integer :: i, n

        n = len(s)
        do i = 1, n
            r(i:i) = s(n - i + 1:n - i + 1)
        end do
    end function reverse

    pure function clean(s) result(r)
        character(len=*), intent(in) :: s
        character(len=:), allocatable :: r
        integer :: i, c

        r = ''
        do i = 1, len(s)
            c = iachar(s(i:i))
            if (c >= iachar('A') .and. c <= iachar('Z')) then
                r = r // achar(c + 32)
            else if (c >= iachar('a') .and. c <= iachar('z')) then
                r = r // s(i:i)
            end if
        end do
    end function clean

    pure logical function is_palindrome(s) result(r)
        character(len=*), intent(in) :: s
        character(len=:), allocatable :: t

        t = clean(s)
        r = t == reverse(t)
    end function is_palindrome
end module palmod

program p3
    use palmod
    implicit none

    print *, reverse('Fortran')
    print *, reverse('abcdef')
    print *, 'level:  ', is_palindrome('level')
    print *, 'radar:  ', is_palindrome('Radar')
    print *, 'sentence:', is_palindrome('A man, a plan, a canal: Panama')
    print *, 'fortran: ', is_palindrome('fortran')
end program p3

Reversing Fortran gives nartroF and reversing abcdef gives fedcba. level and Radar are palindromes, and so is the whole Panama sentence, even though it contains spaces, commas and a colon, because clean removes everything that is not a letter before the comparison. The final F shows that an ordinary word fails the test. The reversed copy is built with an explicit loop rather than a built-in, since Fortran has no reverse intrinsic for strings.

$ gfortran p3.f90 -o p3
$ ./p3
 nartroF
 fedcba
 level:   T
 radar:   T
 sentence: T
 fortran:  F

Replacing substrings

Replacing text is a loop that searches for the old substring, copies the characters before it, writes the new substring, and continues after the match. The function below returns a deferred-length result, because the length of the answer depends on how many matches there are and on how the two substrings differ in length. When the search finds nothing, the rest of the input is appended and the loop ends.

p4.f90
module replacemod
    implicit none
contains
    pure function replace_all(s, old, new) result(r)
        character(len=*), intent(in) :: s, old, new
        character(len=:), allocatable :: r
        integer :: p, n

        r = ''
        p = 1
        do
            n = index(s(p:), old)
            if (n == 0) then
                r = r // s(p:)
                exit
            end if
            r = r // s(p:p + n - 2) // new
            p = p + n + len(old) - 1
            if (p > len(s)) exit
        end do
    end function replace_all
end module replacemod

program p4
    use replacemod
    implicit none
    character(len=:), allocatable :: text

    text = 'the cat sat on the mat'
    print *, replace_all(text, 'the', 'a')
    print *, replace_all(text, 'at', 'og')
    print *, replace_all(text, 'dog', 'cat')
    print *, replace_all(text, ' ', '_')
end program p4

The first call replaces both occurrences of the, the second replaces every at and produces cog, sog and mog, and the third returns the text unchanged because dog never appears. The last call turns the sentence into one long identifier. The position update p = p + n + len(old) - 1 is the part that needs care: it moves past the match, so a replacement whose text contains the search string cannot loop forever.

$ gfortran p4.f90 -o p4
$ ./p4
 a cat sat on a mat
 the cog sog on the mog
 the cat sat on the mat
 the_cat_sat_on_the_mat

Squeezing repeated blanks

Input typed by a person often carries extra spaces: two words separated by several blanks, or a line that begins and ends with them. Squeezing means keeping one blank wherever there was at least one, and dropping the rest. The function below remembers whether the previous character was a blank, which is enough to decide whether the current blank should be copied. Starting with last_blank = .true. also removes the leading blanks.

p5.f90
module squeezemod
    implicit none
contains
    pure function squeeze(s) result(r)
        character(len=*), intent(in) :: s
        character(len=:), allocatable :: r
        logical :: last_blank
        integer :: i

        r = ''
        last_blank = .true.
        do i = 1, len_trim(s)
            if (s(i:i) == ' ') then
                if (.not. last_blank) r = r // ' '
                last_blank = .true.
            else
                r = r // s(i:i)
                last_blank = .false.
            end if
        end do
    end function squeeze
end module squeezemod

program p5
    use squeezemod
    implicit none
    character(len=*), parameter :: text = '  Fortran     is   fun  '

    print *, '[', text, ']'
    print *, '[', squeeze(text), ']'
    print *, 'len before:', len_trim(text)
    print *, 'len after: ', len(squeeze(text))
end program p5

The input has 22 characters up to its last non-blank, and the result has 14: the two leading blanks, the runs between the words and the trailing pair are all gone, leaving single spaces between Fortran, is and fun. Because the loop runs to len_trim(s) rather than len(s), the padding that a fixed-length variable would carry is not scanned at all.

$ gfortran p5.f90 -o p5
$ ./p5
 [  Fortran     is   fun  ]
 [Fortran is fun]
 len before:          22
 len after:           14

Sorting words

Sorting strings uses the comparison rules from the strings article, where shorter operands are padded with blanks and uppercase letters sort before lowercase ones. The program below uses a bubble sort: it compares each neighbouring pair and swaps them when they are out of order, and each pass leaves the largest remaining word at the end of the part that is still unsorted. It is not the fastest algorithm, but it is short enough to read in one screen, and the comparison it performs is the interesting part here.

p6.f90
program p6
    implicit none
    character(len=10) :: w(6), tmp
    integer :: i, j

    w = [character(len=10) :: 'pear', 'apple', 'fig', 'banana', &
                              'cherry', 'date']
    print *, 'before:'
    do i = 1, size(w)
        print *, '[', w(i), ']'
    end do
    do i = 1, size(w) - 1
        do j = 1, size(w) - i
            if (w(j) > w(j + 1)) then
                tmp = w(j)
                w(j) = w(j + 1)
                w(j + 1) = tmp
            end if
        end do
    end do
    print *, 'after:'
    do i = 1, size(w)
        print *, '[', w(i), ']'
    end do
end program p6

The constructor needs the character(len=10) :: prefix because the six literals have different lengths; without it the compiler rejects the mixture. The result is alphabetical, with apple first and pear last, and the brackets show that each word still occupies ten characters, padded with blanks. Since all elements have the same declared length, the comparison never has to pad anything itself, but the padding is still part of every value.

$ gfortran p6.f90 -o p6
$ ./p6
 before:
 [pear      ]
 [apple     ]
 [fig       ]
 [banana    ]
 [cherry    ]
 [date      ]
 after:
 [apple     ]
 [banana    ]
 [cherry    ]
 [date      ]
 [fig       ]
 [pear      ]

Title case

Title case means that each word starts with a capital letter and the remaining letters are lowercase. The function tracks whether the previous character ended a word: at the start of a word a lowercase letter is raised, inside a word an uppercase letter is lowered, and any non-letter begins a new word. This handles input that is all capitals, all lowercase or a mixture, without a separate pass over the text.

p7.f90
module titlemod
    implicit none
contains
    pure function title_case(s) result(r)
        character(len=*), intent(in) :: s
        character(len=len(s)) :: r
        logical :: start
        integer :: i, c

        r = s
        start = .true.
        do i = 1, len(s)
            c = iachar(s(i:i))
            if (c >= iachar('a') .and. c <= iachar('z')) then
                if (start) r(i:i) = achar(c - 32)
                start = .false.
            else if (c >= iachar('A') .and. c <= iachar('Z')) then
                if (.not. start) r(i:i) = achar(c + 32)
                start = .false.
            else
                start = .true.
            end if
        end do
    end function title_case
end module titlemod

program p7
    use titlemod
    implicit none

    print *, title_case('hELLO wORLD')
    print *, title_case('the quick brown fox')
    print *, title_case('fortran 2018 IS fun')
end program p7

The first string is repaired from its mixed capitals, the second is capitalised throughout, and the third shows how digits are treated as word breaks: 2018 stays as it is, while IS becomes Is. Words separated by punctuation would work the same way, because anything that is not a letter sets start back to true. The result has the length of the input, padded if the input was a shorter literal, so the trailing blanks of a fixed-length argument come along unchanged.

$ gfortran p7.f90 -o p7
$ ./p7
 Hello World
 The Quick Brown Fox
 Fortran 2018 Is Fun

Next steps

The Fortran strings article explains the declarations, substrings and intrinsics that every function here relies on. The internal files article shows how to turn numbers into the strings that these functions return, and the reading input article applies the line splitting to data typed at the keyboard or piped into the program. The arrays article covers the array constructors and element loops used in the sorting example.

Source

Fortran 2018 standard (J3/18-007r1)

GNU Fortran: INDEX intrinsic

Fortran-lang: learning resources

In this article we have written string routines that are useful in real programs: changing case, tokenizing a sentence, reversing text, testing palindromes, replacing substrings, squeezing blanks, sorting words and producing title case. The previous article, Fortran strings, covers the character type itself.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.