NullableUnitGenerator 0.0.2.97

dotnet add package NullableUnitGenerator --version 0.0.2.97
NuGet\Install-Package NullableUnitGenerator -Version 0.0.2.97
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NullableUnitGenerator" Version="0.0.2.97">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add NullableUnitGenerator --version 0.0.2.97
#r "nuget: NullableUnitGenerator, 0.0.2.97"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install NullableUnitGenerator as a Cake Addin
#addin nuget:?package=NullableUnitGenerator&version=0.0.2.97

// Install NullableUnitGenerator as a Cake Tool
#tool nuget:?package=NullableUnitGenerator&version=0.0.2.97

This document is a work in progress. (このドキュメントは書きかけです。)

NullableUnitGenerator

GitHub Workflow Status GitHub release (latest by date) Nuget

C# Source Generator to create Value object pattern to support arithmetic operators and serialization and Null and Undefined value.

算術演算子、シリアル化、および Null値とUndefined値をサポートする Value object パターンを作成する C# ソースジェネレーターです。

GitHub: NullableUnitGenerator

NuGet: NullableUnitGenerator

Install-Package NullableUnitGenerator

Thanks

forked from UnitGenerator to support Null and Undefined values. Thanks to the author of UnitGenerator.

Null値 と Undefined値 をサポートするために UnitGenerator から分岐しました。 UnitGenerator の作者様に感謝します。

MessagePackFormatter, EntityFrameworkValueConverter, and Unity are not yet supported.

MessagePackFormatter, EntityFrameworkValueConverter, and Unity are supported by UnitGenerator. However, NullableUnitGenerator does not yet support them. They are described in this document, but cannot be used with NullableUnitGenerator.

MessagePackFormatter、EntityFrameworkValueConverter、Unity は、UnitGenerator でサポートされています。 しかし、NullableUnitGenerator はまだそれらをサポートしていません。 この文書に記述を残していますが、NullableUnitGenerator で使用することはできません。

Introduction

For example, Identifier, UserId is comparable only to UserId, and cannot be assigned to any other type. Also, arithmetic operations are not allowed.

例えば、識別子 UserId は UserId とのみ比較可能であり、他のタイプに割り当てることはできません。 また、算術演算は使用できません。

using NullableUnitGenerator;

[UnitOf(typeof(int))]
public readonly partial struct UserId { }

will generates

以下のようなコードが自動生成されます。

[System.ComponentModel.TypeConverter(typeof(UserIdTypeConverter))]
public readonly partial struct UserId : IEquatable<UserId>, IEqualityComparer<UserId>
{
    // backing field
    readonly int m_value = default;
    readonly UnitState m_state = UnitState.Undef;

    // Constructor
    public UserId(in UserId value) { (m_state, m_value) = (value.m_state, value.m_value); }
    public UserId(in UnitState state, in int value = default) => ...;
    public UserId(in int value) { (m_state, m_value) = (UnitState.Value, value); }
    public UserId(in int? value) => ...;

    // get state
    public bool IsUndef       => m_state == UnitState.Undef;
    public bool IsNull        => m_state == UnitState.Null;
    public bool IsUndefOrNull => m_state != UnitState.Value;
    public bool HasValue      => m_state == UnitState.Value;
    public UnitState State => m_state;

    // get value
    public int  Value         => GetOrThrow();
    public int  AsPrimitive() => Value;
    public int  GetRawValue() => m_value;
    public int  GetOr(in int defaultValue)  => HasValue ? m_value : defaultValue;
    public int? GetOr(in int? defaultValue) => HasValue ? m_value : defaultValue;
    public int  GetOrDefault() => GetOr(default);
    public int? GetOrNull()    => GetOr(null);
    public int  GetOrThrow()   => ...;
    public bool TryGet(out int value, in int defaultValue = default) => ...;

    // GetHashCode, ToString
    public override int GetHashCode()  => (m_state, m_value).GetHashCode();
    public int GetHashCode(UserId obj) => (obj.m_state, obj.m_value).GetHashCode();
    public override string ToString()  => ValueString;

    // implicit, explicit operator
    public static explicit operator int(in UserId value)  => (int)value.GetOrThrow();
    public static explicit operator UserId(in int value)  => new(value);
    public static explicit operator int?(in UserId value) => (int?)value.GetOrNull();
    public static explicit operator UserId(in int? value) => new(value);

    // Equals, IEquatable<UserId>
    public bool Equals(UserId other)         => ...;
    public bool Equals(UserId x, UserId y)   => x.Equals(y);
    public override bool Equals(object? obj) => obj is UserId ts && Equals(ts);

    // ==, != operator
    public static bool operator ==(in UserId x, in UserId y) => x.Equals(y);
    public static bool operator !=(in UserId x, in UserId y) => !(x == y);

    // TypeConverter
    private class UserIdTypeConverter : System.ComponentModel.TypeConverter
    { ... }
}

