4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FortranAdvent Calendar 2024

Day 10

FortranでJavaのScannerクラスみたいなやつを作る

Last updated at Posted at 2024-12-09

背景

Fortranで競技プログラミングをする際に...

競技プログラミングでは入力を読み込む必要があります.
面倒な入力形式が稀によくあります.

例えば...

(略)
4
1 1 a
2 1 2
2 2 30
1 2 z

のように, 3列目の型が文字か数値です.
これらを Q回読み込む必要があります.
do ループで回したいのですが, 数値でも文字でも読み込める型は文字列しかないです.
そのため, 文字列で読み込む → 数値に変換するという手間が掛かります.
Fortran の read 文はトークンを読み込むことは(おそらく)できないため, このような入力の処理は面倒です.
なので, Java 言語の Scanner クラスみたいなものを定義して, トークン毎に文字を読み込みたいです.

JavaのScannerクラス

Java の Scanner クラスを使うと, 上の入力は

// (略)
// 大体こんな感じ(コンパイルできるか試していません)
Scanner sc = new Scanner(System.in);
int q = sc.nextInt();
for (int i = 0; i < q; ++i) {
  int t = sc.nextInt();
  if (t == 1) {
    int iq = sc.nextInt();
    String s = sc.next();
    // 処理
  } else { // t = 2
    int lq = sc.nextInt();
    int rq = sc.nextInt();
    // 処理
  }
}

のように, System.in から空白区切りで数字や文字列を読み込むことができます.
これを Fortran で実現したいです.

数値計算的な用途は...ない?

例えば, 数値計算で入力を用意する場合は, 入力の方を自分で決められるため, Fortranで扱い易い形式にすればよいです.
なので, 数値計算においては scanner クラスの需要はないと思います.

作ってみよう

マイ競プロライブラリにも src/scanner_m.f90 を追加しました.

ソースコード全体

ソースコードは以下の折りたたみの中身の通りです.

scanner_m.f90 (全体)
scanner_m.f90
module scanner_m
  use, intrinsic :: iso_fortran_env
  implicit none
  private
  public scanner
  integer(int32), parameter, public :: max_bufsize = ishft(1_int32, 20) ! ~ 10**6
  !> type(scanner): For reading token like Scanner class in Java.
  !> The default delimiter is space (" ").
  !> If delimiter is space (" "), regard `iostat_eor` as space.
  type :: scanner
     private
     integer(int32) :: pos_ = 0, unit_ = input_unit
     character :: delim_ = " "
     logical :: is_eof_ = .false.
     character(len=max_bufsize) :: buffer_
   contains
     !> setting function
     procedure, pass :: use_delim => use_delim_scanner
     !> read function
     procedure, pass, private :: read_until_delim => read_until_delim_scanner
     !> next functions
     procedure, pass :: next => next_scanner
     generic :: next_int => next_int8, &
          & next_int16, &
          & next_int32, &
          & next_int64
     procedure, pass :: next_int8 => next_int8_scanner
     procedure, pass :: next_int16 => next_int16_scanner
     procedure, pass :: next_int32 => next_int32_scanner
     procedure, pass :: next_int64 => next_int64_scanner
     generic :: next_real => next_real32, &
          & next_real64
     procedure, pass :: next_real32 => next_real32_scanner
     procedure, pass :: next_real64 => next_real64_scanner
     !> state functions
     procedure, pass :: is_eof => is_eof_scanner
  end type scanner
  interface scanner
     module procedure construct_scanner
  end interface scanner
