-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathio.tex
994 lines (934 loc) · 49.3 KB
/
io.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
\chapter{输入与输出}\label{io}
之前我们都只是小打小闹而已, 现在我们来干一些大活儿. 我们来玩玩 \href{https://cdsarc.cds.unistra.fr/viz-bin/cat/I/355}{Gaia DR3}. 我们想知道Gaia DR3里的哪个源距银道面最远且最远有多远, 比较不动脑筋地做这件事呢, 我们想知道哪个源的 $ \left\lvert \sin b\right\rvert/p $ 最大, 其中 $ p $ 是源的视差, $ b $ 是源的银纬. 但这里有一个问题, 就是Gaia DR3显然太大了, 同志们得交个几十杯奶茶钱的网费, 所以我们先用Gaia DR3的一个小样本试试水.
首先我们要在\href{https://cdsarc.cds.unistra.fr/viz-bin/cat/I/355}{这个页面}里找到 \href{https://cdsarc.cds.unistra.fr/viz-bin/ReadMe/I/355?format=html&tex=true}{ReadMe} 点进去, 找到 ``File Summary''.
\begin{lstlisting}
File Summary:
------------------------------------------------------
FileName Lrecl Records Explanations
------------------------------------------------------
ReadMe 80 . This file
gaiadr3.sam 1788 1000 Gaia DR3 source catalog
(1811709771 sources)
......
\end{lstlisting}
从这个 ``File Summary'' 可知, \ttt{gaiadr3.sam} 是星表\footnote{认不得英文词儿的请上\href{https://nadc. china-vo. org/astrodict/}{天文学名词词典}查询.}, 一共1000行, 每行1788个字符. 不对呀, 后面明明写着一共1811709771个源嘛, 怎么只有1000行? 那当然是因为这只是个1000个源的样本啦, 文件名后缀可是 \ttt{.sam} 呢. 于是乎我们要在\href{https://cdsarc.cds.unistra.fr/viz-bin/cat/I/355}{这个页面}里找到 \href{https://cdsarc.cds.unistra.fr/ftp/I/355}{FTP}, 里头找到 \href{https://cdsarc.cds.unistra.fr/ftp/I/355/gaiadr3.sam.gz}{\ttt{gaiadr3.sam.gz}} 下载下来然后解压得到 \ttt{gaiadr3.sam}.
回到ReadMe, 下面有 \ttt{gaiadr3.sam} 的 ``Byte-by-byte Description''.
\begin{lstlisting}
Byte-by-byte Description of file: gaiadr3.sam
------------------------------------------------------
Bytes Format Units Label Explanations
------------------------------------------------------
1- 28 A28 --- DR3Name Unique source
designation (unique
across all Data
Releases)
(designation)
129- 137 F9.4 mas Plx ? Parallax (parallax)
980- 994 F15.11 deg GLAT Galactic latitude (b)
\end{lstlisting}
从这个 ``Byte-by-byte Description'' 可知, \ttt{gaiadr3.sam} 的每一行, 第1--28个字符都是源的编号, 第129--137个字符都是源的视差 (单位为毫角秒), 第980--994个字符都是源的银纬 (单位为度). 好, 现在就造轮子!\label{gaiadr3.sam}
\begin{lstlisting}
program main
use iso_fortran_env, only: dp => real64
implicit none
integer, parameter :: sam_len = 1000
real(dp), parameter :: &
mas2rad = acos(-1.0_dp)/(1.8e2_dp*3.6e6_dp)
real(dp), parameter :: &
deg2rad = acos(-1.0_dp)/(1.8e2_dp)
real(dp) :: p(sam_len), b(sam_len)
real(dp) :: d(sam_len)
p = p * mas2rad
b = b * deg2rad
d = abs(sin(b)) / p ! Unit: AU
print *, maxloc(d), maxval(d)
end program main
\end{lstlisting}
这轮子现在有个大问题, 我们需要把星表里的数据转移到数组 \ttt{p} 和 \ttt{b} 中, 难道用列选择将星表里的数据复制粘贴到轮子里? 好家伙, 一个轮子两千多行, 其中两千行是数据, 要是处理原始星表, 就有三十六亿行是数据, 这不是要崩溃\footnote{不仅我们要崩溃, 文本编辑器也要崩溃\dots{}}? 这时候我们就需要让Fortran自己干转移数据的事情.
\section{文件}
我们平常所说的文件, Fortran称其为外部文件 (external file), 不过一般来说外部文件得是纯文本文件\footnote{某些编译器可能有器规来应对二进制文件. 不知什么是纯文本文件和二进制文件的同志们赶紧恶补.}. 既然是纯文本文件, 就相当于一个字符串, 说到字符串, 就让我们想到字符串变量. Fortran称字符串变量为内部文件 (internal file), 将外部文件与内部文件合称为文件 (file).
我们需要让Fortran知道我们要用什么文件. 内部文件Fortran是认得的, 因为是自家的字符串变量嘛, 但外部文件Fortran不认得. 为了让Fortran认得外部文件, 我们要将外部文件打开 (open). 下面这个轮子, Fortran会在第三行后认得 \ttt{test.txt}, 这个 \ttt{test.txt} 应该和编译完最后生成的 \ttt{.exe} 文件\footnote{注意不是源代码文件.}在一个目录里.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt')
end program main
\end{lstlisting}
其中 \ttt{10} 这个位置填的是文件单位 (file unit), 说白了就是一个编号, 这编号必须是非负整数, 而且最好不小于 \ttt{10}, 因为小于 \ttt{10} 的编号可能有些奇奇怪怪的用途. \ttt{file=} 后加的是文件路径, 不知什么是文件路径的同志们请自行补习. 上个轮子的第三行就是让Fortran认得文件路径为 \ttt{test.txt} 的文件, 并且称其为10号文件.
使用完外部文件后我们应当将其关闭 (close), 也就是让Fortran忘记外部文件. 我们在上面那个轮子中加上一行, Fortran就会在第四行后忘记10号文件(即 \ttt{test.txt} ).
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt')
close(10)
end program main
\end{lstlisting}
每当我们暂时不需要使用外部文件时, 我们都应该将其关闭, 这是一个好习惯. 原因一是打开文件的时候, 文件单位可能会重. 例如下面的程序, 我们让文件单位 \ttt{10} 和 \ttt{test1.txt} 连接, 又让文件单位 \ttt{10} 和 \ttt{test2.txt} 连接, 这个程序能跑, 但因为文件单位 \ttt{10} 重了, 所以电脑其实会悄咪咪地把 \ttt{test1.txt} 关掉, 然后打开 \ttt{test2.txt}, 但悄咪咪关掉文件这事我们很可能会没注意!
\begin{lstlisting}
program main
implicit none
open(10, file='test1.txt')
open(10, file='test2.txt') ! test1.txt closed!
end program main
\end{lstlisting}
原因二是打开文件的时候, 被打开的文件可能被占住, 其他程序用不了. 想象一下, 同学们跑一个程序, 需要用一个文件里的数据, 同学们用程序打开了文件但没关闭, 然后老师或同学们的同学也要跑一个程序, 也需要用这个文件里的数据, 结果同学们居然把文件占住了, 打不开这个文件, 同学们得被暴打一顿. 所以请同学们养成关掉没用文件的好习惯.
\begin{convention}
在不需要使用外部文件的时候将其关闭.
\end{convention}
注意, 内部文件是不需要也没法打开或关闭的.
上面的轮子和下面的轮子等价.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='UNKNOWN')
close(10)
end program main
\end{lstlisting}
其中 \ttt{status='UNKNOWN'} 的意思是这个轮子在打开文件的时候进行了一波操作, 但这波操作是编译器自己决定的, 也就是说不同的编译器可能会有不同的操作, 所以不写 \ttt{status=...} 有点危险\footnote{如果摸透了编译器会怎么决定的话, 偷个懒不写也罢了. \label{no_add}}. 我们可以把 \ttt{'UNKNOWN'} 换成 \ttt{'OLD'}, \ttt{'NEW'}, 或 \ttt{'REPLACE'}. 换成 \ttt{'OLD'} 的话, \ttt{test.txt} 必须在轮子运行前存在, 运行时直接打开. 换成 \ttt{'NEW'} 的话, \ttt{test.txt} 必须在轮子运行前不存在, 运行时, Fortran会自己造一个空白 \ttt{test.txt} 文件并打开. 换成 \ttt{'REPLACE'} 的话, 如果 \ttt{test.txt} 在轮子运行前不存在, 则还是造一个空白 \ttt{test.txt} 文件并打开, 如果 \ttt{test.txt} 在轮子运行前存在, 则会把原来的 \ttt{test.txt} 直接删掉, 再造一个新的空白 \ttt{test.txt} 文件并打开. 请同志们自行造轮子实验上述操作.
上面的程序其实一直有个问题, 就是因为 Fortran 太上古了, 所以用整数的文件单位来表示文件, 但看整数的文件单位没法儿直接看出文件单位到底对应什么文件. 我们可以巧用字面常量给文件单位 ``加注释'', 例如写成下面这样.
\begin{lstlisting}
program main
implicit none
integer, parameter :: test1_txt = 10
integer, parameter :: test2_txt = 11
open(test1_txt, file='test1.txt')
open(test2_txt, file='test2.txt')
close(test1_txt)
close(test2_txt)
end program main
\end{lstlisting}
这样看 \ttt{test1\_{}txt} 就知对应文件 \ttt{test1.txt}, 看 \ttt{test2\_{}txt} 就知对应文件 \ttt{test2.txt}. 但本笔记里的例子都很简单, 安安就偷懒不用字面常量给文件单位 ``加注释'' 了.
最后指明, 文件单位在各个程序单元中是通用的, 也就是说我们完全可以在主程序中打开一文件然后在子程序中关闭此文件, 或在子程序中打开一文件然后在主程序中关闭此文件. 示例如下.
\begin{lstlisting}
program main
implicit none
open(10, file='test1.txt')
call open_and_close()
close(11)
end program main
subroutine open_and_close()
implicit none
open(11, file='test2.txt')
close(10)
end subroutine open_and_close
\end{lstlisting}
\section{读取与写入}
读取 (read) 是把文件里的内容变成Fortran数据实体, 写入 (write) 是把Fortran数据实体变成文件里的内容. 让我们来看下面这个轮子.
\begin{lstlisting}
program main
implicit none
real :: e, pi
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, *) exp(1.0), acos(-1.0)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, *) e, pi
print *, e, pi
close(10)
end program main
\end{lstlisting}
其中 \ttt{write(10, *) ...} 那行是把 \ttt{exp(1.0)} 和 \ttt{acos(-1.0)} 写入到10号文件里去, 所以打开 \ttt{test.txt} 就会看到 $ e $ 和 $ \pi $. \ttt{read(10, *) ...} 那行则是把10号文件里的内容读取出来赋值给 \ttt{e}和 \ttt{pi}, 所以最后会输出 $ e $ 和 $ \pi $.
上面这个轮子还有些细节. \ttt{action=} 后的字符串如果是 \ttt{'READ'}, 表示打开的文件是只读的, 不能写入. \ttt{action=} 后的字符串如果是 \ttt{'WRITE'}, 表示打开的文件是只写的, 不能读取. \ttt{action=} 后的字符串如果是 \ttt{'READWRITE'}, 表示打开的文件是读写的, 读取写入皆可. 这和之前 \ref{arguments} 小节的只读参量只写参量读写参量是很像的. 如果不加 \ttt{action=...}, 则是编译器自己决定是只读的只写的还是读写的, 这又有点危险了, 所以 \ttt{action=...} 还是要加的\footnote{同脚注\ref{no_add}.}.
\ttt{position=...} 则指明打开文件后的 ``文件定位'', 加 \ttt{position=...} 的原因还是不加的话文件定位会由编译器自己决定\footnote{同脚注\ref{no_add}.}. 同志们不需知道文件定位是什么, 只需记得只写文件加 \ttt{position='APPEND'}, 写入时会写入在文件的最后, 只读文件加 \ttt{position='REWIND'}, 读取时会从文件的开头读取. 读写文件嘛, 我们选择不玩儿. 下面用一个长长的轮子来详细讲解.
\begin{lstlisting}
program main
implicit none
real :: e, pi
real :: val
e = exp(1.0)
pi = acos(-1.0)
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
close(10)
open(10, file='test.txt', status='OLD', &
action='WRITE', position='APPEND')
write(10, *) e + pi
write(10, *) e - pi
close(10)
open(10, file='test.txt', status='OLD', &
action='WRITE', position='APPEND')
write(10, *) e * pi
write(10, *) e / pi
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, *) val
print *, val
read(10, *) val
print *, val
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, *) val
print *, val
read(10, *) val
print *, val
close(10)
end program main
\end{lstlisting}
第一次打开关闭文件, 因为 \ttt{status='REPLACE'}, 后面也没有写入, 所以最后得到一个空白文件. 第二次打开关闭文件, 先写入 $ e+\pi $, $ e+\pi $ 后面再写入 $ e-\pi $. 第三次打开关闭文件, 在文件最后先写入 $ e\cdot\pi $, 再写入 $ e/\pi $, 所以最后文件里依次是 $ e+\pi $, $ e-\pi $, $ e\cdot\pi $, $ e/\pi $. 第四次打开关闭文件, 先读取文件开头的 $ e+\pi $ 赋值给 \ttt{val}, 再读取下面的 $ e-\pi $ 赋值给 \ttt{val}. 第五次打开关闭文件, 还是先读取文件开头的 $ e+\pi $ 赋值给 \ttt{val}, 再读取下面的 $ e-\pi $ 赋值给 \ttt{val}, 所以最后输出的依次是 $ e+\pi $, $ e-\pi $, $ e+\pi $, $ e-\pi $.
注意每次 \ttt{read} 或 \ttt{write} 后, 下次 \ttt{read} 或 \ttt{write} 都会\uline{从下一行开始}. 下面这个轮子, 第一次 \ttt{write} 后 \ttt{1} 和 \ttt{2} 写入在第一行, 第二次 \ttt{write} 后 \ttt{3} 和 \ttt{4} 写入在第二行, 第一次 \ttt{read} 从第一行开始, 因为后面只跟着一个 \ttt{val1}, 所以 \ttt{val1} 为 \ttt{1}, 第二次 \ttt{read} 从第二行开始, 因为后面只跟着一个 \ttt{val2}, 所以 \ttt{val2} 为 \ttt{3}, \ttt{2} 和 \ttt{4} 则被无视!
\begin{lstlisting}
program main
implicit none
integer :: val1, val2
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, *) 1, 2
write(10, *) 3, 4
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, *) val1 ! val1=1, while 2 is ignored!
read(10, *) val2 ! val2=3, while 4 is ignored!
print *, val1, val2
close(10)
end program main
\end{lstlisting}
如果读取或写入时碰上数组, 则会把数组里的所有元素按元素顺序挨个读取或写入, 比如下面这个轮子, 先挨个写入1到9, 再写入10, 读取则是反向操作. 特别提醒, \ttt{one2nine(1, 3)} 是 \ttt{7}, \ttt{one2nine(3, 1)} 是 \ttt{3}, 同志们要是迷惑了请自行复习第\ref{fortran_array}章数组元素顺序的内容.
\begin{lstlisting}
program main
implicit none
integer :: i
integer :: one2nine(3, 3), ten
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, *) reshape([(i, i = 1, 9)], [3, 3]), 10
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, *) one2nine, ten
print *, one2nine, ten
print *, one2nine(1, 3), one2nine(3, 1)
close(10)
end program main
\end{lstlisting}
把文件单位换成字符串变量即可读取和写入内部文件了. 注意内部文件是不需要也没法打开或关闭的, 所以没法加 \ttt{position=...}, 每次读取或写入都是从内部文件的开头第一个字符开始的, 不论之前读取写入几次. 下面这个轮子, Ifx可以把 $ e^\pi $ 写入 \ttt{f} 并读取至 \ttt{val}\footnote{这就实现了数字型和字符型的相互转换.}, 但Gfortran不行, 要让Gfortran行, \ttt{f} 的长度必须大于 \ttt{16}. 原因见 \ref{fortran_edit_descriptor} 节. \label{internal_file}
\begin{lstlisting}
program main
implicit none
character(16) :: f
real :: val
write(f, *) exp(1.0) ** acos(-1.0)
print *, f
read(f, *) val
print *, val
end program main
\end{lstlisting}
我们还可以再把字符串变量换成 \ttt{*}, 输出的话 \ttt{*} 通常代表电脑屏幕. 比如下面这个轮子就会把 \ttt{Hello, world!} ``写入电脑屏幕'', 电脑屏幕上就出现 \ttt{Hello, world!} 了. \ttt{print \{fmt\},} 等价于 \ttt{write(*, \{fmt\})}, 也就是说 \ttt{print *,} 等价于 \ttt{write(*, *)}.
\begin{lstlisting}
program main
implicit none
write(*, *) 'Hello, world!'
print *, 'Bye bye, world!'
end program main
\end{lstlisting}
输入的话 \ttt{*} 则通常代表键盘. 比如下面这个轮子, 运行到第五行时程序会停下来, 我们在命令行(cmd呀powershell呀之类的东东)中可以打字儿, 假设打入 \ttt{98 54e1} 然后回车, \ttt{98}和 \ttt{54e1} 就赋值给 \ttt{mat(1, 1)} 和 \ttt{mat(1, 2)} 了, 第七行会干和第五行一样的事儿, 但语法更简单, 假设打入 \ttt{7.6 3.2e0}然后回车, \ttt{7.6}和 \ttt{3.2e0}就赋值给 \ttt{mat(2, 1)} 和 \ttt{mat(2, 2)} 了, 最后输出矩阵行列式. 注意 ``\ttt{*}'' 也是不需要且没法打开或关闭的. \ttt{read \{fmt\},} 等价于 \ttt{read(*, \{fmt\})}, 也就是说 \ttt{read *,} 等价于 \ttt{read(*, *)}.
\begin{lstlisting}
program main
implicit none
real :: mat(2, 2)
print *, 'Calculate determinant (det) '// &
'of 2x2 matrix'
print *, 'row 1 of matrix:'
read(*, *) mat(1, 1), mat(1, 2)
print *, 'row 2 of matrix:'
read *, mat(2, :)
print *, 'det:', &
mat(1, 1)*mat(2, 2) - mat(1, 2)*mat(2, 1)
end program main
\end{lstlisting}
相信同志们现在懂得如何读取和写入外部文件, 内部文件和 ``\ttt{*}'' 了, 不过同志们还需要注意一些细节问题. 在上个轮子中, 我一开始就打出了 ``算 $ 2\times2 $ 矩阵的行列式'' 的提示, 告诉用这个轮子的人这个轮子会干什么, 后面输入和输出是什么也有提示. 如果不这么干, 用轮子的人估计会一脸懵, 轮子在干什么, 自己要干什么, 最后得到的又是什么统统弄不懂, 哪怕是造轮子的人自己可能也会懵, 忘了自己干了什么在干什么该干什么\footnote{如果轮子只是自己用, 还敢赌自己能晓得轮子在干什么, 那想偷懒就偷懒呗.}.
\begin{convention}
输出充足的与程序功能及输入输出有关的提示信息.
\end{convention}
\section{编辑符}\label{fortran_edit_descriptor}
如果我们分别用Ifx和Gfortran跑下面这个处心积虑造出来的简单轮子, 我们会发现结果的格式有点区别, Ifx的结果是用科学计数法表示的而Gfortran的不是.
\begin{lstlisting}
program main
implicit none
print *, 7.0e7
end program main
\end{lstlisting}
这是由于我们让编译器自己决定输入输出的格式, 有时这会出事情, 比如之前第 \pageref{internal_file} 页有个轮子, Ifx跑得了但Gfortran跑不了, 原因在于Gfortran在把 \ttt{exp(1.0) ** acos(-1.0)} 写入内部文件 \ttt{f} 时会在数字前后补上些空格之类的, 按Gfortran自己的规定, 最少要写入17个字符 (包含1个换行符), 而 \ttt{f} 长度只有16不够长, Ifx规定不同, 就没这个问题.
我们可以自己规定输入输出的格式, 比如下面这个轮子\footnote{Ifx编译时会出现一个 ``remark'', 是些建议, 我们选择无视, 不过听从Ifx的建议也是好事.}, 第一个输出的结果一定不是科学计数法表示的, 第二个一定是.
\begin{lstlisting}
program main
implicit none
print "(F10.1)", 7.0e7
print "(ES6.1E1)", 7.0e7
end program main
\end{lstlisting}
再比如修改第 \pageref{internal_file} 页那个轮子可得下面这个轮子, Gfortran也是可以跑的.
\begin{lstlisting}
program main
implicit none
character(16) :: f
real :: val
write(f, "(F15.12)") exp(1.0) ** acos(-1.0)
print *, f
read(f, "(F15.12)") val
print *, val
end program main
\end{lstlisting}
可见自己规定格式就是把原来的 \ttt{*} 换成一个字符串, 这个字符串里是 \ttt{(...)}, 称为格式声明 (format specification), \ttt{*} 则是代表编译器自己决定格式.
我们回归一开始玩Gaia DR3遇到的问题, 现在我们可以把第 \pageref{gaiadr3.sam} 页的轮子补成下面的样子, 其中倒数第二行我们写 \ttt{format} 后跟格式声明 (注意格式声明只是 \ttt{(...)}, 不带引号), 前面加标号, 这样我们就能在倒数第三行用标号代替字符串了\footnote{同志们这么写的时候, 带标号的行最好就在用标号的行的下面, 不然查格式时真的是会找不到的\dots{}}. 不过呢, 同志们会发现怎么输出的最大距离是无穷大, 那是因为星表里有些源根本没有视差的数据, 硬读出来是 $ 0 $, 看来研究还是没那么容易做的\dots{} 不过我们可以用 \ref{where_construct} 小节介绍的 where 结构, 把视差测量值不为 $0$ 的源的数据提出来然后再算, 但安安懒得写了, 请同学们自己练习.
\begin{lstlisting}
program main
use iso_fortran_env, only: dp => real64
implicit none
integer, parameter :: sam_len = 1000
real(dp), parameter :: &
mas2rad = acos(-1.0_dp)/(1.8e2_dp*3.6e6_dp)
real(dp), parameter :: &
deg2rad = acos(-1.0_dp)/(1.8e2_dp)
real(dp) :: p(sam_len), b(sam_len)
real(dp) :: d(sam_len)
integer :: i
open(10, file='gaiadr3.sam ', status='OLD', &
action='READ', position='REWIND')
do i = 1, sam_len
read(10, "(128X,F9.4,842X,F15.11)") p(i), b(i)
end do
close(10)
p = p * mas2rad
b = b * deg2rad
d = abs(sin(b)) / p
print 1000, maxloc(d), maxval(d)
1000 format ('Object: ', I3, ', ', ES11.4, ' AU')
end program main
\end{lstlisting}
每个格式声明都由 \ttt{()} 里的一堆编辑符 (edit descriptor) 组成, 编辑符间用 \ttt{,} 隔开, 每个编辑符都代指一个操作, 比如上面的轮子读取时编辑符依次是 \ttt{128X}, \ttt{F9.4}, \ttt{842X}, \ttt{F15.11}, 依次代表跳过128个字符不读, 读占9个字符的4位小数的实数, 跳过842个字符不读, 读占15个字符的11位小数的实数. 话说我是怎么知道这么读就行的? 我们再次打开 \href{https://cdsarc.cds.unistra.fr/viz-bin/ReadMe/I/355?format=html&tex=true}{ReadMe} 里的 \ttt{gaiadr3.sam} 的 ``Byte-by-byte Description'', 里头写着.
\begin{lstlisting}
Byte-by-byte Description of file: gaiadr3.sam
------------------------------------------------------
Bytes Format Units Label Explanations
------------------------------------------------------
1- 28 A28 --- DR3Name Unique source
designation (unique
across all Data
Releases)
(designation)
129- 137 F9.4 mas Plx ? Parallax (parallax)
980- 994 F15.11 deg GLAT Galactic latitude (b)
\end{lstlisting}
同志们看表里分明写着视差的格式是 ``F9.4'', 银纬的格式是 ``F15.11'', 这不就是编辑符么. 首先视差从第129个字符开始, 所以我们要先跳过前128个字符, 写上 \ttt{128X}, 然后把 \ttt{F9.4} 无脑抄过去, 然后本来是读到第138个字符, 但银纬从第980个字符开始, 所以要再跳过 $ 980-138=842 $ 个字符, 写上 \ttt{842X}, 然后无脑抄上 \ttt{F15.11}, 因为每次 \ttt{read} 完再 \ttt{read} 都会从下一行开始, 所以后面的字符就不用管了. 这里我们的活儿干得不是那么优雅, 因为到底要跳过几个字符我们还要手算一波, 说不定算错了呢. 最好有个轮子, 能接收 ReadMe 文件里的内容, 然后自动算出要跳过几个字符并给出编辑符来, 不过安安还没空开发, 同志们可以自己造个轮子来当练习. 另外给同志们留一个学完本章后的作业: 再次修改上面的轮子, 输出第 \ttt{maxloc(d)} 个源的 ``Unique source designation''.
如果我们把一些编辑符用 \ttt{()} 起来, 前面加个数, 数是几就表示把 \ttt{()} 里的编辑符重复几遍, 比如下面这个轮子写入和读取时格式声明是一样的 (要不然就不知道读出什么东西了).
\begin{lstlisting}
program main
implicit none
real :: e, pi
real :: a, s, m, d
e = exp(1.0)
pi = acos(-1.0)
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, 1006) e+pi, e-pi, e*pi, e/pi
1006 format (F6.4,ES11.4,F6.4,ES11.4)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, 1005) a, s, m, d
1005 format (2(F6.4,ES11.4))
print *, a, s, m, d
close(10)
end program main
\end{lstlisting}
\ttt{()} 还可以嵌套, 比如下面这个轮子两次写入时格式声明是一样的.
\begin{lstlisting}
program main
implicit none
! A staff.
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, 1001) 'OXOX', 'OXXO', 'OX-X', 'OXXX'
1001 format ('X','O','X','O','}',A4,'}', &
'X','O','X','O','}',A4,'}', &
'X','O','X','O','}',A4,'}', &
'X','O','X','O','}',A4,'}')
close(10)
open(10, file='test.txt', status='OLD', &
action='WRITE', position='APPEND')
write(10, 1002) 'OXOX', 'OXXO', 'OX-X', 'OXXX'
1002 format (4(2('X','O'),'}',A4,'}'))
close(10)
end program main
\end{lstlisting}
编辑符又分三大类: 数据编辑符 (data edit descriptor), 控制编辑符 (control edit descriptor), 字符串编辑符 (character string edit descriptor). 接下来我们一个个扒.
\subsection{数据编辑符}
数据编辑符和读取与写入时的数据实体是一一对应的, 比如下面这个轮子, \ttt{A4} 对应 \ttt{'ello'}, \ttt{A5} 对应 \ttt{'world'}, 其他编辑符不是数据编辑符, 没有对应的数据实体.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "('H',A4,',',1X,A5,'!')") &
'ello', 'world'
close(10)
end program main
\end{lstlisting}
每个数据编辑符还都对应于一种数据类型, 比如下面这个轮子照道理是跑不得的, 因为 \ttt{I1} 是整型编辑符而 \ttt{0.0} 是实型的, 但Ifx居然能跑, 可恶的器规又出现了\dots{}
\begin{lstlisting}
program main
implicit none
print "(I1)", 0.0
end program main
\end{lstlisting}
数据编辑符都可以直接在前面加数来表示重复, 比如下面这个轮子三个格式声明全都是一样的.
\begin{lstlisting}
program main
implicit none
! Violin Strings.
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A,A,A,A)") 'G', 'D', 'A', 'E'
write(10, "(4(A))") 'G', 'D', 'A', 'E'
write(10, "(4A)") 'G', 'D', 'A', 'E'
close(10)
end program main
\end{lstlisting}
\subsubsection{整型编辑符}
I$ w $ 编辑符表示一共输出 $ w $ 个字符. 我们需要保证 $ w>0 $. 下面这个轮子, 前面几次写入是正常的, 但最后一次写入的是一堆 \ttt{*}, 因为 \ttt{1000000} 分明是个7位数, 却只能输出6个字符, 编译器只能摆烂了.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6)") 1000
write(10, "(I6)") 10000
write(10, "(I6)") 100000
write(10, "(I6)") 1000000
close(10)
end program main
\end{lstlisting}
下面这个轮子, 后两次写入编译器都摆烂了, 因为 ``\ttt{-}'' 也占1个字符, 千万千万要注意!
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6)") -1000
write(10, "(I6)") -10000
write(10, "(I6)") -100000
write(10, "(I6)") -1000000
close(10)
end program main
\end{lstlisting}
I$ w. m $ 编辑符则表示一共输出 $ w $ 个字符, 其中数字字符 (\ttt{0}--\ttt{9}) 至少 $ m $ 个, 当然必须 $ m\leqslant w $. 下面这个轮子, \ttt{1000} 只占4个字符, 所以前面要补上一个 \ttt{0}, \ttt{10000} 占5个字符, 正常输出, \ttt{100000} 占6个字符, 也正常输出, 因为是\uline{至少}输出5个字符, \ttt{1000000} 还是一堆 \ttt{*}.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6.5)") 1000
write(10, "(I6.5)") 10000
write(10, "(I6.5)") 100000
write(10, "(I6.5)") 1000000
close(10)
end program main
\end{lstlisting}
特别注意输入时 I$w.m $ 的 $. m $ 会被无视, 也就是说 I$w. m $ 等价于 I$w $. 下面这个轮子是能正常运作的, 因为读取的时候 I$ 6.5 $ 等价于 I$6 $.
\begin{lstlisting}
program main
implicit none
integer :: k, dak, hk
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6)") 1000
write(10, "(I6)") 10000
write(10, "(I6)") 100000
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(I6.5)") k
read(10, "(I6.5)") dak
read(10, "(I6.5)") hk
print *, k, dak, hk
close(10)
end program main
\end{lstlisting}
反过来, 下面这个轮子也是能正常运作的, 虽然写入文件的时候 \ttt{1000} 前加了 \ttt{0}, 但读取的时候开头的 \ttt{0} 都会被无视.
\begin{lstlisting}
program main
implicit none
integer :: k, dak, hk
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6.5)") 1000
write(10, "(I6.5)") 10000
write(10, "(I6.5)") 100000
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(I6)") k
read(10, "(I6)") dak
read(10, "(I6)") hk
print *, k, dak, hk
close(10)
end program main
\end{lstlisting}
但下面这个轮子就不对了, 因为写入6个字符但只读取5个字符, 这意味着最后的 \ttt{0} 没被读取, 结果 \ttt{1000}, \ttt{10000}, \ttt{100000} 被读成 \ttt{100}, \ttt{1000}, \ttt{10000}.
\begin{lstlisting}
program main
implicit none
integer :: k, dak, hk
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I6.5)") 1000
write(10, "(I6.5)") 10000
write(10, "(I6.5)") 100000
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(I5)") k
read(10, "(I5)") dak
read(10, "(I5)") hk
print *, k, dak, hk ! Wrong!
close(10)
end program main
\end{lstlisting}
\subsubsection{实型编辑符}
F$ w. d $ 编辑符表示一共输出 $ w $ 个字符, 其中小数部分 $ d $ 个字符, 我们需要保证 $ w>d\geqslant0 $. 下面这个轮子, 第一个输出是正常的, 第二个要输出 ``\ttt{-12.}'' 再加2个字符, 一共6个字符, 但却只能输出5个, 编译器又摆烂了.
\begin{lstlisting}
program main
implicit none
print "(F5.2)", 12.3456789
print "(F5.2)", -12.3456789
end program main
\end{lstlisting}
输入的时候F$ w. d $ 的 $. d $ 则会被无视, 但 $.d$ \uline{不能被省略}. 下面这个轮子, F$ 11.99 $ 看着很鬼, 一共11个字符, 小数部分99个? 但 $.99 $ 会被无视, 反正就是读 $ 11 $ 个字符然后赋值给 \ttt{val} 完事, 所以轮子是跑得的. 在下面的轮子中我们还写入双精度后读取成单精度, 这也没问题, 读取和写入可以跨种别.
\begin{lstlisting}
program main
use iso_fortran_env, only: dp => real64
implicit none
real :: val
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(F11.9)") 0.123456789_dp
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(F11.99)") val
print *, val
close(10)
end program main
\end{lstlisting}
但下面这个轮子的结果是不对的, 因为写入和读取的 \ttt{1000} 没有小数点, 在没有小数点的时候, $. d $ 复活了, 编译器会认为读取到的字符的最后 $ d $ 个是小数部分, 读到 \ttt{1000}, 最右边 \ttt{0} 是小数部分, 前面 \ttt{100} 是小数部分, 当然错啦!
\begin{lstlisting}
program main
implicit none
real :: val
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(I4)") 1000
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(F4.1)") val
print *, val ! Wrong!
close(10)
end program main
\end{lstlisting}
有时我们会碰到 $ +\infty $, $ -\infty $, 和 $ \text{NaN} $. Fortran规定输入输出时用前面加正负号的字符串 \ttt{INF} 或 \ttt{INFINITY} 表示 $ \pm\infty $, 用字符串 \ttt{NAN} 表示 $ \text{NaN} $, 这些字符串不分大小写. 如果遇到 $ \pm\infty $ 或 $ \text{NaN} $, 则不论输入输出F$ w. d $ 的 $. d $ 都会被无视\footnote{虽然Fortran官方规则中没写明, 但想来是这样的.\label{edit_IEEE}}, 比如下面的轮子里 $ d $ 可以随便乱写, 只需保证 $ d\geqslant0 $.
\begin{lstlisting}
program main
implicit none
real :: one, zero
real :: val1, val2, val3, val4
one = 1.0
zero = 0.0
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write (10, "(F9.12)") +(one/zero)
write (10, "(F9.34)") -(one/zero)
write (10, "(F9.56)") +(zero/zero)
write (10, "(F9.78)") -(zero/zero)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(F9.87)") val1
read(10, "(F9.65)") val2
read(10, "(F9.43)") val3
read(10, "(F9.21)") val4
print *, val1, val2, val3, val4
close(10)
end program main
\end{lstlisting}
F编辑符经常会不大好用, 举个例子, 假如我们要输出 $ 1.234\times10^{-3} $, $ 1.234 $, $ 1.234\times10^{3} $ 量级不同的三个数, 编辑符都用F$ 5.3 $, 那就只有 $ 1.234 $ 的输出是比较正常的, $ 1.234\times10^{-3} $ 有效数字丢了, $ 1.234\times10^{3} $ 干脆输出不了, 但麻烦的是实际的观测数据的量级有差别是经常出现的事情.
\begin{lstlisting}
program main
implicit none
print "(F5.3)", 1.234e-3 ! output: 0.001
print "(F5.3)", 1.234 ! output: 1.234
print "(F5.3)", 1.234e+3 ! output: *****
end program main
\end{lstlisting}
这时我们可以用E编辑符, E$ w. d $ 表示一共输出 $ w $ 个字符, 输出时先将实数表示成 $ a\times10^{n} $, $ 0.1\leqslant a<1 $, $ n $ 为整数, 把 $ a $ 转换成小数部分占 $ d $ 个字符的字符串 \ttt{a}, $ n $ 转换成开头带正负号的字符串 \ttt{n}, 然后输出字符串 \ttt{a//'E'//n}. 我们需要保证 $ w>d\geqslant0 $. 最后输出的字符串 \ttt{a//'E'//n} 中, 开头的 \ttt{0} 和中间的 \ttt{E} 可省略, 但输出的字符串去掉 \ttt{a} 后剩下的部分必占4个字符, 所以保证 $ w\geqslant 3+d+4 $ 一般就没什么事情了. 假如我们想保留4位有效数字, 编辑符设成E$ 11.4 $ 就好啦.
\begin{lstlisting}
program main
implicit none
print "(E11.4)", 1.234e-3 ! output: 0.1234E-02
print "(E11.4)", 1.234 ! output: 0.1234E+01
print "(E11.4)", 1.234e+3 ! output: 0.1234E+04
end program main
\end{lstlisting}
用E$ w. d $ 编辑符的时候, \ttt{n} 是三位数则 \ttt{E} 必须省略, \ttt{n} 是四位数就只能罢工了, 虽然平常我们基本上不会用上这么极端的数\dots{}
\begin{lstlisting}
program main
use iso_fortran_env, only: qp => real128
implicit none
print "(E11.4)", 1e10_qp ! output: 0.1000E+11
print "(E11.4)", 1e100_qp ! output: 0.1000+101
print "(E11.4)", 1e1000_qp ! output: ***********
end program main
\end{lstlisting}
这时我们可以用E$ w. d $E$ e $ 编辑符, E$ e $ 表示 \ttt{n} 为正负号后接 $ e $ 个数字的字符串, 其他和E$ w. d $ 相同, 除了 \ttt{E} 不可省略外. 这时我们需要额外保证 $ e>0 $, 再保证 $ w\geqslant 3+d+2+e $ 一般就没什么事情了.
\begin{lstlisting}
program main
use iso_fortran_env, only: qp => real128
implicit none
! output: 0.1000E+0011
print "(E13.4E4)", 1e10_qp
! output: 0.1000E+0101
print "(E13.4E4)", 1e100_qp
! output: 0.1000E+1001
print "(E13.4E4)", 1e1000_qp
end program main
\end{lstlisting}
不过E$ w. d $E0也是合法的编辑符, 其中E0表示 $ e $ 等于 $ n $ 的位数.
\begin{lstlisting}
program main
use iso_fortran_env, only: qp => real128
implicit none
! output: 0.1000E+11
print "(E13.4E0)", 1e10_qp
! output: 0.1000E+101
print "(E13.4E0)", 1e100_qp
! output: 0.1000E+1001
print "(E13.4E0)", 1e1000_qp
end program main
\end{lstlisting}
和F编辑符类似, 输入的时候 $. d $, E$e $, E0都会被无视, $. d$ 不能被省略. 不仅如此, 用F编辑符写入后还能用E编辑符读取, 用E编辑符写入后也能用F编辑符读取, 读取的时候字符 \ttt{E} 还不分大小写.
\begin{lstlisting}
program main
implicit none
character(13+8+1) :: f
real :: val_fe, val_ef
integer :: i
write(f, "(F13.1, E8.1)") 1e10, 1e10
do i = 1, len(f)
if (f(i:i)=='E') then
f(i:i) = 'e'
end if
end do
print *, f
read(f, "(E13.13, F8.8)") val_fe, val_ef
print *, val_fe, val_ef
end program main
\end{lstlisting}
E编辑符也可以应付 $ \pm\infty $ 和 $ \text{NaN} $, 遇到 $ \pm\infty $ 或 $ \text{NaN} $ 的时候 $ d $ 和 $ e $ 被无视, 其他和F编辑符相同\footnote{同脚注\ref{edit_IEEE}}, 也就是说我们又可以乱写了.
\begin{lstlisting}
program main
implicit none
real :: one, zero
real :: val1, val2, val3, val4
one = 1.0
zero = 0.0
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write (10, "(F9.12)") +(one/zero)
write (10, "(E9.34)") -(one/zero)
write (10, "(E9.5E6)") +(zero/zero)
write (10, "(E9.78E0)") -(zero/zero)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(E9.87E0)") val1
read(10, "(E9.6E5)") val2
read(10, "(E9.43)") val3
read(10, "(F9.21)") val4
print *, val1, val2, val3, val4
close(10)
end program main
\end{lstlisting}
E编辑符蛮好用, 就是最后输出的结果不太符合俺们的习惯, 因为不是用科学计数法表示的. 我们只要把E换成ES, 结果就是科学计数法表示的了, 也就是说 $ 1\leqslant a<10 $. 我们还可以把E换成EN, 这样的结果是用工程计数法表示的, $ 1\leqslant a<1000 $ 且 $ n $ 能被 $ 3 $ 整除, 这样单位换算就会比较方便. 除了 $ a $ 和 $ n $ 不同外E编辑符, ES编辑符, EN编辑符没有区别.
\begin{lstlisting}
program main
implicit none
print "(E7.1E1)", 10000.0
print "(ES7.1E1)", 10000.0
print "(EN7.1E1)", 10000.0
end program main
\end{lstlisting}
\subsubsection{复型编辑符}
复型编辑符是没有的, 输出复数的时候, 永远是把实部和虚部分别输出, 我们可以分别给实部和虚部加实型编辑符.
\begin{lstlisting}
program main
implicit none
print "(F4.1,E8.1)", (0.1, 1.0)
end program main
\end{lstlisting}
输入也是一样的道理, 注意输入的时候实型编辑符是可以乱来的.
\begin{lstlisting}
program main
implicit none
complex :: z
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(F4.1,E8.1)") (0.1, 1.0)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(E4.1,F8.1)") z
print *, z
close(10)
end program main
\end{lstlisting}
\subsubsection{字符型编辑符}
A$ w $ 编辑符表示一共输出 $ w $ 个字符. 设字符串长度为 $ l $, 如果 $ l>w $, 则只输出字符串最左边 $ w $ 个字符, 如果 $ l<w $, 则先输出 $ w-l $ 个空格再输出 $ w $ 个字符\footnote{这里是左补空格, 字符串赋值(\ref{fortran_assignment}节)是右补空格.}. A编辑符则表示 $ w=l $.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A4)") 'hello'
write(10, "(A5)") 'hello'
write(10, "(A6)") 'hello'
close(10)
open(10, file='test.txt', status='OLD', &
action='WRITE', position='APPEND')
write(10, "(A)") 'hello'
write(10, "(A)") 'hellohello'
write(10, "(A)") 'hellohellohello'
close(10)
end program main
\end{lstlisting}
输入的话情况比较复杂. 首先同志们要认定每一行最后都有无数个空格, 下面这个轮子, 写入完第一行是 \ttt{1234567890}, 读取完 \ttt{c} 当然等于 \ttt{'12345'}, 第二行是 \ttt{1}, 后面没了, 同学们要认定 \ttt{1} 后面跟着无数个空格, 所以读取完 \ttt{c} 等于 \ttt{'1 '}
\begin{lstlisting}
program main
implicit none
character(5) :: c
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A)") '1234567890'
write(10, "(A)") '1'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(A5)") c
print "(2A)", c, '}'
read(10, "(A5)") c
print "(2A)", c, '}'
close(10)
end program main
\end{lstlisting}
然后A编辑符表示 $ w=l $ 这点不变.
\begin{lstlisting}
program main
implicit none
character(4) :: sc
character(6) :: lc
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A)") '1234567890'
write(10, "(A)") '1234567890'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(A)") sc ! (A4)
print "(2A)", sc, '}'
read(10, "(A)") lc ! (A6)
print "(2A)", lc, '}'
close(10)
end program main
\end{lstlisting}
A$ w $ 编辑符则表示读取 $ w $ 个字符. 若 $ l<w $, 则赋值最右边 $ l $ 个字符\footnote{这里是赋值最右边的字符, 字符串赋值(\ref{fortran_assignment}节)是赋值最左边的字符.}, 若 $ l>w $, 则赋值 $ w $ 个字符后跟 $ l-w $ 个空格.
\begin{lstlisting}
program main
implicit none
character(4) :: sc
character(5) :: nc
character(6) :: lc
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A)") '1234567890'
write(10, "(A)") '1234567890'
write(10, "(A)") '1234567890'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(A5)") sc
print "(2A)", sc, '}'
read(10, "(A5)") nc
print "(2A)", nc, '}'
read(10, "(A5)") lc
print "(2A)", lc, '}'
close(10)
end program main
\end{lstlisting}
可见字符型编辑符十分让人头大, 如果老师敢考我们就揭竿而起\dots{}
注意字符型编辑符和 \ref{character_string_edit_descriptor} 节的字符串编辑符是八竿子打不着的.
\subsubsection{逻辑型编辑符}
L$ w $ 表示一共输出 $ w $ 个字符, 前面 $ w-1 $ 个是空格, 最后1个是 \ttt{T} 或 \ttt{F}, \ttt{T} 代表 \ttt{.true.}, \ttt{F} 代表 \ttt{.false.}.
\begin{lstlisting}
program main
implicit none
print "(L7)", .true.
print "(L7)", .false.
end program main
\end{lstlisting}
输入的时候, 字符 \ttt{T} 和 \ttt{F}, 字符串 \ttt{TRUE} 和 \ttt{FALSE}, 字符串 \ttt{.TRUE.} 和 \ttt{.FALSE.} 都代表 \ttt{.true.} 和 \ttt{.false.}, 而且不分大小写.
\begin{lstlisting}
program main
implicit none
logical :: true, false
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A)") 't f '
write(10, "(A)") 'true false'
write(10, "(A)") '.true. .false. '
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(2L2)") true, false
print "(2L2)", true, false
read(10, "(2L5)") true, false
print "(2L5)", true, false
read(10, "(2L7)") true, false
print "(2L7)", true, false
close(10)
end program main
\end{lstlisting}
\subsection{控制编辑符}
$ n $X编辑符表示把 ``文件定位'' 右移 $ n $ 位, 这通常等价于输出 $ n $ 个空格, 但如果 $ n $X编辑符后没有数据编辑符或字符串编辑符, 则 $ n $X编辑符相当于没有.
\begin{lstlisting}
program main
implicit none
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(2(A,1X))") 'X', 'descriptor'
close(10)
end program main
\end{lstlisting}
输入的时候 $ n $X编辑符则表示跳过 $ n $ 个字符不读.
\begin{lstlisting}
program main
implicit none
character(5) :: world
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A)") 'Hello, world!'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(7X,A5)") world
print "(A)", world
close(10)
end program main
\end{lstlisting}
/ 编辑符表示接下来从下一行第一个字符开始读取或写入.
\begin{lstlisting}
program main
implicit none
character(5) :: hello, world
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "(A,/,A)") 'hello', 'world'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(A,/,A)") hello, world
print "(A,/,A)", hello, world
close(10)
end program main
\end{lstlisting}
SS编辑符表示之后输出正数时最开头都不带正号, SP编辑符则表示都带正号. \footnote{``S'' 代表 ``sign''. ``S'' 代表 ``suppress'', ``P'' 代表 ``plus''.}
\begin{lstlisting}
program main
implicit none
print 1001, 0.1, 2.3, 4.5, 6.7, 8.9
1001 format (SS,F5.1,F5.1,SP,F5.1,SS,F5.1,F5.1)
print 1002, 0.1, 2.3, 4.5, 6.7, 8.9
1002 format (SP,F5.1,F5.1,SS,F5.1,SP,F5.1,F5.1)
end program main
\end{lstlisting}
注意正号会占一个字符, 编译器有可能因此罢工.
\begin{lstlisting}
program main
implicit none
print "(SS,F3.1)", 1.0 ! 1.0
print "(SP,F3.1)", 1.0 ! ***
end program main
\end{lstlisting}
输入的时候SS编辑符和SP编辑符不起作用, 我们又可以乱来了.
\begin{lstlisting}
program main
implicit none
real :: val0, val1, val2, val3, val4
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, 1061) 0.1, 2.3, 4.5, 6.7, 8.9
1061 format (SS,2F5.1,SP,F5.1,SS,2F5.1)
write(10, 1062) 0.1, 2.3, 4.5, 6.7, 8.9
1062 format (SP,2F5.1,SS,F5.1,SP,2F5.1)
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, 1051) val0, val1, val2, val3, val4
1051 format (SP,F5.1,SS,3F5.1,SP,F5.1)
print *, val0, val1, val2, val3, val4
read(10, 1052) val0, val1, val2, val3, val4
1052 format (SS,F5.1,SP,3F5.1,SS,F5.1)
print *, val0, val1, val2, val3, val4
close(10)
end program main
\end{lstlisting}
在输出实数的时候, 我们只能输出若干位有效数字, 所以这里有个舍入的问题. 比如测量值我们希望四舍五入但不确定度我们希望向上舍入. 我们可以用RU, RD, RZ, RC编辑符, 这四个编辑符分别表示之后输出实数时都向上舍入, 都向下舍入, 都向零舍入, 都四舍五入. \footnote{``R'' 代表 ``round''. ``U'' 代表 ``up'', ``D'' 代表 ``down'', ``Z'' 代表 ``zero'', ``C'' 代表 ``compatible''.}
\begin{lstlisting}
program main
implicit none
print "(RU,4F5.1,/,"// &
"RD,4F5.1,/,"// &
"RZ,4F5.1,/,"// &
"RC,4F5.1)", &
-5.6789, -0.1234, +0.1234, +5.6789, &
-5.6789, -0.1234, +0.1234, +5.6789, &
-5.6789, -0.1234, +0.1234, +5.6789, &
-5.6789, -0.1234, +0.1234, +5.6789
end program main
\end{lstlisting}
输入时RU, RD, RZ, RC编辑符也起作用, 因为用字符串表示的实数都是形如 $ \sum_{i=m_\text{min}}^{m_\text{max}}10^{i} $ 的实数, 而电脑是二进制的, 读取后实数都必须是形如 $ \sum_{i=n_\text{min}}^{n_\text{max}}2^{i} $ 的实数, 所以也得舍入.
\subsection{字符串编辑符}\label{character_string_edit_descriptor}
字符串编辑符就是一个字符串, 作用就是输出这个字符串.
\begin{lstlisting}
program main
implicit none
print "(A,', world!')", 'Hello'
print "('Hello, ',A,'!')", 'world'
end program main
\end{lstlisting}
读取和写入的时候, 我们其实可以不加任何数据实体, 所以可以秀波操作.
\begin{lstlisting}
program main
implicit none
print "('Hello, world!')",
end program main
\end{lstlisting}
输入的时候不能用字符串编辑符, 我们可以用 $ n $X编辑符代替.
\begin{lstlisting}
program main
implicit none
character :: the_end
open(10, file='test.txt', status='REPLACE', &
action='WRITE', position='APPEND')
write(10, "('Hello, world',A1)") '!'
close(10)
open(10, file='test.txt', status='OLD', &
action='READ', position='REWIND')
read(10, "(12X,A1)") the_end
print *, the_end
close(10)
end program main
\end{lstlisting}