ZetCode

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.

c1.f90
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.

c2.f90
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.

c3.f90
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.

c5.f90
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.

c4.f90
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.

c6.f90
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)

GNU Fortran manual

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

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.