However, Hp in games, should not be allowed to be assigned to other types, but should support arithmetic operations with int. For example double heal = target.Hp = Hp.Min(target.Hp * 2, target.MaxHp).

しかし、ゲームにおける Hp は、他の型に代入することは許されず、intを使った算術演算をサポートする必要があります。例えば、double heal = target.Hp = Hp.Min(target.Hp * 2, target.MaxHp).

[UnitOf(typeof(int), UnitGenOpts.ImplicitOperator
                   | UnitGenOpts.ParseMethod
                   | UnitGenOpts.MinMaxMethod
                   | UnitGenOpts.ArithmeticOperator
                   | UnitGenOpts.ValueArithmeticOperator
                   | UnitGenOpts.Comparable)]
public readonly partial struct Hp { }

// -- generates

[System.ComponentModel.TypeConverter(typeof(HpTypeConverter))]
public readonly partial struct Hp : IEquatable<Hp> , IComparable<Hp>
{
    // backing field
    readonly int m_value = default;
    readonly UnitState m_state = UnitState.Undef;

    // Constructor
    public Hp(in Hp value)   => ...;
    public Hp(in UnitState state, in int value = default)  => ...;
    public Hp(in int value)  => ...;
    public Hp(in int? value) => ...;

    // get state
    public bool IsUndef       => ...;
    public bool IsNull        => ...;
    public bool IsUndefOrNull => ...;
    public bool HasValue      => ...;
    public UnitState State => ...;

    // get value
    public int Value          => ...;
    public int AsPrimitive()  => ...;
    public int  GetRawValue() => ...;
    public int GetOr(in int defaultValue)   => ...;
    public int? GetOr(in int? defaultValue) => ...;
    public int GetOrDefault() => ...;
    public int? GetOrNull()   => ...;
    public int GetOrThrow()   => ...;
    public bool TryGet(out int value, in int defaultValue = default) => ...;

    // GetHashCode, ToString
    public override int GetHashCode() => ...;
    public int GetHashCode(Hp obj)    => ...;
    public override string ToString() => ...;

    // implicit, explicit operator    // UnitGenOpts.ImplicitOperator
    public static implicit operator int(in Hp value)  => ...;
    public static implicit operator Hp(in int value)  => ...;
    public static implicit operator int?(in Hp value) => ...;
    public static implicit operator Hp(in int? value) => ...;

    // Equals, IEquatable<Hp>
    public bool Equals(Hp other)             => ...;
    public bool Equals(Hp x, Hp y)           => ...;
    public override bool Equals(object? obj) => ...;

    // ==, != operator
    public static bool operator ==(in Hp x, in Hp y) => ...;
    public static bool operator !=(in Hp x, in Hp y) => ...;

    // CompareTo, IComparable<Hp>    // UnitGenOpts.Comparable
    public int CompareTo(Hp other) => ...;

    // >, <, >=, <= operator    // UnitGenOpts.Comparable and WithoutComparisonOperator
    public static bool operator >(in Hp x, in Hp y)  => ...;
    public static bool operator <(in Hp x, in Hp y)  => ...;
    public static bool operator >=(in Hp x, in Hp y) => ...;
    public static bool operator <=(in Hp x, in Hp y) => ...;

    // Parse, TryParse    // UnitGenOpts.ParseMethod
    public static Hp Parse(string s)                     => ...;
    public static bool TryParse(string s, out Hp result) => ...;

    // Min, Max    // UnitGenOpts.MinMaxMethod
    public static Hp Min(Hp x, Hp y) => ...;
    public static Hp Max(Hp x, Hp y) => ...;

    // +, -, *, /, % operator    UnitGenOpts.ArithmeticOperator
    public static Hp operator +(in Hp x, in Hp y) => ...;
    public static Hp operator -(in Hp x, in Hp y) => ...;
    public static Hp operator *(in Hp x, in Hp y) => ...;
    public static Hp operator /(in Hp x, in Hp y) => ...;
    public static Hp operator %(in Hp x, in Hp y) => ...;

    // ++, --, +, -, *, /, % operator    // UnitGenOpts.ValueArithmeticOperator
    public static Hp operator ++(in Hp x)          => ...;
    public static Hp operator --(in Hp x)          => ...;
    public static Hp operator +(in Hp x, in int y) => ...;
    public static Hp operator -(in Hp x, in int y) => ...;
    public static Hp operator *(in Hp x, in int y) => ...;
    public static Hp operator /(in Hp x, in int y) => ...;
    public static Hp operator %(in Hp x, in int y) => ...;

    // TypeConverter
    private class HpTypeConverter : System.ComponentModel.TypeConverter
    { ... }
}

