OpenCDSS / cdss-app-statecu-fortran

Colorado's Decision Support Systems (CDSS) StateCU consumptive use model code, documentation, tests
GNU General Public License v3.0
1 stars 1 forks source link

Compiler warning about comparing string variables of different length #19

Open smalers opened 3 years ago

smalers commented 3 years ago

With older code, string variables of different length might be declared in one place such as the main program and passed to subroutines that declare the strings with an array size. If the arrays are then compared in the subroutine, the following error can result.

stringcompare.for:25:10-23:

   25 |       if ( string1 .eq. string2 ) then
      |          1            2
Error: Shapes for operands at (1) and (2) are not conformable

The following explains how string comparisons are done (strings are padded with spaces in the comparison): https://michaelgoerz.net/notes/some-fortran-string-gotchas.html

The best practice is to use something like the following in the subroutine:

      ! Version that uses variable string sizes:
      subroutine compare2(string1, string2)
      character (len=*) string1
      character (len=*) string2

      if ( string1 .eq. string2 ) then
        write(*,*) 'compare2: Strings are equal.'
      else
        write(*,*) 'compare2: Strings are not equal.'
      endif

      return
      end

For more information, here is a main program and makefile used to test. It is confusing that trim does not seem to impact arrays with fixed dimensions, but maybe that is because Fortran string length is set at the dimensioned length. See the comments in compare1 subroutine for examples of what works and what does not work.

Program

! Test program to evaluate gfortran compiler options
! Test simple initialization of variable with automatic variables.

      program testprog1

      implicit none

      ! Declare variables.
      character (len=80) string80
      character (len=80) string80spaces
      character (len=80) string80read
      character (len=32) string32
      character (len=32) string32spaces
      character (len=32) string32read

      character (len=80) stringinput

      string80 = 'A longer string                 x'
      string32 = 'A short string x'

      ! Call subroutines to do the comparison.

      call compare1(string80,string32)

      call compare2(string80,string32)

      ! Local string comparisons.

      ! Compare strings initialized to different values.
      if ( string80 .eq. string32 ) then
        write(*,*) 'main: Strings of different length are equal.'
      else
        write(*,*) 'main: Strings of different length are not equal.'
      endif

      string80spaces = 'ALFALFA'
      string32spaces = 'ALFALFA     '

      ! Compare strings that have extra spaces on end.
      if ( string80 .eq. string32 ) then
        write(*,*) 'main: Strings with extra spaces are equal.'
      else
        write(*,*) 'main: Strings with extra spaces are not equal.'
      endif

      ! Compare strings read from input.
      ! - won't be equal because strings have extra spaces on the ends
      stringinput = 'ALFALFA      ALFALFA'
      read(stringinput,'(a12,1x,a80)') string32read, string80read
      if ( string80read .eq. string32read ) then
        write(*,*) 'main: Strings read from input are equal'
        write(*,*) '  string32read=|', string32read, "|"
        write(*,*) '    len(string32read)=', len(string32read)
        write(*,*) '  string80read=|', string80read, "|"
        write(*,*) '    len(string80read)=', len(string80read)
      else
        write(*,*) 'main: Strings read from input are not equal.'
        write(*,*) '  string32read=|', string32read, "|"
        write(*,*) '    len(string32read)=', len(string32read)
        write(*,*) '  string80read=|', string80read, "|"
        write(*,*) '    len(string80read)=', len(string80read)
      endif

      string32read = trim(string32read)
      string80read = trim(string80read)
      if ( string80read .eq. string32read ) then
        write(*,*) 'main: Trimmed strings read from input are equal'
        write(*,*) '  string32read=|', string32read, "|"
        write(*,*) '  string80read=|', string80read, "|"
      else
        write(*,*)'main: Trimmed strings read from input are not equal.'
        write(*,*) '  string32read=|', string32read, "|"
        write(*,*) '  string80read=|', string80read, "|"
      endif

      end

      ! ==================================

      ! Version that uses hard-coded string sizes:
      ! - will not compile if declared with comments below

      subroutine compare1(string1, string2)

      ! The following does not compile.
      !character string1(80)
      !character string2(32)

      ! Either of the following compile:
      ! - however, see compare2 for preferred approach
      !character string2*80
      !character string2*32
      character (len=80) string1
      character (len=32) string2

      if ( string1 .eq. string2 ) then
        write(*,*) 'compare1: Strings are equal.'
      else
        write(*,*) 'compare1: Strings are not equal.'
      endif

      return
      end

      ! Version that uses variable string sizes:
      subroutine compare2(string1, string2)
      character (len=*) string1
      character (len=*) string2

      if ( string1 .eq. string2 ) then
        write(*,*) 'compare2: Strings are equal.'
      else
        write(*,*) 'compare2: Strings are not equal.'
      endif

      return
      end

Makefile

# Makefile for test programs.

# ======================================================================
# Declarations
# ======================================================================
# The compiler
FC = gfortran

FFLAGS += -I/usr/include -Wall

PROGRAMS = stringcompare

# ======================================================================
# Main targets
# ======================================================================

stringcompare: \
        stringcompare.o
        $(FC) $(FFLAGS) -o stringcompare.exe $^ $(LDFLAGS)

# ======================================================================
# The general rules.  These generally should not require modification.
# ======================================================================

# General rule for building prog from prog.o; $^ (GNU extension) is
# used in order to list additional object files on which the
# executable depends.
%: %.o
        $(FC) $(FFLAGS) -o $@ $^ $(LDFLAGS)

# General rules for compiling source files into object files.
#
# % = wildcard to match file basename
# $< = source file name

# Compile files with extension .f90 into .o object files.
%.o: %.f90
        $(FC) $(FFLAGS) -c $<

# Compile files with extension .F90 into .o object files.
%.o: %.F90
        $(FC) $(FFLAGS) -c $<

# Compile files with extension .for into .o object files.
%.o: %.for
        $(FC) $(FFLAGS) -c $<

Output

 compare1: Strings are not equal.
 compare2: Strings are not equal.
 main: Strings of different length are not equal.
 main: Strings with extra spaces are not equal.
 main: Strings read from input are equal
   string32read=|ALFALFA                         |
     len(string32read)=          32
   string80read=|ALFALFA                                                                         |
     len(string80read)=          80
 main: Trimmed strings read from input are equal
   string32read=|ALFALFA                         |
   string80read=|ALFALFA                                                                         |
smalers commented 3 years ago

Code was changed to work around the warning described above. For example, the wsupsum.for code has many substring comparisons using (1:12) notation. This should be unnecessary if the character variables are properly defined. Fortran compares strings by comparing the left-most characters and all else is treated as spaces.

These changes should be straightforward.