Struktura danych umożliwiająca dostęp do jednostek miary


17

TL; DR - Próbuję zaprojektować optymalną strukturę danych, aby zdefiniować jednostki w jednostce miary.


A Unit of measurejest zasadniczo value(lub ilością) związaną z unit. Jednostki SI mają siedem zasad lub wymiarów. Mianowicie: długość, masa, czas, prąd elektryczny, temperatura, ilość substancji (mole) i natężenie światła.

Byłoby to dość proste, ale istnieje wiele pochodnych jednostek, a także stawki, których często używamy. Przykładową połączoną jednostką byłby Newton: kg * m / s^2a przykładową stawką byłby tons / hr.

Mamy aplikację, która w dużej mierze opiera się na domyślnych jednostkach. Osadzimy jednostki w nazwie zmiennej lub kolumny. Ale to stwarza problemy, gdy musimy określić jednostkę miary z różnymi jednostkami. Tak, możemy przekonwertować wartości na wejściu i na wyświetlaczu, ale generuje to dużo kodu napowietrznego, który chcielibyśmy zamknąć w swojej klasie.

Istnieje wiele rozwiązań dla codeplex i innych środowisk współpracy. Licencjonowanie projektów jest przyjemne, ale sam projekt zwykle okazuje się zbyt lekki lub zbyt ciężki. Ścigamy własnego jednorożca „w sam raz”.

Idealnie byłoby zdefiniować nową jednostkę miary, używając czegoś takiego:

UOM myUom1 = nowy UOM (10 woltów);
UOM myUom2 = nowy UOM (43,2, niutony);

Oczywiście używamy kombinacji jednostek Imperial i SI w zależności od potrzeb naszych klientów.

Musimy również zsynchronizować tę strukturę jednostek z przyszłą tabelą bazy danych, abyśmy mogli zapewnić ten sam stopień spójności również w naszych danych.


Jaki jest najlepszy sposób definiowania jednostek, jednostek pochodnych i szybkości, których musimy użyć, aby stworzyć naszą klasę jednostek miary? Widziałem użycie jednego lub więcej wyliczeń, ale może to być frustrujące dla innych programistów. Pojedynczy wylicznik byłby ogromny z ponad 200 wpisami, podczas gdy wielokrotne wyliczenia mogłyby być mylące w oparciu o SI względem jednostek imperialnych i dodatkowy podział oparty na kategoryzacji samej jednostki.

Przykłady enum pokazujące niektóre z moich obaw:

myUnits.Volt
myUnits.Newton
myUnits.meter

SIUnit.meter
ImpUnit.foot DrvdUnit.Newton
DrvdUnitSI.Newton
DrvdUnitImp.FtLbs

Nasz zestaw używanych jednostek jest dość dobrze zdefiniowany i ma ograniczoną przestrzeń. Potrzebujemy możliwości rozszerzenia i dodania nowych jednostek pochodnych lub stawek, gdy mamy na nie zapotrzebowanie klientów. Projekt jest w języku C #, chociaż myślę, że szersze aspekty projektowania dotyczą wielu języków.


Jedna z bibliotek, które przeglądałem, umożliwia swobodne wprowadzanie jednostek za pomocą łańcucha. Następnie klasa UOM parsowała łańcuch i odpowiednio wstawiała różne elementy. Wyzwanie związane z tym podejściem polega na tym, że zmusza programistę do myślenia i zapamiętywania poprawnych formatów ciągów. I narażam się na błąd / wyjątek w czasie wykonywania, jeśli nie dodamy dodatkowych kontroli w kodzie, aby sprawdzić ciągi znaków przekazywane do konstruktora.

Inna biblioteka zasadniczo stworzyła zbyt wiele klas, z którymi programista musiałby pracować. Wraz z równoważną JM zapewnił DerivedUnita RateUniti tak dalej. Zasadniczo kod był zbyt skomplikowany w przypadku problemów, które rozwiązujemy. Ta biblioteka zasadniczo umożliwiałaby dowolne: dowolne kombinacje (co jest uzasadnione w świecie jednostek), ale chętnie rozszerzymy nasz problem (uprościć nasz kod), nie zezwalając na każdą możliwą kombinację.

Inne biblioteki były absurdalnie proste i nawet nie brały pod uwagę na przykład przeciążenia operatora.

Ponadto nie martwię się tak bardzo o próby niepoprawnej konwersji (na przykład: woltów na metry). Deweloperzy są jedynymi, którzy uzyskają dostęp na tym poziomie w tym momencie i niekoniecznie musimy chronić się przed tego rodzaju błędami.