contains
  !> construct_scanner: Constructor for `type(scanner)`.
  !> @param unit An unit of input device.
  !> @return A object of scanner type.
  pure type(scanner) function construct_scanner(unit) result(res)
    integer(int32), intent(in) :: unit
    if (unit == output_unit) &
         & error stop "Error: Scanner does not use `output_unit`."
    if (unit == error_unit) &
         & error stop "Error: Scanner does not use `error_unit`."
    res%unit_ = unit
  end function construct_scanner
  !> use_delim_scanner: Modify delimiter.
  !> @param this `this%delim_` changes.
  !> @param delimiter A character of delimiter.
  pure subroutine use_delim_scanner(this, delimiter)
    class(scanner), intent(inout) :: this
    character, intent(in) :: delimiter
    this%delim_ = delimiter
  end subroutine use_delim_scanner
  !> read_until_delim_scanner: Read from `this%unit_` until reaching `this%delim_`.
  !> @param this `this%buffer_(1:this%pos_)` represents token splited by `this%delim_`.
  impure subroutine read_until_delim_scanner(this)
    class(scanner), intent(inout) :: this
    character :: c
    integer(int32) :: iostat
    if (this%pos_ /= 0) return
    read_until_delim: do
       read (this%unit_, '(a1)', advance="no", iostat=iostat) c
       select case (iostat)
       case (iostat_eor)
          if (this%delim_ == " ") exit
          this%pos_ = this%pos_ + 1
          this%buffer_(this%pos_:this%pos_) = achar(10) !> newline code.
          cycle
       case (iostat_end)
          this%is_eof_ = .true.
          exit
       case (0)
          !> do nothing.
       case default
          write (error_unit, *) iostat
          error stop "Error: Unknown iostat"
       end select
       if (c == this%delim_) then
          if (this%pos_ /= 0) exit
          cycle !> The contiguous `this%delim_`.
       end if
       ! write(error_unit, *) c, this%pos_, iostat, iostat_eor, iostat_end
       this%pos_ = this%pos_ + 1
       this%buffer_(this%pos_:this%pos_) = c
    end do read_until_delim
  end subroutine read_until_delim_scanner
  !> next_scanner: Return token as `character(len=:)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res A character of token splited by `this%delim_`.
  impure subroutine next_scanner(this, res)
    class(scanner), intent(inout) :: this
    character(len=:), intent(inout), allocatable :: res
    character(len=:), allocatable :: tmp
    call this%read_until_delim()
    allocate (tmp, source=this%buffer_(1:this%pos_))
    call move_alloc(from=tmp, to=res)
    this%pos_ = 0
  end subroutine next_scanner
  !> next_int8_scanner: Return token as `integer(int8)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(int8)` of token splited by `this%delim_`.
  impure subroutine next_int8_scanner(this, res)
    class(scanner), intent(inout) :: this
    integer(int8), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_int8_scanner
  !> next_int16_scanner: Return token as `integer(int16)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(int16)` of token splited by `this%delim_`.
  impure subroutine next_int16_scanner(this, res)
    class(scanner), intent(inout) :: this
    integer(int16), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_int16_scanner
  !> next_int32_scanner: Return token as `integer(int32)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(int32)` of token splited by `this%delim_`.
  impure subroutine next_int32_scanner(this, res)
    class(scanner), intent(inout) :: this
    integer(int32), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_int32_scanner
  !> next_int64_scanner: Return token as `integer(int64)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(int64)` of token splited by `this%delim_`.
  impure subroutine next_int64_scanner(this, res)
    class(scanner), intent(inout) :: this
    integer(int64), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_int64_scanner
  !> next_real32_scanner: Return token as `real(real32)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(real32)` of token splited by `this%delim_`.
  impure subroutine next_real32_scanner(this, res)
    class(scanner), intent(inout) :: this
    real(real32), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_real32_scanner
  !> next_real64_scanner: Return token as `real(real64)` through `res`.
  !> @param this this%pos_ reset 0.
  !> @param res An `integer(real64)` of token splited by `this%delim_`.
  impure subroutine next_real64_scanner(this, res)
    class(scanner), intent(inout) :: this
    real(real64), intent(inout) :: res
    call this%read_until_delim()
    read (this%buffer_(1:this%pos_), *) res
    this%pos_ = 0
  end subroutine next_real64_scanner
  !> is_eof_scanner: Return is reaching EOF of input file or not.
  !> @param this Read one token.
  !> @return A logical value if reaching EOF or not.
  impure logical function is_eof_scanner(this) result(res)
    class(scanner), intent(inout) :: this
    call this%read_until_delim()
    res = this%is_eof_
  end function is_eof_scanner
end module scanner_m

型を定義する

scanner_m.f90 (型定義)
scanner_m.f90
  integer(int32), parameter, public :: max_bufsize = ishft(1_int32, 20) ! ~ 10**6
  type :: scanner
     private
     integer(int32) :: pos_ = 0, unit_ = input_unit
     character :: delim_ = " "
     logical :: is_eof_ = .false.
     character(len=max_bufsize) :: buffer_
   contains
     !> setting function
     procedure, pass :: use_delim => use_delim_scanner
     !> read function
     procedure, pass, private :: read_until_delim => read_until_delim_scanner
     !> next functions
     procedure, pass :: next => next_scanner
     generic :: next_int => next_int8, &
          & next_int16, &
          & next_int32, &
          & next_int64
     procedure, pass :: next_int8 => next_int8_scanner
     procedure, pass :: next_int16 => next_int16_scanner
     procedure, pass :: next_int32 => next_int32_scanner
     procedure, pass :: next_int64 => next_int64_scanner
! (略)
  end type scanner
  • 型の中の character(len=max_bufsize) :: buffer_ 配列にトークンを保持します.
  • $10^6$ 程度読み込めれば大丈夫でしょう.
  • あとは色々定義します.
  • read_until_delim_scanner はトークンを読み込むルーチンです.
  • ちなみに next_int たちは fypp を使って楽しています.
fypp
#:set SCANNER_INTEGER_KINDS = ["int8", "int16", "int32", "int64"]
     generic :: next_int => ${", &\n& ".join(map(lambda k: f"next_{k}", SCANNER_INTEGER_KINDS))}$
#:for kind in SCANNER_INTEGER_KINDS
     procedure, pass :: next_${kind}$ => next_${kind}$_scanner
#:endfor

トークンを読み込む

scanner_m.f90 (read_until_delimiter)
scanner_m.f90
  impure subroutine read_until_delim_scanner(this)
    class(scanner), intent(inout) :: this
    character :: c
    integer(int32) :: iostat
    if (this%pos_ /= 0) return
    read_until_delim: do
       read (this%unit_, '(a1)', advance="no", iostat=iostat) c
       select case (iostat)
       case (iostat_eor)
          if (this%delim_ == " ") exit
          this%pos_ = this%pos_ + 1
          this%buffer_(this%pos_:this%pos_) = achar(10) !> newline code.
          cycle
       case (iostat_end)
          this%is_eof_ = .true.
          exit
       case (0)
          !> do nothing.
       case default
          write (error_unit, *) iostat
          error stop "Error: Unknown iostat"
       end select
       if (c == this%delim_) then
          if (this%pos_ /= 0) exit
          cycle !> The contiguous `this%delim_`.
       end if
       ! write(error_unit, *) c, this%pos_, iostat, iostat_eor, iostat_end
       this%pos_ = this%pos_ + 1
       this%buffer_(this%pos_:this%pos_) = c
    end do read_until_delim
  end subroutine read_until_delim_scanner
  • read (this%unit_, '(a1)', advance="no", iostat=iostat) c で1文字だけ読み込めます.
  • iostat で改行(iostat_eor)と読み込み終わり(iostat_end)を検知できます.
  • 区切りが " " の場合は改行も区切りとしています.

1文字を this%delim_ を読み込むまで繰り返すだけです.

やってみる

test_scanner_m.f90
test_scanner_m.f90
program part_of_ABC157e
  use, intrinsic :: iso_fortran_env
  use scanner_m
  implicit none
  integer(int32) :: q
  type(scanner) :: sc
  integer(int32) :: i
  read (input_unit, *) q
  sc = scanner(input_unit)
  do i = 1, q
     block
       integer(int32) :: types
       call sc%next_int(types)
       select case (types)
       case (1)
          block
            integer(int32) :: i
            character(len=:), allocatable :: c
            call sc%next_int(i)
            call sc%next(c)
            write (output_unit, '(*(g0, 1x))') types, i, c
          end block
       case (2)
          block
            integer(int32) :: l, r
            call sc%next_int(l)
            call sc%next_int(r)
            write (output_unit, '(*(g0, 1x))') types, l, r
          end block
       end select
     end block
  end do
end program Part_Of_ABC157e
$ gfortran scanner_m.f90 test_scanner.f90 && ./a.out <<< "4             
1 1 a 
2 1 2 
2 2 30
1 2 z"
1 1 a
2 1 2
2 2 30
1 2 z

ちゃんと読み込めています.

  • sc = scanner(input_unit) で初期化します.
  • read した後に scanner クラスで読み込んでも平気そうです.
    • 逆は, scbuffer_ に文字列が読み込まれている可能性があるため危険です.

まとめ

Fortran はトークン毎に文字列を読み込める可能性を秘めています.
ユーザはインターフェースだけ知っていれば良いので楽そうです.
fypp を使うと楽できます.
面倒なことはクラスに処理させましょう.

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?