【Lua】Table をダンプするモジュールの解説

# はじめに
<!--この記事は、私が書いた Lua のモジュールを紹介するページの**サブページ**です。
メインの記事(執筆中)[Lyrica-Lua-Modules]では、他にも再利用できそうなモジュールを紹介しているので、よければ読んでみてください。-->

この記事では、Table をダンプする自作モジュールの、使い方とコードの解説します。

またこの記事は、[Minecraft][Minecraft] の Mod である [OpenComputers][OC-CurseForge](以降 OC)について解説する記事の一貫として作ったものです(執筆中)。
<!--私が OC について書いた記事をまとめた、[ルート記事][Lyrica-OC-Root]も作ったので、そちらもよければ読んでみてください。-->

:::note alert

Lua 初心者が作ったモジュールなので、間違いや非効率なことをしている可能性が高いです(OC で初めて Lua に触りました)

:::

### ソースコード
ここで紹介しているプログラムは、私の GitHub で公開しています。
MIT ライセンスにしてあるので、基本的に自由に使っていただいて大丈夫です。
[ここで紹介しているモジュール][Lyrica-GitHub-common](が入っているディレクトリ)
[OC 用も含めたトップページ][Lyrica-GitHub-all](OC 用はまだ何も無いです)

### 解説文中の 変数 / 関数 の型表記
解説文中の、変数や関数の型表記方法は次のように書きます。
- 変数: `変数名:型`
- 関数: `関数名(引数名:型[, 任意引数名:型 ...]):戻り値1の型, 戻り値2の型 or nil ...`

また、ソースコード中の説明文も同様になっています。