You can configure with UnitGenOpts, which method to implement.

どのメソッドを実装するかは UnitGenOpts で設定することができます。

[Flags]
enum UnitGenOpts
{
    None = 0,
    ImplicitOperator = 1,
    ParseMethod = 2,
    MinMaxMethod = 4,
    ArithmeticOperator = 8,
    ValueArithmeticOperator = 16,
    Comparable = 32,
    Validate = 64,
    JsonConverter = 128,
    MessagePackFormatter = 256,  // Unsupported
    DapperTypeHandler = 512,
    EntityFrameworkValueConverter = 1024,  // Unsupported
    WithoutComparisonOperator = 2048,
    JsonConverterDictionaryKey = 4096
}

UnitGenOpts has some serializer support. For example, a result like Serialize(userId) => { Value = 1111 } is awful. The value-object should be serialized natively, i.e. Serialize(useId) => 1111, and should be able to be added directly to a database, etc. Currently UnitGenerator supports System.Text.Json(JsonSerializer), Dapper, MessagePack for C# and EntityFrameworkCore.

UnitGenOptsには、いくつかのシリアライザーサポートがあります。例えば、Serialize(userId) => { Value = 1111 }のような結果はひどいものです。値オブジェクトはネイティブにシリアライズされるべきで、すなわち Serialize(useId) => 1111 となり、データベースなどに直接追加できるようにすべきです。 現在、UnitGeneratorは、System.Text.Json(JsonSerializer)、DapperMessagePack for C#、EntityFrameworkCore をサポートしています。。

[UnitOf(typeof(int), UnitGenOpts.MessagePackFormatter)]
public readonly partial struct UserId { }

// -- generates

[MessagePackFormatter(typeof(UserIdMessagePackFormatter))]
public readonly partial struct UserId
{
    class UserIdMessagePackFormatter : IMessagePackFormatter<UserId>
    {
        public void Serialize(ref MessagePackWriter writer, UserId value, MessagePackSerializerOptions options)
        {
            options.Resolver.GetFormatterWithVerify<int>().Serialize(ref writer, value.value, options);
        }

        public UserId Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
        {
            return new UserId(options.Resolver.GetFormatterWithVerify<int>().Deserialize(ref reader, options));
        }
    }
}

Table of Contents

UnitOfAttribute

When referring to the UnitGenerator, it generates a internal UnitOfAttribute.

UnitGeneratorを参照すると、内部のUnitOfAttributeを生成します。

namespace UnitGenerator
{
    [AttributeUsage(AttributeTargets.Struct, AllowMultiple = false)]
    internal class UnitOfAttribute : Attribute
    {
        public UnitOfAttribute(Type type, UnitGenOpts options = UnitGenOpts.None, string toStringFormat = null)
    }
}

You can attach this attribute with any specified underlying type to readonly partial struct.

