めのん@ひとりプログラミング同好会

このブログはすべてフィクションであり、実在の人物、団体とは一切関係ありません

C++の練習を兼ねてCSVパーサーを書いてみました!

こんにちは、めのんです!

平日はZigの話題が中心で、週末は主にOpenSiv3Dの話題だったんですけど、今回はちょっと違うことに挑戦してみました。

Twitterでほえほえ@LWPさんが「(CSVを)ベタなコードで解析してほしい」とおっしゃっていましたので、さっそく私も挑戦してみました。 使ったプログラミング言語C++です。 Zigはちょっとまだ無理です💦

まずはCSVの規格の確認からですね。 これはRFC 4180が相当すると思います。

tools.ietf.org

RFC 4180そのままではASCIIしか対応できないと思いましたので、Excelで作成したCSVを読めるように拡張しました。 というか判定がザルなだけなんですけどね。

あと、Excel for Macが出力したCSVは改行文字がCRだということですので、そちらにも対応したつもりです。

では、私の書いたコードを貼ることにします。

#include "menonfled/csv.hpp"
#include <utility>
#include <stdexcept>

namespace menonfled
{
  namespace
  {
    /// TEXTDATAの読み込み
    /// @param[in]  is  入力ストリーム
    /// @return TEXTDATAであればその文字を、それ以外であればナル文字を返す。
    char get_textdata(std::istream& is)
    {
      char c;

      if (is.get(c))
      {
        if (c < 0x20 || c == 0x7f || c == ',' || c == '"')
          is.unget();
        else
          return c;
      }
      return '\0';
    }

    /// non-escapedの読み込み
    /// @param[in]  is  入力ストリーム
    /// @param[in]  c   先頭の1文字
    /// @return 読み込んだフィールドを返す。
    std::string get_nonescaped(std::istream& is, char c)
    {
      std::string str;

      do
      {
        str.push_back(c);
        c = get_textdata(is);
      } while (c);
      return std::move(str);
    }

    /// escapedの読み込み
    /// @param[in]  is  入力ストリーム
    /// @return 読み込んだフィールドを返す。
    /// 先頭の二重引用符はスキップ済み
    std::string get_escaped(std::istream& is)
    {
      std::string str;
      char c;

      while (is.get(c))
      {
        if (c == '"')
        {
          char c2;
          if (!is.get(c2) || c2 != '"')
            break;
        }
        str.push_back(c);
      }
      return std::move(str);
    }

    /// fieldの読み込み
    /// @param[in]  is    入力ストリーム
    /// @param[out] field 入力したフィールドの格納先
    /// @return EOFに達した場合はfalseを、それ以外はtrueを返す。
    bool get_field(std::istream& is, std::string& field)
    {
      std::string str;
      char c;
      bool escaped = false;

      if (!is.get(c))
        return false;

      switch (c)
      {
      case '\r':
      case '\n':
      case ',':
        is.unget();
        field.clear();
        break;
      case '"':
        field = get_escaped(is);
        break;
      default:
        field = get_nonescaped(is, c);
      }
      return true;
    }
  }

  /// CSVの1レコードを読み込む
  /// @param[in]  is    入力ストリーム
  /// @return 読み込んだレコードを文字列の列として返す。
  std::vector<std::string> get_csv_record(std::istream& is)
  {
    std::vector<std::string> record;

    while (is)
    {
      std::string field;
      char c;

      if (!get_field(is, field))
        break;
      record.push_back(field);

      if (!is.get(c))
        break;
      if (c == '\r')
      {
        if (is.get(c) && c != '\n')
          is.unget();
        break;
      }
      if (c == '\n')
        break;
    }
    if (record.size() == 1 && record.front().empty())
      record.clear();
    return std::move(record);
  }
}

いろいろ試行錯誤しながら書いたのでキレイなコードとはいいにくいのですが、恥を忍んで貼ってみました。 こういうのもたまにはいいですね!

それでは!!