TL; DR-最適なデータ構造を設計して、測定単位内の単位を定義しようとしています。
A Unit of measure
は基本的にvalue
に関連付けられたunit
(または数量)です。 SI単位 は、7つのベースまたはディメンションを持ちます。つまり、長さ、質量、時間、電流、温度、物質の量(モル)、および光度です。
これは単純明快ですが、私たちが頻繁に使用する派生単位とレートの数があります。結合ユニットの例は、ニュートンです:kg * m / s^2
で、レートの例はtons / hr
。
暗黙の単位に大きく依存するアプリケーションがあります。変数または列名に単位を埋め込みます。しかし、これは、異なる単位で測定単位を指定する必要がある場合に問題を引き起こします。はい、入力と表示で値を変換できますが、これにより、独自のクラス内にカプセル化したい多くのオーバーヘッドコードが生成されます。
Codeplexやその他のコラボレーション環境には多くのソリューションがあります。プロジェクトのライセンスは同意できますが、プロジェクト自体は通常、非常に軽量または重すぎます。私たちは自分たちの「ちょうどいい」のユニコーンを追いかけています。
理想的には、次のようなものを使用して新しい測定単位を定義できます。
UOM myUom1 =新しいUOM(10、ボルト);
UOM myUom2 =新しいUOM(43.2、ニュートン);
もちろん、クライアントのニーズに基づいて、インペリアル単位とSI単位を組み合わせて使用します。
また、データの同程度の一貫性を提供できるように、このユニットの構造を将来のデータベーステーブルと同期させる必要もあります。
測定単位クラスを作成するために使用する必要がある単位、派生単位、およびレートを定義する最良の方法は何ですか? 1つ以上の列挙型の使用を確認できましたが、他の開発者にとってはイライラするかもしれません。単一の列挙型は200以上のエントリを持つ巨大なものになりますが、複数の列挙型は、SI単位とインペリアル単位に基づいて混乱し、単位自体の分類に基づいてさらに細かくなります。
私の懸念のいくつかを示す列挙型の例:
myUnits.Volt
myUnits.Newton
myUnits.meterSIUnit.meter
ImpUnit.foot DrvdUnit.Newton
DrvdUnitSI.Newton
DrvdUnitImp.FtLbs
使用中のユニットのセットはかなり明確に定義されており、有限の空間です。クライアントの需要がある場合、新しい派生ユニットまたはレートを拡張および追加する機能が必要です。プロジェクトはC#で作成されていますが、より広いデザインの側面は複数の言語に適用できると思います。
私が調べたライブラリの1つは、文字列を介して単位の自由形式の入力を可能にします。次に、そのUOMクラスは文字列を解析し、それに応じてスロットを作成しました。このアプローチの課題は、開発者に正しい文字列形式が何であるかを考え、思い出させることです。また、コンストラクターで渡される文字列を検証するための追加のチェックをコード内に追加しない場合、ランタイムエラー/例外のリスクが発生します。
別のライブラリーは本質的に、開発者が作業しなければならないであろうクラスが多すぎます。同等のUOMとともに、DerivedUnit
およびRateUnit
などが提供されました。基本的に、コードは私たちが解決している問題に対して過度に複雑でした。そのライブラリーは基本的に任意の組み合わせを許可します:(ユニットの世界では正当な)あらゆる組み合わせを許可しますが、考えられるすべての組み合わせを許可しないことで問題を特定(コードを単純化)します。
他のライブラリは、途方もなく単純で、たとえば、オペレーターのオーバーロードさえ考慮していませんでした。
さらに、私は不正確な変換の試みについてはそれほど心配していません(例:ボルトからメートル)。この時点でこのレベルでアクセスするのはDevsだけであり、必ずしもこのようなタイプのミスから保護する必要はありません。
C++のBoostライブラリには、測定単位の処理のサンプル実装を示す 次元分析 に関する記事が含まれています。
要約すると、測定単位はベクトルとして表され、ベクトルの各要素は基本的な次元を表します。
typedef int dimension[7]; // m l t ...
dimension const mass = {1, 0, 0, 0, 0, 0, 0};
dimension const length = {0, 1, 0, 0, 0, 0, 0};
dimension const time = {0, 0, 1, 0, 0, 0, 0};
派生ユニットはこれらの組み合わせです。たとえば、力(質量*距離/時間^ 2)は次のように表されます
dimension const force = {1, 1, -2, 0, 0, 0, 0};
インペリアル単位とSI単位は、変換係数を追加することで処理できます。
この実装はC++固有の手法(テンプレートメタプログラミングを使用してさまざまな測定単位をさまざまなコンパイル時の型に簡単に変換する)に依存していますが、概念は他のプログラミング言語に移行する必要があります。
Units.NETを Github および NuGet でリリースしました。
それはあなたにすべての一般的な単位と変換を提供します。軽量で、単体テスト済みで、PCLをサポートしています。
あなたの質問に向けて:
この分野でのソリューションの聖杯はまだ見ていません。あなたが述べているように、それは簡単に非常に複雑になりすぎたり、あまりにも冗長になりすぎて作業できないことがあります。時には、物事をシンプルに保つことが最善であり、私のニーズにはこのアプローチで十分であることが証明されています。
明示的な変換
Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double yards = meter.Yards; // 1.09361
double feet = meter.Feet; // 3.28084
double inches = meter.Inches; // 39.3701
Pressure p = Pressure.FromPascal(1);
double kpa = p.KiloPascals; // 1000
double bar = p.Bars; // 1 × 10-5
double atm = p.Atmosphere; // 9.86923267 × 10-6
double psi = p.Psi; // 1.45037738 × 10-4
動的変換
// Explicitly
double m = UnitConverter.Convert(1, Unit.Kilometer, Unit.Meter); // 1000
double mi = UnitConverter.Convert(1, Unit.Kilometer, Unit.Mile); // 0.621371
double yds = UnitConverter.Convert(1, Unit.Meter, Unit.Yard); // 1.09361
// Or implicitly.
UnitValue val = GetUnknownValueAndUnit();
// Returns false if conversion was not possible.
double cm;
val.TryConvert(LengthUnit.Centimeter, out cm);
C#を使用してF#への切り替えをプルできる場合、F#には測定値システム(値のメタデータを使用して実装)があり、これはあなたがしようとしていることに適合しているように見えます。
http://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure
特に:
// Additionally, we can define types measures which are derived from existing measures as well:
[<Measure>] type m (* meter *)
[<Measure>] type s (* second *)
[<Measure>] type kg (* kilogram *)
[<Measure>] type N = (kg * m)/(s^2) (* Newtons *)
[<Measure>] type Pa = N/(m^2) (* Pascals *)
必要なすべての変換がスケーリング変換であるという事実に基づいて(温度変換をサポートする必要がある場合を除きます。変換にオフセットが含まれる場合の計算は非常に複雑です)、次のように「測定単位」システムを設計します。
スケーリング係数、ユニットのテキスト表現の文字列、およびunit
のスケーリング先の参照を含むクラスunit
。テキスト表現は、表示の目的であり、異なる単位で値を計算するときに結果がどの単位にあるかを知るための基本単位への参照です。
サポートされているユニットごとに、unit
クラスの静的インスタンスが提供されます。
値と値のUOM
への参照を含むクラスunit
。 UOM
クラスは、別のUOM
を加算/減算するため、および無次元の値で乗算/除算するためのオーバーロードされた演算子を提供します。
同じUOM
を持つ2つのunit
で加算/減算を実行すると、直接実行されます。それ以外の場合は、両方の値がそれぞれの基本単位に変換され、加算/減算されます。結果はベースunit
にあると報告されます。
使用法は次のようになります
unit volts = new unit(1, "V"); // base-unit is self
unit Newtons = new unit(1, "N"); // base-unit is self
unit kiloNewtons = new unit(1000, "kN", Newtons);
//...
UOM myUom1 = new UOM(10, volts);
UOM myUom2 = new UOM(43.2, kiloNewtons);
互換性のないユニットでの操作は問題とは見なされないため、その点で設計をタイプセーフにすることは試みていません。 2つのユニットが同じ基本ユニットを参照していることを確認することにより、実行時チェックを追加できます。
あなたのコードが何をしているか、そしてそれが何を許可するかについて考えてください。可能なすべての単位を含む単純な列挙があると、ボルトをメートルに変換するようなことができます。それは明らかに人間には有効ではありませんが、ソフトウェアは喜んで試みます。
私はこれと漠然と似たようなことを1回行いました。私の実装には、すべてIUnitOfMeasure
を実装した抽象基本クラス(長さ、重みなど)がありました。各抽象基底クラスは、すべての変換作業に使用するデフォルトのタイプを定義しました(クラスLength
にはクラスMeter
のデフォルトの実装がありました)。したがって、IUnitOfMeasure
は2つの異なるメソッドToDefault(decimal)
およびFromDefault(decimal)
を実装しました。
ラップしたい実際の数は、IUnitOfMeasure
をジェネリック引数として受け入れるジェネリック型でした。 Measurement<Meter>(2.0)
のように発声すると、自動的にタイプセーフになります。これらのクラスに適切な暗黙の変換と数学メソッドを実装すると、Measurement<Meter>(2.0) * Measurement<Inch>(12)
などの処理を実行して、結果をデフォルトのタイプ(Meter
)で返すことができます。ニュートンのような派生ユニットを作成したことはありません。私は単にそれらをキログラム*メートル/秒/秒として残しました。
その答えは MarioVWのスタックオーバーフロー応答 にあると私は信じています。
.Net 4-0でタプルを使用できる実際の例
タプルを使用すると、2次元の辞書(またはn次元)を簡単に実装できます。たとえば、このようなディクショナリを使用して、通貨交換マッピングを実装できます。
var forex = new Dictionary<Tuple<string, string>, decimal>();
forex.Add(Tuple.Create("USD", "EUR"), 0.74850m); // 1 USD = 0.74850 EUR
forex.Add(Tuple.Create("USD", "GBP"), 0.64128m);
forex.Add(Tuple.Create("EUR", "USD"), 1.33635m);
forex.Add(Tuple.Create("EUR", "GBP"), 0.85677m);
forex.Add(Tuple.Create("GBP", "USD"), 1.55938m);
forex.Add(Tuple.Create("GBP", "EUR"), 1.16717m);
forex.Add(Tuple.Create("USD", "USD"), 1.00000m);
forex.Add(Tuple.Create("EUR", "EUR"), 1.00000m);
forex.Add(Tuple.Create("GBP", "GBP"), 1.00000m);
decimal result;
result = 35.0m * forex[Tuple.Create("USD", "EUR")]; // USD 35.00 = EUR 26.20
result = 35.0m * forex[Tuple.Create("EUR", "GBP")]; // EUR 35.00 = GBP 29.99
result = 35.0m * forex[Tuple.Create("GBP", "USD")]; // GBP 35.00 = USD 54.58
私のアプリケーションにも同様のニーズがありました。 Tuple
も不変です。これは、ウェイトやメジャーなどのオブジェクトにも当てはまります。
私のプロトタイプコード: http://ideone.com/x7hz7i
私のデザインポイント:
Length len = new Length(); len.Meters = 2.0; Console.WriteLine(len.Feet);
長さlen = Length.FromMeters(2.0);
Console.WriteLine(len.ToString( "ft")); Console.WriteLine(len.ToString( "F15")); Console.WriteLine(len.ToString ( "ftF15"));
長さlenRT = Length.FromMeters(Length.FromFeet(Length.FromMeters(len.Meters).Feet).Meters);
// F#またはC++ MPLを使用しないと、かなり乱雑で、バグが多く、安全ではない可能性があります。 //次元分析はUoMのオプション機能ではありません- //直接使用するかどうか。 必須です。
マガジンには、ドイツ語で書かれた良い記事があります: http://www.dotnetpro.de/articles/onlinearticle1398.aspx
基本的な考え方は、BaseMeasurementを持つLengthのようなUnitクラスを持つことです。このクラスには、変換係数、演算子のオーバーロード、ToStringのオーバーロード、文字列のパーサー、およびインデクサーとしての実装が含まれています。アーキテクチャビューも実装していますが、ライブラリとしてリリースされていません。
public class Length : MeasurementBase
{
protected static double[] LengthFactors = { 1, 100, 1000, 0.001, 100 / 2.54 };
protected static string[] LengthSymbols = { "m", "cm", "mm", "km", "in" };
...
public virtual double this[Units unit]
{
get { return BaseValue * LengthFactors[(int)unit]; }
set { BaseValue = value / LengthFactors[(int)unit]; }
}
...
public static ForceDividedByLength operator *(Length length, Pressure pressure1)
{
return new ForceDividedByLength(pressure1[Pressure.Units.kNm2] * length[Units.m], ForceDividedByLength.Units.kNm);
}
...
したがって、Pressureオペレーターまたは次のように使用法を確認できます。
var l = new Length(5, Length.Units.m)
Area a = l * new Length("5 m");
a.ToString() // -> 25 m^2
double l2 = l[Length.Units.ft];
しかし、あなたが言ったように、私はユニコーンも見つけませんでした:)