この属性は、readonly partial structに指定された基本型と一緒に付けることができます。

[UnitOf(typeof(Guid))]
public readonly partial struct GroupId { }

[UnitOf(typeof(string))]
public readonly partial struct Message { }

[UnitOf(typeof(long))]
public readonly partial struct Power { }

[UnitOf(typeof(byte[]))]
public readonly partial struct Image { }

[UnitOf(typeof(DateTime))]
public readonly partial struct StartDate { }

[UnitOf(typeof((string street, string city)))]
public readonly partial struct StreetAddress { }

Standard UnitOf(UnitGenOpts.None) generates value constructor, explicit operator, implement IEquatable<T>, override GetHashCode, override ToString, == and != operator, TypeConverter for ASP.NET Core binding, AsPrimitive method. If you want to retrieve primitive value, use AsPrimitive() instead of .Value. This is intended to avoid casual getting of primitive values (using the arithmetic operator option if available).

When type is bool, also implements true, false, ! operators.

標準の UnitOf(UnitGenOpts.None) は、値のコンストラクタ、explicit operatorimplement IEquatable<T>override GetHashCodeoverride ToString==!= オペレータ、TypeConverter for ASP.NET Core binding 、AsPrimitive メソッドを生成します。 プリミティブな値を取得したい場合は、.Valueの代わりに AsPrimitive() を使用します。これは、プリミティブな値のカジュアルな取得を避けるためのものです(算術演算子オプションがある場合はそれを使用します)。

タイプがboolの場合、true, false, ! 演算子も実装します。

public static bool operator true(Foo x) => x.value;
public static bool operator false(Foo x) => !x.value;
public static bool operator !(Foo x) => !x.value;

When type is Guid or Ulid, also implements New() and New***() static operator.

型がGuidまたはUlidの場合、 New() および New***() の静的演算子も実装しています。

public static GroupId New();
public static GroupId NewGroupId();

Second parameter UnitGenOpts options can configure which method to implement, default is None. Third parameter strign toStringFormat can configure ToString format. Default is null and output as ${0}.

2番目のパラメータ UnitGenOpts options は、どのメソッドを実装するかを設定することができる(デフォルトは None )。 第3パラメータ strign toStringFormatToString のフォーマットを設定することができる。デフォルトはNULLで、${0}として出力される。

UnitGenOpts

When referring to the UnitGenerator, it generates a internal UnitGenOpts that is bit flag of which method to implement.

UnitGeneratorを参照する場合、どのメソッドを実装するかのビットフラグである内部UnitGenOptsを生成します。

[Flags]
internal enum UnitGenOpts
{
    None = 0,
    ImplicitOperator = 1,
    ParseMethod = 2,
    MinMaxMethod = 4,
    ArithmeticOperator = 8,
    ValueArithmeticOperator = 16,
    Comparable = 32,
    Validate = 64,
    JsonConverter = 128,
    MessagePackFormatter = 256,
    DapperTypeHandler = 512,
    EntityFrameworkValueConverter = 1024,
}

You can use this with [UnitOf].

[UnitOf] 属性の引数に指定できます。

[UnitOf(typeof(int), UnitGenOpts.ArithmeticOperator | UnitGenOpts.ValueArithmeticOperator | UnitGenOpts.Comparable | UnitGenOpts.MinMaxMethod)]
public readonly partial struct Strength { }

[UnitOf(typeof(DateTime), UnitGenOpts.Validate | UnitGenOpts.ParseMethod | UnitGenOpts.Comparable)]
public readonly partial struct EndDate { }

[UnitOf(typeof(double), UnitGenOpts.ParseMethod | UnitGenOpts.MinMaxMethod | UnitGenOpts.ArithmeticOperator | UnitGenOpts.ValueArithmeticOperator | UnitGenOpts.Comparable | UnitGenOpts.Validate | UnitGenOpts.JsonConverter | UnitGenOpts.MessagePackFormatter | UnitGenOpts.DapperTypeHandler | UnitGenOpts.EntityFrameworkValueConverter)]
public readonly partial struct AllOptionsStruct { }

You can setup project default options like this.

このように、プロジェクトのデフォルトオプションを設定することができます。

