C#でiniを読み書き出来る1クラスコード

C#でiniファイルを読み書きしようと思っても、.netの標準がxmlなので標準apiとしてiniの読み書きが提供されていないのはよくある話。なんでxmlが嫌かっていうと、iniを直接編集する時にどの記号をどうエスケープすればいいののか分からないから。
`C:\Users\hoge\test file<kage>.txt`って情報をxmlに書く時、なんて書けばいいか確信を持って即答出来ない。えーとバックスラッシュはそのままでいいんだっけ？どっちがgt(大なり)でどっちがlt(小なり)だっけ・・・？gt ltの前後に書くのはアンドでいいんだっけ？
だから既存の記事はいくらでもある訳だけど、車輪の再発明をしてみた。kernel32.dll使うのなんか嫌だったし、ダブルクオーテーションで囲った時どうなるんだっけ？とか忘れるので。
[＠IT：.NET TIPS INIファイルを読み書きするには？ - C#](http://www.atmarkit.co.jp/fdotnet/dotnettips/039inifile/inifile.html)
[[C#]設定ファイルの読み込みと書き込み　その２ - Qiita](http://qiita.com/caf2for4/items/3078375bb8e79a5771b4)
[C#でiniファイルを取り扱うためのクラス実装例 - Qiita](http://qiita.com/y_minowa/items/685db9926dec0d6b711b)
[C#|INIファイルを読み書きする | 貧脚レーサーのサボり日記](http://anis774.net/codevault/inifile.html)


## 特徴と制限

* 全てc#で書かれている
* 1クラスなので既存のプロジェクトにコピペオッケー
* パブリックドメインとして公開します。全てご自由にどうぞ。クレジット無し、商用全てオッケー
* コメント対応。`#`と`;`に対応
* エスケープ処理一切なし。`￥`含むファイルパスとか書く時に`￥￥`二重にする必要あるんだっけ？とか考える必要なし
* 複数行の文字列に一応対応。ただしコードの中で「このプロパティは複数行です」と宣言する必要があって、複数行としたプロパティに1行の文字列を入れても複数行としてiniに出力される。行の中に`";`と完全一致する行が含まれているとたぶんダメ。
* DateTime等のオブジェクト対応。もちろんテキストへの変換処理は独自実装可能
* キーと値は全てTrim()される。のであえてスペースを入れたり改行を最後に入れたりとかは無理
* 保存するキーを予め一覧に定義する必要がある。後はリフレクションでやります
* セクション非対応。 `[setting]`って構文ね

そんな感じ。

### 改行について

ソースの中の`fieldsOfMutiLine`ってプロパティでプロパティ名を書く必要がある。iniに書き込む時、キーの文字列が`fieldsOfMutiLine`の中に存在した場合、改行フォーマットでiniに書かれる。改行フォーマットはこんな感じ

```
hoge="aaa
bbb
ccc
";
```

読み込む時の処理として、hogeというキーがあったら改行専用の処理に入る。一行づつ読み込んで、行が`";`と完全一致した場合に改行としての読み込みを終了させる。だから終わりは`";`強制で、行の中に`";`が入ってるとそこで切れる。
そういうデータが入る可能性がある時は

```
hoge=PREFIX"aaa
bbb
ccc
";PREFIX
```

みたいに任意の文字列をキーと出来るような構文に対応したコードにする必要がある。

改行についてはかなりダサい事になってるけど、iniから読み込む時に確実に改行コミで読み込む方法が思いつかなかった。
エスケープ処理を入れればいいんだけど、`=`より後ろは全てそのまま読み込んで欲しいから出来なかった。



## 使い方

コードには既にKage MultiLine Hiduke inteja rongu nuru の値が定義されているからこれを参考に

追加する時は

* プロパティを実際に追加する
* fields プロパティにプロパティ名を追加する

以上。

読み書きはこんな感じ

```csharp
var prope = new Properties();
prope.Kage = "一行";
prope.MultiLine = "改行\nほげほげ";
prope.save();
```

## ソースコード

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace ini_test {
    class Properties {
        public String Kage { get; set; } = "";
        public String MultiLine { get; set; } = "";
        public DateTime Hiduke { get; set; } = new DateTime(2000, 1, 1);
        public int inteja = int.MaxValue;
        public long rongu = long.MaxValue;
        public Object nuru = null;
        String IniPath {
            get {
                String iniPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
                iniPath = System.IO.Path.ChangeExtension(iniPath, "ini");
                return iniPath;
            }
        }
        /// <summary>
        /// iniファイルが無い時のデフォルトのファイル リソースから指定。
        /// hoge.kage.sage.txt のように、パッケージ名、ディレクトリ名、ファイル名 をピリオドで区切る
        /// </summary>
        static readonly String DefaultIniResouceName = "";
        List<String> fields = new List<string> { nameof(Kage), nameof(MultiLine), nameof(Hiduke), nameof(inteja), nameof(rongu), nameof(nuru) };
        List<String> fieldsOfMutiLine = new List<string> { nameof(MultiLine) };
        public Properties() {
            if (System.IO.File.Exists(this.IniPath) == false) {
                System.Reflection.Assembly myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
                String val;
                if (DefaultIniResouceName != "" && myAssembly.GetManifestResourceInfo(DefaultIniResouceName) != null) {
                    using (System.IO.StreamReader sr = new System.IO.StreamReader(myAssembly.GetManifestResourceStream(DefaultIniResouceName), System.Text.Encoding.UTF8)) {
                        val = sr.ReadToEnd();
                    }
                    System.IO.File.WriteAllText(this.IniPath, val.Trim());
                }
            }
            String saveIni = this.loadAndGenerateSaveString();
            Console.WriteLine(saveIni);
        }
        public void save() {
            String saveString = this.loadAndGenerateSaveString();
            System.IO.File.WriteAllText(this.IniPath, saveString);
        }
        String loadAndGenerateSaveString() {
            List<String> lines = new List<string>();
            if (System.IO.File.Exists(this.IniPath)) {
                lines = System.IO.File.ReadLines(this.IniPath, Encoding.UTF8).ToList();
            }
            List<String> containFieldNames = new List<string>();
            StringBuilder writeString = new StringBuilder();
            for (int i = 0; i < lines.Count; i++) {
                String line_ = lines[i];
                String line = line_.Trim();
                if (line == "" || line.StartsWith(";") || line.StartsWith("#") || !line.Contains("=")) {
                    writeString.AppendLine(line);
                    continue;
                }
                String key, value;
                key = line.Substring(0, line.IndexOf("=")).Trim();
                value = line.Substring(line.IndexOf("=") + 1).Trim();
                containFieldNames.Add(key);
                if (fields.Contains(key)) {
                    // 今のインスタンスの値から新しく書き込むiniの項目を作成する
                    this.generateIniStringFromNowInstanceField(writeString, key);
                    // 読み込み
                    String rawValue;
                    if (this.fieldsOfMutiLine.Contains(key)) {
                        rawValue = this.loadMultiLine(value, lines, ref i);
                    } else {
                        rawValue = value;
                    }
                    Type fieldType = this.getTypeFromNowClass(key);
                    object setObject = getObjectFromIniString(rawValue, fieldType);
                    this.setValueForField(key, setObject);
                } else {
                    // コメントアウトされていないkey=value形式の行だけど、未知のkeyだった場合はここに来る
                    writeString.AppendLine(line);
                }
            }
            // 既存のiniファイルに無い項目をwriteStringに追加する
            foreach (var field in fields) {
                if (containFieldNames.Contains(field)) {
                    continue;
                }
                // 今のインスタンスの値から新しく書き込むiniの項目を作成する
                this.generateIniStringFromNowInstanceField(writeString, field);
            }
            return writeString.ToString();
        }
        /// <summary>
        /// iniファイル上の文字を任意の型のデータに変換する。
        /// 例えばUnixTimeの数字をDateTime型に戻す処理
        /// </summary>
        /// <param name="rawString"></param>
        /// <param name="fieldType"></param>
        /// <returns></returns>
        static Object getObjectFromIniString(String rawString, Type fieldType) {
            if (fieldType == typeof(object)) {
                return null;
            } else if (fieldType.IsAssignableFrom(typeof(String))) {
                return rawString;
            } else if (fieldType.IsAssignableFrom(typeof(DateTime))) {
                long unixTime = long.Parse(rawString);
                return DateTimeOffset.FromUnixTimeMilliseconds(unixTime).DateTime;
            } else if (fieldType.IsAssignableFrom(typeof(int))) {
                return int.Parse(rawString);
            } else if (fieldType.IsAssignableFrom(typeof(long))) {
                return long.Parse(rawString);
            } else {
                return rawString;
            }
        }
        /// <summary>
        ///  任意のオブジェクトをiniファイルに書き込める文字として返す。
        ///  例えばDateTime型のデータをUnixTimeの数字 の文字列として返す
        /// </summary>
        /// <param name="rawObject"></param>
        /// <returns></returns>
        static String getIniStringFromAnyObject(object rawObject) {
            if (rawObject == null) {
                return "";
            }
            var type = rawObject.GetType();
            if (rawObject is String) {
                return ((String)rawObject).Trim();
            } else if (rawObject is DateTime) {
                return new DateTimeOffset((DateTime)rawObject).ToUnixTimeMilliseconds().ToString();
            } else if (rawObject is int) {
                return rawObject.ToString();
            } else if (rawObject is long) {
                return rawObject.ToString();
            } else {
                return rawObject.ToString();
            }
        }
        Type getTypeFromNowClass(String fieldName) {
            var field = this.GetType().GetField(fieldName);
            if (field != null) {
                return field.FieldType;
            }
            var prop = this.GetType().GetProperty(fieldName);
            if (prop != null) {
                return prop.PropertyType;
            }
            return null;
        }
        void generateIniStringFromNowInstanceField(StringBuilder stringBuilder, String fieldName) {
            object nowFieldValue = getValueFromField(fieldName);
            String value = getIniStringFromAnyObject(nowFieldValue);
            if (this.fieldsOfMutiLine.Contains(fieldName)) {
                stringBuilder.AppendLine($"{fieldName}=\"{value}\n\";");
            } else {
                stringBuilder.AppendLine($"{fieldName}={value}");
            }
        }
        object getValueFromField(String field) {
            var value = this.GetType().InvokeMember(field,
                BindingFlags.Public |
                BindingFlags.NonPublic |
                BindingFlags.Instance |
                BindingFlags.GetProperty |
                BindingFlags.GetField
                , null, this, null);
            return value;
        }
        void setValueForField(String field, Object value) {
            this.GetType().InvokeMember(field,
                BindingFlags.Public |
                BindingFlags.NonPublic |
                BindingFlags.Instance |
                BindingFlags.SetProperty |
                BindingFlags.SetField
                , null, this, new Object[] { value });
        }
        /// <summary>
        /// iniから複数行の文字列を読み込む
        /// </summary>
        /// <param name="value"></param>
        /// <param name="lines"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        String loadMultiLine(String value, List<String> lines, ref int index) {
            // この中で次の行へ移動して、[";]と完全一致する行まで読み込む
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(value.Substring(1));
            for (index++; index < lines.Count; index++) {
                if (lines[index] == @""";") {
                    break;
                }
                sb.AppendLine(lines[index].Trim());
            }
            return sb.ToString().Trim();
        }
    }
}

```