Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8042ec4
feat: add exact rational arithmetic and generic math support
ScottishTapWater Oct 21, 2026
a9afd4f
feat: add rational dimension algebra and operators
ScottishTapWater Oct 21, 2026
ead045e
chore: adopt C# editor configuration
ScottishTapWater Oct 21, 2026
ac735a0
chore: enforce LF line endings for text files
ScottishTapWater Oct 21, 2026
421d0cd
feat: add quantity type system and exact number codec
ScottishTapWater Oct 21, 2026
f323924
chore: ignore local handoff files
ScottishTapWater Oct 21, 2026
fdf7bff
refactor: rename quantity SI value property
ScottishTapWater Oct 21, 2026
aebb131
refactor: pair dimension exponents with component names
ScottishTapWater Oct 21, 2026
8da6c02
feat: generate dimension names and SI unit expressions
ScottishTapWater Oct 21, 2026
56cd6e1
refactor: omit zero dimension components
ScottishTapWater Oct 21, 2026
1d292fb
refactor: move quantity into type system
ScottishTapWater Oct 21, 2026
593ffb5
chore: Deleted placeholders
ScottishTapWater Oct 21, 2026
10aa7c0
test: verify packaged generator loading on .NET 10
ScottishTapWater Oct 21, 2026
e5136ad
feat: add v1 engineering quantity catalogue
ScottishTapWater Oct 21, 2026
ed2d591
feat: generate Voltage aliases and unit helpers
ScottishTapWater Oct 22, 2026
ef75026
feat: generate SI-prefixed Voltage conversions
ScottishTapWater Oct 22, 2026
5e322cc
feat: generate SI base quantities with shared unit metadata
ScottishTapWater Oct 22, 2026
d3e16b8
feat: generate engineering quantities with compound unit conversions
ScottishTapWater Oct 22, 2026
403a20d
refactor: clarify catalogue unit expressions with named SI prefixes
ScottishTapWater Oct 22, 2026
4f1686b
feat: centralise quantity conversions and generate prefixed unit helpers
ScottishTapWater Oct 22, 2026
dcfcf3c
refactor: Pulled discovery and source emission out
ScottishTapWater Oct 22, 2026
714ff4c
refactor: render quantity source from definitions and templates
ScottishTapWater Oct 22, 2026
29a8c98
refactor: organise dimensions units and naming namespaces
ScottishTapWater Oct 22, 2026
a8e4af6
feat: discover and generate quantity arithmetic
ScottishTapWater Oct 22, 2026
4ae5b34
ci: configure NuGet trusted publishing and package metadata
ScottishTapWater Oct 22, 2026
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
104 changes: 104 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
root = true

[*.cs]
indent_style = tab
indent_size = tab
tab_width = 4
csharp_new_line_before_open_brace = all
csharp_style_namespace_declarations = file_scoped

# State accessibility explicitly on types and non-interface members.
dotnet_style_require_accessibility_modifiers = for_non_interface_members
dotnet_diagnostic.IDE0040.severity = warning

# Braces are required even around a single statement.
csharp_prefer_braces = true
dotnet_diagnostic.IDE0011.severity = warning

# Long lines are acceptable; this is an editor guide, not a compiler limit.
max_line_length = 240

# Declare the type explicitly, including when the initializer makes it obvious.
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
csharp_style_var_elsewhere = false
dotnet_diagnostic.IDE0008.severity = warning

# Use target-typed new() when the destination type is apparent.
csharp_style_implicit_object_creation_when_type_is_apparent = true
dotnet_diagnostic.IDE0090.severity = warning

# Do not suggest primary constructors for classes or structs. This does not
# diagnose existing primary constructors; the record-only rule needs review
# guidance or a separate analyzer.
csharp_style_prefer_primary_constructors = false
dotnet_diagnostic.IDE0290.severity = none

# Requires StyleCop.Analyzers in the consuming project. Its default setting
# only covers some type kinds; strict one-type-per-file also needs stylecop.json.
dotnet_diagnostic.SA1402.severity = warning

# Standard C# identifier casing.
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = *
dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate
dotnet_naming_symbols.types.applicable_accessibilities = *
dotnet_naming_symbols.members.applicable_kinds = property, method, event, local_function
dotnet_naming_symbols.members.applicable_accessibilities = *
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_symbols.constants.applicable_kinds = field
dotnet_naming_symbols.constants.applicable_accessibilities = *
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = local, parameter
dotnet_naming_symbols.locals_and_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *

dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.interface_pascal_case.capitalization = pascal_case
dotnet_naming_style.interface_pascal_case.required_prefix = I
dotnet_naming_style.camel_case.capitalization = camel_case
dotnet_naming_style.private_field_camel_case.capitalization = camel_case
dotnet_naming_style.private_field_camel_case.required_prefix = _
dotnet_naming_style.type_parameter_pascal_case.capitalization = pascal_case
dotnet_naming_style.type_parameter_pascal_case.required_prefix = T

dotnet_naming_rule.interfaces.symbols = interfaces
dotnet_naming_rule.interfaces.style = interface_pascal_case
dotnet_naming_rule.interfaces.severity = warning
dotnet_naming_rule.types.symbols = types
dotnet_naming_rule.types.style = pascal_case
dotnet_naming_rule.types.severity = warning
dotnet_naming_rule.members.symbols = members
dotnet_naming_rule.members.style = pascal_case
dotnet_naming_rule.members.severity = warning
dotnet_naming_rule.private_fields.symbols = private_fields
dotnet_naming_rule.private_fields.style = private_field_camel_case
dotnet_naming_rule.private_fields.severity = warning
dotnet_naming_rule.constants.symbols = constants
dotnet_naming_rule.constants.style = pascal_case
dotnet_naming_rule.constants.severity = warning
dotnet_naming_rule.locals_and_parameters.symbols = locals_and_parameters
dotnet_naming_rule.locals_and_parameters.style = camel_case
dotnet_naming_rule.locals_and_parameters.severity = warning
dotnet_naming_rule.type_parameters.symbols = type_parameters
dotnet_naming_rule.type_parameters.style = type_parameter_pascal_case
dotnet_naming_rule.type_parameters.severity = warning
dotnet_diagnostic.IDE1006.severity = warning

# Keep analyzer warnings out of generated source.
[*.g.cs]
generated_code = true

[*.g.i.cs]
generated_code = true

[*.generated.cs]
generated_code = true

[*.designer.cs]
generated_code = true

[*.Designer.cs]
generated_code = true
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
4 changes: 4 additions & 0 deletions .github/scripts/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ if [[ -n "${ACT:-}" ]]; then
fi
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
: "${GITHUB_REPOSITORY_OWNER:?GITHUB_REPOSITORY_OWNER is required}"
: "${NUGET_API_KEY:?NUGET_API_KEY is required}"
dotnet nuget push 'artifacts/packages/*.nupkg' \
--source 'https://api.nuget.org/v3/index.json' \
--api-key "$NUGET_API_KEY" --skip-duplicate
dotnet nuget push 'artifacts/packages/*.nupkg' \
--source "https://nuget.pkg.github.com/${GITHUB_REPOSITORY_OWNER}/index.json" \
--api-key "$GITHUB_TOKEN" --skip-duplicate
2 changes: 1 addition & 1 deletion .github/testing/run-act.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ def git(*arguments):
if args.workflow in ("ci", "both"):
subprocess.run([*base, "pull_request", "-W", ".github/workflows/ci.yml"], cwd=fixture, check=True)
if args.workflow in ("release", "both"):
subprocess.run([*base, "push", "-W", ".github/workflows/release.yml"], cwd=fixture, check=True)
subprocess.run([*base, "push", "-W", ".github/workflows/cd.yml"], cwd=fixture, check=True)
16 changes: 12 additions & 4 deletions .github/workflows/release.yml → .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,29 @@ jobs:
permissions:
contents: write
packages: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
- uses: actions/setup-dotnet@v6.0.0
with:
global-json-file: global.json
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
- uses: actions/setup-node@v7.1.0
with:
node-version: '24.10.0'
- name: Prepare isolated act release destination
if: env.ACT
run: bash .github/testing/prepare-local-remote.sh
- name: NuGet trusted publishing login
if: ${{ !env.ACT }}
uses: NuGet/login@v1
id: nuget-login
with:
user: ${{ vars.NUGET_USER }}
- name: Version, package and release
id: release
uses: cycjimmy/semantic-release-action@d9bf53a078bd2f86aaffaec796dbb3d28f1b6f34 # v8.1.3
uses: cycjimmy/semantic-release-action@v8.1.3
with:
semantic_version: 25.0.9
repository_url: ${{ env.ACT && 'file:///tmp/dimensional-quantities-remote.git' || '' }}
Expand All @@ -46,6 +53,7 @@ jobs:
conventional-changelog-conventionalcommits@9.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }}
- name: Verify local release
if: env.ACT
env:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Check conventional PR title
if: github.event_name == 'pull_request'
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: python3 .github/scripts/check-pr-title.py
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
- uses: actions/setup-dotnet@v6.0.0
with:
global-json-file: global.json
- name: Restore
run: dotnet restore src/DimensionalQuantities.slnx
- name: Build
run: dotnet build src/DimensionalQuantities.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true
- name: Test
run: dotnet test --solution src/DimensionalQuantities.slnx -c Release --no-build --no-restore --report-trx --ignore-exit-code 8 --results-directory artifacts/test-results
run: dotnet test --solution src/DimensionalQuantities.slnx -c Release --no-build --no-restore --report-trx --results-directory artifacts/test-results
- name: Check NuGet package
run: bash .github/scripts/pack.sh 0.0.0-ci
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- uses: actions/upload-artifact@v7.0.2
if: ${{ always() && !env.ACT }}
with:
name: ci-results
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ TestResults/
*.snupkg
artifacts/
node_modules/
HANDOFF*.md
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 GoSim

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# DimensionalQuantities

