Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract out creation "Of" and conversion "To" Quantity members #8

Merged
merged 30 commits into from
Jun 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e02bfb8
Create first skeleton of systems
atmoos Apr 29, 2023
e77814c
Simplify SI units.
atmoos Apr 30, 2023
1875c33
Introduce concept of a root for Quantities.
atmoos Apr 30, 2023
76909ec
Try out new instantiation pattern.
atmoos Apr 30, 2023
bce8196
Use new instantiation on ElectricPotential.
atmoos Apr 30, 2023
66cfabb
Clean up quantity factories.
atmoos May 1, 2023
a66562a
Use "To" and "Of" factories on Length & El. P.
atmoos May 1, 2023
5f2cf2b
Use new "Of" and "To" members on Force and Mass.
atmoos May 1, 2023
597fdd4
Use "Of" and "To" members on temperature.
atmoos May 1, 2023
f8150c7
Use "Of" and "To" on Area, also fix IImperial{Unit}
atmoos May 1, 2023
1ab56be
Clean up ICreate factories.
atmoos May 4, 2023
e6190a1
Let ICreate return Quant instances.
atmoos May 6, 2023
dd7d8b7
Complete remaining trivial To & Of conversions.
atmoos May 6, 2023
9497e39
Use better name for former ICreate<T> interface.
atmoos May 7, 2023
0079826
Convert Velocity to "To" & "Of" pattern.
atmoos May 7, 2023
b781322
Use structs where possible.
atmoos May 7, 2023
9a895ad
Add minor simplifications & improvements.
atmoos May 21, 2023
8874900
Apply "To" and "Of" refactoring to Data.
atmoos May 21, 2023
d2d2f49
Speed-up the PerFactory.
atmoos May 21, 2023
32e61fa
Start simplifying the PerFactory
atmoos May 21, 2023
1998096
Use "To" & "Of" pattern on DataRate.
atmoos May 21, 2023
f2718bf
Use "To" & "Of" pattern on Power.
atmoos May 21, 2023
35cca5e
Use "To" and "Of" pattern on Energy.
atmoos May 21, 2023
cc0f266
Simplify where possible.
atmoos May 21, 2023
2f8510e
Update documentation.
atmoos May 22, 2023
e123436
Re-work the README.
atmoos Jun 10, 2023
3e2c1ea
Re-shuffle the README.
atmoos Jun 10, 2023
79151a7
Prove user defined prefixes and units are possible.
atmoos Jun 10, 2023
2406311
Let all factories "implement" IFactory.
atmoos Jun 10, 2023
5c81005
Final touch up.
atmoos Jun 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 124 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,79 +5,46 @@ A library to safely handle various types of quantities, typically physical quant
[![master status](https://github.com/atmoos/Quantities/actions/workflows/dotnet.yml/badge.svg)](https://github.com/atmoos/Quantities/actions/workflows/dotnet.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/atmoos/Quantities/blob/master/LICENSE)

## Project Goals

Dealing with quantities (Metre, Yard, etc.) is not trivial. There are many areas where things can go wrong, such as forgetting to convert from one unit to the next or converting the wrong way.
This library set's out to remove that burden from developers and API designers alike.

### A Generic API

This is primarily a project that lets me explore how far one can push C#'s generics in an API. The goal is to create an api where generics apply naturally and enhance readability.
On the flip side, some implementation details in this library are plain out scary and weird, but heaps of fun to explore.

### Why Physical Quantities?

Using physical quantities as test subject seemed appropriate, as there are a limited number of units and SI-prefixes. Using generics, these prefixes and units can be combined neatly to create all sorts of representations of quantities. The generic constraints then allow for the API to restrict the prefixes and units to a subset that actually make sense on a particular quantity.
A concrete example helps to illustrate that point: A length may be represented in the SI-unit of metres or imperial units feet, but not with a unit that is used to represent time. Furthermore, it is standard usage to use the SI-units with prefixes, such as "Kilo" or "Milli", but not on imperial units. Hence, the generic constraints are set accordingly.

## Should I Use It?

It's a library that is still evolving rapidly. Try at your own risk or - even better - contribute :-)

## ToDo

- [x] Enable [binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix).
- Enabling things like "KiB", i.e. "kibi Byte".
- [ ] Enable serialisation
- [ ] Extend unit tests
- [ ] More rigours benchmarking
- [ ] Add more quantities
- [ ] Add "Zero" and "One/Unit" static properties
- i.e. enabling additive and multiplicative identities.
- [ ] Add a "Normalize()" method to each quantity
- This should then generate a "human readable" representation
- example: 3'456 Km/d => 40 m/s
- [ ] Rename the [Quant](quantities/measures/Quant.cs) type
- Top candidate: "Amount"

## Examples

Usage is designed to be intuitive using:
Usage is designed to be intuitive:

- instantiation with static factory methods
- instance conversion methods
- operator overloads
- Instantiation *of* quantities with static factory methods
- `Quantity.Of(42).Metric<Unit>()`
- Conversion *to* other units with instance conversion methods
- `quantity.To.Imperial<Unit>()`
- Use of operator overloads

### Instantiation

```csharp
Length metres = Length.Si<Metre>(4);
Length miles = Length.Imperial<Mile>(12);
Length kilometres = Length.Si<Kilo, Metre>(18);
Velocity kilometresPerHour = Velocity.Si<Kilo, Metre>(4).Per<Hour>();
Length metres = Length.Of(4).Si<Metre>();
Length miles = Length.Of(12).Imperial<Mile>();
Length kilometres = Length.Of(18).Si<Kilo, Metre>();
Velocity kilometresPerHour = Velocity.Of(4).Si<Kilo, Metre>().Per.Metric<Hour>();
```

### Conversion

```csharp
Length miles = metres.ToImperial<Mile>();
Length kilometres = metres.ToSi<Kilo, Metre>();
Velocity metresPerSecond = kilometresPerHour.To<Metre>().PerSecond();
Velocity milesPerHour = kilometresPerHour.ToImperial<Mile>().Per<Hour>();
Length miles = metres.To.Imperial<Mile>();
atmoos marked this conversation as resolved.
Show resolved Hide resolved
Length kilometres = metres.To.Si<Kilo, Metre>();
Velocity metresPerSecond = kilometresPerHour.To.Si<Metre>().Per.Si<Second>();
Velocity milesPerHour = kilometresPerHour.To.Imperial<Mile>().Per.Metric<Hour>();
```

### Operator Overloads

Quantities support common operations such as addition, subtraction, multiplication and division. The operations are "left associative", meaning the units of the left operand are "carried over" to the result when possible.

```csharp
Time time = Time.In<Hour>(3);
Time time = Time.Of(3).Metric<Hour>();

Velocity metricVelocity = kilometres / time; // 6 km/h
Velocity imperialVelocity = miles / time; // 4 mi/h

Area metricArea = kilometres * miles; // 347.62 Km²
Area imperialArea = miles * kilometres ; // 134.22 mi²
Area imperialArea = miles * kilometres; // 134.22 mi²
Console.WriteLine($"Equal area: {metricArea.Equals(imperialArea)}"); // Equal area: True

Length metricSum = kilometres + miles - metres; // 37.308 Km
Expand All @@ -92,8 +59,8 @@ As one of the primary goals it to ensure safety when using quantities, type safe
Additive operations only work on instances of the same type

```csharp
Power power = Power.Si<Watt>(36);
Mass mass = Mass.Metric<Tonne>(0.2);
Power power = Power.Of(36).Si<Watt>();
Mass mass = Mass.Of(0.2).Metric<Tonne>();

// Doesn't compile:
// Cannot implicitly convert type 'double' to 'Power'
Expand All @@ -105,16 +72,16 @@ Multiplication of different quantities is very common, hence compile errors are

```csharp
// Common operation: Ohm's Law
ElectricCurrent ampere = ElectricCurrent.Si<Ampere>(3);
ElectricalResistance ohm = ElectricalResistance.Si<Ohm>(7);
ElectricCurrent ampere = ElectricCurrent.Of(3).Si<Ampere>();
ElectricalResistance ohm = ElectricalResistance.Of(7).Si<Ohm>();

// U = R * I
// The multiplicative result is a different type: ElectricPotential
ElectricPotential potential = ohm * ampere; // 21 V

// Eccentric operation
Time time = Time.In<Hour>(5);
Mass mass = Mass.Metric<Tonne>(0.2);
Time time = Time.Of(5).Metric<Hour>();
Mass mass = Mass.Of(0.2).Metric<Tonne>();

// Doesn't compile
// Operator '*' is ambiguous on operands of type 'Mass' and 'Time'
Expand All @@ -128,7 +95,107 @@ var fooBar = mass * time;
Different types of prefixes are also supported. This is useful for [IEC binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix).

```csharp
Data kibiByte = Data.In<Kibi, Byte>(1); // 1 KiB, binary prefix
Data kiloByte = Data.In<Byte>(1024).To<Kilo, Byte>(); // 1 KB, metric prefix
Data kibiByte = Data.Of(1).Binary<Kibi, Byte>(); // 1 KiB, binary prefix
Data kiloByte = Data.Of(1.024).Metric<Kilo, Byte>(); // 1 KB, metric prefix
Console.WriteLine($"Equal amount of data: {kiloByte.Equals(kibiByte)}"); // Equal amount of data: True
```

## Design Philosophy

This library is intended to solve the ambiguity faced when dealing with physical units. Represented here by the umbrella term "quantities". Within a narrow scope (a method or private members of a class) it often suffices to declare the units that are in use with a comment and just use a `double` or a `float` to represent any quantity within that scope. The real issue arises when **designing APIs** where quantities with their associated units start to play a relevant role.
This is where this library comes into play, enabling a type-safe means of declaring quantities on an API as "drop in replacements" for, say `double` or `float`.

### A Motivating Example

Let's look at a simple example of calculating a velocity:

```csharp
public Double CalculateVelocity(Double lengthInMeters, Double timeInSeconds) => lengthInMeters / timeInSeconds;
```

This method declares what units are expected in the names of the arguments and relies on any caller to heed the indicated units. Also, it assumes the caller has enough knowledge of physics that he or she will be able to infer that the returned units are *probably* "m/s", although there is *no guarantee*.

With this library the API would read much clearer:

```csharp
public Velocity CalculateVelocity(Length length, Time time) => length / time;
```

The units have become irrelevant, but the expected quantities are declared in a type-safe way. Also, the returned quantity is now obvious, it's `Velocity`. But "what about the units" you may be asking yourself? Well they are a priori and de facto irrelevant for the caller and the method implementation. However, both the caller and implementer have the power to explicity *choose* whatever makes most sense for their use case or domain.

Again, let's look at an example for the caller and assume she wants to print the result to the console. Let's assume she's a researcher so she'll likely be wanting the SI unit "m/s", which is what she's easily able to express:

```csharp
Velocity velocity = CalculateVelocity(/* any values */); // The units in which the calculation is performed are irrelevant.

Console.WriteLine($"The velocity is: {velocity.To.Si<Metre>().Per.Si<Second>()}"); // The velocity is: 42 m/s;
```

Let's look at an example for someone implementing the `CalculateVelocity` method. This time let's assume it's train manufacturer, so "Kilometres" and "Hours" might be most familiar to them. Let's further assume that complex logic is required that may even involve some other dependency which lets them choose to do the actual calculation using `double`:

```csharp
public Velocity CalculateVelocity(Length length, Time time)
{
Double lengthInKilometres = length.To.Si<Kilo, Metre>();
Double timeInHours = time.To.Metric<Hour>();

Double velocityInKilometresPerHours = /* complex logic */;

return Velocity.Of(velocityInKilometresPerHours).Si<Kilo, Metre>().Per.Metric<Hour>();
}
```

To maintainers of the above code snippet it will always be clear what units are in use in the implementation, as the scope is narrow. For callers the API is clear and expressive: the quantity is obvious (Velocity) and the units can be anything they chose.

### Design Principles

With the above example established, we'd like to state some design principles:

- This library is domain agnostic.
- Hence, we make no assumptions of what users might wan't to model.
- Nor what kind of data would be modelled.
- This library does not validate input.
- As long as it's a valid `Double`, we'll take it.
- Example: A negative `Length` is a valid value. (It's a valid floating point value.)
- If users need to constrain the value of a quantity, the'll need to do that themselves.
- This includes "Divide by zero" scenarios, which we leave to .Net to handle.
- This library is designed to be fast.
- It's not as fast as using `Double` directly though.
- However, precision wins out over speed in some cases.
- This library aims to avoid memory allocations.
- This holds true for many quantities.
- Units and prefixes are represented by types, not values.
- This allows users to easily [use their own](./quantities.test/UserDefined.cs) units.
- We don't see a scenario for user defined prefixes, but it's possible to do none the less.

### Naming

The naming of units and prefixes follows the definitions given by the [International System of Units](https://en.wikipedia.org/wiki/International_System_of_Units) (SI). If no naming can be found there, the consensus formed on the corresponding english Wikipedia page will be used.
This leads to the following list of naming conventions:

- We use the *international* name as defined by SI or Wikipedia
- Many units are named after individuals. We respect the way they spell their own name.
- Hence we use [Ångström](./quantities/units/Si/Metric/Ångström.cs), not "Angstrom".
- We can do this since C# source code is UTF-8 and supports special characters
- Potential duplicate names are resolved via namespaces.
- Examples are the well known unit of force, the [Newton](./quantities/units/Si/Derived/Newton.cs) and the lesser known unit of temperature, the [Newton](./quantities/units/NonStandard/Temperature/Newton.cs).

## Should I use this Library?

It's a library that is still evolving rapidly. Try at your own risk or - even better - contribute :-)

## ToDo

- [x] Enable [binary prefixes](https://en.wikipedia.org/wiki/Binary_prefix).
- Enabling things like "KiB", i.e. "kibi Byte".
- [ ] Enable serialisation
- [ ] Extend unit tests
- [ ] More rigours benchmarking
- [ ] Add more quantities
- [ ] Add "Zero" and "One/Unit" static properties
- i.e. enabling additive and multiplicative identities.
- [ ] Add a "Normalize()" method to each quantity
- This should then generate a "human readable" representation
- example: 3'456 Km/d => 40 m/s
- [ ] Rename the [Quant](quantities/measures/Quant.cs) type
- Top candidate: "Amount"
Loading