Fortran is not widely used today - mostly in the scientific computing / super computing / number crunching field.
But Fortran was my first programming language, so I always had a special connection to it.
Fortran is one of the oldest high level languages. First Fortran compiler was completed in 1957.
Back then the name was usually all uppercase aka FORTRAN, but I will use the modern captitalization Fortran.
The language is standardized via ANSI/ISO:
This article will describe Fortran 77, which was the last Fortran version to be part of mainstream programming. Newer versions has been overshadowed by newer programming languages.
The traditional platforms for Fortran were:
Examples are tested with GCC g77 on Windows and HP Fortran on OpenVMS Alpha.
If you need a Fortran compiler then get GCC. Older versions include g77 and newer versions include gfortran.
Fortran 77 source code is not free format like newer programming languages but fixed format where position matters (newer Fortran versions support free format).
Structure is:
A program starts with a PROGRAM statement and ends with an END statement.
helloworld.for:
PROGRAM HELLOWORLD
WRITE(*,*) 'Hello world !'
END
Note that the statements start in position 7.
WRITE(*,*) will be explained later, but the meaning is rather obvious.
Traditionally Fortran code is written in all upper case, but it is not required. Fortran is not case sensitive.
Comments are indicated by a C in position 1.
Any character not space can be used in position 6 to indicate that the line is a continutation, but traditionally a + is used.
Modified helloworld.for:
C
C This is a hello world example in Fortran
C
PROGRAM HELLOWORLD
WRITE(*,*)
+ 'Hello world !'
END
Note that spaces are insiginificant in Fortran.
MYVAR=1
and:
M Y V A R = 1
has the exact same meaning.
Obviously you would never write code like the last version.
Fortran 77 standard defines the following data types:
There is a de facto industry standard that also numerical types can be suffixed with *length (where length is number of bytes).
So usually the following types are available:
Fortran type | Meaning (common platforms) | C/C++ equivalent (typical compiler) | Java equivalent | C# equivalent |
---|---|---|---|---|
INTEGER*2 | 16 bit integer | short int | short | short |
INTEGER*4 | 32 bit integer | int or long int | int | int |
INTEGER*8 | 64 bit integer | long long int | long | long |
LOGICAL*4 | true or false | bool | boolean | bool |
CHARACTER*length | Fixed length string with specified length | char[length+1] in C, string in C++ (variable length) | String (variable length) | string (variable length) |
REAL*4 | 32 bit floating point | float | float | float |
REAL*8 | 64 bit floating point | double | double | double |
REAL*16 | 128 bit floating point | (none) | (none) | (none) |
COMPLEX*8 | 64 bit complex = 32 bit real part + 32 bit imaginary part | (none) | (none) | (none) |
COMPLEX*16 | 128 bit complex = 64 bit real part + 64 bit imaginary part | (none) | (none) | (none) |
It seems obvious from the above that Fortran is a great language for scientific calcultaions due to the rich REAL and COMPLEX data types and a poor language for text processing due to only having fixed length strings.
Example with declarations and some assignments:
PROGRAM VARASGN
INTEGER*2 V1
INTEGER*4 V2
INTEGER*8 V3
LOGICAL*4 V4
CHARACTER*16 V5
REAL*4 V6
REAL*8 V7
REAL*16 V8
COMPLEX*8 V9
COMPLEX*16 V10
V1=123
V2=123
V3=123
V4=.TRUE.
V5='This is a test'
V6=123.456
V7=123.456D0
V8=123.456Q0
V9=(1.2,3.4)
V10=(1.2D0,3.4D0)
WRITE(*,*) V1
WRITE(*,*) V2
WRITE(*,*) V3
WRITE(*,*) V4
WRITE(*,*) V5
WRITE(*,*) V6
WRITE(*,*) V7
WRITE(*,*) V8
WRITE(*,*) V9
WRITE(*,*) V10
END
Variables do not need to be declared in Fortran. If they are not declared they get a type based on the variable name:
This feature can be disabled with:
IMPLICIT NONE
It is recommended to use that option.
Multiple variables of the same type can be declared in a single declaration.
Constants can be defined via PARAMETER declaration.
Example:
PROGRAM VARASGN
IMPLICIT NONE
INTEGER*4 A,B,C,ZERO
PARAMETER(ZERO=0)
A=1
B=2
C=3
WRITE(*,*) A,B,C,ZERO
END
Fortran has all the basic operators and functions.
Fortran | Meaning | C/C++ equivalent | Java equivalent | C# equivalent |
---|---|---|---|---|
+ | arithmetic plus | + | + | + |
- | arithmetic minus | - | - | - |
* | arithmetic multiply | * | * | * |
/ | arithmetic integer division | / | / | / |
MOD function | arithmetic modulus | % | % | % |
** | power | pow function | Math.pow method | Math.Pow method |
ABS function | absolute value | abs/fabs function | Math.abs method | Math.Abs method |
AINT function | round down to integer | floor function | Math.floor method | Math.Floor method |
NINT function | round to nearest integer | round function | Math.round method | Math.Round method |
MIN function | smallest value | min function | Math.min method | Math.Min method |
MAX function | greatest value | max function | Math.max method | Math.Max method |
.EQ. | equal | == | == | == |
.NE. | not equal | != | != | != |
.LT. | less than | < | < | < |
.LE. | less than or equal | <= | <= | <= |
.GT. | greater than | > | > | > |
.GE. | greater than or equal | >= | >= | >= |
.AND. | logical and (not short circuit) | & (often short circuit logical and && is used) | & (often short circuit logical and && is used) | & (often short circuit logical and && is used) |
.OR. | logical or (not short ciruit) | | (often short circuit logical or || is used) | | (often short circuit logical or || is used) | | (often short circuit logical or || is used) |
.NOT. | logical negation | ! | ! | ! |
Fortran also has a bunch of mathematical functions:
Note that these functions exist for all precisions of floating point.
Fortran also have a few functions for strings:
Note that character indexes in strings are 1 based not 0 based.
Example:
PROGRAM OPS
INTEGER*4 N,M
REAL*8 X
CHARACTER*15 S
N=7
M=5
WRITE(*,*) N+M,N-M,N*M,N/M,MOD(N,M)
WRITE(*,*) N.EQ.M,N.NE.M,N.LT.M,N.GT.M
X=2.5
WRITE(*,*) SQRT(X**2),LOG(EXP(X)),LOG10(10**X)
WRITE(*,*) ACOS(0.0),ASIN(1.0)
WRITE(*,*) COSD(90.0),SIND(90.0)
S='This is a test'
WRITE(*,*) LEN(S),INDEX(S,'is'),S(6:7)
END
Again we see rich support for scientific calculations and poor support for text processing.
Fortran has 5 different flavors of IF statement.
Three of them are very simple and similar to other languauges:
IF(condition) singlestatement
IF(condition) THEN
multistatements
ENDIF
IF(condition) THEN
multistatements
ELSE
multistatements
ENDIF
Then there is an IF/GOTO construct to jump to 3 different labels depending on whether a value is negative, zero or positive:
IF(someexpr) labelfornegative,labelforzero,labelforpositive
And then there is a IF/GOTO to jump to different labels based on value that is used as index into the labels:
GOTO(label1,label2,label3,label4) someexpr
The latter construct is really a switch statement that can only be used for sequential integer values.
Example:
PROGRAM COND
INTEGER*4 V
V=2
C single statement IF
IF(V.GT.1) WRITE(*,*) 'V > 1'
C block statement IF ELSE
IF(V.GT.1) THEN
WRITE(*,*) 'V > 1'
ELSE
WRITE(*,*) 'V <= 1'
ENDIF
C arithmentic IF
IF(V) 100,200,300
100 WRITE(*,*) 'V < 0'
GOTO 400
200 WRITE(*,*) 'V = 0'
GOTO 400
300 WRITE(*,*) 'V > 0'
400 CONTINUE
C computed GOTO
GOTO (500,600,700,800) V
500 WRITE(*,*) 'V = 1'
GOTO 900
600 WRITE(*,*) 'V = 2'
GOTO 900
700 WRITE(*,*) 'V = 3'
GOTO 900
800 WRITE(*,*) 'V = 4'
GOTO 900
900 CONTINUE
END
The example also uses simple GOTO statements, but their meaning should be obvious.
It is possible to write horrible spagetti code in Fortran. But it is also possible to write nice structured code.
Standard Fortran only got one type of loop the DO loop, which is equivalent to for loop in many other languages:
DO label counter=startval,endval
multistatements
label CONTINUE
DO label counter=startval,endval,step
multistatements
label CONTINUE
But most Fortran 77 dialects also have a DO WHILE loop:
DO WHILE expression
multistatement
ENDDO
Example:
PROGRAM LOOPS
INTEGER*4 I
DO 100 I=1,5
WRITE(*,*) I
100 CONTINUE
DO 200 I=1,5,2
WRITE(*,*) I
200 CONTINUE
I=1
DO WHILE(I.LE.5)
WRITE(*,*) I
I=I+2
ENDDO
END
Note that if these loops are not sufficient then it is always possible to use IF and GOTO. These are actually used in Fortran.
In Fortran one distinguishes between subroutines that does not return a value and functions that return a value. Subroutines are known as void functions/methods in C/C++/Java/C#.
Example showing syntax:
PROGRAM SUB
INTEGER*4 I
DO 100 I=1,10
CALL TESTFAC(I)
100 CONTINUE
END
C
SUBROUTINE TESTFAC(N)
INTEGER*4 N
INTEGER*4 FAC
EXTERNAL FAC
WRITE(*,*) N,FAC(N)
RETURN
END
C
INTEGER*4 FUNCTION FAC(N)
INTEGER*4 N
INTEGER*4 RES,I
RES=1
DO 100 I=1,N
RES=RES*I
100 CONTINUE
FAC=RES
RETURN
END
The construct:
EXTERNAL FAC
tell Fortran that FAC is a user specified function not a builtin function.
Note that standard Fortran 77 does not support recursion. Many Fortran 77 compilers do support recursion though. And newer Fortran standards require support for recursion.
Some Fortran 77 compilers that do not support recursion store local variables on heap instead of stack. This mean that they can maintain their values between calls. This is not guaranteed to work with all compilers.
To make it work with all compilers use:
typeofvariable variablename
SAVE variablename
Fortran does not have real global variables as known from C/C++ or equivalent in Java/C# public static fields.
Instead Fortran has an advanced export/import feature called common blocks to share data between subroutines/functions.
Syntax:
COMMON /commonname/var1,var2,var3
Example:
PROGRAM COMM
INTEGER*4 I,J,K
COMMON /B1/I
COMMON /B2/J
COMMON /B3/K
I=123
J=456
K=65537
CALL SUB1
CALL SUB2
CALL SUB3
END
C
SUBROUTINE SUB1
INTEGER*4 I
COMMON /B1/I
WRITE(*,*) I
RETURN
END
C
SUBROUTINE SUB2
INTEGER*4 J
COMMON /B2/J
WRITE(*,*) J
RETURN
END
C
SUBROUTINE SUB3
INTEGER*2 K1,K2
COMMON /B3/K1,K2
WRITE(*,*) K1,K2
RETURN
END
Part 3 illustrates an ugly little detail. Common blocks are just pieces of memory and they are not type safe.
To make it easier to manage then most Fortran 77 dialects support an INCLUDE statement.
If comm2.inc contains:
INTEGER*4 I,J
COMMON /B/I,J
then we can use:
PROGRAM COMM
INCLUDE 'comm2.inc'
I=123
J=456
CALL SUB1
CALL SUB2
END
C
SUBROUTINE SUB1
INCLUDE 'comm2.inc'
WRITE(*,*) I
RETURN
END
C
SUBROUTINE SUB2
INCLUDE 'comm2.inc'
WRITE(*,*) J
RETURN
END
Fortran IO is a bit special but is actually rather powerful.
The two primary IO statements are READ and WRITE.
They exist in many flavors including:
READ(*,*) var1,var2,...
READ(n,*) var1,var2,...
READ(*,formatlabel) var1,var2,...
READ(n,formatlabel) var1,var2,...
READ(UNIT=n,FMT=formatlabel) var1,var2,...
WRITE(*,*) var1,var2,...
WRITE(n,*) var1,var2,...
WRITE(*,formatlabel) var1,var2,...
WRITE(n,formatlabel) var1,var2,...
WRITE(UNIT=n,FMT=formatlabel) var1,var2,...
UNIT is a file number. * means console. 5 means console input. 6 means console output. Per old tradition.
A format label consist of:
label FORMAT(formatinfo)
where format info consist of a number of comma separated format specifications of the form:
Note that per very old tradition then the first column in output is used for printer control.
Simple example:
PROGRAM IO
INTEGER*4 I,N
INTEGER*4 FAC
EXTERNAL FAC
WRITE(6,1000)
READ(5,1100) N
DO 100 I=1,N
WRITE(6,1200) I,FAC(I)
100 CONTINUE
1000 FORMAT(1X,'Enter max value: ',$)
1100 FORMAT(I2)
1200 FORMAT(1X,I2,1X,I8)
END
C
INTEGER*4 FUNCTION FAC(N)
INTEGER*4 N
INTEGER*4 RES,I
RES=1
DO 100 I=1,N
RES=RES*I
100 CONTINUE
FAC=RES
RETURN
END
More advanced example:
PROGRAM IO
INTEGER*4 I,N
INTEGER*4 FAC
EXTERNAL FAC
WRITE(6,1000)
READ(5,1100) N
DO 100 I=1,N
WRITE(6,1200) 'I=',I,'FAC(I)=',FAC(I),
+ 'I=',N+I,'FAC(I)=',FAC(N+I)
100 CONTINUE
1000 FORMAT(1X,'Enter max value: ',$)
1100 FORMAT(I2)
1200 FORMAT(1X,2(A2,I2,1X,A7,I8,1X))
END
C
INTEGER*4 FUNCTION FAC(N)
INTEGER*4 N
INTEGER*4 RES,I
RES=1
DO 100 I=1,N
RES=RES*I
100 CONTINUE
FAC=RES
RETURN
END
For files then besides READ and WRITE statements then OPEN and CLOSE statements are also needed.
Example:
PROGRAM FCOPY
INTEGER*4 IX
CHARACTER*80 LINE
OPEN(UNIT=1,FILE='in.txt',STATUS='OLD')
OPEN(UNIT=2,FILE='out.txt',STATUS='NEW')
100 READ(UNIT=1,FMT=1000,END=300) LINE
IX=80
200 IF(LINE(IX:IX).EQ.' ') THEN
IX=IX-1
GOTO 200
ENDIF
WRITE(UNIT=2,FMT=1100) LINE(1:IX)
GOTO 100
300 CLOSE(UNIT=2)
CLOSE(UNIT=1)
1000 FORMAT(A)
1100 FORMAT(A)
END
The manual trimming of extra space in fixed length strings again emphasizes what Fortran is not good at.
Standard Fortran 77 supports unformatted IO and direct access to fixed length files by line number and that is really a primitive database.
Create file with records:
PROGRAM DIRCRE
INTEGER*4 I
CHARACTER*80 BUF
OPEN(UNIT=1,FILE='db.dat',STATUS='NEW',
+ FORM='UNFORMATTED',ACCESS='DIRECT',RECL=80)
DO 100 I=1,100
WRITE(BUF,1000) I
WRITE(UNIT=1,REC=I) BUF
100 CONTINUE
CLOSE(UNIT=1)
1000 FORMAT('This is record ',I3)
END
Lookup by record number:
PROGRAM DIRLOOKUP
INTEGER*4 R,IX
CHARACTER*80 BUF
OPEN(UNIT=1,FILE='db.dat',STATUS='OLD',
+ FORM='UNFORMATTED',ACCESS='DIRECT',RECL=80)
WRITE(6,1000)
READ(5,1100) R
READ(UNIT=1,REC=R) BUF
IX=80
100 IF(BUF(IX:IX).EQ.' ') THEN
IX=IX-1
GOTO 100
ENDIF
WRITE(6,1200) BUF(1:IX)
CLOSE(UNIT=1)
1000 FORMAT(1X,'Enter record number: ',$)
1100 FORMAT(I3)
1200 FORMAT(1X,A)
END
Fortran obviously supports arrays. In fact Fortran has one of the best implementations of multi dimensional arrays.
Declaration of arrays:
typeofvar varname(dim1)
typeofvar varname(dim1,dim2)
typeofvar varname(dim1,dim2,dim3)
And elements are referenced as:
Note that array element indexes are 1 based and not 0 based.
Example:
PROGRAM ARR
INTEGER*4 N
PARAMETER(N=10)
INTEGER*4 I
REAL*8 X(N)
DO 100 I=1,N
X(I)=SQRT(1.0D0*I)
100 CONTINUE
DO 200 I=1,N
WRITE(*,*) I,X(I)
200 CONTINUE
END
Another example with multi dimensional array and use of subroutine/function:
PROGRAM MULTIARR
INTEGER*4 N,M
PARAMETER(N=10,M=10)
INTEGER*4 I,J
REAL*8 X(N,M)
REAL*8 MXSUM
EXTERNAL MXSUM
DO 200 I=1,N
DO 100 J=1,M
X(I,J)=I*J
100 CONTINUE
200 CONTINUE
WRITE(*,*) MXSUM(X,N,M)
END
C
REAL*8 FUNCTION MXSUM(X,N,M)
INTEGER*4 N,M
REAL*8 X(N,M)
INTEGER*4 I,J
REAL*8 RES
RES=0
DO 200 I=1,N
DO 100 J=1,M
RES=RES+X(I,J)
100 CONTINUE
200 CONTINUE
MXSUM=RES
END
Note that the dimension of the array is passsed as arguments to the function, so the function can be reused for all possible dimensions.
Example using array initialization with DATA statement and implied loop in output:
PROGRAM MOREARR
INTEGER*4 N,M
PARAMETER(N=10,M=10)
INTEGER*4 I,J
REAL*8 X(N,M)
DATA X/1*1.0d0,2*2.0d0,4*3.0d0,8*4.0d0,16*5.0d0,32*6.0d0,37*7.0/
DO 100 I=1,N
WRITE(6,1000) (X(I,J),J=1,M)
100 CONTINUE
1000 FORMAT(1X,10(F4.2,1X))
END
Here we see that Fortran store two dimensional arrays by column and not by row like most other languages.
GOTO can use a variable a socalled assigned GOTO.
Example:
PROGRAM ASGNGOTO
IMPLICIT NONE
INTEGER*4 MYJUMP
ASSIGN 100 TO MYJUMP
GOTO MYJUMP
WRITE(*,*) 'You will not see this'
GOTO 200
100 WRITE(*,*) 'OK'
200 CONTINUE
END
Usage of this feature may not be good for code readability.
It is possible to overlay two variables so they are the same place in memory (similar to C/C++ union).
Example:
PROGRAM EQ
INTEGER*2 A(2)
INTEGER*4 B
EQUIVALENCE(A,B)
B=65537
WRITE(*,*) A(1),A(2)
END
Subroutines and functions can have multiple returns like in most other languages, but they can also have multiple entry points, which is more unusual.
Example:
PROGRAM ENT
CALL SUB2(1,2)
CALL SUB1(1)
CALL SUB0()
END
C
SUBROUTINE SUB2(I,J)
INTEGER*4 I,J
INTEGER*4 I2,J2
I2=I
J2=J
GOTO 100
ENTRY SUB1(I)
I2=I
J2=0
GOTO 100
ENTRY SUB0()
I2=0
J2=0
100 WRITE(*,*) I2,J2
RETURN
END
Fortran IO has a rather unique capability when reading a text file. It is possible to move backwards to read the same lines again using the BACKSPACE statement.
Example:
PROGRAM BKSP
INTEGER*4 IV
CHARACTER*80 SV
OPEN(UNIT=1,FILE='demo.txt',STATUS='OLD')
100 READ(UNIT=1,FMT=1000,END=500) SV
IX=80
200 IF(SV(IX:IX).EQ.' ') THEN
IX=IX-1
GOTO 200
ENDIF
DO 300 I=1,IX
IF(INDEX('0123456789',SV(I:I)).LE.0) GOTO 400
300 CONTINUE
BACKSPACE(UNIT=1)
READ(UNIT=1,FMT=1100,END=500) IV
WRITE(*,*) 'IV=',IV
GOTO 100
400 WRITE(*,*) 'SV=',SV(1:IX)
GOTO 100
500 CLOSE(UNIT=1)
1000 FORMAT(A)
1100 FORMAT(I8)
END
It is possible to have one line functions inline socalled statement functions.
Example:
PROGRAM FP
REAL*8 F1,F2,F3
REAL*8 X
F1(X)=2*X+1
F2(X)=X*X-2
F3(X)=X**3+7
DO 100 I=0,3
WRITE(*,*) I,F1(I),F2(I),F3(I)
100 CONTINUE
END
It is possible to pass subroutines/functions as argument to subroutine/function.
Example:
PROGRAM FP
REAL*8 F
EXTERNAL F
DO 100 I=1,16
WRITE(*,*) I,SOLVE(F,DBLE(I))
100 CONTINUE
END
C
REAL*8 FUNCTION F(A,X)
REAL*8 A,X
F=X**2-A
RETURN
END
C
REAL*8 FUNCTION SOLVE(F,A)
REAL*8 A
REAL*8 F
EXTERNAL F
REAL*8 X,XN
REAL*8 DIFF
EXTERNAL DIFF
XN=1
100 X=XN
XN=X-F(A,X)/DIFF(F,A,X)
IF(ABS(XN-X).GE.0.0000001) GOTO 100
SOLVE=X
RETURN
END
C
REAL*8 FUNCTION DIFF(F,A,X)
REAL*8 A,X
REAL*8 F
EXTERNAL F
REAL*8 DELTA
PARAMETER(DELTA=0.0000001)
DIFF=(F(A,X+DELTA)-F(A,X))/DELTA
RETURN
END
The last two examples again show Fortran used for floating point scientific calculations.
Fortran does have a reputation for being an ugly language. And while it is possible to write very nice Fortran code, then it is possible to write some really ugly code using various obscure features.
Let us see a couple of examples.
PROGRAM MODFUN
I = 12
J = I MOD 5
WRITE(*,*) I,J
END
produces an "unexpected" output like:
12 4198999
The explanation is that the code is really:
PROGRAM MODFUN
INTEGER*4 I,J,IMOD5
I = 12
J = IMOD5
WRITE(*,*) I,J
END
This is due to some rules in Fortran:
And the code that produces the "expected" output is:
PROGRAM MODFUN
I = 12
J = MOD(I,5)
WRITE(*,*) I,J
END
Let us see the same issue being triggered by using a period instead of a comma.
PROGRAM DOFUN
DO 100 I=1.10
WRITE(*,*) I
100 CONTINUE
END
produces an "unexpected" output like:
39167304
The explanation is that the code is really:
PROGRAM DOFUN
INTEGER*4 I
REAL*4 DO100I
DO100I = 1.10
WRITE(*,*) I
100 CONTINUE
END
due to the same rules as in the previous example.
And the code that produces the "expected" output is:
PROGRAM DOFUN
DO 100 I=1,10
WRITE(*,*) I
100 CONTINUE
END
And now let us see an absolutely horror with lots of jumping around in the code.
What does the following code write out?
PROGRAM GOTOFUN
INTEGER*4 LBL
INTEGER*4 I
731 GOTO 113
ASSIGN 399 TO LBL
113 I=0
WRITE(*,*) I
247 ASSIGN 601 TO LBL
IF(I) 588,922,399
588 I=I+1
WRITE(*,*) I
GOTO 399
761 GOTO LBL
922 I=I+1
WRITE(*,*) I
GOTO(588,399,922) I
399 GOTO 761
601 CONTINUE
END
Answer:
0 1 2
Explanation:
PROGRAM GOTOFUN
INTEGER*4 LBL
INTEGER*4 I
731 GOTO 113 ! 1: jump to 113
ASSIGN 399 TO LBL
113 I=0 ! 2: I=0
WRITE(*,*) I ! 3: write 0
247 ASSIGN 601 TO LBL ! 4: save 601 in lbl
IF(I) 588,922,399 ! 5: I is 0 so jump to 922 (arithmethic if is 3 labels for value <0 =0 and >0)
588 I=I+1 ! 9: I=I+1=2
WRITE(*,*) I ! 10: write 2
GOTO 399 ! 11: jump tp 399
761 GOTO LBL ! 13: jump to lbl=601
922 I=I+1 ! 6: I=I+1=1
WRITE(*,*) I ! 7: write 1
GOTO(588,399,922) I ! 8: I is 1 so jump to 588 (computed goto jumps to label 1,2,3,... depending on value)
399 GOTO 761 ! 12: jump to 761
601 CONTINUE ! 14: done
END
Version | Date | Description |
---|---|---|
1.0 | July 20th 2008 | Initial version (in Danish) published on Eksperten.dk |
2.0 | August 27th 2016 | Translation to English and complete reformatting and publishing here |
2.1 | October 8th 2016 | Add content overview |
2.2 | July 6th 2017 | Add ugly examples |
See list of all articles here
Please send comments to Arne Vajhøj