# 0. 目次
1. [モジュールの概要](#1-モジュールの概要)
2. [コードの解説](#2-コードの解説)
3. [通常版の追加機能](#3-通常版の追加機能)
  3.1 [追加機能概要](#追加機能概要)
  3.2 [詳しい解説](#詳しい解説)
  3.3 [オプションの初期値](#オプションの初期値)
  3.4 [(おまけ)関数を指定するオプションの設定例](#おまけ関数を指定するオプションの設定例)
4. [おわりに](#4-おわりに)

# 1. モジュールの概要
<!--念の為概要をもう一度記載しておきます。
※ この項目の内容はメインの記事(執筆中)[Lyrica-Lua-Modules]に記載している情報と全く同じです。-->

テーブルを `tostring()` すると `table: 01234567abc` のようになってしまうので、中身を文字列にする(ダンプする)モジュールを作りました。
循環参照でも大丈夫です。
モジュール名: `table_dumper` (通常版) / `tdumper_mini` (簡易版)
使い方:

```lua
local tdumper = require("table_dumper") -- 通常版
local td_mini = require("tdumper_mini") -- 簡易版

local tbl = { ... } -- テーブルの中身は省略

print(tdumper:dump(tbl, "tbl_name")) -- 通常版は :dump()
print(td_mini.dump(tbl, "namaemae")) -- 簡易版は .dump()
```

※必須モジュール
`table_dumper` (通常版)では私の作った以下のモジューを使用しています(全て公開されています)。
- `lazy_importer`: モジュールの遅延読み込みをサポートするユーティリティ
- `string_builder`: 文字列を組み立てやすくするヘルパーモジュール
- `simple_logger`: ロガー未設定時に使われる `print()` するだけのロガー

`tdumper_mini` (簡易版)では他のモジュールを使用していません。

<details><summary><b>ダンプした時の表示例</b></summary>

次のコードを実行すると、`表示例` のようにダンプされます。
※ `table_dumper` (通常版) を使用しています

```lua: ダンプする.lua
----------------------------
-- ダンプするテーブルを定義
----------------------------
local tbl = {
  [123] = 123,
  ["456"] = "456",
  multi_line = [[
1行目
2行目
3行目]],
  child1 = {
    func = function() end
  },
}
-- tbl.child1.inner に循環参照を追加
tbl.child1.inner = tbl.child1
-- tbl.child2 に共有参照を追加
tbl.child2 = tbl.child1
-- tbl に __tostring を設定
setmetatable(tbl, {
  __tostring = function(_)
    return "tbl の説明\nです！"
  end,
})

------------------
-- ダンプする部分
------------------
-- モジュールを読み込み
local tdumper = require("table_dumper") -- 通常版を使用
-- テーブルをダンプして表示
print(tdumper:dump(tbl, "table")) -- 第2引数は表示上のテーブル名
```

```text: 表示例
table = {
  123 = 123,
  "456" = "456",
  "child1" = {
    "func" = function: 0000000000123abc,
    "inner" = {
      * 既に表示済み(循環参照) -> table."child1"
    },
  },
  "child2" = {
    * 既に表示済み(共有参照) -> table."child1"
  },
  "multi_line" = "1行目
                  2行目
                  3行目",
  <tostring()> = "tbl の説明
                  です！"
}

```

※ 数字と文字列は、"" がついているかどうかで判別できます。

***

</details>

<details><summary>(OC 用の情報)</summary>

OC 環境で動かす場合の OC コンピュータの必要スペックは以下の通りです。
※ 検証バージョン -> GTNH: v2.7.4 / OC: v1.10.30-GTNH

\--------------
**`table_dumper` (通常版)**
- Graphics Card: Tier 1
- CPU: Tier 1
- **Memory: Tier1 * 2 または Tier1.5 * 1**
- HDD: Tier 1

\--------------
**`tdumper_mini` (簡易版)**
- Graphics Card: Tier 1
- CPU: Tier 1
- **Memory: Tier1 * 1**
- HDD: Tier 1

</details>

# 2. コードの解説

`table_dumper` モジュールは色々な機能を詰め込み過ぎて読みにくくなってしまったので、軽量 & 簡易版の `tdumper_mini` も作りました。
この項目では簡易版でコードの解説をして、[次の項目](#3-通常版の追加機能)で通常版に追加される機能の紹介をします。
全体のソースコードを見たい方は、[ソースコード](#ソースコード)のリンクから見てください。

※ この解説はかなり冗長です。大したことはしていないので、慣れている方はコードだけ読めば分かると思います(コメントもたくさん付けておきました)

***

まず再帰呼び出しの前までの、コードの前半部分を解説します。

```lua: tdumper_mini.lua
-- 結果を返すための補助関数
-- テーブル t1 に テーブル t2 の要素を追加する
local table_concat_array = function(t1, t2)
  for i = 1, #t2 do table.insert(t1, t2[i]) end
end

-- 本体
local TDumperMini
TDumperMini = {
  -- テーブルを表示する際の、1階層ごとにつけるインデント
  indent_unit = "  ",
  
  -- テーブルの中身を再帰的に表示する(循環参照も OK)
  -- tbl:table > 中身を表示したいテーブル
  -- [tbl_name:string] > テーブル名(省略可)
  -- :string > テーブルの中身を表した文字列(戻り値)
  dump = function(tbl, tbl_name)
    local result = {} -- 出力を溜める配列
    
    -- tbl が table型では無かった時は、エラーメッセージを表示する
    if type(tbl) ~= "table" then
      table.insert(result, "関数: TDumperMini.dump() でエラーが発生しました")
      table.insert(result, "引数: tbl(" .. tostring(tbl_name) .. ") は table型が必要です(受け取った型: " .. type(tbl) .. ")")
      return table.concat(result, "\n")
    end
    
    -- トップレベルの表記
    -- tbl_name が nil か空文字の時はテーブル名を "<top_table>" にする
    if not tbl_name or tbl_name == "" then
      tbl_name = "<top_table>"
    end
    tbl_name = tostring(tbl_name) -- tbl_name が string でなくても安全なように
    TDumperMini.indent_unit = tostring(TDumperMini.indent_unit) -- indent_unit も同様
    
    -- 出力
    table.insert(result, tbl_name .. " = {")
    -- 実際にテーブルをダンプする処理(再帰関数)
    table_concat_array(result, TDumperMini._inner_dump(tbl, tbl_name, TDumperMini.indent_unit))
    table.insert(result, "}") -- 最後にテーブルを閉じる
    
    return table.concat(result, "\n")
  end,
  
  (省略)
}
```

最初に `local TDumperMini` とだけ定義してあるのは、関数内で `TDumperMini` の要素を使いたい時に、先に定義していないとエラーになるからです。

<details><summary>おまけ(エラーになる理由)</summary>

<無駄話>

私は Lua にあまり詳しくないので間違っているかもしれませんが、エラーになる原因は以下の通りだと思います(読み飛ばして OK)
1. `local a = "b"` とした時は、まず `"b"` が初期化されてから `a` が初期化され、 `a` に `"b"` が代入される
2. `local tbl = { ... }` とした時も同様に、`{ ... }` が初期化されてから `tbl` に代入される
3. `{ ... }` の中に `func = function()` が定義されていたら、この初期化の段階で関数が作成 & 初期化される
4. 関数内で `tbl.xxx` と書かれていると、まだ `tbl` は代入されていないので、**グローバル変数の** `tbl` を参照する
5. その後 `tbl` に `{ ... }` が代入されるが、関数内の参照はグローバル変数のまま
  (「`a = 1` `b = a` の後に `a = 2` としても、`b` は `1` のまま」※という現象に似ている)
6. よって、関数実行時に `nil.xxx` にアクセスしようとして、エラーになる

※ この「**定義時の変数を参照する**」という性質を利用して、`関数A`  の中で `関数B` を作って返すと、本来は `関数A` の中でしか使えないローカル変数を、`関数B` がずっと保持できる
(この仕組みをクロージャという)

\</無駄話>

</details>

\---------------------------------
`TDumperMini.indent_unit` は、テーブルの子要素を表示する時に、1階層ごとにつけるインデントを文字列で指定します。
空白以外も設定できるので、`"@@"` を設定すればインデントを目立たせることもできます(見にくい)。

\---------------------------------
`TDumperMini.dump(tbl:table[, tbl_name:string]):string`
ダンプする時に使う関数です。
第一引数にダンプしたいテーブルを、第二引数に最初のテーブル名として表示する名前を渡します。
第二引数を省略した場合は、`"<top_table>"` になります。

:::note warn

`table_dumper` では `tdumper:dump()` と `:` を使って呼び出すのに対し、`tdumper_mini` では `tdumper.dump()` と `.` で呼び出すので注意

:::

\---------------------------------
`local result = {}`
この変数に、出力する文字列を `table.insert()` で追加し、最後に `table.concat()` で連結して戻り値にしています。

\---------------------------------
引数 `tbl` の型チェックや、`tbl_name` の初期値を設定した後、
**`table_concat_array(result, TDumperMini._inner_dump(...))`**
としているところが、実際にテーブルをダンプしている部分です。
まず前半の `table_concat_array()` は最初に定義してありますが、第一引数のテーブルに第二引数のテーブルの要素を追加する関数です。
つまり `result` に `_inner_dump()` で返ってきたテーブルの要素を追加しています。

***

`TDumperMini._inner_dump()` は次のコードになります。

```lua: tdumper_mini.lua
TDumperMini = {
  -- 実際にテーブルをダンプする関数(再帰関数)
  -- tbl:table > ダンプするテーブル
  -- key_path:string > 親から辿ってきたキー名を、全て "." で繋いだ文字列
  -- indent:string > 現在のインデント
  -- [visited:table] > 既にダンプしたテーブルの一覧(循環参照検出時に使う)(省略可)
  -- :table > テーブルをダンプした文字列の配列(戻り値)
  _inner_dump = function(tbl, key_path, indent, visited)
    visited = visited or {}
    local result = {} -- 出力を溜める配列
    
    -- 循環参照検出時はその先の検索を中止
    if visited[tbl] then
      table.insert(result, indent .. "* 既に表示済み -> " .. visited[tbl])
      return result
    end
    -- tbl をダンプ済みテーブルとしてマークする
    visited[tbl] = key_path
    
    -- テーブルに含まれる要素をチェックし、値の型によって動作を分ける
    for k, v in pairs(tbl) do
      local key_str = tostring(k) -- キー名を結合できるように string にする
      -- キーが string の時は "" で囲む
      if type(k) == "string" then
        key_str = "\"" .. key_str .. "\""
      end
      
      local value_type = type(v)
      if value_type == "table" then
        -- 値がテーブルの時は、再帰的に子要素を検索
        table.insert(result, indent .. key_str .. " = {")
        -- 再帰的呼び出し
        local new_key_path = key_path .. "." .. key_str
        local new_indent = indent .. TDumperMini.indent_unit
        table_concat_array(result, TDumperMini._inner_dump(v, new_key_path, new_indent, visited))
        table.insert(result, indent .. "},")
      elseif value_type == "string" then
        -- 値が string の時は "" で囲んで出力
        table.insert(result, indent .. key_str .. " = \"" .. v .. "\",")
      else
        -- 値がそれ以外の時は、そのまま string に変換して出力
        table.insert(result, indent .. key_str .. " = " .. tostring(v) .. ",")
      end
    end
    
    -- 最後に __tostring があるものは、それを追記する
    local mt = getmetatable(tbl)
    if mt and type(mt.__tostring) == "function" then -- メタテーブルに __tostring が設定されている場合
      -- 出力
      table.insert(result, indent .. "<tostring()> = \"" .. tostring(tbl) .. "\"")
    end
    
    return result
  end,
}
```

まず関数の定義は、
- `_inner_dump(tbl:table, key_path:string, indent:string, visited:table):table`

です。それぞれの引数は、
- `tbl`: ダンプする対象のテーブル
- `key_path`: 親から辿ってきたキー名を、全て `"."` で繋いだ文字列
- `indent`: 現在のインデントの文字列(階層が深くなるとインデントが増えます)
- `visited`: キーが既にダンプした(ダンプ中も含めて)テーブルで、値がそのテーブルの `key_path` となっているテーブル(循環参照検出に使います)

この関数を再帰的に呼び出して、テーブルをダンプしていきます。
最初に `dump()` から呼び出される時の引数は、
- `tbl`: dump() に渡されたテーブル
- `key_path`: dump() に渡されたテーブル名(または`"<top_table>"`)
- `indent`: `TDumperMini.indent_unit` (最初の子要素はインデント1つ分)
- `visited`: 省略( `nil` )

***

関数の中身を順番に解説します。

循環参照を検出する部分
```lua
-- 循環参照検出時はその先の検索を中止
if visited[tbl] then
  table.insert(result, indent .. "* 既に表示済み -> " .. visited[tbl])
  return result
end
```
`visited[tbl]` が文字列(`nil` ではない)の場合は既にダンプ済みのテーブルなので、メッセージを残してこのテーブルのダンプを終了します。
`tdumper_mini` では、循環参照と共有参照の判別ができないので、`"* 既に表示済み"` とだけ書きます。
`visited[tbl]` はそのテーブルをダンプした時の `key_path` が入っているので、どのテーブルへの循環参照があったのかが分かります。

\---------------------------------
循環参照の検出が終わったら、確実にダンプを実行するので、`visited[tbl] = key_path` を先に書いておきます。
実際にダンプする前に書くことで、子要素を検索中に自分を参照するテーブルがあった場合でも、循環参照を検出できます(というか循環参照の場合は必ずこのパターン)。

\---------------------------------
その後は `for` 文でテーブルの要素を順番にダンプしていきます。
要素の値の型によってダンプする処理が違いますが、テーブル以外については文字列を連結して `result` に追加してるだけなので、すぐ分かると思います。
※ `tdumper_mini` では、改行を含む文字列に、改行後にインデントを挿入していませんが、文字列をコピーしたい時はこちらの方が便利です

テーブルの場合は、
`table_concat_array(result, TDumperMini._inner_dump(...))`
で再帰的に `_inner_dump()` を呼び出して、子テーブルを全てダンプしていきます。
`_inner_dump()` に渡す引数は以下の通りです。
- `tbl`: 子テーブル
- `key_path`: 自分の `key_path` に、`"."` と子テーブルのキー名を連結した文字列
- `indent`: 現在のインデントに `TDumperMini.indent_unit` を連結した文字列(インデントを 1つ増やす)
- `visited`: 自分の `key_path` を登録済みの、自分の `visited` (コピーなどはしないので、全員が同じテーブルを参照することになります)

これで子テーブルにも同じ処理が繰り返され、戻ってきた値を `result` に追加します。

\---------------------------------
最後に、メタテーブルに `__tostring` が設定されている場合は、有益な情報が入っているかもしれないので、これも一緒に表示します。
メタテーブルに `__tostring` が設定されているかどうかを判別するには、
`mt = getmetatable(tbl)` でメタテーブルを取得した後、`mt.__tostring` の型が `"function"` かどうかで判定できます(`mt` の `nil` チェックも忘れずに)。

\---------------------------------
`_inner_dump()` の最後で、要素をダンプした文字列を順番に追加した配列 `result` を返します。
返された値は、`table_concat_array()` で親の要素が入った、親の `result` に追加され、その後親のダンプ処理が再開します。

\---------------------------------
そして、`_inner_dump()` の処理が全て終わると、呼び出し元の `dump()` に戻り、最後の
`return table.concat(result, "\n")` で `result` の要素を最初から順番に、文字列の間を `"\n"` で繋いで 1つの文字列にします。
それが `dump()` の戻り値になります。

## 3. 通常版の追加機能
※ 以下の定義をしたものとして解説します

```lua
local tdumper = require("table_dumper")
local td_mini = require("tdumper_mini")
```

### 追加機能概要
`table_dumper` (通常版) は `tdumper_mini` (簡易版) と比べて、多数のオプションで動作を切り替えられます。

また、オプション以外の違いとしては、以下の機能があります。
- 循環参照と共有参照を判別して出力する
  ( `tdumper_mini` では「* 既に表示済み -> <キーのパス>」とだけ表示されます)
- `print(tdumper)` とすると、モジュールで使用できるオプションと関数の説明を表示する
- ロガーを使ってエラー出力、デバッグ出力をする
- `tdumper.new()` を使用してオブジェクトを作り、複数の設定を使い分けられる
    - `tdumper.new()` をしないで使うこともできます

:::note warn

`table_dumper` では `tdumper:dump()` と `:` を使って呼び出すのに対し、`tdumper_mini` では `td_mini.dump()` と `.` で呼び出すので注意

:::

設定できるオプションと簡単な説明は以下の通りです。
- `logger:table`: エラー & デバッグログを出力するロガー。初期値は `print()` するだけのロガー
- `verbose_level:number`: デバッグログの出力レベル。0 でログなし、1 で通常、2 で詳細なログを出力
- `indent_unit:string`: 子テーブル 1階層ごとに付けるインデント。`tdumper_mini` にも同じオプションがあります
- `insert_indent:boolean`: 改行を含む文字列を表示する時に、改行後にインデントを挿入するか
  ( `tdumper_mini` ではインデントは挿入されません)
- `insert_indent_tostring:boolean or 0`: `tostring()` を表示する時に改行を含む場合、改行後にインデントを挿入するか
  ( `tdumper_mini` ではインデントは挿入されません)
- `max_depth:number`: ネストした子テーブルを表示する時の最大深度
- `max_items_per_table:number`: テーブルの要素を表示する時の最大要素数
- `show_tostring:boolean`: テーブルの `tostring()` の値を表示するか
  ( `tdumper_mini` では常に表示されます)
- `show_metatable:boolean`: テーブルの各メタテーブルが設定済みかどうかだけ表示するフラグ
- `ignore_key_types:table or nil`: 表示をスキップする**キー**の型リスト。`{ function = true }` の様に書く
- `ignore_value_types:table or nil`: 表示をスキップする**値**の型リスト。`{ function = true }` の様に書く
- `filter:function or nil`: 表示する要素のみ `true` を返す関数
- `post_filter:function or nil`: 表示する要素のみ `true` を返す関数(ダンプ後出力前に呼ばれます)
  ※ `dump()` の最後にも呼ばれます
- `comparator:function or nil`: テーブルとメタテーブルを**キー**でソートする時に用いる関数
  ( `tdumper_mini` ではソートしません)
- `value_comparator:function or nil`: テーブルを**値**でソートする時に用いる関数
  ( `tdumper_mini` ではソートしません)
- `top_table_name:string`: テーブル名を省略した場合に、代わりに表示される名前(循環参照検出時のみ)
- `strict_mode:boolean`: `true` にすると、エラーログを出力する代わりにエラーを投げる
- `on_value_dumped:function or nil`: 要素がダンプされる直前に呼ばれるフック関数
  ※ `dump()` の最後にも呼ばれます
- `key_formatter:function or nil`: **キー**の表示を変更する関数
  ※ `dump()` に渡されたテーブル名に対しては**呼ばれません**
- `value_formatter:function or nil`: **値**の表示を変更する関数
  ※ `dump()` に渡されたテーブルをダンプした結果に対しても**呼ばれます**

### 詳しい解説
分かりにくそうなものだけ解説します。

まずはオプション以外のものについて。
- ロガーは `logger.error(meg:string)` `logger.debug(msg:string)` でログ出力できるものを想定しています
    - お使いのロガーと関数名などが合わない場合は、ラッパーを作るなどして対応してください
- 最初に `tdumper.<オプション>` にアクセスすると、自動的に `default_dumper` に `new()` が代入され、以降 `tdumper` 経由でアクセスできるようになります
  ※ いきなり `tdumper:dump()` としても大丈夫です
- `tdumper.new()` をすると、「その時点でのデフォルト値(`tdumper.<オプション>` の値)がオプションに設定されたオブジェクト」が戻り値になります
    - その後に `tdumper` の値を変更しても、それ以前に `new()` されたオブジェクトには影響がありません
    - ただし `default_dumper` だけは `tdumper` の変更が反映されます(というか `tdumper` の実体です)
    - `new()` の引数には、`logger` と `verbose_level` を指定できますが、省略可能です

次にオプションについて。
- `logger` に `nil` が設定されていると、`dump()` 実行時にオブジェクトの `logger` にデフォルトロガーが代入されます
- `insert_indent_tostring` に `0` が設定されていると、`dump()` 実行時の `insert_indent` の値を代わりに使用します(上書きはしません)
- `max_depth` や `max_items_per_table` に `-1` が設定されていると、無制限に表示します
- `show_metatable` が `true` の場合、以下のように表示されます(メタテーブルが何も設定されていない場合は、何も表示されません)
    ```
    table = {
      <metatable> = {
        __index : 設定済み
        __tostring : 設定済み
      }
    }
    ```
    ※ `<tostring()> = ""` の後と `<metatable> = {}` の後に `","` が無いのは仕様です(データではないため)
- `filter` には、`function(key:全ての型, value:全ての型):boolean` の形式の関数を設定します
    - `true` が返された要素のみ表示します
    - `nil` を設定した場合はフィルタリングをスキップします(全て表示)
- `post_filter` には `function(key:全ての型, value:全ての型, key_str:string, value_str:string):boolean` の形式の関数を設定します
    - 要素のダンプが終わった後、最終的に出力に含めるかを決定します
    - `key` と `value` は生のキーと値、`key_str` と `value_str` は表示予定のキーと値の文字列が渡されます
    - `true` が返された要素のみ表示します
    - `nil` を設定した場合はフィルタリングをスキップします(全て表示)
- `comparator` はテーブルとメタテーブルを**キー**でソートする時に用いる関数です
    - `function(key_a:全ての型, key_b:全ての型):boolean` の形式で `key_a` < `key_b` の時に `true` を返す関数です
    - 値でソートされなかった時のみ使用されます
    - `nil` を設定すると、ソートをスキップします(毎回ランダムな並び順になります)
- `value_comparator` はテーブルを**値**でソートする時に用いる関数です
    - `function(value_a:全ての型, value_b:全ての型):boolean or nil` の形式で `value_a` < `value_b` の時に `true` を返す関数です
    - キーのソートより優先されます
    - `nil` を返すとソートをキーに任せます
    - 関数の代わりに `nil` を設定すると、ソートをスキップします(毎回ランダムな並び順になります)
- `top_table_name` の値は循環参照時の `" -> <top_table_name>.child.･･･"` と表示する時に使用されます
    - `on_value_dumped` オプションに設定する関数にも、キーのリストとして `top_table_name` の値が渡されます
    - `table_dumper` と `tdumper_mini` で、テーブル名を省略した時の表示は以下の通りです
        
        ```: table_dumper
        {
          * 既に表示済み(循環参照) -> <top_table>
        }
        ```
        
        ```: tdumper_mini
        <top_table> = {
          * 既に表示済み -> <top_table>
        }
        ```
- `on_value_dumped` には、`function(key:全ての型, value:全ての型, key_str:string, value_str:string, key_array:table)` の形式の関数を設定します
    - `key` と `value` は生のキーと値、`key_str` と `value_str` は実際に出力するキーと値の文字列、`key_array` は親から辿ってきたキー名が親から順に入った配列が渡されます
    - ここで値を変更しても、出力には影響しません
- `key_formatter` と `value_formatter` には `function(key_or_value:全ての型, key_or_value_str:string):string` の形式の関数を設定します
    - `key_or_value` は生のキーまたは値、`key_or_value_str` は表示予定のキーまたは値の文字列が渡されます
    - 戻り値で返した文字列が実際に出力されます
    - `key_or_value_str` をそのまま返せば、変更せずに出力できます
    - 関数の代わりに `nil` を設定すると、変更をスキップします

### オプションの初期値
初期状態で各オプションに設定されている値は、次の通りです
- `logger:table`: `nil` (実行時にデフォルトロガーが代入されます)
- `verbose_level:number`: `0`
- `indent_unit:string`: `"  "`
- `insert_indent:boolean`: `true`
- `insert_indent_tostring:boolean or 0`: `0` (実行時の `insert_indent` の値を参照します)
- `max_depth:number`: `-1`
- `max_items_per_table:number`: `-1`
- `show_tostring:boolean`: `true`
- `show_metatable:boolean`: `false`
- `ignore_key_types:table or nil`: `{}`
- `ignore_value_types:table or nil`: `{}`
- `filter:function or nil`: `nil`
- `post_filter:function or nil`: `nil`
- `comparator:function or nil`: ソースコードの上の方に定義してあるコンパレータ
- `value_comparator:function or nil`: `nil`
- `top_table_name:string`: `"<top_table>"`
- `strict_mode:boolean`: `false`
- `on_value_dumped:function or nil`: `nil`
- `key_formatter:function or nil`: `nil`
- `value_formatter:function or nil`: `nil`

### (おまけ)関数を指定するオプションの設定例
関数を指定するオプションは、どのような関数を設定すればいいかが分かりにくいと思ったので、設定例を載せておきます。

項目内ジャンプ
- [filter & post_filter](#filter--post_filter)
- [comparator](#comparator)
- [value_comparator](#value_comparator)
- [on_value_dumped](#on_value_dumped)
- [key_formatter](#key_formatter)
- [value_formatter](#value_formatter)

#### filter & post_filter
特定の要素のみ表示したい場合は、`filter` と `post_filter` に以下のような関数を設定します。
ここではキー名が `name` または `age` の要素のみ表示するようにします。
ネストしたテーブル内に要素があるかもしれないので、`filter` は table型も OK にします。

まずは `filter` のコード:

```lua: filter.lua
tdumper.filter = function(key, value)
  return key == "name" or
         key == "age" or
         type(value) == "table"
end
```

このままでは、テーブルに `name` や `age` が入っていない場合、空のテーブルが表示されてしまうので、空のテーブルは表示しないように `post_filter` で設定します。

`post_filter` のコード:

```lua: post_filter.lua
tdumper.post_filter = function(key, value, key_str, value_str)
  if type(value) == "table" then
    local _, count = value_str:gsub("\n", "")
    return count > 1 -- 改行が 2回以上なら要素がある
  end
  return true -- 他は全て表示
end
```

これで `name` や `age` が入っているテーブルのみ表示されます。
ちなみに、`dump()` に渡したテーブルの表示がスキップされた場合は、次のように表示されます。
`<テーブル名> -> post_filter によりスキップされました`

#### comparator
付属のコンパレータでは、「数字に変換できるなら数字として判定」→「できなければ文字列で判定」→ ･･･ のように判定しています。
これだと、 `"33. いちごミルク"` と `"4. コーヒー牛乳"` は文字列で判定してしまうので、`"33. いちごミルク"` の方が先に並んでしまいます。
最初が数字から始まっている場合は、その数字で判定する関数を設定してみます。

```lua: comparator.lua
-- デフォルトのコンパレータを保存
local default_comparator = tdumper.comparator

tdumper.comparator = function(key_a, key_b)
  local type_a, type_b = type(key_a), type(key_b)
  if (type_a == "number" or type_a == "string") and
     (type_b == "number" or type_b == "string") then
    -- 数字または文字列同士の比較のみ変更
    local num_a, str_a = tostring(key_a):match("^(%d+)(.*)$") -- 数字とそれ以外を分ける
    local num_b, str_b = tostring(key_b):match("^(%d+)(.*)$")
    num_a, num_b = tonumber(num_a), tonumber(num_b)

    if num_a ~= nil then -- key_a の最初が数字の時
      if num_b ~= nil then -- key_b の最初も数字の時
        if num_a ~= num_b then -- 値が違う時
          return num_a < num_b -- 値の大小で判定
        elseif str_a ~= str_b then
	      return default_comparator(str_a, str_b) -- 同じ時は後ろ部分で判定
	    end
	    return default_comparator(key_a, key_b) -- 後ろ部分も同じ時は元の値で判定
      else
        return true -- key_b の最初が数字で無い時は、key_a を先に表示
      end
    else
      -- key_a の最初が数字で無い時
      if num_b ~= nil then -- key_b の最初が数字の時
        return false -- key_b を先に表示
      end
    end
  end

  return default_comparator(key_a, key_b) -- それ以外はデフォルトコンパレータを使用
end
```


#### value_comparator
値がテーブルの場合に、そのテーブルの `id` の値でソートする関数を設定します。
`id` の値が同じ時(ダメ)はキー順でソートします
`id` が無い時、またはテーブルでない時は、`id` 付きテーブルの後ろにキー順でソートします

設定する関数のコード:

```lua: value_comparator.lua
tdumper.value_comparator = function(value_a, value_b)
  local has_id_a = type(value_a) == "table" and type(value_a.id) == "number"
  local has_id_b = type(value_b) == "table" and type(value_b.id) == "number"
  
  if has_id_a then -- value_a に id がある時
    if has_id_b then -- value_b にも id がある時
      if value_a.id ~= value_b.id then -- id の値が違う時
        return value_a.id < value_b.id -- id の値でソート
      end
      return nil -- 値が同じ時はソートをキーに任せる
    else
      return true -- value_b に id が無い時は value_a を先に表示
    end
  else
    -- value_a に id が無い時
    if has_id_b then -- value_b には id がある時
      return false -- value_b を先に表示
    end
  end
  
  return nil -- どちらにも id が無い時は、ソートをキーに任せる
end
```

#### on_value_dumped
ここで設定する関数は、色付き文字対応のコンソールに出力する時、値の型によって色を分けて出力する関数です。
(ソースコードを書き換えたほうが早いですが、例として載せます)

<details><summary>設定する関数の説明(長いので折りたたみ)</summary>

まず `on_value_dumped` の仕様として、要素にテーブルがあった場合、そのテーブルの要素を全てダンプした後に、このフックが呼ばれます。
そのため、`テーブル内の要素` → `テーブル` の順番でフックが呼ばれるので、単純に `print()` すると以下のようになります。

```
"name" = "Lyrica",
"age" = 12,
author = {
    "name" = "Lyrica",
    "age" = 12,
  },
table = {
  author = {
    "name" = "Lyrica",
    "age" = 12,
  },
}
```

このため、表示するためのデータを別に作り、最後に出力する必要があります。
**(絶対自分でループを回したほうが早い･･･)**
※ このコードでは、\<tostring()> の情報と、\<metatable> の情報は出力されません

また、ここではコンソールに色付き文字を出力する関数が以下であるものとして書きます。
```
console.cprint(msg:string, clr:color) -- 改行あり
console.cwrite(msg:string, clr:color) -- 改行なし
※ color は console.colors[<色名>] に入っているものとします
```

```lua: on_value_dumped.lua
local colored_segments = {} -- 文字列と色のセットを入れる配列(ネストする)

local add_segment -- 上記に単体のデータを追加する関数の宣言
local add_table_segments -- 上記にテーブルのデータを追加する関数の宣言
local print_to_console -- 最後にコンソール出力をする関数の宣言

local type_colors = { -- 型ごとに何色で表示するか
  -- Qiita の表示が崩れるので ["xxx"] の形式にしています
  ["table"] = "WHITE", -- 「{」と「}」の部分のみ
  ["number"] = "PINK",
  ["string"] = "ORANGE",
  ["function"] = "BLUE",
  ["other"] = "RED", -- 上記以外の型
  ["comment"] = "GRAY", -- 「* 既に表示済み」などの文字
  ["symbol"] = "PURPLE", -- 「 = 」と、行頭のスペースと、行末の「,」
  ["key"] = "RAINBOW", -- キー名
}
-- ※ 改行入り文字列に追加されるインデントは string の色になりますが、通常空白に色は付きません
--    気になる方は insert_indent を false にして、自分でインデントを入れてください

-- table_dumper に追加するフック関数
tdumper.on_value_dumped = function(key, value, key_str, value_str, key_array)
  local value_type = type(value) -- 値の型によって色を分ける
  local nesting_level = #key_array -- 現在のネストの深さ
  
  -- 現在の階層のテーブルがない場合は作る
  if colored_segments[nesting_level] == nil then
    colored_segments[nesting_level] = {}
  end
  
  -- 現在のインデント
  local indent = string.rep(tdumper.indent_unit, nesting_level - 1)
  
  if value_type == "table" then -- 値が table型の時
    -- 循環参照が検出されたかチェック ※最初が * から始まるのは循環参照時のみ
    -- 以下の条件に当てはまる部分を抜き出す
    -- 最初 { 改行 空白0+ * 任意の文字1+ 改行 空白0+ } 最後
    -- の * 任意の文字1+ の部分
    local content, count = value_str:gsub("^%{\n *(%*.+)\n *%}$", "%1")
    
    if count > 0 then -- 1回以上置換された場合は循環参照
      -- 検出メッセージを登録
      add_table_segments(nesting_level, key_str, content, indent)
    else
      -- 循環参照ではない場合
      local child = colored_segments[nesting_level + 1] -- 子要素
      -- テーブルのデータを登録
      add_table_segments(nesting_level, key_str, child, indent)
      -- 最後に子要素の情報をテーブルから削除
      colored_segments[nesting_level + 1] = nil
    end

    -- nesting_level が 1、つまりダンプが終わったら画面に出力する
    if nesting_level == 1 then
      print_to_console(colored_segments[1])
    end
  else
    -- table 以外の型は、色だけ変えてそのまま情報を追加
    if value_type ~= "number" and
       value_type ~= "string" and
       value_type ~= "function" then
      value_type = "other"
    end
    -- 出力用のテーブルに、テキストと色を登録
    add_segment(nesting_level, "symbol", indent) -- インデント
    add_segment(nesting_level, "key", key_str) -- キー名
    add_segment(nesting_level, "symbol", " = ") -- 「 = 」の部分
    add_segment(nesting_level, value_type, value_str) -- 値
    add_segment(nesting_level, "symbol", ",", true) --「,」(改行)
  end
end

-- 指定された階層の情報を保存しているテーブルに、引数のデータを追加
add_segment = function(nesting_level, type_name, data, newline)
  local color = type_colors[type_name or ""]
  table.insert(colored_segments[nesting_level], {
    color = color, -- 色(文字列 or nil)
    data = data, -- 表示する文字列 or 子要素のデータが入った配列
    newline = newline, -- 改行するか
  })
end

-- テーブルの情報を、colored_segments に追加する
add_table_segments = function(nesting_level, key_str, data, indent)
  -- 出力用のテーブルに、テキストと色を登録
  add_segment(nesting_level, "symbol", indent) -- インデント
  add_segment(nesting_level, "key", key_str) -- キー名
  add_segment(nesting_level, "symbol", " = ") -- 「 = 」の部分
  add_segment(nesting_level, "table", "{", true) -- 「{」(改行)

  -- テーブルの中身部分
  if type(data) == "string" then
    -- 循環参照の場合
    local indent2 = indent .. tdumper.indent_unit -- 子要素のインデント
    add_segment(nesting_level, "symbol", indent2) -- 子要素のインデントを追加
    add_segment(nesting_level, "comment", data, true) -- 検出メッセージ(改行)
  else
    -- 循環参照でない場合は子要素の情報を、現在の階層に追加
    if data ~= nil then
      -- 子要素のデータが入ったテーブルをそのまま入れる
      add_segment(nesting_level, nil, data) -- data の最後は必ず改行入り
    end
  end
    
  add_segment(nesting_level, "symbol", indent) -- インデント
  -- nesting_level が 1 (つまりトップのテーブル)には , を付けない
  if nesting_level == 1 then
    add_segment(nesting_level, "table", "}", true) -- 「}」(改行)
  else
    add_segment(nesting_level, "table", "}") -- 「}」
    add_segment(nesting_level, "symbol", ",", true) -- 「,」(改行)
  end
end

-- 実際に画面に色付き文字を表示する
-- 引数はデータが入った配列(最初の呼び出しは colored_segments[1])
print_to_console = function(segment_array)
  -- 配列内のデータを順番に表示していく
  for _, segment in ipairs(segment_array) do
    if type(segment.data) == "string" then
      -- ネストしたデータでない場合
      if segment.newline then
        -- 改行付きで表示
        console.cprint(segment.data, console.colors[segment.color])
      else
        -- 改行なしで表示
        console.cwrite(segment.data, console.colors[segment.color])
      end
    else
      -- ネストしたデータの場合は再帰呼び出し
      print_to_console(segment.data)
    end
  end
end
```

</details>

#### key_formatter
ここで設定する関数は、キー名の表示を [1] から [2] の形式に変更する関数です。
[1] 文字列キー: `"キー名"` / 数字キー: `キー名`
[2] 文字列キー: `キー名` / 数字キー: `[キー名]`

設定する関数のコード:

```lua: key_formatter.lua
table_dumper.key_formatter = function(key, key_str)
  local key_type = type(key)

  if key_type == "string" then
    return key -- string の場合はそのまま
  elseif key_type == "number" then
    return "[" .. tostring(key) .. "]" -- number は [] で囲む
  end

  return key_str -- 他は通常の形式
end
```

#### value_formatter
ここで設定する関数は、テーブル内に要素がない時の表示を [1] から [2] の形式に変更する関数です。

```text: [1] 通常の表示
{
  empty_table = {
  },
}
```

```text: [2] 変更後の表示
{
  empty_table = {},
}
```

設定する関数のコード:

```lua: value_formatter.lua
table_dumper.value_formatter = function(value, value_str)
  if type(value) == "table" then
    local _, count = value_str:gsub("\n", "")
    if count == 1 then -- 改行が 1回なら要素なし
      return "{}" -- 要素がない時は {} を返す
    end
  end

  return value_str -- それ以外はそのまま表示
end
```

# 4. おわりに
以上が、Lua のテーブをダンプするモジュールの解説でした。
<font color=gray><i>全部読む人は多分いないですね･･･</i></font>
複雑なテーブルの構造を、見やすく出力したい時の助けになれば幸いです。

こんな記事を読んでくださり、ありがとうございました！


[Minecraft]: https://www.minecraft.net/ja-jp
[OC-CurseForge]:https://www.curseforge.com/minecraft/mc-mods/opencomputers

[Lyrica-GitHub-all]: https://github.com/Aciryl/luautils-ocmix
[Lyrica-GitHub-common]: https://github.com/Aciryl/luautils-ocmix/tree/main/common

[Lyrica-OC-Root]: https://
[Lyrica-Lua-Modules]: https://