Text.Parsecで固定長データをパースする(2)

一昨日の記事で、パーサの型も(一応)解った。なので、今度は Text.Parsecで固定長データをパースする で省いていたヘッダのパースをすることにする。さらに、パースしたデータをCSV形式のデータに変換する。

まずは、対象の地震動波形のファイルを再掲しておこう。

Example wave 1
NUMBER OF DATA=12001, DT=0.010, MAX.=-779.834268(TIME= 22.520), FORMAT=10F8.2
   -0.05   -0.05   -0.05   -0.05   -0.06   -0.06   -0.06   -0.06   -0.06   -0.07
   -0.07   -0.07   -0.07   -0.07   -0.08   -0.08   -0.08   -0.09   -0.09   -0.09
   -0.10   -0.10   -0.10   -0.11   -0.11   -0.12   -0.13   -0.13   -0.14   -0.15
   -0.16   -0.17   -0.18   -0.20   -0.22   -0.24   -0.27   -0.29   -0.33   -0.37
   -0.41   -0.45   -0.50   -0.56   -0.61   -0.68   -0.74   -0.82   -0.89   -0.97
   -1.06   -1.15   -1.25   -1.35   -1.45   -1.56   -1.68   -1.79   -1.92   -2.05
   -2.18   -2.32   -2.46   -2.61   -2.76   -2.91   -3.07   -3.23   -3.39   -3.55
   -3.72   -3.89   -4.06   -4.24   -4.42   -4.60   -4.78   -4.96   -5.14   -5.31
(以下略)

ヘッダは2行。1行目は地震動の名前、2行目はいくつかのパラメータで、ここで重要なのは「DT=0.010」の部分。DT は時刻刻み―つまりサンプリングレートで、0.01秒ごとにデータを記録していることを意味している。ほかのパラメータは今回は無視。

で、手始めに地震動を表すデータ型 Wave を作った。

data Wave = Wave { wvName :: String
                 , wvDT :: Double
                 , wvData :: [Double]
                 } deriving Show

それから、ヘッダを含む地震動ファイル全体をパースする関数 waveFile

waveFile :: Parser Wave
waveFile = do
  n <- waveName
  dt <- waveDT
  w <- wave
  eof
  return Wave { wvName = n, wvDT = dt, wvData = w }

waveFileWave を返すので、型は Parser Wave だ。

あとは、コード全体を示そう。

module Main where

import System.Environment ( getArgs )
import Text.Parsec
import Text.Parsec.String
import Data.List
import Text.Printf

--------------------------------------------------------------------------------

data Wave = Wave { wvName :: String
                 , wvDT :: Double
                 , wvData :: [Double]
                 } deriving Show

--------------------------------------------------------------------------------

main :: IO ()
main = do
  args <- getArgs
  cs <- readFile (head args)
  let wv = parseWave cs
  either (\ e -> print e) (\ w -> putStr $ formatWave w) wv

--------------------------------------------------------------------------------

formatWave :: Wave -> String
formatWave w = unlines $ header : wv
  where
    header = "," ++ (wvName w)
    wv = map (\ (t, d) -> printf "%.2f,%.2f" t d) $ zip tm (wvData w)
    tm = 0.0 : unfoldr (\ x -> Just (x + dt, x + dt)) 0.0
    dt = wvDT w

--------------------------------------------------------------------------------

-- Parsers

waveFile :: Parser Wave
waveFile = do
  n <- waveName
  dt <- waveDT
  w <- wave
  eof
  return Wave { wvName = n, wvDT = dt, wvData = w }

line :: Parser String
line = do
  l <- many1 (noneOf "\n")
  newline
  return l

waveName :: Parser String
waveName = line

waveDT :: Parser Double
waveDT = do
  string "NUMBER OF DATA="
  many1 digit
  string ", DT="
  dt <- many1 dataChar
  many1 (noneOf "\n")
  newline
  return $ read dt

wave :: Parser [Double]
wave = do
  ls <- many1
  waveLine
  return $ concat ls

waveLine :: Parser [Double]
waveLine = do
  ws <- many1
  waveData
  newline
  return ws

waveData :: Parser Double
waveData = do
  d <- count 8 dataChar
  return $ read d

dataChar :: Parser Char
dataChar = digit <|> oneOf "-. "

--------------------------------------------------------------------------------

parseWave :: String -> Either ParseError Wave
parseWave input = parse waveFile "(unknown)" input

--------------------------------------------------------------------------------

これで出来たはずだ。

実行例:

takatoh@apostrophe $ runhaskell parsewave4.hs data1.dat > result.csv
takatoh@apostrophe $ head result.csv
,Example wave 1
0.00,-0.05
0.01,-0.05
0.02,-0.05
0.03,-0.05
0.04,-0.06
0.05,-0.06
0.06,-0.06
0.07,-0.06
0.08,-0.06

OK。うまくいった。