Variable repeat factor / Variable Format Expressions

I suspect that Variable Repeat Factor and Variable Format Expression are almost similar.

I am looking something like <n>i5 where n is a variable.

I have seen that some compilers accept in format syntax expressions like:

i<max(i,5)>
<i>f10.<i-1>

and others not.

Please let me know in short what is the current support. Are there any prospects in the future to become a standard feature of the language? Tia

1 Like

Not directly answering your questions, but it is possible to write a format string at run-time to get functionality similar to a variable format expression.

1 Like

@Beliavsky I suspect you mean by composing the format string by myself of other sub-strings, don’t you?

Yes. Here is an example.

program main
implicit none
integer :: n, nprint
real, allocatable :: x(:)
character (len=100) :: sfmt
call random_seed()
print*,"How many random variates, and how many to print per line?"
read (*,*) n,nprint
allocate (x(n))
call random_number(x)
write (sfmt,"('(',i0,'(1x,f7.4))')") nprint
print*,"sfmt = ",sfmt
write (*,sfmt) x
end program main

Output:

 How many random variates, and how many to print per line?
6 2
 sfmt = (2(1x,f7.4))                                                                                        
  0.3935  0.0134
  0.2488  0.5306
  0.9213  0.6546
1 Like

@FLNewbiee ,

You may know from online references or from the book Modern Fortran Explained (MFE) or the proxy document toward the standard what the language currently offers “officially” is the so-called unlimited-format-item which is asterisk (*) followed by the format-item in parenthesis. Other compiles offer the nonstandard variable factor you have noticed.

If there is a good proposal for further improvements and support for it, perhaps the committee might include them in the future. You can suggest ideas at J3 Fortran proposals site.at GitHub.

In the meantime, you may know formats such as the following can be used where you will see the * unlimited repeat specifier and also a : (colon) terminator:

   integer, allocatable :: x(:)
   x = [ 1, 2, 3, 4, 5 ]
   print "(g0,*(g0,:',',1x))", "x = ", x
end
C:\temp>ifort /standard-semantics p.f90
Intel(R) Fortran Intel(R) 64 Compiler Classic for applications running on Intel(R) 64, Version 2021.7.0 Build 20220726_000000
Copyright (C) 1985-2022 Intel Corporation.  All rights reserved.

Microsoft (R) Incremental Linker Version 14.33.31630.0
Copyright (C) Microsoft Corporation.  All rights reserved.

-out:p.exe
-subsystem:console
p.obj

C:\temp>p.exe
x = 1, 2, 3, 4, 5

Refer to MFE and/or the standard proxy for further details.

1 Like

Variable format expressions are an old DEC extension from the PDP-11 era, and they are miserable to implement. Writable Format strings, and as mentioned above things such as unlimited repeat count and the colon edit descriptor, eliminate the need for VFEs.

1 Like

@Beliavsky Tal for the example! Please see also last comment.

@FortranFan unlimited-format-item(*) is not very helpful when different VFE’s have to been used in the same format in the same line. As regards IT books, they are depreciated in short time. It is not a clever investment at all any longer!

@sblionel

they are miserable to implement.

There are features which are not essential for the strict scope of a project, but they give simplicity, comfort, pleasure, beauty, etc which make lives easier, happier, more elegant, etc. For example windows and toilets in the airplanes. They are totally useless for the scope of the trip. But I do not think that your life and passengers’ lives would become happier if you evacuate on yourself during a flight. That’s misery (from manufacturer’s side)!

@Beliavsky Composing format string is a solution indeed, but it is not so elegant and for each case a new composition has to be carried out. What I am thinking is a function which substitutes < and > or other eligible semantics with //, for example:

s1 = '(1x, <n1>i5, <n2>f5.2, ..., <n>a5)'
write(*,sfrmt(s1)) var1, var2, ..., varN !< if accepted, otherwise
frmt = sfrmt(s1)
write(*,frmt) var1, var2, ..., varN

where
sfrmt calculates '(1x, '//n1//'i5, '//n2//'f5.2, '...//n//'a5')' and returns something like '(1x, 3i5, 5f5.2, ..., 2a5)'

I suspect that compilers supporting VFE’s may implement something similar.

Perhaps, a library supporting VFE’s already exists.

@ALL I will publish very soon an actual case, Tfyt!

