CSV and text data in Fortran
last modified November 14, 2026
Most data that arrives from the outside world is text: a table exported from a
spreadsheet, a column of measurements, a log of readings. The common shape is
the CSV file, short for comma-separated values, where each line is a
record and each record is divided into fields by commas. The
first record is often a header that names the columns. Fortran can
read such a file in two layers: read copies a line into a
character variable, and an internal read on that variable separates and
converts the fields, as in the
internal files article. This article
reads a whole table into arrays, handles a missing value, computes statistics,
writes a formatted report, and sorts the rows by one column.
Reading records and fields
The first program shows the two layers. The file is created at the start so
that the example runs in an empty directory; then the header is read and
printed, and the loop reads one record at a time. The internal read
read (line, *, iostat=ios) name, value splits the record at the
comma, because list-directed input treats a comma as a separator, and converts
the second field to a real number.
program c1
implicit none
character(len=80) :: line
character(len=20) :: name
real :: value
integer :: u, ios, rows
open (newunit=u, file='data.csv', status='replace')
write (u, '(a)') 'name,value'
write (u, '(a)') 'alpha,12.5'
write (u, '(a)') 'beta,7.25'
write (u, '(a)') 'gamma,19.0'
close (u)
open (newunit=u, file='data.csv', status='old')
read (u, '(a)') line
print *, 'header: ', trim(line)
rows = 0
do
read (u, '(a)', iostat=ios) line
if (ios /= 0) exit
read (line, *, iostat=ios) name, value
if (ios /= 0) cycle
rows = rows + 1
print *, rows, trim(name), value
end do
close (u)
print *, 'rows:', rows
end program c1
The header is consumed by its own read before the loop, which is why the loop
starts at the first data record. The second iostat test belongs to
the internal read, not to the file: a record that cannot be converted sets a
positive status, and the cycle statement skips it without stopping
the loop. Three rows are counted, and the names print with their padding
trimmed away. The value of the second field is printed with the default number
of digits, about seven significant ones for a default real.
$ gfortran c1.f90 -o c1
$ ./c1
header: name,value
1 alpha 12.5000000
2 beta 7.25000000
3 gamma 19.0000000
rows: 3
Growing arrays as rows arrive
A program that does not know how many rows to expect can either count them
first or grow its arrays as it goes. Growth uses the automatic reallocation
that Fortran 2003 gives to allocatable arrays: the expression
[values, value] is a new array one element longer than the old
one, and the assignment makes values take it over. The names
follow the same pattern, with one difference: a plain constructor requires all
elements to have the same length, so the typed form
[character(len=20) :: names, name] is needed to pad the scalar to
the length of the array.
program c2
implicit none
character(len=100) :: line
character(len=20) :: name
real :: value
real, allocatable :: values(:)
character(len=20), allocatable :: names(:)
integer :: u, ios, n, i
open (newunit=u, file='data.csv', status='replace')
write (u, '(a)') 'alpha,12.5'
write (u, '(a)') 'beta,7.25'
write (u, '(a)') 'gamma,19.0'
close (u)
n = 0
allocate (values(0), names(0))
open (newunit=u, file='data.csv', status='old')
do
read (u, '(a)', iostat=ios) line
if (ios /= 0) exit
read (line, *, iostat=ios) name, value
if (ios /= 0) cycle
names = [character(len=20) :: names, name]
values = [values, value]
n = n + 1
end do
close (u)
print *, 'rows: ', n
do i = 1, n
print *, i, trim(names(i)), values(i)
end do
print *, 'mean: ', sum(values) / n
print *, 'max: ', maxval(values), 'at', maxloc(values, 1)
print *, 'min: ', minval(values), 'at', minloc(values, 1)
deallocate (names, values)
end program c2
Starting from allocate (values(0)) makes the first append produce a
single element, and the count n stays equal to the length of both
arrays. The three rows give a mean of 12.9167, a maximum of 19 at position 3
and a minimum of 7.25 at position 2, all computed by array intrinsics from the
array intrinsics article. This style
is easy to read but copies the whole array on every new row, so for a large
file the two-pass method of the next section is much faster.
$ gfortran c2.f90 -o c2
$ ./c2
rows: 3
1 alpha 12.5000000
2 beta 7.25000000
3 gamma 19.0000000
mean: 12.9166670
max: 19.0000000 at 3
min: 7.25000000 at 2
Two passes over the file
The alternative is to read the file twice: the first pass counts the records,
the second fills an array that was allocated with the right size.
rewind moves the position back to the first record without closing
the unit, so both passes use the same open file. The program below computes the
mean and the standard deviation, the average distance of the values from their
mean, of six numbers.
program c3
implicit none
real, allocatable :: v(:)
character(len=80) :: line
real :: x, mean, sd
integer :: u, ios, n
open (newunit=u, file='numbers.csv', status='replace')
write (u, '(a)') '1.5'
write (u, '(a)') '2.5'
write (u, '(a)') '2.0'
write (u, '(a)') '3.0'
write (u, '(a)') '4.0'
write (u, '(a)') '2.0'
close (u)
n = 0
open (newunit=u, file='numbers.csv', status='old')
do
read (u, '(a)', iostat=ios) line
if (ios /= 0) exit
n = n + 1
end do
rewind (u)
allocate (v(n))
do ios = 1, n
read (u, '(a)') line
read (line, *) x
v(ios) = x
end do
close (u)
mean = sum(v) / n
sd = sqrt(sum((v - mean)**2) / n)
print *, 'count:', n
print *, 'mean: ', mean
print *, 'sd: ', sd
print *, 'sum: ', sum(v)
end program c3
Six values with a sum of 15 give a mean of 2.5 and a standard deviation of
about 0.8165. The second loop reuses the variable ios as a
counter, which is sloppy but harmless here because the status is no longer
needed; a separate i would read better. The expression
v - mean subtracts a scalar from every element and produces an
array, which is then squared, summed and divided in one statement, an example
of the whole-array style that Fortran is known for.
$ gfortran c3.f90 -o c3 $ ./c3 count: 6 mean: 2.50000000 sd: 0.816496611 sum: 15.0000000
Missing and malformed values
Real tables contain holes: an empty field, a dash, or the letters
n/a where a number should be. A program that stops at the first
hole is useless for such data, so the parser tests the status of each
conversion and counts the failures. The program below splits each record at its
comma with index, reads only the part after the comma, and treats
any field that does not convert as a missing value.
program c5
implicit none
character(len=80) :: line
character(len=20) :: field
real :: total, v
integer :: u, ios, ok, bad, comma
open (newunit=u, file='mixed.csv', status='replace')
write (u, '(a)') 'sensor,value'
write (u, '(a)') 't1,21.5'
write (u, '(a)') 't2,'
write (u, '(a)') 't3,19.0'
write (u, '(a)') 't4,n/a'
close (u)
ok = 0
bad = 0
total = 0.0
open (newunit=u, file='mixed.csv', status='old')
read (u, '(a)') line
do
read (u, '(a)', iostat=ios) line
if (ios /= 0) exit
comma = index(line, ',')
field = line(comma + 1:)
read (field, *, iostat=ios) v
if (ios /= 0) then
bad = bad + 1
print *, 'skipped:', trim(line)
else
ok = ok + 1
total = total + v
end if
end do
close (u)
print *, 'good:', ok, 'bad:', bad
print *, 'sum: ', total
end program c5
Two rows are good and two are skipped, and the sum covers only the good ones.
The empty field is the harder case: read (field, *) v finds only
blanks, which is an end-of-record condition, and the status is negative rather
than positive. Testing ios /= 0 catches both kinds of failure,
whereas testing for a positive value would let the empty field through and
leave v at its previous value, silently adding the same number
twice.
$ gfortran c5.f90 -o c5 $ ./c5 skipped:t2, skipped:t4,n/a good: 2 bad: 2 sum: 40.5000000
Writing a formatted report
The last step of most data programs is a table for a person to read. The
program below writes an inventory to a file with four columns, a rule above and
below the rows, and a total at the end. Every line uses the same widths, so the
columns line up, and the rule is produced by repeat('-', 42)
rather than typed out. Writing to a file instead of the screen makes the result
easy to print or to send.
program c4
implicit none
character(len=10) :: name(4)
integer :: qty(4)
real :: price(4)
integer :: u, i
real :: total
name = ['bolt ', 'nut ', 'screw ', 'washer']
qty = [120, 300, 75, 500]
price = [0.12, 0.07, 0.25, 0.03]
open (newunit=u, file='report.txt', status='replace')
write (u, '(a12, a8, a10, a12)') 'item', 'qty', 'price', 'value'
write (u, '(a)') repeat('-', 42)
total = 0.0
do i = 1, 4
write (u, '(a12, i8, f10.2, f12.2)') trim(name(i)), qty(i), &
price(i), qty(i) * price(i)
total = total + qty(i) * price(i)
end do
write (u, '(a)') repeat('-', 42)
write (u, '(a30, f12.2)') 'total', total
close (u)
print *, 'report.txt written'
end program c4
The widths twelve, eight, ten and twelve add up to the 42 characters of the
rule, so the horizontal lines end exactly where the columns do. The header uses
a descriptors with the same widths, and the total line uses
a30 to push the word to the right of the first three columns.
Each value is the quantity multiplied by the price, and the total, 69.15, is
the sum of the four. The amounts are shown with two decimals because money is
written that way, even though the underlying real has more digits.
$ gfortran c4.f90 -o c4
$ ./c4; cat report.txt
report.txt written
item qty price value
------------------------------------------
bolt 120 0.12 14.40
nut 300 0.07 21.00
screw 75 0.25 18.75
washer 500 0.03 15.00
------------------------------------------
total 69.15
Sorting the rows
A table is often wanted in a particular order, and Fortran has no built-in sort for parallel arrays. The program below keeps the names and the values in two arrays of the same length and sorts them together: whenever two values are out of order, the values are swapped and so are the names at the same positions. The comparison is written for descending order, so the largest value comes first.
program c6
implicit none
character(len=10) :: name(5), t
character(len=80) :: line
real :: value(5), tv
integer :: u, ios, n, i, j
name = ['delta ', 'alpha ', 'echo ', 'bravo ', 'charlie ']
value = [4.0, 1.0, 5.0, 2.0, 3.0]
do i = 1, 4
do j = i + 1, 5
if (value(j) > value(i)) then
tv = value(i)
value(i) = value(j)
value(j) = tv
t = name(i)
name(i) = name(j)
name(j) = t
end if
end do
end do
open (newunit=u, file='sorted.csv', status='replace')
write (u, '(a)') 'name,value'
do n = 1, 5
write (u, '(a, a, f5.1)') trim(name(n)), ',', value(n)
end do
close (u)
open (newunit=u, file='sorted.csv', status='old')
do
read (u, '(a)', iostat=ios) line
if (ios /= 0) exit
print *, trim(line)
end do
close (u)
end program c6
The selection sort makes four passes over the array, and each pass finds the
largest remaining value and moves it to its place, so the final order runs from
5 down to 1. Writing the table and reading it back is a way to check the
result: the printed lines are exactly the contents of the file, with no
padding, because the fields were written with trim. The same
pattern works for sorting by a name instead of a number, since character
comparison uses the rules from the
strings article.
$ gfortran c6.f90 -o c6 $ ./c6 name,value echo, 5.0 delta, 4.0 charlie, 3.0 bravo, 2.0 alpha, 1.0
Next steps
The files article covers the
open, close and iostat statements used
throughout, and the internal files
article explains the conversion from text to numbers in detail. The
derived types article replaces the
parallel arrays of the sorting example with one array of records, and the
namelists article offers a friendlier format
for configuration. The formatted output
article explains the edit descriptors behind the report.
Source
Fortran 2018 standard (J3/18-007r1)
Fortran-lang: learning resources
In this article we have read CSV records field by field, grown arrays as the rows arrived, counted a file before allocating, skipped missing values, written an aligned report and sorted a table by one column. The previous article, Fortran namelists, covers a format where the field names travel with the values.
Author
List all Fortran tutorials.