internal static class UnitOfOptions
{
    public const UnitGenOpts Default = UnitGenOpts.ArithmeticOperator | UnitGenOpts.ValueArithmeticOperator | UnitGenOpts.Comparable | UnitGenOpts.MinMaxMethod;
}

[UnitOf(typeof(int), UnitOfOptions.Default)]
public readonly partial struct Hp { }

ImplicitOperator

// Default
public static explicit operator U(T value) => value.value;
public static explicit operator T(U value) => new T(value);

// UnitGenOpts.ImplicitOperator
public static implicit operator U(T value) => value.value;
public static implicit operator T(U value) => new T(value);

ParseMethod

public static T Parse(string s)
public static bool TryParse(string s, out T result)

MinMaxMethod

public static T Min(T x, T y)
public static T Max(T x, T y)

ArithmeticOperator

public static T operator +(in T x, in T y) => new T(checked((U)(x.value + y.value)));
public static T operator -(in T x, in T y) => new T(checked((U)(x.value - y.value)));
public static T operator *(in T x, in T y) => new T(checked((U)(x.value * y.value)));
public static T operator /(in T x, in T y) => new T(checked((U)(x.value / y.value)));

ValueArithmeticOperator

public static T operator ++(in T x) => new T(checked((U)(x.value + 1)));
public static T operator --(in T x) => new T(checked((U)(x.value - 1)));
public static T operator +(in T x, in U y) => new T(checked((U)(x.value + y)));
public static T operator -(in T x, in U y) => new T(checked((U)(x.value - y)));
public static T operator *(in T x, in U y) => new T(checked((U)(x.value * y)));
public static T operator /(in T x, in U y) => new T(checked((U)(x.value / y)));

Comparable

Implements IComparable<T> and >, <, >=, <= operators.

IComparable<T>>, <, >=, <= 演算子を実装しています。

public U CompareTo(T other) => value.CompareTo(other.value);
public static bool operator >(in T x, in T y) => x.value > y.value;
public static bool operator <(in T x, in T y) => x.value < y.value;
public static bool operator >=(in T x, in T y) => x.value >= y.value;
public static bool operator <=(in T x, in T y) => x.value <= y.value;

WithoutComparisonOperator

Without implements >, <, >=, <= operators. For example, useful for Guid.

例えば、Guid は、 >, <, >=, <= 演算子 は を実装しません。

[UnitOf(typeof(Guid), UnitGenOpts.Comparable | UnitGenOpts.WithoutComparisonOperator)]
public readonly partial struct FooId { }

Validate

Implements partial void Validate() method that is called on constructor.

コンストラクタで呼び出される partial void Validate() メソッドを実装しています。

// You can implement this custom validate method.
[UnitOf(typeof(int), UnitGenOpts.Validate)]
public readonly partial struct SampleValidate
{
    // impl here.
    private partial void Validate()
    {
        if (value > 9999) throw new Exception("Invalid value range: " + value);
    }
}

// Source generator generate this codes.
public T(int value)
{
    this.value = value;
    this.Validate();
}

private partial void Validate();

JsonConverter

Implements System.Text.Json's JsonConverter. It will be used JsonSerializer automatically.

System.Text.JsonJsonConverter を実装しています。自動的に JsonSerializer が使用されます。

[JsonConverter(typeof(UserIdJsonConverter))]
public readonly partial struct UserId
{
    class UserIdJsonConverter : JsonConverter<UserId>
}

JsonConverterDictionaryKey

Implements JsonConverter's WriteAsPropertyName/ReadAsPropertyName. It supports from .NET 6, supports Dictionary's Key.

JsonConverterWriteAsPropertyName/ReadAsPropertyName を実装しています。.NET 6からサポートされ、DictionaryのKeyをサポートしています。

var dict = Dictionary<UserId, int>
JsonSerializer.Serialize(dict);

DapperTypeHandler

Implements Dapper's TypeHandler by public accessibility. TypeHandler is automatically registered at the time of Module initialization.

DapperのTypeHandlerをパブリックアクセシビリティで実装しています。TypeHandlerは、Moduleの初期化時に自動的に登録されます。