Next, I submit an actual case. It has to do, let’s say, with statistical pattern analysis or something similar. (My absence of 30 years allows me to do some mistakes :wink: :grinning:)

Well, I have an initial time series and calculate quantities like: -n, @n, +n

-n implies that -1 has appeared n times consequently ie -1, -1, ..., -1, n times
@n implies that 0 has appeared n times consequently ie 0, 0, ..., 0, n times
+n implies that +1 has appeared n times consequently ie +1, +1, ..., +1, n times

So, I get another series of integers like:
-5, +1, -2, @1, +2, -1, @3, ...

From that I get a vector with unique values in order, like:
-5, -2, -1, @1, @3, +1, +2, ...
which I want to print it.

write(*, '(1x, <neg>i3, <zero>('@',i1), <pos>i2)') (vector(i), i=1,size(vector))

where
neg the number of negative unique cumulative units
zero the number of zero unique cumulative units
pos the number of positive unique cumulative units

@FortranFan @sblionel Would you please write the format string with *, and : to see which way is easiest and more elegant? Talia

In addition to * and : there is non-advancing I/O which allows for the asterisk to be used multiple times in composing a formatted output line; which you may find greatly increases the flexibility in composing an output line.

I still have a few places where I create a format statement, but very few. Using a simple function that returns a string from a number and concatenation, as I think you are referring to, makes that relatively simple as well.

A Q&D function to convert an integer to a string shows that you can easily use expressions and concatenation to easily build formats on the fly, which is another “new” feature.

program testit
integer :: printme(3,4)
   printme(1,:)=[1,2,3,4]
   printme(2,:)=[10,20,30,40]
   printme(3,:)=[-100,-200,-300,-400]
   call printi(printme)
contains