Czy możesz wyjaśnić, w jaki sposób znalezione biblioteki nie odpowiadają twoim potrzebom?
svick


1
@MainMa - dzięki za ten link. Nie musimy przeprowadzać analizy wymiarowej, ponieważ nasza przestrzeń problemów jest wystarczająco mała, aby po prostu zadeklarować dozwolone konwersje. Będzie to generowanie hasła, ale to jednorazowy koszt.

1
Czy możesz wyjaśnić, jakiego rodzaju konwersji potrzebujesz? Czy to tylko konwersja skalowania (np. Metr na centymetr), czy też konwersje między wymiarami (np. Masa do siły)?
Bart van Ingen Schenau

1
Czy rozważałeś przeniesienie części kodu do F #? Ten język ma jednostki miary build int.
Pete

Odpowiedzi:


11

Biblioteki Boost dla C ++ zawierają artykuł na temat analizy wymiarowej, który przedstawia przykładową implementację jednostek miary.

Podsumowując: Jednostki miary są reprezentowane jako wektory, przy czym każdy element wektora reprezentuje podstawowy wymiar:

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};

Jednostki pochodne to ich kombinacje. Na przykład siła (masa * odległość / czas ^ 2) byłaby reprezentowana jako

dimension const force  = {1, 1, -2, 0, 0, 0, 0};

Jednostki imperialne kontra jednostki SI można by obsłużyć, dodając współczynnik konwersji.

Ta implementacja opiera się na technikach specyficznych dla C ++ (przy użyciu metaprogramowania szablonów w celu łatwego przekształcenia różnych jednostek miar w różne typy czasu kompilacji), ale koncepcje powinny zostać przeniesione na inne języki programowania.


Więc wszystkie wyprowadzone jednostki są równoważne stałym C ++? Przypuszczam, że są owinięte w przestrzeń nazw, aby uniknąć zanieczyszczenia rzeczy?

1
@ GlenH7 - Przechodzi do metaprogramowania szablonu. Są one w rzeczywistości reprezentowane jako osobne typy (np. mpl::vector_c<int,1,0,0,0,0,0,0>) Zamiast stałych; artykuł przedstawia najpierw podejście consts jako wyjaśnienie (i prawdopodobnie nie wyjaśniłem tego dobrze). Użycie consts działałoby jako alternatywa (straciłbyś trochę bezpieczeństwa podczas kompilacji). Korzystanie z przestrzeni nazw w celu uniknięcia zanieczyszczenia nazwy jest z pewnością opcją.
Josh Kelley

8

Właśnie opublikowałem Units.NET na Github i NuGet .

Daje ci wszystkie wspólne jednostki i konwersje. Jest lekki, przetestowany jednostkowo i obsługuje PCL.

W kierunku twojego pytania:

  • Jest to na lżejszym końcu implementacji. Celem jest pomoc w jednoznacznym przedstawieniu, konwersji i konstrukcji jednostek miary.
  • Nie ma rozwiązania równań, nie wyprowadza automatycznie nowych jednostek z obliczeń.
  • Jeden duży wyliczenie do definiowania jednostek.
  • Klasa UnitConverter do dynamicznej konwersji między jednostkami.
  • Niezmienne struktury danych do jawnej konwersji między jednostkami.
  • Przeciążone operatory dla prostej arytmetyki.
  • Rozszerzenie na nowe jednostki i konwersje polega na dodaniu nowego wyliczenia do konwersji dynamicznej i dodaniu jednostki klasy pomiaru, takiej jak Długość, w celu zdefiniowania jawnych właściwości konwersji i przeciążenia operatora.

Jeszcze nie widziałem świętego Graala rozwiązań w tej dziedzinie. Jak twierdzisz, może łatwo stać się zbyt skomplikowane lub zbyt szczegółowe, aby z nim pracować. Czasami najlepiej jest zachować prostotę i dla moich potrzeb takie podejście okazało się wystarczające.

Jawna konwersja

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

Dynamiczna konwersja

// 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);

W twoim przykładzie wydaje się, że Truple<T1, T2, T3>(x, y, z)
używasz

Nie jestem pewien, co masz na myśli, dla każdej jednostki przechowywana jest tylko jedna wartość. Dla długości ma pole metrum typu podwójnego, a dla masy - kilogramy. Podczas konwersji na inne jednostki uruchamia tę wartość za pomocą funkcji konwersji. Te próbki są teraz trochę przestarzałe, ale obowiązuje ta sama koncepcja.
angularsen