public readonly partial struct UserId
{
    public class UserIdTypeHandler : Dapper.SqlMapper.TypeHandler<UserId>
}

[ModuleInitializer]
public static void AddTypeHandler()
{
    Dapper.SqlMapper.AddTypeHandler(new A.ATypeHandler());
}

MessagePackFormatter

<details> <summary>Explanation in UnitGenerager (UnitGeneragerでの説明)</summary>

Implements MessagePack for C#'s MessagePackFormatter. It will be used MessagePackSerializer automatically.

C#の MessagePackFormatter 用のMessagePackを実装しています。自動的に MessagePackSerializer が使用されます。

[MessagePackFormatter(typeof(UserIdMessagePackFormatter))]
public readonly partial struct UserId
{
    class UserIdMessagePackFormatter : IMessagePackFormatter<UserId>
}

</details>

EntityFrameworkValueConverter

<details> <summary>Explanation in UnitGenerager (UnitGeneragerでの説明)</summary>

Implements EntityFrameworkCore's ValueConverter by public accessibility. It is not registered automatically so you need to register manually.

EntityFrameworkCoreのValueConverterをパブリックアクセシビリティで実装します。自動的には登録されないので、手動で登録する必要がある。

public readonly partial struct UserId
{
    public class UserIdValueConverter : ValueConverter<UserId, int>
}

// setup handler manually
builder.HasConversion(new UserId.UserIdValueConverter());

</details>

Use for Unity

<details> <summary>Explanation in UnitGenerager (UnitGeneragerでの説明)</summary>

C# Source Generator feature is rely on C#10. If you are using Unity 2021.2, that supports Source Generators. Add the UnitGenerator.dll from the releases page, disable Any Platform, disable Include all platforms and set label as RoslynAnalyzer. It works in Unity Editor however does not work on IDE because Unity does not generate analyzer reference to .csproj. We provides CsprojModifer to analyzer support, uses Add analyzer references to generated .csproj supports both IDE and Unity Editor. Unity(2020) does not support C#10 so can not use directly. However, C# Source Genertor supports output source as file.

C#ソースジェネレータ機能は、C#10に依存しています。Unity 2021.2を使用している場合、Source Generatorsをサポートしています。リリースページ](https://github.com/Cysharp/UnitGenerator/releases)からUnitGenerator.dllを追加し、Any Platformを無効にし、Include all platformsを無効にし、ラベルをRoslynAnalyzerと設定します。 Unity Editorでは動作しますが、IDEではUnityがアナライザー参照を生成しないため、動作しません。CsprojModifer](https://github.com/Cysharp/CsprojModifier)を提供し、生成された.csprojにアナライザ参照を追加することで、IDEとUnity Editorの両方をサポートします。 Unity(2020)はC#10をサポートしていないため、直接使用することはできません。ただし、C# Source Genertorはソースをファイルとして出力することが可能です。

  1. Create UnitSourceGen.csproj.
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>

        
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(ProjectDir)..\Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>

    <ItemGroup>
        
        <PackageReference Include="UnitGenerator" Version="1.0.0" />

        
        <Compile Include="..\MyUnity\Assets\Scripts\Models\**\*.cs" />
    </ItemGroup>
</Project>
  1. install .NET SDK and run this command. </details>

generated file and folder

dotnet build UnitSourceGen.csproj

File will be generated under NullableUnitGenerator\NullableUnitGenerator.SourceGenerator\*.g.cs. UnitOfAttribute is also included in generated folder, so at first, run build command and get attribute to configure.

ファイルは NullableUnitGenerator\NullableUnitGenerator.SourceGenerator\*.g.cs として生成されます。生成されたフォルダにはUnitOfAttributeも含まれているので、まずはbuildコマンドを実行して属性を取得し、設定します。

License

This library is under the MIT License.

本ライブラリは、MITライセンスのもとで提供されています。

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.0.2.97 208 11/26/2023
0.0.2.95 125 11/13/2023
0.0.2.94 107 11/13/2023
0.0.2.93 199 10/27/2023
0.0.2.92 164 10/26/2023
0.0.2.91 151 10/25/2023
0.0.2.90 143 6/29/2023

beta