プログラムの事とか

お約束ですが「掲載内容は私個人の見解です」

Azure Table Storage で NaN をどうにかしたい

何の話かというと NaN の話です。おしまい

おまけ

数年前まで Table Storage を扱う際には WindowsAzure.Storage パッケージを入れて参照させていました。ほとんどの人がそうでしょう、多分

f:id:puni-o:20210705105105j:plain

そんなライブラリも現在ではご覧の通りの非推奨です

f:id:puni-o:20210705105303p:plain

代替パッケージも教えてくれていますし、サクッと更新でしょう。普通は

現在は Azure.Data.Tables が正式リリースされましたし、こっちを使っているかもしれませんね

変更点

WindowsAzure.Storage とそれ以降のライブラリでは非常に大きな、無視できない違いがあります

WindowsAzure.Storage ではdouble.NaN , double.PositiveInfinity , double.NegativeInfinity などが保存・読み込みできたのですがこれらが扱えなくなっています

GitHub でも Issue が2年近く前に上がっているんですが全くCloseされていません

github.com

たしか JSON に NaN が定義されていない気がしたのでその辺が影響しているんですかね

私はいままで double.NaN を当たり前に使っていたので 既存データのいたるところに NaN が入っているし、保存時にも無効値として NaN をバンバン使っているのでどこで何が起こるかわからない状態です

試してみる

まずは WindowsAzure.Storage で NaN ありデータを突っ込みます

なんとなくわかるような感じに抜粋 (完全ソースは最後にリンク貼っておきます)

public static async Task WindowsAzureStorage.TestTableService.SetTestData(string connectionString){
    await table.ExecuteAsync(TableOperation.InsertOrReplace(new TestTableEntity
    {
        PartitionKey = "Test1",
        RowKey = "NormalValue",
        Value = 1.23
    }));
    await table.ExecuteAsync(TableOperation.InsertOrReplace(new TestTableEntity
    {
        PartitionKey = "Test2",
        RowKey = "NaN",
        Value = double.NaN
    }));
    await table.ExecuteAsync(TableOperation.InsertOrReplace(new TestTableEntity
    {
        PartitionKey = "Test3",
        RowKey = "PositiveInfinity",
        Value = double.PositiveInfinity
    }));
    await table.ExecuteAsync(TableOperation.InsertOrReplace(new TestTableEntity
    {
        PartitionKey = "Test4",
        RowKey = "NegativeInfinity",
        Value = double.NegativeInfinity
    }));
}

これを Azure.Data.Tables で順番に読み込んでみます

public static async Task AzureDataTables.TestTableService.GetTestData(string connectionString)
{
    var tableClient = new TableClient(connectionString, "AzureTestTable");
    var queryResults = tableClient.QueryAsync<TestTableEntity>("", 1);
    await foreach (var entity in queryResults)
    {
        Console.WriteLine($"{entity.PartitionKey} , {entity.RowKey} , {entity.Value}");
    }
}

await WindowsAzureStorage.TestTableService.SetTestData(configuration.GetConnectionString("StorageConnection"));

try
{
    await AzureDataTables.TestTableService.GetTestData(configuration.GetConnectionString("StorageConnection"));
}
catch (Exception e)
{
    Console.WriteLine($"{e.Message}");
}

結果は

Test1 , NormalValue , 1.23
Object of type 'System.String' cannot be converted to type 'System.Double'.

2行目の double.NaN を取得するところで「文字列は変換できねーよ」と怒られて終了します

読めるようにしてみる

読み込む前にごにょごにょします

public static void AzureDataTables.TestTableService.GonyoGonyo()
{
    var libAssembly = Assembly.GetAssembly(typeof(global::Azure.Data.Tables.TableClient));
    var type = libAssembly.GetType("Azure.Data.Tables.DictionaryTableExtensions");
    var typeActions = type.GetField("typeActions", BindingFlags.Static | BindingFlags.NonPublic);
    ((Dictionary<Type, Action<PropertyInfo, object, object>>)typeActions.GetValue(null))[typeof(double)] = SetDouble;
}
private static void SetDouble(PropertyInfo property, object propertyValue, object result)
{
    if (propertyValue is string s)
    {
        if (s == double.NaN.ToString(CultureInfo.InvariantCulture)) property.SetValue(result, double.NaN);
        if (s == double.PositiveInfinity.ToString(CultureInfo.InvariantCulture)) property.SetValue(result, double.PositiveInfinity);
        if (s == double.NegativeInfinity.ToString(CultureInfo.InvariantCulture)) property.SetValue(result, double.NegativeInfinity);
    }
    else { property.SetValue(result, propertyValue); }
}

try
{
    AzureDataTables.TestTableService.GonyoGonyo();
    await AzureDataTables.TestTableService.GetTestData(configuration.GetConnectionString("StorageConnection"));
}
catch (Exception e)
{
    Console.WriteLine($"{e.Message}");
}
Test1 , NormalValue , 1.23
Test2 , NaN , NaN
Test3 , PositiveInfinity , ∞
Test4 , NegativeInfinity , -∞

読めました!

ちょっとだけ解説

Azure.Data.Tablesでは値の設定時に型毎の変換処理テーブル(?)をDicrionaryで持っているのでそこのdouble型を書き換えてあげれば読めます。double?やfloatなんかも書き換えれば一応読み込みはできるんじゃないんですかね。現バージョンなら

同様に書き込み側もできるかと思ったのですが、書き込みはswitch文でがっつり書かれているので私にはどうにもなりませんでした

まとめ

リフレクションでどうにかするのはあまり良くないですよね。元の実装が変わったらそれまでですし

やはりプルリクを投げてみるのがいいのかもしれませんが、この問題はそれなりにはまっている人がいると思うのですがIssueもプルリクも全くないところをみると NaN はあまり使われていないんですかねぇ (世界的には忌避されているレベルとかだといやだな~

ダメ元でプルリク投げるかどうかは・・・びみょう

おしまい

ソースはこちら

github.com