Chyba źle wymówiłem i doszedłem do wniosków ... miałem na myśli Tuple. Nie widzę twojej UnitConverterklasy, ale wygląda na to, że IMO może mieć podobną funkcjonalność jak Tupleklasa.
Chef_Code

Nadal nie jestem pewien co do porównania Tuple, ale na stronie github znajdziesz zaktualizowane przykłady użycia.
angularsen

3

Jeśli możesz użyć przełączania na F # zamiast C #, F # ma system jednostek miary (zaimplementowany przy użyciu metadanych wartości), który wygląda tak, jakby pasował do tego, co próbujesz zrobić:

http://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure

Szczególnie:

// 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 *)

Dobra sugestia i rozważaliśmy to. Nie sądzę, aby F # daje nam możliwość kontrolowania sposobu wyświetlania jednostek na wyjściu.

2
@ GlenH7 Uważam, że masz rację:Important: Units of measure look like a data type, but they aren't. .NET's type system does not support the behaviors that units of measure have, such as being able to square, divide, or raise datatypes to powers. This functionality is provided by the F# static type checker at compile time, **but units are erased from compiled code**. Consequently, it is not possible to determine value's unit at runtime.
Paul

3

W oparciu o fakt, że wszystkie wymagane konwersje są konwersjami skalowanymi (z wyjątkiem sytuacji, gdy trzeba wesprzeć konwersje temperatury. Obliczenia, w których konwersja obejmuje przesunięcie, są znacznie bardziej złożone), zaprojektowałbym mój system „jednostki miary” w następujący sposób:

  • Klasa unitzawierająca współczynnik skalowania, ciąg znaków dla reprezentacji tekstowej jednostki i odwołanie, do którego unitskaluje się. Reprezentacja tekstowa służy do celów wyświetlania, a odniesienie do jednostki podstawowej pozwala ustalić, w której jednostce jest wynik, wykonując matematykę na wartościach z różnymi jednostkami.

    Dla każdej obsługiwanej jednostki unitzapewniona jest statyczna instancja klasy.

  • Klasa UOMzawierająca wartość i odwołanie do wartości unit. UOMKlasa dostarcza przeciążone operatory dodawania / odejmowania inny UOMi do mnożenia / dzielenia o wartości bezwymiarowej.

    Jeśli dodawanie / odejmowanie jest wykonywane na dwóch UOMz tym samym unit, jest wykonywane bezpośrednio. W przeciwnym razie obie wartości są konwertowane na odpowiednie jednostki podstawowe i dodawane / odejmowane. Wynik jest zgłaszany jako znajdujący się w bazie unit.

Wykorzystanie byłoby jak

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);

Ponieważ operacje na niekompatybilnych urządzeniach nie są uważane za problem, nie próbowałem uczynić projektu bezpiecznym pod tym względem. Można dodać kontrolę czasu wykonywania, sprawdzając, czy dwie jednostki odnoszą się do tej samej jednostki podstawowej.


Ponieważ wspomniałeś o temperaturze: co to jest 95F - 85F? Co to jest 20C - 15C? W obu przykładach oba UOMmiałyby to samo unit. Czy odejmowania byłyby wykonywane bezpośrednio?

@MattFenwick: Wyniki byłyby odpowiednio 10 Fi 5 C. Obliczenia są wykonywane, jeśli to możliwe, bezpośrednio, aby uniknąć niepotrzebnych konwersji. Dodanie metod konwersji jednostek byłoby dość trywialne UOM, ale w przypadku konwersji Celsjusza-Fahrenheita unitklasa musiałaby zostać rozszerzona o możliwość przesunięcia oprócz współczynnika skalowania.
Bart van Ingen Schenau

Ale 95F - 85F! = 10F.

1
@MattFenwick: Proszę, oświeć mnie. Jak zimno to uzyskać, jeśli obniżyć temperaturę 95Fo 85F? Według mojej wiedzy Fahrenheit jest nadal skalą liniową.
Bart van Ingen Schenau

2
Zróbmy przykład Celsjusza, ponieważ łatwiej jest przekonwertować na Kelvin: jeśli mówimy 20C - 15C = 5C, to mówimy 293.15K - 288.15K = 278.15K, co jest oczywiście błędne.

2

Zastanów się, co robi Twój kod i na co pozwoli. Posiadanie prostego wyliczenia ze wszystkimi możliwymi jednostkami pozwala mi na coś takiego, jak przeliczenie woltów na metry. To oczywiście nie dotyczy człowieka, ale oprogramowanie chętnie spróbuje.