subroutine printi(arr)
implicit none
!@(#) print small 2d integer arrays in row-column format
integer,intent(in)           :: arr(:,:)
integer                      :: i
character(len=:),allocatable :: biggest
   ! find how many characters to use for integers and use this format to write a row
   biggest='(" > [",*(i'//ito(ceiling(log10(real(maxval(abs(arr)))))+2)//':,","))'
   ! print one row of array at a time
   do i=1,size(arr,dim=1)
      write(*,fmt=biggest,advance='no')arr(i,:)
      write(*,'(" ]")')
   enddo
end subroutine printi

function ito(int)
integer,intent(in) :: int
character,allocatable :: ito
   ito=repeat(' ',32)
   write(ito,'(i0)')int
   ito=trim(ito)
end function ito

end program testit
./a.out
 > [    1,    2,    3,    4 ]
 > [   10,   20,   30,   40 ]
 > [ -100, -200, -300, -400 ]

So between being able to use non-constant character variables as formats, * and : and g, and non-advancing I/O I no longer miss the DEC extension; which before these features were available I thought should be added to the standard.

Using one vector to contain three different types of information complicates the non-advancing I/O strategy. A user type with three different components or separate names associated to the sections would make it prettier, but just brute-forcing it

istart=1
! neg the number of negative unique cumulative units
write(*, '(1x, *(i3))',advance='no') vector(istart:istart+neg-1)
istart=istart+neg
! zero the number of zero unique cumulative units
write(*, '(*("@",i1))',advance='no')  vector(istart:istart+zero-1)
istart=istart+zero
! pos the number of positive unique cumulative units
write(*, '(*(i2))')                  vector(istart:istart+pos-1)
1 Like

@urbanjost Just count the minimum number of characters required in both cases.

write(*,'(1x,<n>i3,<z>('@',i1),<p>i2)') v(1:size(v)) !< 52 characters
!<                                 vs
write(*,'(1x,*(i3))',advance='no') v(i:i+n) !< 113 characters
write(*,'(*("@",i1)',advance='no') v(i:i+z)
write(*,'(*(i2))') v(i:i+p)

52 against 113 ie 117+% more characters!

Maybe even better,

write(*,'(1x,<n>i3,<z>('@',i1),<p>i2)') v !< 41 characters

If above is valid, then we get 175+% more characters.

Definitely more verbose. I have a module that converts up to twenty arbitrary intrinisics to a string
that lets you build a format by mixing just a list of strings and integers in M_msg; a string replacement function in M_strings and a few other things that make it shorter if you would need to do that a lot with current standard functionality as well; as well as a function that takes a value and a format and converts it to a string. They all create a much prettier interface; but I really was just constructing an example that shows the other features like non-advancing I/O that make the asterisk more flexible than it seems at first glance. Having tried several methods in the past I think the prettiest was to overload concatenation or +
with functions like that so you can do something like:

fmt= string // number //string //number …

Like in M_overload, if you really have a lot of this.

A brief description and example is at

https://urbanjost.github.io/M_overload/M_overload.3m_overload.html

If you use fpm(1) all of those can be used as dependencies, or they have a make(1) file as well, but
I was initially just mentioning basic built-in functionality.

1 Like

@urbanjost Please consider writing a small library which will add in a certain way (maybe through write overloading) VFE support in all positions. I have already checked your libraries and, in fact, I use a couple of them in my project. Talfyt and the libraries, of course.

This is typically done in fortran using list-directed i/o conventions. The above would be specified as

5*-1 1 2*-1 0 2*1 -1 3*0

Fortran can convert that string to the array with

read(string,*) ivec(1:15)

and some i/o libraries will even produce that string on output given the original expanded integer vector. Given the original vector, it is not difficult to construct that character string manually, which then eliminates some of the ambiguity the fortran i/o library is allowed. This is called run-length encoding, and it has been in fortran since even before f77. This convention with repeat counts is also used in namelist i/o and in the source code in data statements.

@RonShepard I am not sure that I understand what you mean. My knowledge is limited and mostly based on what I remember and search in the internet.

Let’s say, the initial time series is of the form:

-1.002, 1.01, -1.01, -1.02, 0.001, -0.001, ...

which next is, let’s name, signed-unit-ized (I do not know/remember the correct term of this function) like

-1, 1, -1, -1, 0, 0, ... (sign is the important part and not the magnitude)

from which the cumulative signed units come from, like

-1, 1, -2, @2, ...

This is the last form of the data, with which I work and make calculations.

My point was that if you use fortran conventions for the run length encoding, then you can get some help from the language for the conversions. Taking your data as an example:

real :: vals(6)=[-1.002, 1.01, -1.01, -1.02, 0.001, -0.001]
character(80) :: string
integer :: ivals(6)
ivals = nint(vals)
write(*,'(*(i0,1x))') ivals
string='-1 1 2*-1 2*0'
read(string,*) ivals
write(*,'(*(i0,1x))') ivals
write(string,*) ivals
write(*,'(a)') string
end

If you run this, you should get something like this output

$ gfortran ldio.F90 && a.out
 -1 1 -1 -1 0 0
 -1 1 -1 -1 0 0
          -1           1          -1          -1           0           0 

This shows that the run length encoded string expands the same way to the original integer vector, just using the internal read statement.

Unfortunately, my compiler (gfortran) does not produce that string when internal i/o is used, as is shown in the last line. So efficient run length encoding must be done manually. That’s not difficult to do, it just isn’t a single write statement.

The reason this came to mind is that back in the 1980s I wrote the f77 code that did this. Basically, I was duplicating list-directed i/o, but I had more control over the output format, including when repetition counts were used. F77 did not allow internal list-directed i/o, so that was the only option then. After f2003 (I think that was the change), I could replace half of my library code with just an internal list-directed read statement. Forming the compact string from the integer array still doesn’t work the right way, as seen above, so that part of the library code is still required.

This is not a bug with gfortran. It is allowed to add spaces and extra records and to ignore when repetition counts could be used. So if you want the compact encoded string, you still must do it yourself.

@RonShepard Now, I see. Tal

In Fortran, overwriting a statement like WRITE() is not possible that I know of, so building a format on the fly, macro substitution, multiple write statements, internal writes, generic functions like in M_msg, etc, … needs used as far as I can think of. Since variable names are not exposed in Fortran, except arguably in NAMELIST groups, I was experimenting with different approaches. Here is one. I might clean up a few others. If so, I will add them here.

CLICK TO EXPAND EXAMPLE PROGRAMS

sequential macro substitution

program demo_M_vfe
implicit none
integer,allocatable          :: vector(:)
integer                      :: neg, zero, pos
   neg=3
   zero=4
   pos=5
   vector=[11,22,33,5,6,7,8,9,10,11,12]
   ! macros are <1>, <2>, <3>, ... which are placed where integer values can go
   ! and then that number of integer values are given. In this simple version
   ! <N> cannot appear as a string otherwise in the format; all macros must be
   ! given a value greater than zero, and extra numeric values have no affect.
   write(*, vfe("(1x, <1>('N',i0:,1x), <2>('@',i0:,1x), <3>('P',i0:,1x))",[neg,zero,pos])) vector
   ! was not actually sure a function returning a string could be used as a format,
   ! but it appears it can; which could have other uses for selecting languages, 
   ! globally available formats, ... .
contains

function vfe(fstring,ints) result (out)
use,intrinsic :: iso_fortran_env, only : int64
character(len=*),intent(in)     :: fstring
integer,intent(in)              :: ints(:)
character(len=:),allocatable    :: out, macro, value
character(len=range(0_int64)+3) :: temp
   out=fstring
   do i=1,size(ints)
      write(temp,'(i0)')ints(i)
      value=trim(temp)
      write(temp,'("<",i0,">")')i
      macro=trim(temp)
      out=replace(out,macro,value)
   enddo
end function vfe

function replace(original,old,new) result (out)

! ident_11="@(#) M_strings replace(3f) replace one substring for another in string"

character(len=*),intent(in)   :: original, old, new
character(len=:),allocatable  :: out
integer                       :: icount,ichange
integer                       :: len_old, len_new, ladd, ind, ic, ichr
integer                       :: right_margin

   right_margin=len_trim(original)                     ! get non-blank length of input line
   len_old=len(old)                                    ! length of old substring to be replaced
   if(len_old.eq.0)then                                ! c//new/ means insert new at beginning of line (or left margin)
      out=original(1:right_margin)
      return
   endif
   len_new=len(new)                                    ! length of new substring to replace old substring

   icount=0                                            ! initialize error flag/change count
   ichange=0                                           ! initialize error flag/change count
   out=''                                              ! begin with a blank line as output string
   ichr=1                                              ! place to put characters into output string
   ic=1                                                ! place looking at in input string
   loop: do
                                                       ! try finding start of OLD in remaining part of input in change window
      ind=index(original(ic:),old(:len_old))+ic-1
      if(ind.eq.ic-1.or.ind.gt.right_margin)then       ! did not find old string or found old string past edit window
         exit loop                                     ! no more changes left to make
      endif
      icount=icount+1                                  ! found an old string to change, so increment count of change candidates
      if(ind.gt.ic)then                                ! if found old string past at current position in input string copy unchanged
         ladd=ind-ic                                   ! find length of character range to copy as-is from input to output
         out=out(:ichr-1)//original(ic:ind-1)
         ichr=ichr+ladd
      endif
      if(icount.ge.1.and.icount.le.right_margin)then   ! check if this is an instance to change or keep
         ichange=ichange+1
         if(len_new.ne.0)then                          ! put in new string
            out=out(:ichr-1)//new(:len_new)
            ichr=ichr+len_new
         endif
      else
         if(len_old.ne.0)then                          ! put in copy of old string
            out=out(:ichr-1)//old(:len_old)
            ichr=ichr+len_old
         endif
      endif
      ic=ind+len_old
   enddo loop

   select case (ichange)
   case (0)                                            ! there were no changes made to the window
      out=original                                     ! if no changes made output should be input
   case default
      if(ic.le.len(original))then                      ! if there is more after last change on original line add it
         out=out(:ichr-1)//original(ic:max(ic,right_margin))
      endif
   end select

end function replace

end program demo_M_vfe

Learned about using a function for a Format and what zero repeat counts in a Fortran do in different compilers, which I had never thought about before!

Unlimited Polymorphic
module M_build
use,intrinsic :: iso_fortran_env, only : int8, int16, int32, int64, real32, real64, real128
implicit none
private
public build
contains
!   build(3f) - [M_build] converts up to twenty scalar integers and strings to a string (LICENSE:PD)
function build(g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, ga, gb, gc, gd, ge, gf, gg, gh, gi, gj) 
implicit none
class(*),intent(in),optional  :: g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, ga, gb, gc, gd, ge, gf, gg, gh, gi, gj
character(len=:),allocatable  :: build
character(len=4096)           :: line
integer                       :: istart
integer                       :: increment
character(len=:),allocatable  :: sep
   sep=''
   increment=len(sep)+1
   istart=1
   line=''
   if(present(g0))call print_g(g0)
   if(present(g1))call print_g(g1)
   if(present(g2))call print_g(g2)
   if(present(g3))call print_g(g3)
   if(present(g4))call print_g(g4)
   if(present(g5))call print_g(g5)
   if(present(g6))call print_g(g6)
   if(present(g7))call print_g(g7)
   if(present(g8))call print_g(g8)
   if(present(g9))call print_g(g9)
   if(present(ga))call print_g(ga)
   if(present(gb))call print_g(gb)
   if(present(gc))call print_g(gc)
   if(present(gd))call print_g(gd)
   if(present(ge))call print_g(ge)
   if(present(gf))call print_g(gf)
   if(present(gg))call print_g(gg)
   if(present(gh))call print_g(gh)
   if(present(gi))call print_g(gi)
   if(present(gj))call print_g(gj)
   build=trim(line)
contains
subroutine print_g(g)
use,intrinsic :: iso_fortran_env, only : int8, int16, int32, int64, real32, real64, real128
class(*),intent(in) :: g
   select type(g)
      type is (integer);           write(line(istart:),'(i0)') g
      type is (character(len=*));  write(line(istart:),'(a)') trim(g)
   end select
   istart=len_trim(line)+increment
   line=trim(line)//sep
end subroutine print_g
end function build
end module M_build

program demo_build
use M_build, only : build
implicit none
integer,allocatable          :: vector(:)
integer                      :: neg, zero, pos
   neg=3
   zero=4
   pos=5
   vector=[11,22,33,5,6,7,8,9,10,11,12]
   ! build with unlimited polymorphic
   write(*, build("(1x,",neg,"('N',i0:,1x),",zero,"('@',i0:,1x),",pos,"('P',i0:,1x))")) vector
end program demo_build

Fancy Overload
module M_overloading
implicit none
private
public operator(//)
interface operator ( // )
   module procedure g_g
end interface operator ( // )
contains
function g_g(value1,value2) result (string)
! @(#)M_overloading::g_g(3f): convert two single intrinsic values to a string
class(*),intent(in)          :: value1, value2
character(len=:),allocatable :: string1
character(len=:),allocatable :: string2
character(len=:),allocatable :: string
   ! use this so character variables are not trimmed and/or spaces are not added
   !ifort_bug!string = ffmt(value1,'(g0)') // ffmt(value2,'(g0)')
   string1 = ffmt(value1)
   string2 = ffmt(value2)
   allocate(character(len=len(string1)+len(string2)) :: string)
   string(1:len(string1))=string1
   string(len(string1)+1:)=string2
end function g_g

function ffmt(generic) result (line)
use,intrinsic :: iso_fortran_env, only : int8, int16, int32, int64
! @(#)M_overloading::ffmt(3f): convert intrinsic to a string using specified format
class(*),intent(in)          :: generic
character(len=:),allocatable :: line
integer                      :: ios
character(len=255)           :: msg
character(len=1),parameter   :: nill=char(0) ! use to keep trailing spaces in strings
integer                      :: ilen
   allocate(character(len=256) :: line) ! cannot currently write into allocatable variable
   ios=0
   select type(generic)
      type is (integer(kind=int8));   write(line,'(i0,a)',iostat=ios,iomsg=msg) generic,nill
      type is (integer(kind=int16));  write(line,'(i0,a)',iostat=ios,iomsg=msg) generic,nill
      type is (integer(kind=int32));  write(line,'(i0,a)',iostat=ios,iomsg=msg) generic,nill
      type is (integer(kind=int64));  write(line,'(i0,a)',iostat=ios,iomsg=msg) generic,nill
      type is (character(len=*));     write(line,'(a,a)',iostat=ios,iomsg=msg) generic,nill
   end select
   if(ios.ne.0)then
      line='<ERROR>'//trim(msg)
   else
      ilen=index(line,nill,back=.true.)
      if(ilen.eq.0)ilen=len(line)
      line=line(:ilen-1)
   endif
end function ffmt

end module M_overloading
program testit
use M_overloading, only : operator(//)
implicit none
integer,allocatable :: vector(:)
integer :: neg, zero, pos
integer :: left

   vector=[1,2,3,4,5,6,7,8,9,10,11,12]
   neg=3
   zero=4
   pos=5

   write(*, "(1x,"//neg//"('N',i0:,1x),"//zero//"('@',i0:,1x),"//pos//"('P',i0:,1x))") vector

end program testit

The overload could be considerably simpler, but this is set up to easily be extended to the // operator can work with any intrinsic types by just adding a line.

2 Likes