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.
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.
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.
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.
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.
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.
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.
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)
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
List all Fortran tutorials.