From 0643a6d34e09dbb1baaa4e7450bfc6f422985fd9 Mon Sep 17 00:00:00 2001 From: James McMahon Date: Sun, 25 Oct 2026 00:28:27 +0100 Subject: [PATCH] feat: add nautical unit conversions --- README.md | 11 ++++ ...uantities.Extensions.Nautical.Tests.csproj | 19 +++++++ .../NauticalConversionsTests.cs | 51 +++++++++++++++++++ ...ionalQuantities.Extensions.Nautical.csproj | 35 +++++++++++++ .../NauticalSymbols.cs | 15 ++++++ .../NauticalUnits.cs | 23 +++++++++ .../Packaging/PackagedGeneratorTests.cs | 25 +++++++++ .../Packaging/SymbolPackageTests.cs | 1 + src/DimensionalQuantities.slnx | 4 ++ 9 files changed, 184 insertions(+) create mode 100644 src/DimensionalQuantities.Extensions.Nautical.Tests/DimensionalQuantities.Extensions.Nautical.Tests.csproj create mode 100644 src/DimensionalQuantities.Extensions.Nautical.Tests/NauticalConversionsTests.cs create mode 100644 src/DimensionalQuantities.Extensions.Nautical/DimensionalQuantities.Extensions.Nautical.csproj create mode 100644 src/DimensionalQuantities.Extensions.Nautical/NauticalSymbols.cs create mode 100644 src/DimensionalQuantities.Extensions.Nautical/NauticalUnits.cs diff --git a/README.md b/README.md index a6a294f..df5105d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ double metres = depth.AsSiBaseUnit(); // 1.8288 `BritishThermalUnitIT` is the International Table definition (exactly 1055.05585262 J); other BTU definitions differ. US and Imperial liquid measures are named separately. Imperial helpers do not generate SI-prefixed variants. +Add `DimensionalQuantities.Extensions.Nautical` for international nautical miles, fathoms, square nautical miles and knots. Import its namespace to use the helpers: + +```csharp +using DimensionalQuantities.Extensions.Nautical; + +Length passage = Length.FromNauticalMiles(12); +Speed vesselSpeed = Speed.FromKnots(8); +``` + +A nautical mile is exactly 1852 m, and a knot is one nautical mile per hour. Nautical helpers do not generate SI-prefixed variants. + ```csharp Voltage voltage = Voltage.FromVolts(12); Current current = Current.FromAmps(3); diff --git a/src/DimensionalQuantities.Extensions.Nautical.Tests/DimensionalQuantities.Extensions.Nautical.Tests.csproj b/src/DimensionalQuantities.Extensions.Nautical.Tests/DimensionalQuantities.Extensions.Nautical.Tests.csproj new file mode 100644 index 0000000..acd5cd6 --- /dev/null +++ b/src/DimensionalQuantities.Extensions.Nautical.Tests/DimensionalQuantities.Extensions.Nautical.Tests.csproj @@ -0,0 +1,19 @@ + + + net11.0 + false + true + Exe + true + + + + + + + + + + + diff --git a/src/DimensionalQuantities.Extensions.Nautical.Tests/NauticalConversionsTests.cs b/src/DimensionalQuantities.Extensions.Nautical.Tests/NauticalConversionsTests.cs new file mode 100644 index 0000000..4067292 --- /dev/null +++ b/src/DimensionalQuantities.Extensions.Nautical.Tests/NauticalConversionsTests.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using DimensionalQuantities.Extensions.Nautical; +using Xunit; + +namespace DimensionalQuantities.Extensions.Nautical.Tests; + +public sealed class NauticalConversionsTests +{ + [Fact] + public void NauticalUnitsConvertToExpectedSiValues() + { + double[] result = + [ + Length.FromNauticalMiles(1).AsSiBaseUnit(), + Length.FromFathoms(1).AsSiBaseUnit(), + Area.FromSquareNauticalMiles(1).AsSiBaseUnit(), + Speed.FromKnots(1).AsSiBaseUnit(), + ]; + + double[] expected = [1852d, 1.8288d, 3_429_904d, 1852d / 3600d]; + Assert.Equal(expected, result); + } + + [Fact] + public void NauticalUnitsConvertBackFromSiValues() + { + double[] result = + [ + Length.FromSiBaseUnit(1852).AsNauticalMiles(), + Length.FromSiBaseUnit(1.8288).AsFathoms(), + Area.FromSiBaseUnit(3_429_904).AsSquareNauticalMiles(), + Speed.FromSiBaseUnit(1852d / 3600d).AsKnots(), + ]; + + Assert.Equal([1d, 1d, 1d, 1d], result); + } + + [Fact] + public void NauticalUnitsDoNotGenerateSiPrefixedConversions() + { + string[] names = Assembly.Load("DimensionalQuantities.Extensions.Nautical").GetExportedTypes() + .Where(type => type.Namespace == "DimensionalQuantities.Extensions.Nautical") + .SelectMany(type => type.GetMethods()).Select(method => method.Name).ToArray(); + + Assert.Multiple( + () => Assert.DoesNotContain("FromKiloNauticalMiles", names), + () => Assert.DoesNotContain("FromMilliKnots", names), + () => Assert.DoesNotContain("FromPrefixedUnits", names), + () => Assert.DoesNotContain("AsPrefixedUnits", names)); + } +} diff --git a/src/DimensionalQuantities.Extensions.Nautical/DimensionalQuantities.Extensions.Nautical.csproj b/src/DimensionalQuantities.Extensions.Nautical/DimensionalQuantities.Extensions.Nautical.csproj new file mode 100644 index 0000000..a72f3bb --- /dev/null +++ b/src/DimensionalQuantities.Extensions.Nautical/DimensionalQuantities.Extensions.Nautical.csproj @@ -0,0 +1,35 @@ + + + netstandard2.0 + GoSim + MIT + README.md + https://github.com/GoSimulate/DimensionalQuantities + Named nautical conversions for dimensional quantities. + git + true + $(BaseIntermediateOutputPath)$(Configuration)/$(TargetFramework)/generated + true + snupkg + true + true + true + portable + + + + + + + + + + + <_ProjectReferencesWithVersions Update="@(_ProjectReferencesWithVersions)" + Condition="'%(_ProjectReferencesWithVersions.Filename)' == 'DimensionalQuantities'"> + [$(PackageVersion)] + + + + diff --git a/src/DimensionalQuantities.Extensions.Nautical/NauticalSymbols.cs b/src/DimensionalQuantities.Extensions.Nautical/NauticalSymbols.cs new file mode 100644 index 0000000..f326077 --- /dev/null +++ b/src/DimensionalQuantities.Extensions.Nautical/NauticalSymbols.cs @@ -0,0 +1,15 @@ +using DimensionalQuantities.Units.Declarations; + +namespace DimensionalQuantities.Extensions.Nautical; + +internal static class NauticalSymbols +{ + [UnitSymbol("NauticalMile", "NauticalMiles", "nmi", Scale = 1852)] + internal sealed class NauticalMile { } + + [UnitSymbol("Fathom", "Fathoms", "ftm", Scale = 1.8288)] + internal sealed class Fathom { } + + [UnitSymbol("Knot", "Knots", "kn", Scale = 1852d / 3600d)] + internal sealed class Knot { } +} diff --git a/src/DimensionalQuantities.Extensions.Nautical/NauticalUnits.cs b/src/DimensionalQuantities.Extensions.Nautical/NauticalUnits.cs new file mode 100644 index 0000000..50b51c2 --- /dev/null +++ b/src/DimensionalQuantities.Extensions.Nautical/NauticalUnits.cs @@ -0,0 +1,23 @@ +using DimensionalQuantities.Units.Declarations; + +namespace DimensionalQuantities.Extensions.Nautical; + +[UnitProvider] +internal static class NauticalUnits +{ + [QuantityUnit(typeof(Length))] + [UnitFactor(typeof(NauticalSymbols.NauticalMile))] + private sealed class NauticalMileUnit { } + + [QuantityUnit(typeof(Length))] + [UnitFactor(typeof(NauticalSymbols.Fathom))] + private sealed class FathomUnit { } + + [QuantityUnit(typeof(Area))] + [UnitFactor(typeof(NauticalSymbols.NauticalMile), Power = 2)] + private sealed class SquareNauticalMileUnit { } + + [QuantityUnit(typeof(Speed))] + [UnitFactor(typeof(NauticalSymbols.Knot))] + private sealed class KnotUnit { } +} diff --git a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs index 2e283fd..77a5551 100644 --- a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs +++ b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs @@ -99,6 +99,31 @@ public static class Consumer () => Assert.Equal(273.15, InvokeDouble(type, "ConvertTemperature"), 12)); } + [Fact] + public void NauticalPackageProvidesConversionsThroughCoreDependency() + { + using PackageRepository repository = PackageRepository.Create(_scratch, new Uri("https://api.nuget.org/v3/index.json")); + PackCore(repository); + Package nautical = PackSourceProject(repository, "DimensionalQuantities.Extensions.Nautical"); + ProjectCreator consumer = CreateProject("NauticalConsumer", """ + using DimensionalQuantities.Extensions.Nautical; + + public static class Consumer + { + public static double ConvertLength() => Length.FromNauticalMiles(1).AsSiBaseUnit(); + public static double ConvertSpeed() => Speed.FromKnots(1).AsSiBaseUnit(); + } + """, nautical); + Build(consumer); + + using PackageAssemblyContext context = new(OutputDirectory("NauticalConsumer")); + Type type = context.LoadFromAssemblyPath(Path.Combine(OutputDirectory("NauticalConsumer"), "NauticalConsumer.dll")) + .GetType("Consumer", throwOnError: true)!; + Assert.Multiple( + () => Assert.Equal(1852, InvokeDouble(type, "ConvertLength")), + () => Assert.Equal(1852d / 3600d, InvokeDouble(type, "ConvertSpeed"))); + } + [Fact] public void MetricNamedConversionsRequireNamespaceImport() { diff --git a/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs b/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs index f299460..5bfeef4 100644 --- a/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs +++ b/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs @@ -52,6 +52,7 @@ public void CoreSymbolPackageMatchesRuntimeAndAnalyzerAssemblies() [Theory] [InlineData("Metric")] [InlineData("Imperial")] + [InlineData("Nautical")] public void ExtensionSymbolPackageMatchesAssemblyAndEmbedsGeneratedSource(string extension) { string projectName = "DimensionalQuantities.Extensions." + extension; diff --git a/src/DimensionalQuantities.slnx b/src/DimensionalQuantities.slnx index 4e0c151..75ae68d 100644 --- a/src/DimensionalQuantities.slnx +++ b/src/DimensionalQuantities.slnx @@ -11,6 +11,10 @@ + + + +