TextFileクラスの改良

TextFileクラスの改良

「改良」なのかどうかはわからんが。

ちなみに、元祖TextFileクラスについては、

akashi-keirin.hatenablog.com

コチラをどうぞ。

改良後のTextFileクラス

リスト1 クラスモジュール
'オブジェクト名はTextFile'
Option Explicit

'Constants'
Private Enum ErrorTypes  '……(**)'
  etFileNotFound = 1
  etLineNotExists
  etNotInitialized
  etErrorOccurred
End Enum

'Module Level Variables'
Private isInitialized As Boolean
Private fileFullName As String
Private line_() As String
Private fsObj As FileSystemObject

'Properties'
Public Property Get Line(ByVal numberOf As Long) As String
  Const ERR_SOURCE As String = _
          "TextFile Class, Property Get Line"
  'initメソッド未実行ならエラー。'
  If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)
  Dim ret As String
  '存在しない行番号を指定していたらエラー。'
  If UBound(line_) + 1 < numberOf Or _
     numberOf < 0 Then _
    Call raiseError(etLineNotExists, ERR_SOURCE)
  On Error GoTo ErrorHandler
  ret = line_(numberOf - 1)
  Line = ret
  Exit Property
ErrorHandler:
  '何かしらエラーが出たら、イミディエイトに情報を表示して中断。'
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Property

Public Property Get LineCount() As Long
  Const ERR_SOURCE As String = _
          "TextFile Class, Property Get LineCount"
  'initメソッド未実行ならエラー。'
  If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)
  On Error GoTo ErrorHandler
  Dim ret As Long
  If IsEmpty(line_) Then ret = 0: GoTo Finalizer
  ret = UBound(line_) + 1
Finalizer:
  LineCount = ret
  Exit Property
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Property

'Constructor'
Private Sub Class_Initialize()
  isInitialized = False
  Set fsObj = New FileSystemObject
End Sub

Public Sub init(ByVal targetFullName As String)
  Const ERR_SOURCE As String = _
          "TextFile class,init Method"
  '対象ファイルの存否確認。なければエラー。'
  If Not fsObj.FileExists(targetFullName) Then _
    Call raiseError(etFileNotFound, "TextFile Class, init Method")
  On Error GoTo ErrorHandler
  'モジュールレベル変数に対象ファイルのフルパスを保存'
  fileFullName = targetFullName
  'テキストファイルからデータを取得'
  line_ = getLines(targetFullName)
  isInitialized = True
  Exit Sub
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Sub

'テキストファイル読み込み'
Private Function getLines( _
             ByVal targetFullName As String) As String()
  Const ERR_SOURCE As String = _
          "TextFile class, getLines Method"
  On Error GoTo ErrorHandler
  Dim ret() As String
  Dim n As Long
  n = 0
  ReDim ret(n)
  Dim txtStream As Scripting.TextStream
  Set txtStream = fsObj.OpenTextFile( _
                          FileName:=targetFullName, _
                          IOMode:=ForReading, _
                          Create:=False)
  Do
    ret(n) = txtStream.ReadLine
    '最終行まで読み込んだらExit'
    If txtStream.AtEndOfLine Then Exit Do
    n = n + 1
    ReDim Preserve ret(n)
  Loop
  Call txtStream.Close
  Set txtStream = Nothing
  line_ = ret
  getLines = line_
  Exit Function
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Function

'Destructor'
Private Sub Class_Terminate()
  Set fsObj = Nothing
End Sub

'Methods'
Public Sub regetData(Optional ByVal targetFullName As String)
  Const ERR_SOURCE As String = _
          "TextFile Class, regetData Method"
  'initメソッド未実行ならエラー。'
  If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)
  If targetFullName = "" Then GoTo MainProcess
  If Not fsObj.FileExists(targetFullName) Then _
    Call raiseError(etFileNotFound, ERR_SOURCE)
  '新たにファイルフルパスが渡されたら、それを新しいファイルフルパスにする'
  fileFullName = targetFullName
MainProcess:
  On Error GoTo ErrorHandler
  Erase line_
  Call Me.init(fileFullName)
  Exit Sub
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Sub