A C# library for dimensional quantities, with generated type aliases, SI unit conversions, and compile-time dimensional arithmetic.

> [!WARNING]
> We developed this library for our own internal use and are sharing it because we believe it may be useful to the wider developer community. Issues and pull requests are welcome, but we make no support commitments or warranties, and we cannot guarantee that we will respond to issues or review or merge pull requests.

## Usage

Requires .NET 10 and C# 14. The `DimensionalQuantities` NuGet package includes both the runtime library and its source generator.

```csharp
Voltage voltage = Voltage.FromVolts(12);
Current current = Current.FromAmps(3);

Resistance resistance = voltage / current;
Power power = voltage * current;

double ohms = resistance.AsOhms(); // 4
double watts = power.AsWatts(); // 36
```

### SI prefixes and compound units

Named helpers handle conversion into and out of SI prefixes, including powers of prefixed units:

```csharp
Length length = Length.FromMilliMetres(2500);
double metres = length.AsMetres(); // 2.5

Density density = Density.FromGramsPerCubicCentiMetre(1);
double kilogramsPerCubicMetre = density.AsSiBaseUnit(); // 1000
```

For more control, `FromPrefixedUnits` and `AsPrefixedUnits` accept decimal prefix exponents for each unit factor. Powers are taken into account automatically:

```csharp
// 1 A/mm² = 1,000,000 A/m²
CurrentDensity density = CurrentDensity.FromPrefixedUnits(
1, ampPrefixExponent: 0, metrePrefixExponent: -3);

double ampsPerSquareMillimetre = density.AsPrefixedUnits(
ampPrefixExponent: 0, metrePrefixExponent: -3); // 1
```

Named compound units remain a single factor where appropriate: `Voltage.FromPrefixedUnits(2, -3)` means 2 millivolts, without requiring you to know the SI dimensions of a volt.

### Arithmetic and generated intermediate quantities

The generator follows multiplication and division through nested expressions, derives their dimensions, and generates the required operators and intermediate aliases:

```csharp
Current current = Current.FromAmps(3);
Resistance resistance = Resistance.FromOhms(4);

CurrentSquared squared = current * current;
Power heating = current * current * resistance; // 36 W
Current doubled = 2 * current;
Current halved = current / 2;
Frequency frequency = 1 / Time.FromSeconds(0.02); // 50 Hz
```

Known dimensions use names from the engineering catalogue, such as `Power`, `Voltage`, `Permeability` and `MagneticFluxDensity`. Other dimensions receive mechanical names such as `CurrentSquared` or `TimePerCurrent`. Discovered quantities have SI base-unit and generic prefix conversion helpers.

Aliases with the same dimensions share a type. For example, `Energy` and `Torque` are interchangeable: the library checks dimensions, not the physical meaning of a calculation. Magnitudes are stored as `double` in coherent SI base units.

### Current scope

Arithmetic discovery currently supports multiplication and division with known quantity operands, scalar factors, and nested expressions. Roots, powers, dimensionless results and inference through unresolved intermediate local variables are not yet supported by discovery.

The library is under active development; the API and supported expression patterns are still evolving.

## Licence

[MIT](LICENSE), copyright GoSim.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../DimensionalQuantities/DimensionalQuantities.csproj" />
<ProjectReference Include="../DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj" />
<ProjectReference Include="../DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<Analyzer Include="../DimensionalQuantities/bin/$(Configuration)/net10.0/DimensionalQuantities.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="Microsoft.Build" Version="18.6.3" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.11.2" />
<PackageReference Include="MSBuild.ProjectCreation" Version="19.1.4" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Xunit;

namespace DimensionalQuantities.IntegrationTests.GeneratedQuantityTests;

[CollectionDefinition("MSBuild", DisableParallelization = true)]
public sealed class MsbuildCollection;
Loading
Loading