diff --git a/.claude/criteria-for-adding-quantities-and-units.md b/.claude/criteria-for-adding-quantities-and-units.md index 81786f2ef3..ab0766c477 100644 --- a/.claude/criteria-for-adding-quantities-and-units.md +++ b/.claude/criteria-for-adding-quantities-and-units.md @@ -1,6 +1,6 @@ # Criteria for adding quantities and units -Related wiki page: https://github.com/angularsen/UnitsNet/wiki/Adding-a-New-Unit#a-quantity-is-a-good-fit-to-add-if-it +See also: [Docs/adding-a-new-unit.md](../Docs/adding-a-new-unit.md#great-but-before-you-start) for the full contributor guide. To avoid bloating the library, we want to ensure quantities and units are widely used and well defined. Avoid little used units that are obscure or too domain specific. diff --git a/CLAUDE.md b/CLAUDE.md index 83ee8dd868..9d426d0d2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,10 +58,11 @@ The project uses a sophisticated code generation system: ### Adding or Modifying Units 1. Edit or create JSON file in `Common/UnitDefinitions/` -2. Follow conversion function guidelines: +2. Follow conversion function guidelines in [Docs/adding-a-new-unit.md](Docs/adding-a-new-unit.md): - Use multiplication for `FromUnitToBaseFunc` - Use division for `FromBaseToUnitFunc` - Prefer scientific notation (1e3, 1e-5) + - Use exact constituent constants instead of pre-computed decimals 3. Run `generate-code.bat` 4. Add tests if needed @@ -118,6 +119,15 @@ The project uses a sophisticated code generation system: - Execute: `dotnet run -c Release --project UnitsNet.Benchmark` - Results saved to `Artifacts/` folder +## Documentation + +All contributor and user documentation lives in [Docs/](Docs/README.md), including: +- [Adding a New Unit](Docs/adding-a-new-unit.md) — step-by-step guide with JSON schema conventions +- [Adding Operator Overloads](Docs/adding-operator-overloads.md) +- [Precision](Docs/precision.md) — conversion precision and test value guidelines +- [Serialization](Docs/serialization.md), [String Formatting](Docs/string-formatting.md), [Saving to Database](Docs/saving-to-database.md) +- [Upgrade Guides](Docs/README.md#upgrade-guides) for major version migrations + ## Pull request reviews ### Adding new quantities or units diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22d7954002..2645925997 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ -# Contributing to Units.NET +# Contributing to Units.NET Guidelines for contributing to the repo. ## We want your help and we are friendly to first-time contributors! -Adding a new unit or a new quantity is easy! We have detailed the steps here and if you need any assistance we are happy to help! +Adding a new unit or a new quantity is easy! See the detailed step-by-step guide: -https://github.com/angularsen/UnitsNet/wiki/Adding-a-New-Unit +**[Adding a New Quantity or Unit](Docs/adding-a-new-unit.md)** We also want the person with the idea, suggestion or bug report to implement the change in code and get a sense of ownership for that piece of the library. This is to help grow the number of people that can contribute to the project and after someone new lands that first PR we often see more PRs from that person later. @@ -24,19 +24,11 @@ This is to help grow the number of people that can contribute to the project and * Test method: `__` (`Parse_AmbiguousUnits_ThrowsException`) * If there are many tests for a single method, you can wrap those in an inner class named the same as the method and then you can skip that part of the test method names -## Unit definitions (.JSON) +## Unit Definitions (.JSON) For a fairly complete summary of the unit definition JSON schema, see [Meter of Length](https://github.com/angularsen/UnitsNet/blob/master/Common/UnitDefinitions/Length.json). It has prefix units and multiple cultures. -### Conversion functions - -Converting from unit A to B is achieved by first converting from unit A to the base unit, then from the base unit to unit B. To achieve this, each unit defines two conversion functions. - -* Prefer multiplication for `FromUnitToBaseFunc` (`{x} * 2.54e-2` for `Inch` to `Meter`) -* Prefer division for `FromBaseToUnitFunc` (`{x} / 2.54e-2` for `Meter` to `Inch`) -* Prefer scientific notation `1e3` and `1e-5` instead of `1000` and `0.00001` -* Prefer a constant if the conversion factor is finite (`{x} * 2.54e-2` for `Inch`) -* Prefer a calculation if the conversion factor is infinite (`({x} / 72.27)*2.54e-2` for `PrinterPoint`) +For detailed conventions on conversion functions, abbreviations, base dimensions, and more, see [Adding a New Unit](Docs/adding-a-new-unit.md#1-add-or-modify-json-file-for-a-quantity-class). ### Units @@ -46,17 +38,10 @@ Generally we try to name the units as what is the most widely used. **Note:** We should really consider switching variant prefix to suffix, since that plays better with kilo, mega etc.. Currently we have units named `KilousGallon` and `KiloimperialGallon`, these would be better named `KilogallonUs` and `KilogallonImperial`. -### Unit abbreviations - -A unit can have multiple abbreviations per culture/language, the first one is used by `ToString()` while all of them are used by `Parse()`. +## More Documentation -* Prefer the most widely used abbreviation in the domain, but try to adapt to our conventions -* Add other popular variants to be able to parse those too, but take care to avoid abbreviation conflicts of units of the same quantity -* Use superscript (`cm²`, `m³`) instead of `cm^2`, `m^3` -* Use `∆` for delta (not `▲`) -* Use `·` for products (`N·m` instead of `Nm`, `N*m` or `N.m`) -* Prefer `/` over `⁻¹`, such as `km/h` and `J/(mol·K)` -* Use `h` for hours, `min` for minutes and `s` for seconds (`m` is ambiguous with meters) -* Use suffixes to distinguish variants of similar units, such as `gal (U.S.)` vs `gal (imp.)` for gallons - * `(U.S.)` for United States - * `(imp.)` for imperial / British units +See [Docs/](Docs/README.md) for the full documentation index, including: +- [Precision](Docs/precision.md) — conversion precision and test guidelines +- [Adding Operator Overloads](Docs/adding-operator-overloads.md) +- [Serialization](Docs/serialization.md) +- [Upgrade Guides](Docs/README.md#upgrade-guides) diff --git a/Docs/README.md b/Docs/README.md new file mode 100644 index 0000000000..06b0314dde --- /dev/null +++ b/Docs/README.md @@ -0,0 +1,35 @@ +# Units.NET Documentation + +## Contributing + +- [Adding a New Quantity or Unit](adding-a-new-unit.md) — step-by-step guide for adding quantities and units +- [Adding Operator Overloads](adding-operator-overloads.md) — how to add strongly-typed arithmetic operators +- [Precision](precision.md) — how conversions work and their precision limits + +## Using the Library + +- [String Formatting](string-formatting.md) — format specifiers and culture-aware output +- [Serialization](serialization.md) — JSON, XML, and custom DTO serialization +- [Saving to Database](saving-to-database.md) — strategies for persisting quantities +- [Extending with Custom Units](extending-with-custom-units.md) — adding runtime custom quantities + +## Platform Support + +- [.NET nanoFramework](nanoframework.md) — embedded device support +- [Experimental: Generic Math](experimental-generic-math.md) — .NET 7+ generic math interfaces + +## Upgrade Guides + +- [Upgrading from 3.x to 4.x](upgrading-from-3.x-to-4.x.md) +- [Upgrading from 4.x to 5.x](upgrading-from-4.x-to-5.x.md) +- [Upgrading from 5.x to 6.x](upgrading-from-5.x-to-6.x.md) + +## Collaborator Guides + +- [Issues and Pull Requests](collaborators/issues-and-pull-requests.md) — guidelines for reviewing and merging +- [Releasing NuGet Packages](collaborators/releasing-nugets.md) — how to publish new versions +- [Regenerate NuGet API Key](collaborators/regenerate-nuget-api-key.md) — annual key rotation + +## Other + +- [Top Dependencies](top-dependencies.md) — notable projects using UnitsNet diff --git a/Docs/adding-a-new-unit.md b/Docs/adding-a-new-unit.md new file mode 100644 index 0000000000..93534442a2 --- /dev/null +++ b/Docs/adding-a-new-unit.md @@ -0,0 +1,223 @@ +# Adding a New Quantity or Unit + +So you want to add a quantity or unit that is not yet part of Units.NET? + +- [Great, but before you start!](#great-but-before-you-start) +- [Requirements](#requirements) +- [Quick Summary of Steps](#quick-summary-of-steps) +- [Detailed steps](#detailed-steps) + - [1. Add or modify JSON file for a quantity class](#1-add-or-modify-json-file-for-a-quantity-class) + - [2. Run generate-code.bat](#2-run-generate-codebat) + - [3. Reopen solution to load all new files](#3-reopen-solution-to-load-all-new-files) + - [4. Fix generated test stubs to resolve compile errors](#4-fix-generated-test-stubs-to-resolve-compile-errors) + - [5. Run tests](#5-run-tests) + - [6. Create pull request](#6-create-pull-request) +- [Logarithmic Units](#logarithmic-units) +- [Code Style](#code-style) + +## Great, but before you start! + +Sometimes we just have to say no, sorry! We simply want to avoid bloating the library. + +### A quantity is a good fit to add, if it + +- [x] Is well documented and unambiguous, e.g. has a wiki page and generally easy to find on Google +- [x] Is widely used, preferably across domains +- [x] Has multiple units to convert between (e.g. `Length` has kilometer, feet, nanometer etc.) +- [x] Can convert to other quantities (e.g. `Length x Length = Area`) +- [x] Can be represented by a `double` numeric value, integer values are not well supported and may suffer from precision errors +- [x] Is not [dimensionless/unitless](https://en.wikipedia.org/wiki/Dimensionless_quantity) (consider using `Ratio`) + +### A unit is a good fit to add to a quantity, if it + +- [x] Is well documented and unambiguous, e.g. has a wiki page or found in online unit converters +- [x] Is widely used +- [x] Can be converted to other units of the same quantity +- [x] The conversion function is well established without ambiguous competing standards + +### A note on X-per-Y units + +There are extremely many variations of unit A over unit B, such as [LengthPerAngle](https://github.com/angularsen/UnitsNet/issues/1519). +Generally speaking, we are less inclined to add these unless they are very common and not too domain specific. + +We have made some exceptions to all the above "rules" so [start a discussion with us](https://github.com/angularsen/UnitsNet/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) if you still think it belongs in the library. + +Ok, enough of that. Let's move on! + +## Requirements + +* [`.NET 9 SDK`](https://dotnet.microsoft.com/download) to generate and build code + +## Quick Summary of Steps + +Units.NET uses [CodeGen](https://github.com/angularsen/UnitsNet/tree/master/CodeGen), a C# command line app that reads [JSON files with quantity and unit definitions](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions) and generates C# code. + +To add a quantity or a unit: + +- Add or change a quantity JSON file. +- Run `generate-code.bat` file. +- Specify test values for the new units in the generated test code. + +Not too difficult. Below are the detailed steps. + +## Detailed steps + +### 1. Add or modify JSON file for a quantity class + +* Place in [Common/UnitDefinitions](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions) +* See [Length.json](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions/Length.json) as an example. +* Use reliable references, such as [UN/ECE Recommendation No. 21](https://unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf), Google, Wolfram Alpha or online converters. + +#### Conversion function conventions + +* Prefer multiplication (`*`) for `FromUnitToBaseFunc` and division (`/`) for `FromBaseToUnitFunc`. As an example, `Length.Centimeter` is defined as `"FromUnitToBaseFunc": "{x} / 100"` and `"FromBaseToUnitFunc": "{x} * 100"`, instead of `{x} * 0.01` and `{x} / 0.01`. +* Prefer `1e3` and `1e-5` notation instead of `1000` and `0.00001` +* Prefer a constant if the conversion factor is finite and well known (`Inch FromUnitToBase: {x} * 2.54e-2`) +* Prefer a calculation if the conversion factor is infinite (`PrinterPoint FromUnitToBase: ({x} / 72.27) * 2.54e-2`). If the calculation is not available, specify the most precise constant you can. `double` numeric type can represent [15-17 significant decimal digits](https://en.wikipedia.org/wiki/Double-precision_floating-point_format#IEEE_754_double-precision_binary_floating-point_format:_binary64) +* Use exact constituent constants instead of pre-computed decimals -- e.g. `{x} * 4.4482216152605 / 9.290304e-2` instead of `{x} * 47.880258980335840277`, so the physical constants remain visible and no precision is lost + +#### Abbreviation naming conventions + +Prefer the most widely used abbreviation in the domain, but try to adapt to our conventions. + +* Use superscript (`cm²`, `m³`) instead of `cm^2`, `m^3` +* Use `∆` for delta (not `▲`) +* Use `·` for products (`N·m` instead of `Nm`, `N*m` or `N.m`) +* Use `/` over `⁻¹`, such as `km/h` and `J/(mol·K)` +* Use `h` for hours, `min` for minutes and `s` for seconds (`m` is ambiguous with meters) +* Use abbreviations defined by [SI Unit System](https://en.wikipedia.org/wiki/International_System_of_Units), such as `l` instead of `L` for liters +* Use suffixes to distinguish variants of similar units, such as `gal (U.S.)` vs `gal (imp.)` for gallons + * `(U.S.)` for United States + * `(imp.)` for imperial / British units +* Add other popular variants to be able to parse those too, e.g. `[ "tsp", "t", "ts", "tspn", "t.", "ts.", "tsp.", "tspn.", "teaspoon" ]` for `VolumeUnit.MetricTeaspoon` where `tsp` is used by default in `ToString()` + +#### `BaseDimensions` + +The base unit dimensions of the quantity, such as `"L": 1` for `Length` and `"L": 2` for `Area` (`Length*Length`). + +The [7 SI base units](https://en.wikipedia.org/wiki/SI_base_unit#Seven_SI_base_units) are: +- `L` - Length +- `M` - Mass +- `T` - Time +- `I` - ElectricCurrent +- `Theta` - Temperature +- `N` - AmountOfSubstance +- `J` - LuminousIntensity + +#### `BaseUnit` - the intermediate unit of a quantity + +When converting from one unit to another with `FromUnitToBaseFunc` and `FromBaseToUnitFunc` conversion functions. It is typically chosen as an SI derived unit (`Meter`, `Newtonmeter`, `Squaremeter` etc). This choice affects the precision of conversions for much bigger/smaller units than `BaseUnit`. + +#### `BaseUnits` (optional) - the [SI base units](https://en.wikipedia.org/wiki/SI_base_unit#Seven_SI_base_units) of a unit + +Don't confuse this with the quantity's `BaseUnit`, which is [discussed to be renamed](https://github.com/angularsen/UnitsNet/issues/563#issuecomment-467029946). + +If specified, you can create quantities with consistent units for a given unit system: +```c# +new Length(1, UnitSystem.SI).ToString() // "1 m" +new Length(1, myBritishEngineeringUnitSystem).ToString() // "1 ft" +``` + +Examples on `BaseUnits` values: +- `LengthUnit.Inch` has `{ "L": "Inch" }` (L=1) +- `AreaUnit.SquareCentimeter` has `{ "L": "Centimeter" }`, because we ignore dimensions (L=2) +- `VolumeUnit.Cubicfeet` has `{ "L": "Foot" }`, because we ignore dimensions (L=3) +- `ForceUnit.Newton` has `{ "L": "Meter", "M": "Kilogram", "T": "Second" }`, because `N = 1 kg * 1 m / s^2 = Kilogram * Meter / Second^2` and we ignore the dimensions +- `ForceUnit.PoundForce` has `{ "L": "Foot", "M": "Pound", "T": "Second" }`, because `N = 1 lbm * 1 ft / s^2 = Pound * Foot / Second^2` and we ignore the dimensions +- `MassConcentrationUnit.GramPerDeciliter` has `{ "L": "Centimeter", "M": "Gram" }`, because `Deciliter = 1 cm * 1cm * 1cm = Centimeter^3` and we ignore the dimensions + +Examples of units with no meaningful mapping to SI base units: +The only consequence of not specifying `BaseUnits` is that you cannot construct these units by passing a `UnitSystem` to the quantity constructor as in the example above. + +- `VolumeUnit.ImperialGallon` has no `BaseUnits`, because `Volume = Length^3` and there is no length unit that when multiplied three times would result in imperial gallon. +- `RatioUnit.DecimalFraction` has no `BaseUnits`, because dimensionless units are not made up by any SI base units. + +### 2. Run [generate-code.bat](https://github.com/angularsen/UnitsNet/blob/master/generate-code.bat) + +To generate unit classes, unit enumerations and base class for tests. + +### 3. Reopen solution to load all new files + +This step _might_ no longer be necessary with modern Visual Studio and the new .csproj format. + +### 4. Fix generated test stubs to resolve compile errors + +* Override the missing abstract properties in the unit test class (ex: [LengthTests.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomCode/LengthTests.cs)) +* Specify value as a constant, not a calculation, with preferably **at least 7** [significant figures](https://en.wikipedia.org/wiki/Significant_figures) where possible. Beyond 16 significant digits is not useful due to `double` precision limits. +* Triple-check the number you write here, this is **the most important piece** as it verifies your conversion function in the .JSON file +* If possible, add a comment next to the value with a URL to where the value can be verified. +* Example: `InchesInOneMeter` in [LengthTests.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomCode/LengthTests.cs) + * I find the conversion factor to be `39.37007874` from an online unit conversion calculator, it has 10 significant figures so that is plenty + * I override the `InchesInOneMeter` property (see below snippet) + * I Google to double-check: `Inches In 1 Meter` and it tells me `1 Meter = 39.3701 Inches` (Google typically has fewer significant figures) + * If Google can't help me, I find a second source to confirm the conversion factor (another conversion website, wikipedia, Wolfram Alpha etc) + * I check again by intuition, is there really around 40 inches in a meter? Yes, sounds about right. + +Example code snippet: +```cs +/// https://link-to-where-i-found-the-value.com +protected override double InchesInOneMeter => 39.37007874; +``` + +### 5. Run tests + +Make sure all the tests pass. +Either run [build.bat](https://github.com/angularsen/UnitsNet/blob/master/build.bat) or run the tests from within Visual Studio with ReSharper or the built-in test runner. + +### 6. Create pull request + +Please see [GitHub: Creating a pull request](https://help.github.com/articles/creating-a-pull-request/). If you still have any questions, you can reach out in [Discussion](https://github.com/angularsen/UnitsNet/discussions). + +There are many ways to do this, here is one way: +- Click the Fork button to get a copy of this repo on `https://github.com/your_user/UnitsNet` +- Go to your fork and clone it to a directory on your PC (see instructions in the **Code button**) +- Create a branch, e.g. `add-somenewunit` +- Do your work, including generating code, commit and push to your fork where you have full write-access +- Visit your fork on github.com with a browser, then create a pull request from your branch to the angularsen/UnitsNet repo. + +For one-offs, this is enough. +If you need to create multiple pull requests based on the latest `master` branch, or simply keep your fork's branches up to date with the main repo's `master` branch, then you need to add the angularsen/UnitsNet repo as a remote to your git clone and fetch it. + +```sh +# Add main repo as a remote named 'angularsen' +git remote add angularsen https://github.com/angularsen/UnitsNet + +# Fetch branches/tags from all your remotes +git fetch + +# Create and checkout new branch based on latest master +git checkout -b add-another-unit angularsen/master + +# With multiple remotes, you need to tell it what remote a branch should push/pull to, assuming 'origin' is your fork. +git branch --set-upstream-to=origin/add-another-unit + +# Do your work, stage changes, commit and push to your fork. +git add -A +git commit -m "My commit message" +git push + +# If you need to keep your branch up to date, merge in angularsen's master branch to the current branch you are on. Also push this to your fork. +git merge angularsen/master +git push + +# Then visit your fork at https://github.com/your_user/UnitsNet to create a pull request. +``` + +## Logarithmic Units + +Units.NET supports logarithmic units by adding `Logarithmic` and `LogarithmicScalingFactor` (optional) properties. + +* `LogarithmicScalingFactor` is used to provide a scaling factor in the logarithmic conversion. For example, a scaling factor of `2` is required when implementing the ratio of the squares of two field amplitude quantities such as voltage. In most cases `LogarithmicScalingFactor` will be `1`. + +To create a logarithmic unit, follow the same steps from the previous section making the following adjustments: + +Step 1. Add property `"Logarithmic": "True"` to the JSON file, just after `BaseUnit`. `LogarithmicScalingFactor` defaults to `1` if not defined. + +Step 4. Provide custom implementations for logarithmic addition and subtraction unit tests. See [LevelTests.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomCode/LevelTests.cs) for an example. + +Refer to [Level.json](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions/Level.json) as an example implementation of logarithmic units. + +## Code Style + +* If you have the [ReSharper plugin](https://www.jetbrains.com/resharper) installed, there are code formatting settings checked into the repository that will take effect automatically. +* If you don't use ReSharper, at least follow the same conventions as in the existing code. diff --git a/Docs/adding-operator-overloads.md b/Docs/adding-operator-overloads.md new file mode 100644 index 0000000000..46f453a0a7 --- /dev/null +++ b/Docs/adding-operator-overloads.md @@ -0,0 +1,30 @@ +# Adding Operator Overloads + +There is a large number of operator overloads, to facilitate strongly typed computations such as `Speed speed = Length.FromMeters(100) / TimeSpan.FromSeconds(9)`. + +1. Put operator overload in `Length.extra.cs` if the **first parameter** is `Length` +2. Add a short xmldoc summary as per the example below. You can add more descriptions if it is useful. +3. Add a unit test case and place it in the equivalent file `LengthTests.cs`. + +The reason is to have a consistent place to find the operator overloads and the compiler complains if the containing type/file does not match any of the parameters of the operator overload method. + +## Example + +[UnitsNet/CustomCode/Quantities/Length.extra.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/CustomCode/Quantities/Length.extra.cs) +```cs +/// Get from times . +public static Volume operator *(Length length, Area area) +{ + return Volume.FromCubicMeters(area.SquareMeters * length.Meters); +} +``` + +[UnitsNet.Tests/CustomCode/LengthTests.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomCode/LengthTests.cs) +```cs +[Fact] +public void LengthTimesAreaEqualsVolume() +{ + Volume volume = Length.FromMeters(3) * Area.FromSquareMeters(9); + Assert.Equal(volume, Volume.FromCubicMeters(27)); +} +``` diff --git a/Docs/collaborators/issues-and-pull-requests.md b/Docs/collaborators/issues-and-pull-requests.md new file mode 100644 index 0000000000..41ef59450a --- /dev/null +++ b/Docs/collaborators/issues-and-pull-requests.md @@ -0,0 +1,30 @@ +# Collaborators: Issues and Pull Requests + +## Merging pull requests + +1. Fix the PR title, it should be short and use the imperative/commanding form: `Add Frequency.BeatPerMinute` +2. Use the `Squash and merge` option, this keeps the git history tidy +3. If you have the option to delete the branch afterwards, please do so (branches on `angularsen` repo). +4. After merging a PR, or a batch of PRs, you should [release a new NuGet](releasing-nugets.md) + +## Be nice + +If someone put the effort into creating a PR, even if it is way off and will be rejected, please respect the time they put into it and help them know what to do differently the next time. + +## Help people help themselves + +If someone comes asking about a new feature or a bug, don't just go and fix it. Help them do it. + +### It adds future contributors and project maintainers + +Getting a contribution accepted for a project they use in their daily work or hobby is very rewarding and will often keep them coming back for more once they get past the initial hurdle of learning the code base and build systems for a project. For many this will be their first contribution to open source at all and helps the open source community grow. + +### They often know best + +People asking about something may not know the library well or how to technically fit the change the best way. That's where project maintainers come in. They do however know exactly how they want to consume the said feature or for bugs they have a good way to reproduce. Especially for quantities and conversions in less known domains, this is extremely valuable to get right. + +## Invite active contributors to become project maintainers + +It is very important to grow the list of project maintainers to keep the library alive. Interest in the library will typically wane over time for most people, so to keep the interest up we need to on-board new maintainers. I have had nothing but great experiences in adding people. If they have more than a handful of pull requests with reasonable quality and they keep coming back for a time period of more than a few weeks, that is a strong indicator they will be both interested and capable in helping out for an extended period of time. + +I have always been very clear that I expect very little commitment besides helping out replying to issues and reviewing PRs whenever they feel they have the time. Anything beyond that is just a bonus. diff --git a/Docs/collaborators/regenerate-nuget-api-key.md b/Docs/collaborators/regenerate-nuget-api-key.md new file mode 100644 index 0000000000..5e187dd3d4 --- /dev/null +++ b/Docs/collaborators/regenerate-nuget-api-key.md @@ -0,0 +1,36 @@ +# Collaborators: Regenerate nuget.org API Key + +Every year the nuget.org API keys expire and must be regenerated. A reminder email is sent out to the registered owners on nuget.org. + +## Steps + +- Log in to https://www.nuget.org/account/ApiKeys with an account that is admin of [UnitsNet organization](https://www.nuget.org/organization/UnitsNet) +- Create or regenerate key with name `AppVeyor for UnitsNet project` + - Expires in: `365 days` + - Package owner: `UnitsNet` + - Select scopes: `Push > Push new packages and package versions` + - Select packages > Glob pattern: `*` to include all UnitsNet packages +- Copy the API key +- Encrypt the API key by pasting it into https://ci.appveyor.com/tools/encrypt (log in with account that is admin of UnitsNet project) +- Update [appveyor.yml](https://github.com/angularsen/UnitsNet/blob/master/appveyor.yml) with the **ENCRYPTED** API key +```yaml +deploy: +- provider: NuGet + api_key: + secure: + on: + branch: master + +- provider: NuGet + api_key: + secure: + on: + branch: release/* +``` +- Commit and push to `master`, or go via pull request if you want some extra eyes on it +- Bump nuget versions to test the new API key + - On `master` branch, run https://github.com/angularsen/UnitsNet/blob/master/Build/bump-version-UnitsNet-patch.bat + - Push the bumped version commits +- Verify that within 20 minutes, the new [UnitsNet package](https://www.nuget.org/packages/UnitsNet/) version is out on nuget.org + - Monitor or troubleshoot the build at: https://ci.appveyor.com/project/angularsen/unitsnet/ + - You will typically receive an email when the new package version is made available diff --git a/Docs/collaborators/releasing-nugets.md b/Docs/collaborators/releasing-nugets.md new file mode 100644 index 0000000000..1a0f17309a --- /dev/null +++ b/Docs/collaborators/releasing-nugets.md @@ -0,0 +1,122 @@ +# Collaborators: Releasing NuGet Packages + +Collaborators are able to release new nugets by pushing new versions of project files directly to the `master` branch, although the same could be achieved by pull requests from anyone. Usually we release a new version of nugets for every merged pull request, or for a batch of PRs if several were merged in. + +## Release New Nugets Using AppVeyor Build Server + +The build server will build and attempt to push nugets for every git push to the `master` branch. It will only succeed to push new nugets if the version changed since nuget.org does not accept publishing the same version twice. + +### Prerequisites + +Set up your local `master` branch to use `angularsen/UnitsNet` repo as upstream. Optionally, use your fork `myuser/UnitsNet` as upstream for feature branches. [See guide](#initial-setup) on setting this up. + +### Typical example in Git for Windows / Bash + +This is what I do to publish new UnitsNet nugets. I have checked out `angularsen/UnitsNet` repo as my `origin` remote. +``` +git checkout master # Checkout master +git pull --fast-forward # Pull latest origin/master, fail if there are local commits +./Build/bump-version-UnitsNet-minor.bat # Increase the version and create an annotated git tag +git log # Verify that it is only the version commit about to be pushed +git push --follow-tags # Push commits and tags +git log --oneline --first-parent # Compact log for copy & paste of commits into release page, see below +``` + +* [Create GitHub release](https://github.com/angularsen/UnitsNet/releases) by editing the newly published tag in GitHub releases page + - The title should read something like `UnitsNet/4.10.0` + - Copy in all notable commit messages since previous tag (usually pull request commits) + - Post a link to the release in the respective pull requests, so it is easy to backtrack what release a PR was included in +* Optionally, view build progress and details at https://ci.appveyor.com/project/angularsen/unitsnet + +### Versioning + +Versioning follows [SemVer](https://semver.org/). +- Major **for breaking changes** +- Minor **for new things** +- Patch **for bug fixes** + +### Versioning scripts: UnitsNet + +When adding units or new features in UnitsNet, bump the version of `UnitsNet` nugets. +* [/Build/bump-version-UnitsNet-major.bat](https://github.com/angularsen/UnitsNet/blob/master/Build/bump-version-UnitsNet-major.bat) +* [/Build/bump-version-UnitsNet-minor.bat](https://github.com/angularsen/UnitsNet/blob/master/Build/bump-version-UnitsNet-minor.bat) +* [/Build/bump-version-UnitsNet-patch.bat](https://github.com/angularsen/UnitsNet/blob/master/Build/bump-version-UnitsNet-patch.bat) + +### Versioning scripts: UnitsNet.Serialization.JsonNet + +If JSON serialization has changed or it is dependent on a newer version of UnitsNet, then the JSON library version should be bumped similar to above. +* `/Build/bump-version-UnitsNet.Serialization.JsonNet-major.bat` +* `/Build/bump-version-UnitsNet.Serialization.JsonNet-minor.bat` +* `/Build/bump-version-UnitsNet.Serialization.JsonNet-patch.bat` + +## NuGet Packages + +We currently have 3 nugets: + +### UnitsNet (Core) + +The core nuget holds all the widely used units and conversions. + +* [UnitsNet on nuget.org](https://nuget.org/packages/UnitsNet) + +### UnitsNet.Serialization.JsonNet + +Serialization using Json.NET. + +* [UnitsNet.Serialization.JsonNet on nuget.org](https://nuget.org/packages/UnitsNet.Serialization.JsonNet) + +## Initial setup + +The goal of this guide is to have a consistent setup of git remotes: +* `origin`: `angularsen/UnitsNet`, for master branch +* `my`: `myuser/UnitsNet`, for pull request branches + +### If you have cloned your fork + +Then point `master` to official repo. + +``` +# rename the fork remote to 'my' +git remote rename origin my + +# Add official repo as origin remote using either SSH or HTTPS url +git remote add origin git@github.com:angularsen/UnitsNet.git +git remote add origin https://github.com/angularsen/UnitsNet.git + +# Checkout master and point it to origin/master +git checkout master +git branch --set-upstream-to=origin/master +``` + +### If you have cloned the official repo + +Then `master` branch is already pointing to official repo and you just need to add your fork as a remote to push feature branches there. + +``` +# Add your fork repo as 'my' remote using either SSH or HTTPS url +git remote add my git@github.com:myuser/UnitsNet.git +git remote add my https://github.com/myuser/UnitsNet.git +``` + +### Use your fork for pull requests and experimental branches + +Use your fork when creating and pushing new branches. This is to avoid a clutter of work-in-progress branches before they are ready for review. + +``` +git checkout -b new-branch origin/master # new branch, based on remote master +# Add some commits... +git push my new-branch # push branch to your fork +# Create PR from your fork's github web page +``` + +### Alternative: Manually push nugets + +This should normally never be necessary, but if for any reason you need to deploy new nugets outside AppVeyor build server then you can do it like this. + +The general flow is documented at nuget.org: https://docs.microsoft.com/en-us/nuget/create-packages/publish-a-package + +* Obtain nuget API key with access to push `UnitsNet` and related nuget packages +* Set nuget API key on your PC: `nuget setApiKey Your-API-Key` +* Run `build.bat` to build everything +* Run `/Build/push-nugets.bat` to push nugets +* Alternatively run `dotnet nuget push ` for nugets in `/Artifacts/NuGet` folder diff --git a/Docs/experimental-generic-math.md b/Docs/experimental-generic-math.md new file mode 100644 index 0000000000..758600e31e --- /dev/null +++ b/Docs/experimental-generic-math.md @@ -0,0 +1,97 @@ +# Experimental: Generic Math and INumber + +Generic math was introduced in .NET 7. +https://learn.microsoft.com/en-us/dotnet/standard/generics/math + +We wanted to see what works and what doesn't for Units.NET, so we added some experimental support for the generic math interfaces in +[Generic math for UnitsNet in .NET 7 - Pull Request #1164](https://github.com/angularsen/UnitsNet/pull/1164). + +Today, all quantities use `double`. +With generics, the consumer would be able to choose the numeric type, including new types like `float`. + +## What can you do + +Sum and Average, for now. + +```cs +[Fact] +public void CanCalcSum() +{ + Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) }; + + Assert.Equal(Length.FromCentimeters(300), values.Sum()); +} + +[Fact] +public void CanCalcAverage_ForQuantitiesWithDoubleValueType() +{ + Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) }; + + Assert.Equal(Length.FromCentimeters(150), values.Average()); +} +``` + +It seems there are no implementations shipped with .NET yet, so we provide these two extension methods as a proof of concept. +We can add more if there is a need for it. + +https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GenericMath/GenericMathExtensions.cs + +```cs +public static T Sum(this IEnumerable source) + where T : IAdditionOperators, IAdditiveIdentity +{ + // Put accumulator on right hand side of the addition operator to construct quantities with the same unit as the values. + // The addition operator implementation picks the unit from the left hand side, and the additive identity (e.g. Length.Zero) is always the base unit. + return source.Aggregate(T.AdditiveIdentity, (acc, item) => item + acc); +} + +public static T Average(this IEnumerable source) + where T : IAdditionOperators, IAdditiveIdentity, IDivisionOperators +{ + // Put accumulator on right hand side of the addition operator to construct quantities with the same unit as the values. + // The addition operator implementation picks the unit from the left hand side, and the additive identity (e.g. Length.Zero) is always the base unit. + (T value, int count) result = source.Aggregate( + (value: T.AdditiveIdentity, count: 0), + (acc, item) => (value: item + acc.value, count: acc.count + 1)); + + return result.value / result.count; +} +``` + +## Some quirks so far + +### INumber.Min/Max not well defined + +[INumber](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumber-1) + +UnitsNet does not provide Min/Max values for quantities, since you quickly run into overflow exceptions when converting to other units. +Also the Min/Max could change when introducing bigger/smaller units. + +### IAdditiveIdentity (Zero) not intuitive for quantities like Temperature + +Temperature has its own quirks with arithmetic in general. + +0 Celsius != 0 Fahrenheit != 0 Kelvin. + +So for example 20 degrees C + 5 degrees C is ambiguous. +It could mean 25 degrees C, or it could mean 293.15 K + 278.15 K. + +This made it hard to implement [IAdditiveIdentity](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.iadditiveidentity-2) in a way that is intuitive, which is essential to arithmetic like `Average()`. + +We previously introduced `TemperatureDelta` to make arithmetic more clear, but this is not compatible with generic math. + +### Could not implement generic Average() + +[I was not able](https://github.com/angularsen/UnitsNet/pull/1164#issuecomment-1366726144) to create a truly generic `Average()` for both `double` and `decimal`, the compiler would complain about ambiguity, so I wound up with `DecimalGenericMathExtensions.Average`. It doesn't mean it can't be done, but I could not figure it out. + +### Quantities are not numbers, they are numbers + units + +We can't implement `INumber<>`, but we can implement some individual generic math interfaces like `IAdditionOperators`, `IMultiplyOperators`, `IAdditiveIdentity`. + +Units can trip up things like `Average()` using `IAdditiveIdentity` as the starting value, which typically maps to `Length.Zero`. Since addition picks the left hand side unit, the addition argument order must be so that the accumulated value is on the right hand side, to avoid getting the base unit instead of the first item's unit. + +## Decisions and challenges + +1. Can we avoid having to specify the numeric type `Length` everywhere? + 1. We could have a default `Length` and a generic `Length`, where the default was like today with `double` for all or most quantities. Due to `struct` not supporting inheritance, we would have to create twice as many quantities as today. + 1. If changed from `struct` to `class`, we could create derived types in different namespaces, like `UnitsNet.Double.Length` and `UnitsNet.Decimal.Length` deriving from `Length`. diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md new file mode 100644 index 0000000000..0ff86eb36e --- /dev/null +++ b/Docs/extending-with-custom-units.md @@ -0,0 +1,119 @@ +# Extending with Custom Units + +This article is for when you want to add your own custom quantities and units at runtime, not included in the UnitsNet nuget. + +To add new quantities or units to the `UnitsNet` nuget, please see [Adding a New Unit](adding-a-new-unit.md). + +## Disclaimer: This is highly experimental and incomplete + +You miss out on the statically generated code for members like `Length.FromMeters(1)` and `myLength.Meters`. +Conversion methods like `myLength.As()` and `myLength.ToUnit()` currently only support their respective unit enums, in this case `LengthUnit`. + +## Can I add a custom unit to an existing quantity in UnitsNet? + +Currently, no. + +You can only add new custom quantities with their own units and you can only add it at runtime. No code generation. + +See [example quantity `HowMuch` below](#example-custom-quantity-howmuch-with-units-howmuchunit). + +Since UnitsNet is so statically typed, your options are limited to: +1. Submit a pull request to [add a new unit](adding-a-new-unit.md) to the UnitsNet nuget +2. Build your own custom version of UnitsNet + +## Why add a custom quantity? + +Good question. + +In its current state, the support for custom quantities and units is limited and provides limited integration with the existing units and code. +We consider it exploratory, to see what is possible, and we welcome ideas on how it can be improved. + +### Key benefits + +- Reuse functionality that operates on `IQuantity` + - Dynamically convert to unit with `.As(Enum)` + - `UnitMath` with min, max, sum, average etc. + - .NET generic math support, currently sum and average. +- Reuse `UnitConverter` to dynamically convert between units by their enum values + - Also allows you to dynamically convert between your custom units and the built-in units, such as `CustomLengthUnit.ElbowToThumb` to `LengthUnit.Meter`. +- Reuse `QuantityParser` and `UnitParser` to parse quantity strings like "5 cm" and "cm" for your own quantities and units + +### What could be better + +- Source generators via nuget, if possible [Using source generators #902](https://github.com/angularsen/UnitsNet/issues/902) +- String-based lookup instead of enum-based for quantity methods like `As()` and `ToUnit()`, required for [XP One nuget per quantity #1181](https://github.com/angularsen/UnitsNet/pull/1181) + +Got more ideas? Create a discussion or issue. + +## Units.NET structure + +Units.NET roughly consists of these parts: +* Quantities like `Length` and `Force` +* Unit enum values like `LengthUnit.Meter` and `ForceUnit.Newton` +* `UnitAbbreviationsCache`, `UnitParser`, `QuantityParser` and `UnitConverter` for parsing and converting quantities and units +* JSON files for defining units, conversion functions and abbreviations +* `CodeGen` console app to generate C# code based on JSON files + +## Example: Custom quantity `HowMuch` with units `HowMuchUnit` + +### Sample output +``` +GetDefaultAbbreviation(): sm, lts, tns +Parse(): Some, Lots, Tons + +Convert 10 tons to: +200 sm +100 lts +10 tns +``` + +### Map unit enum values to unit abbreviations + +```c# +UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Some, "sm"); +UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Lots, "lts"); +UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Tons, "tns"); +``` + +### Lookup unit abbreviations from enum values + +```c# +Console.WriteLine("GetDefaultAbbreviation(): " + string.Join(", ", + UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Some), // "sm" + UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Lots), // "lts" + UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Tons) // "tns" +)); +``` + +### Parse unit abbreviations back to enum values + +```c# +Console.WriteLine("Parse(): " + string.Join(", ", + UnitParser.Default.Parse("sm"), // Some + UnitParser.Default.Parse("lts"), // Lots + UnitParser.Default.Parse("tns") // Tons +)); +``` + +### Convert between units of custom quantity + +```c# +var unitConverter = UnitConverter.Default; +unitConverter.SetConversionFunction(HowMuchUnit.Lots, HowMuchUnit.Some, x => new HowMuch(x.Value * 2, HowMuchUnit.Some)); +unitConverter.SetConversionFunction(HowMuchUnit.Tons, HowMuchUnit.Lots, x => new HowMuch(x.Value * 10, HowMuchUnit.Lots)); +unitConverter.SetConversionFunction(HowMuchUnit.Tons, HowMuchUnit.Some, x => new HowMuch(x.Value * 20, HowMuchUnit.Some)); + +var from = new HowMuch(10, HowMuchUnit.Tons); +IQuantity Convert(HowMuchUnit toUnit) => unitConverter.GetConversionFunction(from.Unit, toUnit)(from); + +Console.WriteLine($"Convert 10 tons to:"); +Console.WriteLine(Convert(HowMuchUnit.Some)); // 200 sm +Console.WriteLine(Convert(HowMuchUnit.Lots)); // 100 lts +Console.WriteLine(Convert(HowMuchUnit.Tons)); // 10 tns +``` + +### Sample quantity + +See the sample implementation in the test suite: +- [HowMuchUnit.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomQuantities/HowMuchUnit.cs) +- [HowMuch.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomQuantities/HowMuch.cs) diff --git a/Docs/nanoframework.md b/Docs/nanoframework.md new file mode 100644 index 0000000000..d1aca78d8b --- /dev/null +++ b/Docs/nanoframework.md @@ -0,0 +1,19 @@ +# .NET nanoFramework + +.NET nanoFramework is a free and open-source platform that enables the writing of managed code applications for constrained embedded devices. It is suitable for many types of projects including IoT sensors, wearables, academic proof of concept, robotics, hobbyist/makers creations or even complex industrial equipment. + +https://www.nanoframework.net/ + +## Update nanoFramework dependencies + +Units.NET publishes quantities as individual nuget packages, which means there is a large number of projects to maintain. +To overcome this, the CodeGen project generates the solution file, project files and .nuspec files - in addition to source code. + +The dependencies are hard coded in the code generator, so in order to update the dependencies you must specify an extra flag. + +```sh +cd CodeGen +dotnet run --update-nano-framework-dependencies +``` + +As of this writing, `mscorlib` and `System.Math` are the two nuget dependencies to update. diff --git a/Docs/precision.md b/Docs/precision.md new file mode 100644 index 0000000000..6f929d4798 --- /dev/null +++ b/Docs/precision.md @@ -0,0 +1,20 @@ +# Precision of Conversions and Representations + +Units.NET was not designed for high-precision, but rather a tool of convenience and simplicity. As a result, there is usually a small error involved in both representing a value of a unit and converting between units. We are open to ideas how to improve this, while still keeping it simple and convenient. + +- A base unit is chosen for all quantities + - SI base unit is preferred where available, such as `LengthUnit.Meter` and `VolumeUnit.CubicMeter`. + - `MassUnit.Gram` was chosen to better support SI prefixes like `kilo`, `mega` etc. +- The value is typically represented by a `double` value (64-bit) +- Conversions go via the base unit. + - Centimeter => Meter => Kilometer + - As a result, most conversions have a rounding error. The error is larger for units that are way larger or way smaller than the base unit. + - A rounding error of `1e-5` is accepted for round-trip conversion of most units in the library. In many use cases this is sufficient, but for others this may not be acceptable. + - There is support for [custom conversion functions](https://github.com/angularsen/UnitsNet#convert-between-units-of-custom-quantity) between unit A to unit B, typically to add 3rd party units. This can also be used to improve the precision for specific conversions since it no longer converts via the base unit. + +## Test precision + +When adding test values for unit conversions: +- Use **at least 7** [significant figures](https://en.wikipedia.org/wiki/Significant_figures) where possible +- Beyond **16** significant digits is not useful due to `double` precision limits +- Tests accept an error margin of `1e-5` for most units diff --git a/Docs/saving-to-database.md b/Docs/saving-to-database.md new file mode 100644 index 0000000000..ea770ba994 --- /dev/null +++ b/Docs/saving-to-database.md @@ -0,0 +1,48 @@ +# Saving to Database + +There is currently no out-of-the-box solution for storing UnitsNet quantities and units in an SQL database, or any other database. + +The main reason is that UnitsNet does occasionally need to do breaking changes on the unit definitions: +- Rename or remove a quantity or a unit +- Change the unit abbreviations for a unit + +It is not trivial to ensure backwards compatibility for these changes, for different types of databases and ORM frameworks. +Handling it properly may involve storing the version of UnitsNet with the data, and having migration steps between versions. + +## Simple approaches + +### 1. Single column for the value + +Store the `Value` of the quantity, converted to _some_ base unit (e.g. storing all values as `MassUnit.Gram` or `VolumeUnit.Milliliter`). This is probably the only option that can support direct database queries (anything else would require some form of a `CASE` switching query on the set of applicable units). This option assumes that the UI is responsible for actually selecting an appropriate display unit for the quantity. + +### 2. Two columns for value and unit + +Map the `{Value, Unit}` pair to two columns. This assumes that the user is expecting to see the quantity in the same unit as it was entered. The slight problem here is when dealing with the non-default abbreviations — if the UI supports the input of the quantity using _all_ of the unit abbreviations, then you wouldn't be able to restore the _exact quantity string_ (e.g. expecting "ts." instead of "tsp" for the `MetricTeaspoon`). This is of course not a huge issue, but something to keep in mind when mapping _custom abbreviations_. + +### 3. Single string column + +The code for parsing a quantity from string is still very slow, so option 2 is likely better for performance. + +## Recommended approach + +Serialize `double Value` and `string UnitName` similar to how we do [JSON serialization](serialization.md), but use your own custom serialization format to avoid any breaking changes in names of UnitsNet: + +```cs +IQuantity myQuantity = ...; + +var myDbEntity = new MyDbEntity { + Value = myQuantity.Value, + UnitName = myQuantity switch { + LengthUnit.Meter => "Length:Meter", + LengthUnit.Centimeter => "Length:Centimeter", + MassUnit.Kilogram => "Mass:Kilogram", + // ... and any other units we want to support serializing + _ => throw new NotImplementedException("Unit not supported for serialization to SQL: " + myQuantity.Unit); + } +}; +``` + +You would then have to parse it back out with a simple `string.Split` and similar switch conditions. +It's a boring one time job, but then you are in full control of any future changes to the library and will get compile errors if anything is renamed or removed. + +From discussion: https://github.com/angularsen/UnitsNet/discussions/1513#discussioncomment-12243230 diff --git a/Docs/serialization.md b/Docs/serialization.md new file mode 100644 index 0000000000..34e1be5482 --- /dev/null +++ b/Docs/serialization.md @@ -0,0 +1,155 @@ +# Serialization + +- [(Recommended) Map to your own custom DTO types](#recommended-map-to-your-own-custom-dto-types) +- [UnitsNet.Serialization.JsonNet with Json.NET (Newtonsoft)](#unitsnetserializationjsonnet-with-jsonnet-newtonsoft) +- [DataContractSerializer for XML](#datacontractserializer-for-xml) +- [DataContractJsonSerializer for JSON (not recommended)](#datacontractjsonserializer-for-json-not-recommended) +- [System.Text.Json (not yet implemented)](#systemtextjson-not-yet-implemented) +- [Protobuf and other `[DataContract]` compatible serializers](#protobuf-and-other-datacontract-compatible-serializers) +- [Backwards compatibility](#backwards-compatibility) + +## (Recommended) Map to your own custom DTO types + +The recommended approach is to create your own data transfer object types (DTO) and map to/from `IQuantity`. +This way you are in full control of the shape of your JSON, XML, etc. and also any breaking changes or deprecations to UnitsNet. + +It could be solved like this, storing the value, quantity name and unit name: + +```c# +// Your custom DTO type for quantities. +public record QuantityDto(double Value, string QuantityName, string UnitName); + +// The original quantity. +IQuantity q = Length.FromCentimeters(5); + +// Map to your custom DTO type. +QuantityDto dto = new( + Value: (double)q.Value, + QuantityName: q.QuantityInfo.Name, + UnitName: q.Unit.ToString()); + +/* Serialize to JSON: +{ + "Value": 5, + "QuantityName": "Length", + "UnitName": "Centimeter" +} +*/ +string json = System.Text.Json.JsonSerializer.Serialize(dto); + +// Deserialize from JSON. +QuantityDto deserialized = System.Text.Json.JsonSerializer.Deserialize(json)!; + +// Map back to IQuantity. +if (Quantity.TryFrom(deserialized.Value, deserialized.QuantityName, deserialized.UnitName, out IQuantity? deserializedQuantity)) +{ + // Take your quantity and run with it. +} +``` + +Alternatively, you can choose to use our custom serializers to map to/from `IQuantity` to JSON, XML etc. +We strive to avoid breaking changes, but we can't guarantee it. + +## UnitsNet.Serialization.JsonNet with Json.NET (Newtonsoft) + +### Example + +```c# +var jsonSerializerSettings = new JsonSerializerSettings {Formatting = Formatting.Indented}; +jsonSerializerSettings.Converters.Add(new UnitsNetIQuantityJsonConverter()); + +string json = JsonConvert.SerializeObject(new { Name = "Raiden", Weight = Mass.FromKilograms(90) }, jsonSerializerSettings); + +object obj = JsonConvert.DeserializeObject(json); +``` + +JSON output: +```json +{ + "Name": "Raiden", + "Weight": { + "Unit": "MassUnit.Kilogram", + "Value": 90.0 + } +} +``` + +### Serializing `IComparable` + +If you need to support deserializing into properties/fields of type `IComparable` instead of type `IQuantity`, then you can add +```c# +jsonSerializerSettings.Converters.Add(new UnitsNetIComparableJsonConverter()); +``` + +## DataContractSerializer for XML + +All quantities and the `IQuantity` interface have `[DataContract]` annotations and can be serialized by the built-in XML [DataContractSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer). + +```xml + + 1.20 + Milliwatt + +``` + +Serializing `IQuantity` with additional type information: +```c# +[DataContract] +[KnownType(typeof(Mass))] +[KnownType(typeof(Information))] +public class Foo +{ + [DataMember] + public IQuantity Quantity { get; set; } +} + +// Serialized object +new Foo { Quantity = new Information(1.20m, InformationUnit.Exabyte) }; +``` +```xml + + + 1.20 + Exabyte + + +``` + +## DataContractJsonSerializer for JSON (not recommended) + +For JSON, we recommend [UnitsNet.Serialization.JsonNet](https://www.nuget.org/packages/UnitsNet.Serialization.JsonNet) with Json.NET (Newtonsoft) instead. + +All quantities and the `IQuantity` interface have `[DataContract]` annotations and can be serialized by the built-in JSON [DataContractJsonSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.json.datacontractjsonserializer). + +It is not recommended, because the enum value is serialized as integer and this value is not stable. + +Schema: +```json +{ + "__type": "Information:#UnitsNet", + "Value": 1.20, + "Unit": 4 +} +``` + +## System.Text.Json (not yet implemented) + +See +- [WIP: Add serialization support for System.Text.Json #905](https://github.com/angularsen/UnitsNet/pull/905) +- [Add serialization support for System.Text.Json (Attempt #2) #966](https://github.com/angularsen/UnitsNet/pull/966) + +## Protobuf and other `[DataContract]` compatible serializers + +TODO Test and document here. + +## Backwards compatibility + +We strive to maintain backwards compatibility of round-trip serialization within a major version. +However, the quantities and units themselves are inherently not stable: + +- The base unit of quantities has changed several times in the history, e.g. Kilogram -> Gram. +- The unit enum value is not stable due to code generator sorting units alphabetically. + +This is why the full unit name is serialized in Json.NET, so we can avoid ambiguity and be robust to any internal changes of the quantities and units. diff --git a/Docs/string-formatting.md b/Docs/string-formatting.md new file mode 100644 index 0000000000..9f6ce8ec20 --- /dev/null +++ b/Docs/string-formatting.md @@ -0,0 +1,51 @@ +# String Formatting + +## Common examples + +Assuming computer running with US English culture. +```c# +var length = Length.FromCentimeters(3.14159265358979); + +// Typical formats +length.ToString(); // 3.14 cm +length.ToString("s4"); // 3.1416 cm + +// Localized +length.ToString(new CultureInfo("nb-NO")); // 3,14 cm +length.ToString(new CultureInfo("ru-RU")); // 3,14 sm (Cyrillic) + +// Converted +length.As(LengthUnit.Meters).ToString(); // 0.13 m + +// Use .NET's built-in formatting methods +Console.WriteLine("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft" +string.Format("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft" +$"Length is {l:v} {l:a}"; // "Length is 3.14159265358979 ft" +``` + +## Standard Quantity Format Strings + +| Format specifier | Description | Examples | +|------------------|-------------|---------| +| "g" | General quantity pattern. Equivalent to parameterless `ToString()`. Rounds to 2 significant digits after the radix. | `Length.FromFeet(Math.PI).ToString("g")` -> 3.14 ft | +| `f`, `f2`, ... `e`, `e3`, ... `r` `#.0` `00000.0` | [Standard numeric formatting](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#standard-format-specifiers) of value, with unit appended. | `Length.FromFeet(Math.PI).ToString("f")` -> 3.140 ft, `Length.FromFeet(Math.PI).ToString("f1")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("r")` -> 3.141592653589793 ft, `Length.FromFeet(Math.PI).ToString("e2")` -> 3.14e+000 ft, `Length.FromFeet(Math.PI).ToString("#.0")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("00.0")` -> 003.1 ft | +| "aXX" | Unit abbreviation pattern. If more than one abbreviation is defined for the unit, then XX specifies the zero-indexed position in the array of abbreviations. XX defaults to 0. If the position is not found, `System.FormatException` is thrown. | `Length.FromFeet(Math.PI).ToString("a")` -> ft, `Length.FromFeet(Math.PI).ToString("a0")` -> ft, `Length.FromFeet(Math.PI).ToString("a1")` -> ', `Length.FromFeet(Math.PI).ToString("a2")` -> prime symbol, `Length.FromFeet(Math.PI).ToString("a3")` -> System.FormatException | +| "q" | Quantity name pattern. Outputs the corresponding QuantityType enum name. | `Length.FromFeet(Math.PI).ToString("q")` -> Length, `Mass.FromTonnes(Math.PI).ToString("u")` -> Mass | +| "u" | Unit name pattern. Each quantity type has a corresponding unit enum, such as `Length` quantity having `LengthUnit` unit enum with values `Meter`, `Centimeter` etc. This pattern outputs the unit enum name. | `Length.FromFeet(Math.PI).ToString("u")` -> Foot, `Mass.FromTonnes(Math.PI).ToString("u")` -> Tonne | + +There are three different overloads of the ToString() method to provide a string representation of a value and its units. + +## Number Formatting + +For "g" pattern (or if no pattern is specified), the number will be formatted with scientific notation for very small or very large values to increase readability. We did not find .NET's default behavior to work well for this so we created our own rules. + +| Interval | Format | Examples | +|-----------|-----------|-------------| +| `(-inf <= x < 1e-03]` | scientific notation | 1e-04; 2.13e-05 | +| `[1e-03 <= x < 1e+03]` | fixed point notation | 0.001; 0.01; 100 | +| `[1e+03 <= x < 1e+06]` | fixed point notation with digit grouping | 1,000; 10,000; 100,000 | +| `[1e+06 <= x <= +inf)` | scientific notation | 1.1e+06; 3.14e+07 | + +The symbols used for digit grouping and radix point are culture-sensitive. The above examples use `CultureInfo.InvariantCulture`. + +For more examples, refer to the unit tests in [UnitsNet/UnitFormatter.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/UnitFormatter.cs). diff --git a/Docs/upgrading-from-3.x-to-4.x.md b/Docs/upgrading-from-3.x-to-4.x.md new file mode 100644 index 0000000000..adf180e031 --- /dev/null +++ b/Docs/upgrading-from-3.x-to-4.x.md @@ -0,0 +1,114 @@ +# Upgrading from 3.x to 4.x + +The 4.0 release addresses a long wishlist of breaking changes accumulated over the years. The main theme was about binary size so we removed a lot of code we considered unnecessary syntactic sugar, such as number extension methods and nullable factory methods. We also heavily restructured some core types such as splitting up the `UnitSystem` God-class and addressed behavior we considered incorrect or not optimal. + +This document lists the most common upgrade paths we expect you to come across when upgrading. + +## UnitSystem removed + +`UnitSystem` was first split up and removed into more specific classes. Later an entirely new `UnitSystem` was added to represent a choice of base unit dimensions. + +### UnitSystem abbreviations => UnitAbbreviationsCache + +For looking up or mapping custom unit abbreviations for unit enum types only known at runtime, use `UnitAbbreviationsCache` instead of `UnitSystem`: +```c# +var abbreviations = UnitAbbreviationsCache.Default; +abbreviations.GetAllUnitAbbreviationsForQuantity(typeof(LengthUnit)); // ["cm", "dm",...] +abbreviations.GetUnitAbbreviations(typeof(LengthUnit), 6); // ["ft", "'", "`"] +abbreviations.GetDefaultAbbreviation(typeof(LengthUnit), 1); // "cm" +abbreviations.MapUnitToAbbreviation(typeof(LengthUnit), 1, new CultureInfo("en-US"), "foo"); // add custom abbreviation for centimeter +``` + +Looking up abbreviations for static quantity types, you can use `Length.Units` as before. + +### UnitSystem parsing => UnitParser + +Dynamic parsing of unit abbreviations, where unit enum type is only known at runtime, is now done by `UnitParser`: +```c# +var parser = UnitParser.Default; +parser.Parse("cm", typeof(LengthUnit)); // 1 +parser.TryParse("cm", typeof(LengthUnit), out object val); // returns true, val = 1 +``` + +For static parsing, you can use `Length.ParseUnit("cm")` as before. + +### UnitSystem.DefaultCulture => GlobalConfiguration.DefaultCulture + +To change the default culture used by `ToString()` in quantities: +```c# +GlobalConfiguration.DefaultCulture = new CultureInfo("en-US"); // instead of UnitSystem.DefaultCulture +``` + +### Removed static methods on UnitSystem + +Use `UnitParser.Default` or `UnitAbbreviationsCache.Default` instead, or create an instance for the culture of your own choice. + +## Quantity types + +### Nullable From-methods removed + +```c# +int? val = null; + +// Instead of this +Length? l = Length.FromMeters(val); + +// Do this, or create your own convenience factory method +Length? l = val == null ? (Length?)null : Length.FromMeters(val.Value); +``` + +### Remove unit parameter from ToString() methods + +```c# +// Instead of +myLength.ToString(LengthUnit.Centimeter); // "100 cm" + +// Do this +myLength.ToUnit(LengthUnit.Centimeter).ToString(); // "100 cm" +``` + +### Stricter parsing + +```c# +// Instead of +var l = Length.Parse("5ft 3in"); // Throws FormatException + +// Do this, special case for feet/inches +var l = Length.ParseFeetInches("5ft 3in"); + +// Everything else only supports these formats +var l = Length.Parse("1cm"); +var l = Length.Parse("1 cm"); +var l = Length.Parse("1 cm"); // Multi-space allowed +var l = Length.Parse(" 1 cm "); // Input is trimmed +``` + +## Length2d removed + +Provide your own or use `Area` if applicable. + +## Number extension methods removed + +Either create your own or use From factory methods. +```c# +Length m = 1.Meters(); // No longer possible +Length m = Length.FromMeters(1); // Use this instead +``` + +## Do not allow NaN or Infinity + +```c# +// Instead of +Length m = new Length(double.NaN, LengthUnit.Meter); // Throws ArgumentException +if(double.IsNaN(m.Value)) + // handle NaN + +// Do this +Length? m = null; +if(m == null) + // handle null +``` + +## UnitsNet.Serialization.JsonNet 1.x to 4.x + +There are no breaking changes in this version jump, the change only reflects a new versioning strategy to share major semver version with UnitsNet. This means consumers need to upgrade this library to 4.x to use UnitsNet 4.x, but no other actions or migrations are needed. diff --git a/Docs/upgrading-from-4.x-to-5.x.md b/Docs/upgrading-from-4.x-to-5.x.md new file mode 100644 index 0000000000..ef5048be29 --- /dev/null +++ b/Docs/upgrading-from-4.x-to-5.x.md @@ -0,0 +1,33 @@ +# Upgrading from 4.x to 5.x + +Most of the removed code was marked as obsolete for a long time in v4 before removed in v5 with details on what to replace the usage with. + +## Breaking changes + +1. Localization is based on `CultureInfo.CurrentCulture` instead of `CurrentUICulture` (#795, #986) +1. `IQuantity` returns `QuantityValue` instead of `double` (#1074) +1. Decimal based quantities return `decimal` instead of `double` (`Power`, `BitRate` and `Information`) (#1074) +1. Equality changed to strict equality so BOTH unit and value must match exactly. 100 cm != 1 m. [Read more on why.](https://github.com/angularsen/UnitsNet/issues/1193#issuecomment-1424843242) **Fix: Use `Equals(other, tolerance, comparisonType)`.** + +## New + +1. `QuantityValue`: Implement IEquality, IComparable, IFormattable +1. `QuantityValue`: 16 bytes instead of 40 bytes (#1084) +1. Add `[DataContract]` annotations (#972) + +## Removed + +1. Remove targets: net40, net47, Windows Runtime Component. **Fix: Use netstandard2.0.** +1. Remove `Undefined` enum value for all unit enum types. **Fix: Use `null`.** +1. Remove `QuantityType` enum. **Fix: Use strings.** +1. Remove `IQuantity.Units` and `.UnitNames`. **Fix: Use QuantityInfo.** +1. Remove `GlobalConfiguration`. **Fix: Change CultureInfo.CurrentCulture.** +1. Remove `Molarity` ctor and operator overloads. **Fix: Use MassConcentration.FromMolarity(), ToMolarity().** +1. Remove `MinValue`, `MaxValue` per quantity due to ambiguity. **Fix: Define your own min/max quantity values.** +1. Remove string format specifiers: "v", "s". **Fix: Use `Value` property and standard .NET numeric format strings.** +1. json: Remove UnitsNetJsonConverter. **Fix: Use UnitsNetIQuantityJsonConverter or AbbreviatedUnitsConverter.** + +## JSON unit definition schema changes + +Rename `BaseType` to `ValueType`, for values "double" and "decimal". +Rename `XmlDoc` to `XmlDocSummary`. diff --git a/Docs/upgrading-from-5.x-to-6.x.md b/Docs/upgrading-from-5.x-to-6.x.md new file mode 100644 index 0000000000..feef675caf --- /dev/null +++ b/Docs/upgrading-from-5.x-to-6.x.md @@ -0,0 +1,63 @@ +# Upgrading from 5.x to 6.x + +Before upgrading to a new major version, first upgrade to the latest minor version and follow instructions on any build warnings by code marked as obsolete. This can make it easier to migrate. + +## Summary of changes in v6 + +The biggest change is removing support for `decimal` quantities and converting `Power`, `Information`, `BitRate` from `decimal` to `double`. +The value holder type `QuantityValue` is replaced by `double`. + +The motivation was to remove a lot of complexity in the code base. Decimal was initially added for precision issues, but this was later fixed by storing both value and unit. Only `Information` still had any real benefit from `decimal`, to better represent `Bit` as an integer type and avoid rounding errors. + +If there is still enough demand for representing bits as an integer or avoiding rounding errors in `Information` and other quantities, then we can approach that in a simpler way like `TimeSpan.Ticks` vs `TimeSpan.TotalSeconds`. + +If there is sufficient demand for supporting any number type like `float`, `decimal` or even `Half`, then a more holistic approach is required using generics, which brings its own challenges. + +## New + +- Allow `NaN`, `Inf` values for quantities with `double` value type #1289 + +## Breaking changes + +### Binary incompatible + +- Remove `decimal` support in quantities #1359, everything is now `double` + - Convert quantities `Power`, `Information`, `BitRate` from `decimal` to `double` #1195, #1353 + - Remove `QuantityValue`, replaced with `double` +- Remove `TValueType` from interfaces + - Remove `IQuantity` + - Remove `IValueQuantity` + - Change `IQuantity` to `IQuantity` + - Change `IArithmeticQuantity` to `IArithmeticQuantity` +- Remove obsolete units #1372 + - `CoefficientOfThermalExpansion.InverseKelvin`, `InverseDegreeCelsius`, `InverseDegreeFahrenheit` + - `HeatTransferCoefficient.BtuPerSquareFootDegreeFahrenheit` +- Fix typo in plural form of several units #1347, #1351 + - `TemperatureGradient.DegreesCelsiusPerMeter` + - `Density.GramsPerDeciliter` + - `ElectricPotentialChangeRate.VoltsPerSecond`, `VoltsPerMicrosecond`, `VoltsPerMinute`, `VoltsPerHour` + - `FuelEfficiency.KilometersPerLiter` + - `Speed.MetersPerMinute` +- Moved 29 operator overloads for multiply or division to another type ([details](https://github.com/angularsen/UnitsNet/pull/1329#discussion_r1451794868)), e.g. `Energy op_Multiply(Duration, Power)` moved from `Power` to `Duration` #1329 + +### Source incompatible + +- `IQuantity.UnitInfo` is now a interface default member on .NET5+, and may compete with any custom property implemented in third party quantities #1649 + +### Behavioral change + +None. + +### Description of different kinds of incompatible changes + +https://learn.microsoft.com/en-us/dotnet/core/compatibility/8.0 + +> Binary incompatible - When run against the new runtime or component, existing binaries may encounter a breaking change in behavior, such as failure to load or execute, and if so, require recompilation. +> +> Source incompatible - When recompiled using the new SDK or component or to target the new runtime, existing source code may require source changes to compile successfully. +> +> Behavioral change - Existing code and binaries may behave differently at run time. If the new behavior is undesirable, existing code would need to be updated and recompiled. + +## JSON unit definition schema changes + +- Removed `"ValueType": "decimal"` used for `decimal` quantities