'データ書き換え'  '……(*)'
Public Sub setData(ByVal targetLine As Long, _
                   ByVal targetData As String)
  Const ERR_SOURCE As String = _
          "TextFile Class, setData Method"
  'initメソッド未実行ならエラー。'
  If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)
  Dim ret As String
  '存在しない行番号を指定していたらエラー。'
  If targetLine < 1 Or _
     UBound(line_) + 1 < targetLine Then _
    Call raiseError(etLineNotExists, ERR_SOURCE)
  On Error GoTo ErrorHandler
  'メインの処理'
  line_(targetLine - 1) = targetData
  Dim txtStream As Scripting.TextStream
  Set txtStream = fsObj.OpenTextFile( _
                          FileName:=fileFullName, _
                          IOMode:=ForWriting, _
                          Create:=False)
  Dim i As Long
  For i = 0 To UBound(line_)
    Call txtStream.WriteLine(line_(i))
  Next
  Call txtStream.Close
  Set txtStream = Nothing
  Exit Sub
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Sub

'エラー発生用'
Private Sub raiseError(ByVal typeOfError As ErrorTypes, _
              Optional ByVal errorSource As String)
  Dim msg As String
  msg = getErrorMessage(typeOfError)
  Call Err.Raise(Number:=10000 + typeOfError, _
                 Source:=errorSource, _
                 Description:=msg)
End Sub

Private Function getErrorMessage( _
          ByVal typeOfError As ErrorTypes) As String  '……(***)'
  Const ERR_SOURCE As String = _
          "TextFile class, getErrorMessage Method"
  On Error GoTo ErrorHandler
  Dim ret As String
  Select Case typeOfError
    Case etFileNotFound
      ret = "The file you specified isn't found."
    Case etLineNotExists
      ret = "This file doesn't have so many lines."
    Case etNotInitialized
      ret = "You must run ""init"" method!"
    Case etErrorOccurred
      ret = "Some Error has occurred"
  End Select
  getErrorMessage = ret
  Exit Function
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Function

メソッドを一つ追加した(*)ことに伴い、列挙体の要素を増やし(**)、エラーメッセージのパターンも増やした(***)。

setDataメソッド
Public Sub setData(ByVal targetLine As Long, _
                   ByVal targetData As String)  '……(1)'
  Const ERR_SOURCE As String = _
          "TextFile Class, setData Method"
  'initメソッド未実行ならエラー。'  '……(2)'
  If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)
  Dim ret As String
  '存在しない行番号を指定していたらエラー。'  '……(3)'
  If targetLine < 0 Or _
     UBound(line_) + 1 < targetLine Then _
    Call raiseError(etLineNotExists, ERR_SOURCE)
  On Error GoTo ErrorHandler
  'メインの処理'
  line_(targetLine - 1) = targetData  '……(4)'
  Dim txtStream As Scripting.TextStream  '……(5)'
  Set txtStream = fsObj.OpenTextFile( _
                          FileName:=fileFullName, _
                          IOMode:=ForWriting, _
                          Create:=False)
  Dim i As Long  '……(6)'
  For i = 0 To UBound(line_)
    Call txtStream.WriteLine(line_(i))
  Next
  Call txtStream.Close  '……(7)'
  Set txtStream = Nothing
  Exit Sub
ErrorHandler:
  Debug.Print "Number : " & Err.Number
  Debug.Print "Description : " & Err.Description
  Call raiseError(etErrorOccurred, ERR_SOURCE)
End Sub

まず(1)の

Public Sub setData(ByVal targetLine As Long, _
                   ByVal targetData As String) 

で引数の設定。

targetLineで何行目のデータを書き換えるのかを指定し、targetDataで書き換えるデータを指定する。

(2)の2行(実質1行)

If Not isInitialized Then _
    Call raiseError(etNotInitialized, ERR_SOURCE)

はガード節。

擬似コンストラクinitメソッドの末尾に

isInitialized = True

があり、Module LevelのisInitializedTrueにするようにしている。こうすることで擬似コンストラクinitメソッドの実行を強制している。

(3)の3行(実質1行)もガード節。

If targetLine < 1 Or _
   UBound(line_) + 1 < targetLine Then _
  Call raiseError(etLineNotExists, ERR_SOURCE)

このメソッドでは、既にある行のデータを書き換えることを想定しているので、元のテキストファイルの行数を超える行番号は指定できないようにしている。もちろん、0行目とか、負の数行目もあり得ないので、ここで弾く。

不正な引数とか、あり得ない操作については、このようにメソッド冒頭で弾いてしまう、という「ガード節」の考え方が気に入っている。good mannerだと思う。

テキストファイルにデータを追記するために、次はappendDataメソッドを作る必要があるかもしれない。