Kiedyś zrobiłem coś nieco podobnego do tego, a moja implementacja miała abstrakcyjne klasy podstawowe (długość, waga itp.), Które wszystkie zaimplementowały IUnitOfMeasure. Każda klasa baz abstrakcyjnych zdefiniowała typ domyślny (klasa Lengthmiała domyślną implementację klasy Meter), której używałaby do wszystkich prac związanych z konwersją. Dlatego IUnitOfMeasurewdrożono dwie różne metody ToDefault(decimal)oraz FromDefault(decimal).

Rzeczywista liczba, którą chciałem zawinąć, to typ ogólny, który przyjmuje IUnitOfMeasurejako swój ogólny argument. Powiedzenie czegoś takiego Measurement<Meter>(2.0)zapewnia automatyczne bezpieczeństwo typu. Wdrożenie poprawnych niejawnych konwersji i metod matematycznych na tych klasach pozwala wykonywać takie rzeczy jak Measurement<Meter>(2.0) * Measurement<Inch>(12)i zwracać wynik w domyślnym typie ( Meter). Nigdy nie opracowałem pochodnych jednostek, takich jak Newtony; Po prostu zostawiłem je jako kilogram * metr / sekundę / sekundę.


Podoba mi się podejście, które sugerujesz przy użyciu typów ogólnych.

1

Wierzę, że odpowiedź leży w odpowiedzi Przepełnienia stosu MarioVW na:

Praktyczny przykład Gdzie można używać krotki w .Net 4-0?

Za pomocą krotek można łatwo wdrożyć słownik dwuwymiarowy (lub n-wymiarowy w tym przypadku). Na przykład możesz użyć takiego słownika do wdrożenia odwzorowania wymiany walut:

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

Miałem podobną potrzebę mojej aplikacji. Tuplejest również niezmienne, co dotyczy również przedmiotów takich jak ciężary i miary… Jak mówi przysłowie: „półtora funta za cały świat”.


0

Mój prototypowy kod: http://ideone.com/x7hz7i

Moje punkty konstrukcyjne:

  1. Wybór JM (Jednostka miary) jako Get / set Właściwość
    Długość len = nowa długość ();
    len.Meters = 2.0;
    Console.WriteLine (len.Feet);
    
  2. Nazwany konstruktor do wyboru UoM
    Długość len = Length.FromMeters (2.0);
    
  3. Obsługa ToString dla UoM
    Console.WriteLine (len.ToString („ft”));
    Console.WriteLine (len.ToString („F15”));
    Console.WriteLine (len.ToString („ftF15”));
    
  4. Konwersja w obie strony (nieznaczna utrata zaokrąglania dozwolona przez podwójną precyzję)
    Długość lenRT = Length.FromMeters (Length.FromFeet (Length.FromMeters (len.Meters) .Feet) .Meters);
    
  5. Przeciążenie operatora (ale brak kontroli typu wymiarowego)
    // Dość niechlujny, błędny, niebezpieczny i może nie być możliwy bez użycia F # lub C ++ MPL.
    // To idzie dalej, że Analiza wymiarowa jest nie opcjonalna funkcja dla JM -
    // czy używasz go bezpośrednio, czy nie. Jest ona wymagana .
    

0

Jest dobry artykuł w czasopiśmie, który bez wątpienia jest w języku niemieckim: http://www.dotnetpro.de/articles/onlinearticle1398.aspx

Podstawową ideą jest posiadanie klasy Unit, takiej jak Length, z BaseMeasurement. Klasa zawiera współczynnik konwersji, przeciążenia operatora, przeciążenia ToString, parser ciągów oraz implementację jako indeksator. Wdrożyliśmy nawet pogląd Architekta, ale nie jest on udostępniany jako biblioteka.

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);
        }

...

Więc widzisz użycie z operatorem ciśnienia lub po prostu:

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];

Ale jak powiedziałeś, nie znalazłem też jednorożca :)


-1

Jest to racja bytuunits komendy Unix , która robi to wszystko przy użyciu podejścia opartego na plikach danych do określania relacji.


Dziękuję za wzmiankę units. Głównym powodem, dla którego jednostki nie będą działać w moim szerszym rozwiązaniu, są ciągi znaków w dowolnej formie. To prawda, że ​​w zamian dostarcza komunikaty o błędach, ale to podejście jest skierowane do programistów, którzy zintegrują ten kod z naszą aplikacją. Ciągi w dowolnej formie stanowią zbyt dużą szansę na błąd.

1
Powinieneś spojrzeć na unitsplik danych. Sposób, w jaki definiuje relacje między ilościami, jest bardzo czysty i może być przydatny dla twojego problemu.
Ross Patterson,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.