ここまで来たら、ほぼ安全なので、メインの処理に移る。

(4)の

line_(targetLine - 1) = targetData

で、元のテキストファイルの各行のデータを保持したModule Levelの配列line_()の書き換え対象データを書き換える。

行番号と配列の添字がずれるのはイマイチなので、今にして思えば配列を1はじまりにした方が良かったかもしれない。このTextFileクラスのLineプロパティはItemみたいなものなのだから……。

まあ、「1ずれる」という部分は、クラスモジュール内に隠蔽してしまっているので、利用する側は何も困らないのだけれど。これもオブジェクト指向の強みだね。

次に、(5)からの5行(実質2行)

Dim txtStream As Scripting.TextStream
  Set txtStream = fsObj.OpenTextFile( _
                          FileName:=fileFullName, _
                          IOMode:=ForWriting, _
                          Create:=False)

Scripting.TextStreamオブジェクトを準備する。

OpenTextFileメソッドの引数IOModeForWritingにしているので、書き込みモードだ。

Scripting.TextStreamオブジェクトについては、詳しくはコチラをどうぞ。とにかく、テキストファイルの読み書きを行うときには、TextStreamオブジェクトを通して行う、ぐらいの理解で良いと思う。

これで、TextStreamクラスのインスタンスtxtStreamが用意できたので、コイツを通じてテキストファイルを操作することができる。

あとは、(6)からの4行

Dim i As Long
For i = 0 To UBound(line_)
  Call txtStream.WriteLine(line_(i))
Next

で配列line_()の各要素を1行目から最終行まで書き込む。

変更していない行まで書き込みし直すのは無駄な気がするけれど、仕方ない。

指定した行だけを書き換える方法ってあるのかな??? あったら誰か教えてくだされ。

最後に(7)の

Call txtStream.Close

txtStream(テキストファイルとの接続?)を閉じておしまい。

実験

このプロジェクトと同じフォルダ内に、test.txtというテキストファイルを置き、内容を

f:id:akashi_keirin:20190717083008j:plain

にしておく。相変わらずアホな内容ですまん。

次のコードを標準モジュールに書く。

スト2 標準モジュール
Private Sub testTextFileClass()
  Dim targetPath As String
  targetPath = ThisDocument.Path & "\" & "test.txt"  '"
  Dim txtFile As TextFile
  Set txtFile = New TextFile
  Call txtFile.init(targetPath)
  Dim ar() As String
  Dim i As Long  '……(1)'
  For i = 1 To txtFile.LineCount
    Debug.Print txtFile.Line(i)
    Call WindowsAPI.waitFor(300)
  Next
  Debug.Print "3行目を書き換えるよ。"  '……(2)'
  Call txtFile.setData(3, "ち~んw")
  Call WindowsAPI.waitFor(500)
  For i = 1 To txtFile.LineCount  '……(3)'
    Debug.Print txtFile.Line(i)
    Call WindowsAPI.waitFor(300)
  Next
End Sub

(1)の

Dim i As Long  '……(1)'
For i = 1 To txtFile.LineCount
  Debug.Print txtFile.Line(i)
  Call WindowsAPI.waitFor(300)
Next

では、まずは普通にtxtFileインスタンスに蓄えられた元のテキストファイル(test.txt)の各行のデータ(笑)を吐き出している。

ループ1回ごとに自作WindowsAPIクラスのwaitForメソッドでポーズを入れている((2)も(3)も同様。)。

WindowsAPIクラスについては、

akashi-keirin.hatenablog.com

コチラをどうぞ。

(2)の

Debug.Print "3行目を書き換えるよ。"
Call txtFile.setData(3, "ち~んw")

では、イミディエイトに書き換え宣言を表示させた後、

Call txtFile.setData(3, "ち~んw")

で新作のsetDataメソッドを実行。引数に「3」と「"ち~んw"」を渡しているので、test.txtの「3」行目を「ち~んw」というデータ(笑)に書き換えることになる。

あとは、(3)の

For i = 1 To txtFile.LineCount
  Debug.Print txtFile.Line(i)
  Call WindowsAPI.waitFor(300)
Next

で、新生test.txtのデータ(笑)を全てゆっくり吐き出す。

スト2を実行すると……

f:id:akashi_keirin:20190717083027g:plain

このとおり。

test.txt

f:id:akashi_keirin:20190717083010j:plain

こうなっている。

おわりに

異様に長い割に、読んでもあまりためにならない記事になってしまい、深く反省している……。