diff --git a/.github/scripts/check-package.py b/.github/scripts/check-package.py
index 68276f7..ff69e4b 100644
--- a/.github/scripts/check-package.py
+++ b/.github/scripts/check-package.py
@@ -8,17 +8,17 @@
version = sys.argv[1]
package_directory = Path("artifacts/packages")
core_id = "DimensionalQuantities"
-metric_id = "DimensionalQuantities.Extensions.Metric"
-expected_packages = {
- f"{core_id}.{version}.nupkg",
- f"{metric_id}.{version}.nupkg",
-}
-expected_symbols = {
- f"{core_id}.{version}.snupkg",
- f"{metric_id}.{version}.snupkg",
-}
-actual_packages = {package.name for package in package_directory.glob("*.nupkg")}
-actual_symbols = {package.name for package in package_directory.glob("*.snupkg")}
+extension_ids = sorted(
+ Path(project.attrib["Path"]).stem
+ for project in ET.parse("src/DimensionalQuantities.slnx").iter("Project")
+ if Path(project.attrib["Path"]).stem.startswith("DimensionalQuantities.Extensions.")
+ and not Path(project.attrib["Path"]).stem.endswith(".Tests")
+)
+package_ids = [core_id, *extension_ids]
+expected_packages = {f"{package_id}.{version}.nupkg" for package_id in package_ids}
+expected_symbols = {f"{package_id}.{version}.snupkg" for package_id in package_ids}
+actual_packages = {package.name for package in package_directory.glob(f"*.{version}.nupkg")}
+actual_symbols = {package.name for package in package_directory.glob(f"*.{version}.snupkg")}
assert actual_packages == expected_packages, (actual_packages, expected_packages)
assert actual_symbols == expected_symbols, (actual_symbols, expected_symbols)
@@ -35,13 +35,12 @@ def read_package(package_id, extension):
return names, metadata
-core_files, core_metadata = read_package(core_id, "nupkg")
-metric_files, metric_metadata = read_package(metric_id, "nupkg")
-core_symbols, _ = read_package(core_id, "snupkg")
-metric_symbols, _ = read_package(metric_id, "snupkg")
+packages = {package_id: read_package(package_id, "nupkg") for package_id in package_ids}
+symbols = {package_id: read_package(package_id, "snupkg")[0] for package_id in package_ids}
+core_files, core_metadata = packages[core_id]
+core_symbols = symbols[core_id]
core_runtime = "lib/netstandard2.0/DimensionalQuantities.dll"
-metric_runtime = "lib/netstandard2.0/DimensionalQuantities.Extensions.Metric.dll"
analyzer_directory = "analyzers/dotnet/cs/"
tooling = {
f"{analyzer_directory}DimensionalQuantities.dll",
@@ -50,15 +49,18 @@ def read_package(package_id, extension):
}
assert core_runtime in core_files
assert tooling <= core_files, tooling - core_files
-assert metric_runtime in metric_files
-assert core_runtime not in metric_files
-assert not any(name.startswith(analyzer_directory) for name in metric_files)
+for extension_id in extension_ids:
+ extension_files, _ = packages[extension_id]
+ assert f"lib/netstandard2.0/{extension_id}.dll" in extension_files
+ assert core_runtime not in extension_files
+ assert not any(name.startswith(analyzer_directory) for name in extension_files)
+ assert f"lib/netstandard2.0/{extension_id}.pdb" in symbols[extension_id]
-for names in (core_files, metric_files):
+for names, _ in packages.values():
assert not any("Microsoft.CodeAnalysis" in name for name in names)
assert not any(".Tests" in name for name in names)
-for metadata in (core_metadata, metric_metadata):
+for _, metadata in packages.values():
repository = metadata.find("{*}repository")
assert repository is not None
assert repository.attrib["type"] == "git"
@@ -71,17 +73,18 @@ def read_package(package_id, extension):
assert not any("Roslyn" in item.attrib["id"] or "CodeAnalysis" in item.attrib["id"] for item in dependencies)
core_dependencies = core_metadata.findall(".//{*}dependency")
-assert not any(item.attrib["id"] == metric_id for item in core_dependencies)
-metric_dependencies = metric_metadata.findall(".//{*}dependency")
-core_dependency = [item for item in metric_dependencies if item.attrib["id"] == core_id]
-assert len(core_dependency) == 1, core_dependency
-assert core_dependency[0].attrib["version"] == f"[{version}]", core_dependency[0].attrib
-assert core_dependency[0].attrib.get("include", "All").lower() == "all", core_dependency[0].attrib
-assert "analyzers" not in core_dependency[0].attrib.get("exclude", "").lower(), core_dependency[0].attrib
+assert not any(item.attrib["id"] in extension_ids for item in core_dependencies)
+for extension_id in extension_ids:
+ _, metadata = packages[extension_id]
+ dependencies = metadata.findall(".//{*}dependency")
+ core_dependency = [item for item in dependencies if item.attrib["id"] == core_id]
+ assert len(core_dependency) == 1, (extension_id, core_dependency)
+ assert core_dependency[0].attrib["version"] == f"[{version}]", core_dependency[0].attrib
+ assert core_dependency[0].attrib.get("include", "All").lower() == "all", core_dependency[0].attrib
+ assert "analyzers" not in core_dependency[0].attrib.get("exclude", "").lower(), core_dependency[0].attrib
assert "lib/netstandard2.0/DimensionalQuantities.pdb" in core_symbols
-assert "lib/netstandard2.0/DimensionalQuantities.Extensions.Metric.pdb" in metric_symbols
for assembly in tooling:
assert assembly.removesuffix(".dll") + ".pdb" in core_symbols
-print(f"Verified {core_id} and {metric_id} packages and symbols at {version}")
+print(f"Verified {', '.join(package_ids)} packages and symbols at {version}")
diff --git a/README.md b/README.md
index ff59b85..a6a294f 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,18 @@ using DimensionalQuantities.Extensions.Metric;
Metric depends on the matching core version, so referencing Metric also supplies core and its compiler tooling. Quantity names and dimensional arithmetic are available with core alone.
+Add `DimensionalQuantities.Extensions.Imperial` for British and US customary conversions. It includes feet, miles, stones, pounds-force, US and Imperial liquid measures, Fahrenheit, mechanical horsepower and the International Table British thermal unit. Import `DimensionalQuantities.Extensions.Imperial` to use its helpers:
+
+```csharp
+Length depth = Length.FromFeet(6);
+Volume fuel = Volume.FromImperialGallons(2);
+Energy heat = Energy.FromBritishThermalUnitsIT(1);
+
+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.
+
```csharp
Voltage voltage = Voltage.FromVolts(12);
Current current = Current.FromAmps(3);
diff --git a/src/DimensionalQuantities.Extensions.Imperial.Tests/DimensionalQuantities.Extensions.Imperial.Tests.csproj b/src/DimensionalQuantities.Extensions.Imperial.Tests/DimensionalQuantities.Extensions.Imperial.Tests.csproj
new file mode 100644
index 0000000..a74ac6f
--- /dev/null
+++ b/src/DimensionalQuantities.Extensions.Imperial.Tests/DimensionalQuantities.Extensions.Imperial.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+ net11.0
+ false
+ true
+ Exe
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs b/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs
new file mode 100644
index 0000000..d61d0e5
--- /dev/null
+++ b/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs
@@ -0,0 +1,92 @@
+using System.Reflection;
+using DimensionalQuantities.Extensions.Imperial;
+using Xunit;
+
+namespace DimensionalQuantities.Extensions.Imperial.Tests;
+
+public sealed class ImperialConversionsTests
+{
+ [Fact]
+ public void LengthAndAreaUseInternationalFoot()
+ {
+ Assert.Multiple(
+ () => Assert.Equal(0.0254, Length.FromInches(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.3048, Length.FromFeet(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.9144, Length.FromYards(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(1609.344, Length.FromMiles(1).AsSiBaseUnit(), 9),
+ () => Assert.Equal(20.1168, Length.FromChains(1).AsSiBaseUnit(), 10),
+ () => Assert.Equal(201.168, Length.FromFurlongs(1).AsSiBaseUnit(), 10),
+ () => Assert.Equal(0.09290304, Area.FromSquareFeet(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(4046.8564224, Area.FromAcres(1).AsSiBaseUnit(), 8),
+ () => Assert.Equal(5280, Length.FromMiles(1).AsFeet(), 9));
+ }
+
+ [Fact]
+ public void VolumeKeepsUsAndImperialLiquidMeasuresDistinct()
+ {
+ Assert.Multiple(
+ () => Assert.Equal(0.000016387064, Volume.FromCubicInches(1).AsSiBaseUnit(), 15),
+ () => Assert.Equal(0.028316846592, Volume.FromCubicFeet(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.764554857984, Volume.FromCubicYards(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.003785411784, Volume.FromUSGallons(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.000946352946, Volume.FromUSQuarts(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.000473176473, Volume.FromUSPints(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.0000295735295625, Volume.FromUSFluidOunces(1).AsSiBaseUnit(), 15),
+ () => Assert.Equal(0.00454609, Volume.FromImperialGallons(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.0011365225, Volume.FromImperialQuarts(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.00056826125, Volume.FromImperialPints(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.0000284130625, Volume.FromImperialFluidOunces(1).AsSiBaseUnit(), 15),
+ () => Assert.Equal(128, Volume.FromUSGallons(1).AsUSFluidOunces(), 10),
+ () => Assert.Equal(160, Volume.FromImperialGallons(1).AsImperialFluidOunces(), 10));
+ }
+
+ [Fact]
+ public void MassSpeedForceAndPressureUseTheirOwnDimensions()
+ {
+ Assert.Multiple(
+ () => Assert.Equal(0.028349523125, Mass.FromOunces(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.45359237, Mass.FromPounds(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(6.35029318, Mass.FromStones(1).AsSiBaseUnit(), 10),
+ () => Assert.Equal(907.18474, Mass.FromUSShortTons(1).AsSiBaseUnit(), 8),
+ () => Assert.Equal(1016.0469088, Mass.FromImperialLongTons(1).AsSiBaseUnit(), 8),
+ () => Assert.Equal(0.44704, Speed.FromMilesPerHour(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(0.3048, Speed.FromFeetPerSecond(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(4.4482216152605, Force.FromPoundForces(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(4.4482216152605 / (0.0254 * 0.0254), Pressure.FromPsi(1).AsSiBaseUnit(), 9),
+ () => Assert.Equal(1, Stress.FromPsi(1).AsPsi(), 12),
+ () => Assert.Equal(1, YoungsModulus.FromPsi(1).AsPsi(), 12));
+ }
+
+ [Fact]
+ public void EnergyAndPowerUseExplicitBritishThermalUnitDefinition()
+ {
+ Assert.Multiple(
+ () => Assert.Equal(1.3558179483314004, Energy.FromFootPoundForces(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(1055.05585262, Energy.FromBritishThermalUnitsIT(1).AsSiBaseUnit(), 9),
+ () => Assert.Equal(745.69987158227022, Power.FromMechanicalHorsepower(1).AsSiBaseUnit(), 9),
+ () => Assert.Equal(1055.05585262 / 3600, Power.FromBritishThermalUnitsITPerHour(1).AsSiBaseUnit(), 12),
+ () => Assert.Equal(1, Energy.FromSiBaseUnit(1055.05585262).AsBritishThermalUnitsIT(), 12),
+ () => Assert.Equal(1, Power.FromSiBaseUnit(1055.05585262 / 3600).AsBritishThermalUnitsITPerHour(), 12));
+ }
+
+ [Fact]
+ public void FahrenheitUsesAffineConversion()
+ {
+ Assert.Multiple(
+ () => Assert.Equal(273.15, Temperature.FromFahrenheit(32).AsKelvin(), 10),
+ () => Assert.Equal(212, Temperature.FromKelvin(373.15).AsFahrenheit(), 10));
+ }
+
+ [Fact]
+ public void ProviderDoesNotGenerateSiPrefixHelpers()
+ {
+ string[] names = Assembly.Load("DimensionalQuantities.Extensions.Imperial").GetExportedTypes()
+ .Where(type => type.Namespace == "DimensionalQuantities.Extensions.Imperial")
+ .SelectMany(type => type.GetMethods()).Select(method => method.Name).ToArray();
+ Assert.Multiple(
+ () => Assert.DoesNotContain("FromKiloFeet", names),
+ () => Assert.DoesNotContain("AsMilliPounds", names),
+ () => Assert.DoesNotContain("FromPrefixedUnits", names),
+ () => Assert.DoesNotContain("AsPrefixedUnits", names));
+ }
+}
diff --git a/src/DimensionalQuantities.Extensions.Imperial/DimensionalQuantities.Extensions.Imperial.csproj b/src/DimensionalQuantities.Extensions.Imperial/DimensionalQuantities.Extensions.Imperial.csproj
new file mode 100644
index 0000000..c3bdfe7
--- /dev/null
+++ b/src/DimensionalQuantities.Extensions.Imperial/DimensionalQuantities.Extensions.Imperial.csproj
@@ -0,0 +1,35 @@
+
+
+ netstandard2.0
+ GoSim
+ MIT
+ README.md
+ https://github.com/GoSimulate/DimensionalQuantities
+ Named Imperial and US customary 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.Imperial/ImperialSymbols.cs b/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs
new file mode 100644
index 0000000..bd468b0
--- /dev/null
+++ b/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs
@@ -0,0 +1,59 @@
+using DimensionalQuantities.Units.Declarations;
+
+namespace DimensionalQuantities.Extensions.Imperial;
+
+internal static class ImperialSymbols
+{
+ [UnitSymbol("Inch", "Inches", "in", Scale = 0.0254)]
+ internal sealed class Inch { }
+ [UnitSymbol("Foot", "Feet", "ft", Scale = 0.3048)]
+ internal sealed class Foot { }
+ [UnitSymbol("Yard", "Yards", "yd", Scale = 0.9144)]
+ internal sealed class Yard { }
+ [UnitSymbol("Mile", "Miles", "mi", Scale = 1609.344)]
+ internal sealed class Mile { }
+ [UnitSymbol("Chain", "Chains", "ch", Scale = 20.1168)]
+ internal sealed class Chain { }
+ [UnitSymbol("Furlong", "Furlongs", "fur", Scale = 201.168)]
+ internal sealed class Furlong { }
+ [UnitSymbol("Acre", "Acres", "ac", Scale = 4046.8564224)]
+ internal sealed class Acre { }
+ [UnitSymbol("USGallon", "USGallons", "gal (US)", Scale = 0.003785411784)]
+ internal sealed class USGallon { }
+ [UnitSymbol("USQuart", "USQuarts", "qt (US)", Scale = 0.000946352946)]
+ internal sealed class USQuart { }
+ [UnitSymbol("USPint", "USPints", "pt (US)", Scale = 0.000473176473)]
+ internal sealed class USPint { }
+ [UnitSymbol("USFluidOunce", "USFluidOunces", "fl oz (US)", Scale = 0.0000295735295625)]
+ internal sealed class USFluidOunce { }
+ [UnitSymbol("ImperialGallon", "ImperialGallons", "gal (imp)", Scale = 0.00454609)]
+ internal sealed class ImperialGallon { }
+ [UnitSymbol("ImperialQuart", "ImperialQuarts", "qt (imp)", Scale = 0.0011365225)]
+ internal sealed class ImperialQuart { }
+ [UnitSymbol("ImperialPint", "ImperialPints", "pt (imp)", Scale = 0.00056826125)]
+ internal sealed class ImperialPint { }
+ [UnitSymbol("ImperialFluidOunce", "ImperialFluidOunces", "fl oz (imp)", Scale = 0.0000284130625)]
+ internal sealed class ImperialFluidOunce { }
+ [UnitSymbol("Ounce", "Ounces", "oz", Scale = 0.028349523125)]
+ internal sealed class Ounce { }
+ [UnitSymbol("Pound", "Pounds", "lb", Scale = 0.45359237)]
+ internal sealed class Pound { }
+ [UnitSymbol("Stone", "Stones", "st", Scale = 6.35029318)]
+ internal sealed class Stone { }
+ [UnitSymbol("USShortTon", "USShortTons", "ton (US)", Scale = 907.18474)]
+ internal sealed class USShortTon { }
+ [UnitSymbol("ImperialLongTon", "ImperialLongTons", "ton (imp)", Scale = 1016.0469088)]
+ internal sealed class ImperialLongTon { }
+ [UnitSymbol("Hour", "Hours", "h", Scale = 3600)]
+ internal sealed class Hour { }
+ [UnitSymbol("PoundForce", "PoundForces", "lbf", Scale = 4.4482216152605)]
+ internal sealed class PoundForce { }
+ [UnitSymbol("Psi", "Psi", "psi", Scale = 4.4482216152605 / (0.0254 * 0.0254))]
+ internal sealed class Psi { }
+ [UnitSymbol("BritishThermalUnitIT", "BritishThermalUnitsIT", "Btu(IT)", Scale = 1055.05585262)]
+ internal sealed class BritishThermalUnitIT { }
+ [UnitSymbol("MechanicalHorsepower", "MechanicalHorsepower", "hp", Scale = 745.69987158227022)]
+ internal sealed class MechanicalHorsepower { }
+ [UnitSymbol("Fahrenheit", "Fahrenheit", "°F", Scale = 5d / 9d)]
+ internal sealed class Fahrenheit { }
+}
diff --git a/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs b/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs
new file mode 100644
index 0000000..5f2aa61
--- /dev/null
+++ b/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs
@@ -0,0 +1,124 @@
+using DimensionalQuantities.Units.Declarations;
+using DimensionalQuantities.Units.Symbols;
+
+namespace DimensionalQuantities.Extensions.Imperial;
+
+[UnitProvider]
+internal static class ImperialUnits
+{
+ [QuantityUnit(typeof(Length))]
+ [UnitFactor(typeof(ImperialSymbols.Foot))]
+ [AdditionalUnit(typeof(InchUnit))]
+ [AdditionalUnit(typeof(YardUnit))]
+ [AdditionalUnit(typeof(MileUnit))]
+ [AdditionalUnit(typeof(ChainUnit))]
+ [AdditionalUnit(typeof(FurlongUnit))]
+ private sealed class LengthUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Inch))]
+ private sealed class InchUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Yard))]
+ private sealed class YardUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Mile))]
+ private sealed class MileUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Chain))]
+ private sealed class ChainUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Furlong))]
+ private sealed class FurlongUnit { }
+
+ [QuantityUnit(typeof(Area))]
+ [UnitFactor(typeof(ImperialSymbols.Foot), Power = 2)]
+ [AdditionalUnit(typeof(AcreUnit))]
+ private sealed class AreaUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Acre))]
+ private sealed class AcreUnit { }
+
+ [QuantityUnit(typeof(Volume))]
+ [UnitFactor(typeof(ImperialSymbols.Foot), Power = 3)]
+ [AdditionalUnit(typeof(CubicInchUnit))]
+ [AdditionalUnit(typeof(CubicYardUnit))]
+ [AdditionalUnit(typeof(USGallonUnit))]
+ [AdditionalUnit(typeof(USQuartUnit))]
+ [AdditionalUnit(typeof(USPintUnit))]
+ [AdditionalUnit(typeof(USFluidOunceUnit))]
+ [AdditionalUnit(typeof(ImperialGallonUnit))]
+ [AdditionalUnit(typeof(ImperialQuartUnit))]
+ [AdditionalUnit(typeof(ImperialPintUnit))]
+ [AdditionalUnit(typeof(ImperialFluidOunceUnit))]
+ private sealed class VolumeUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Inch), Power = 3)]
+ private sealed class CubicInchUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Yard), Power = 3)]
+ private sealed class CubicYardUnit { }
+ [UnitFactor(typeof(ImperialSymbols.USGallon))]
+ private sealed class USGallonUnit { }
+ [UnitFactor(typeof(ImperialSymbols.USQuart))]
+ private sealed class USQuartUnit { }
+ [UnitFactor(typeof(ImperialSymbols.USPint))]
+ private sealed class USPintUnit { }
+ [UnitFactor(typeof(ImperialSymbols.USFluidOunce))]
+ private sealed class USFluidOunceUnit { }
+ [UnitFactor(typeof(ImperialSymbols.ImperialGallon))]
+ private sealed class ImperialGallonUnit { }
+ [UnitFactor(typeof(ImperialSymbols.ImperialQuart))]
+ private sealed class ImperialQuartUnit { }
+ [UnitFactor(typeof(ImperialSymbols.ImperialPint))]
+ private sealed class ImperialPintUnit { }
+ [UnitFactor(typeof(ImperialSymbols.ImperialFluidOunce))]
+ private sealed class ImperialFluidOunceUnit { }
+
+ [QuantityUnit(typeof(Mass))]
+ [UnitFactor(typeof(ImperialSymbols.Pound))]
+ [AdditionalUnit(typeof(OunceUnit))]
+ [AdditionalUnit(typeof(StoneUnit))]
+ [AdditionalUnit(typeof(USShortTonUnit))]
+ [AdditionalUnit(typeof(ImperialLongTonUnit))]
+ private sealed class MassUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Ounce))]
+ private sealed class OunceUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Stone))]
+ private sealed class StoneUnit { }
+ [UnitFactor(typeof(ImperialSymbols.USShortTon))]
+ private sealed class USShortTonUnit { }
+ [UnitFactor(typeof(ImperialSymbols.ImperialLongTon))]
+ private sealed class ImperialLongTonUnit { }
+
+ [QuantityUnit(typeof(Speed))]
+ [UnitFactor(typeof(ImperialSymbols.Mile))]
+ [UnitFactor(typeof(ImperialSymbols.Hour), Power = -1)]
+ [AdditionalUnit(typeof(FeetPerSecondUnit))]
+ private sealed class SpeedUnit { }
+ [UnitFactor(typeof(ImperialSymbols.Foot))]
+ [UnitFactor(typeof(Second), Power = -1)]
+ private sealed class FeetPerSecondUnit { }
+
+ [QuantityUnit(typeof(Force))]
+ [UnitFactor(typeof(ImperialSymbols.PoundForce))]
+ private sealed class ForceUnit { }
+
+ [QuantityUnit(typeof(Pressure))]
+ [QuantityUnit(typeof(Stress))]
+ [QuantityUnit(typeof(YoungsModulus))]
+ [UnitFactor(typeof(ImperialSymbols.Psi))]
+ private sealed class PressureUnit { }
+
+ [QuantityUnit(typeof(Energy))]
+ [UnitFactor(typeof(ImperialSymbols.Foot))]
+ [UnitFactor(typeof(ImperialSymbols.PoundForce))]
+ [AdditionalUnit(typeof(BritishThermalUnitITUnit))]
+ private sealed class EnergyUnit { }
+ [UnitFactor(typeof(ImperialSymbols.BritishThermalUnitIT))]
+ private sealed class BritishThermalUnitITUnit { }
+
+ [QuantityUnit(typeof(Power))]
+ [UnitFactor(typeof(ImperialSymbols.MechanicalHorsepower))]
+ [AdditionalUnit(typeof(BritishThermalUnitsITPerHourUnit))]
+ private sealed class PowerUnit { }
+ [UnitFactor(typeof(ImperialSymbols.BritishThermalUnitIT))]
+ [UnitFactor(typeof(ImperialSymbols.Hour), Power = -1)]
+ private sealed class BritishThermalUnitsITPerHourUnit { }
+
+ [QuantityUnit(typeof(Temperature))]
+ [UnitConversion(Offset = 459.67d * 5d / 9d)]
+ [UnitFactor(typeof(ImperialSymbols.Fahrenheit))]
+ private sealed class TemperatureUnit { }
+}
diff --git a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs
index d251bd5..2e283fd 100644
--- a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs
+++ b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs
@@ -74,6 +74,31 @@ public static double MultiplyVoltageCurrent(double volts, double amps)
() => Assert.Equal(25, InvokeDouble(type, "MultiplyVoltageCurrent", 12.5, 2.0)));
}
+ [Fact]
+ public void ImperialPackageProvidesConversionsThroughCoreDependency()
+ {
+ using PackageRepository repository = PackageRepository.Create(_scratch, new Uri("https://api.nuget.org/v3/index.json"));
+ PackCore(repository);
+ Package imperial = PackSourceProject(repository, "DimensionalQuantities.Extensions.Imperial");
+ ProjectCreator consumer = CreateProject("ImperialConsumer", """
+ using DimensionalQuantities.Extensions.Imperial;
+
+ public static class Consumer
+ {
+ public static double ConvertLength() => Length.FromFeet(6).AsSiBaseUnit();
+ public static double ConvertTemperature() => Temperature.FromFahrenheit(32).AsSiBaseUnit();
+ }
+ """, imperial);
+ Build(consumer);
+
+ using PackageAssemblyContext context = new(OutputDirectory("ImperialConsumer"));
+ Type type = context.LoadFromAssemblyPath(Path.Combine(OutputDirectory("ImperialConsumer"), "ImperialConsumer.dll"))
+ .GetType("Consumer", throwOnError: true)!;
+ Assert.Multiple(
+ () => Assert.Equal(1.8288, InvokeDouble(type, "ConvertLength"), 12),
+ () => Assert.Equal(273.15, InvokeDouble(type, "ConvertTemperature"), 12));
+ }
+
[Fact]
public void MetricNamedConversionsRequireNamespaceImport()
{
diff --git a/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs b/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs
index 1f7ec60..f299460 100644
--- a/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs
+++ b/src/DimensionalQuantities.IntegrationTests/Packaging/SymbolPackageTests.cs
@@ -49,23 +49,27 @@ public void CoreSymbolPackageMatchesRuntimeAndAnalyzerAssemblies()
}
}
- [Fact]
- public void MetricSymbolPackageMatchesAssemblyAndEmbedsGeneratedSource()
+ [Theory]
+ [InlineData("Metric")]
+ [InlineData("Imperial")]
+ public void ExtensionSymbolPackageMatchesAssemblyAndEmbedsGeneratedSource(string extension)
{
- (FileInfo package, FileInfo symbols) = Pack("DimensionalQuantities.Extensions.Metric");
+ string projectName = "DimensionalQuantities.Extensions." + extension;
+ (FileInfo package, FileInfo symbols) = Pack(projectName);
using ZipArchive binaries = ZipFile.OpenRead(package.FullName);
using ZipArchive symbolArchive = ZipFile.OpenRead(symbols.FullName);
- SymbolDetails details = ReadSymbolDetails(binaries, symbolArchive, "lib/netstandard2.0/DimensionalQuantities.Extensions.Metric");
+ SymbolDetails details = ReadSymbolDetails(binaries, symbolArchive, "lib/netstandard2.0/" + projectName,
+ projectName + "." + extension + "Units.g.cs");
string commit = GetCurrentCommit();
string sourceDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
string configuration = typeof(SymbolPackageTests).Assembly.GetCustomAttribute()!.Configuration;
- byte[] generatedSource = File.ReadAllBytes(Path.Combine(sourceDirectory, "DimensionalQuantities.Extensions.Metric", "obj", configuration, "netstandard2.0",
- "generated", "DimensionalQuantities.Roslyn", "DimensionalQuantities.Roslyn.Generators.QuantityGenerator", "DimensionalQuantities.Extensions.Metric.MetricUnits.g.cs"));
+ byte[] generatedSource = File.ReadAllBytes(Path.Combine(sourceDirectory, projectName, "obj", configuration, "netstandard2.0",
+ "generated", "DimensionalQuantities.Roslyn", "DimensionalQuantities.Roslyn.Generators.QuantityGenerator", projectName + "." + extension + "Units.g.cs"));
Assert.Multiple(
() => Assert.Equal(details.AssemblyGuid, details.PdbGuid),
() => Assert.Equal(details.AssemblyStamp, details.PdbStamp),
() => Assert.Contains($"https://raw.githubusercontent.com/GoSimulate/DimensionalQuantities/{commit}/*", details.SourceUrls),
- () => Assert.Equal(generatedSource, details.EmbeddedMetricConversions));
+ () => Assert.Equal(generatedSource, details.EmbeddedGeneratedSource));
}
private (FileInfo Package, FileInfo Symbols) Pack(string projectName)
@@ -80,7 +84,7 @@ public void MetricSymbolPackageMatchesAssemblyAndEmbedsGeneratedSource()
return (package, symbols);
}
- private static SymbolDetails ReadSymbolDetails(ZipArchive binaries, ZipArchive symbols, string stem)
+ private static SymbolDetails ReadSymbolDetails(ZipArchive binaries, ZipArchive symbols, string stem, string? generatedFileName = null)
{
byte[] assemblyBytes = ReadEntry(binaries, stem + ".dll");
byte[] pdbBytes = ReadEntry(symbols, stem + ".pdb");
@@ -98,14 +102,14 @@ private static SymbolDetails ReadSymbolDetails(ZipArchive binaries, ZipArchive s
using JsonDocument sourceLinkDocument = JsonDocument.Parse(sourceLink);
string[] sourceUrls = sourceLinkDocument.RootElement.GetProperty("documents").EnumerateObject()
.Select(mapping => mapping.Value.GetString()!).ToArray();
- byte[]? embeddedMetricConversions = reader.Documents
- .Where(handle => reader.GetString(reader.GetDocument(handle).Name).EndsWith("DimensionalQuantities.Extensions.Metric.MetricUnits.g.cs", StringComparison.Ordinal))
+ byte[]? embeddedGeneratedSource = reader.Documents
+ .Where(handle => generatedFileName is not null && reader.GetString(reader.GetDocument(handle).Name).EndsWith(generatedFileName, StringComparison.Ordinal))
.SelectMany(handle => reader.GetCustomDebugInformation(handle))
.Select(reader.GetCustomDebugInformation)
.Where(info => reader.GetGuid(info.Kind) == _embeddedSourceKind)
.Select(info => DecodeEmbeddedSource(reader.GetBlobBytes(info.Value)))
.SingleOrDefault();
- return new(codeViewData.Guid, codeView.Stamp, new Guid(pdbId.AsSpan(0, 16)), BitConverter.ToUInt32(pdbId, 16), sourceUrls, embeddedMetricConversions);
+ return new(codeViewData.Guid, codeView.Stamp, new Guid(pdbId.AsSpan(0, 16)), BitConverter.ToUInt32(pdbId, 16), sourceUrls, embeddedGeneratedSource);
}
private static byte[] DecodeEmbeddedSource(byte[] blob)
@@ -161,7 +165,7 @@ private static string GetCurrentCommit()
return commit;
}
- private sealed record SymbolDetails(Guid AssemblyGuid, uint AssemblyStamp, Guid PdbGuid, uint PdbStamp, string[] SourceUrls, byte[]? EmbeddedMetricConversions);
+ private sealed record SymbolDetails(Guid AssemblyGuid, uint AssemblyStamp, Guid PdbGuid, uint PdbStamp, string[] SourceUrls, byte[]? EmbeddedGeneratedSource);
public void Dispose() => Directory.Delete(_scratch, recursive: true);
}
diff --git a/src/DimensionalQuantities.slnx b/src/DimensionalQuantities.slnx
index bb1d34b..4e0c151 100644
--- a/src/DimensionalQuantities.slnx
+++ b/src/DimensionalQuantities.slnx
@@ -7,6 +7,10 @@
+
+
+
+
diff --git a/src/DimensionalQuantities/Units/EngineeringQuantityAliases.cs b/src/DimensionalQuantities/Units/EngineeringQuantityAliases.cs
index 3e571c4..1679397 100644
--- a/src/DimensionalQuantities/Units/EngineeringQuantityAliases.cs
+++ b/src/DimensionalQuantities/Units/EngineeringQuantityAliases.cs
@@ -8,7 +8,9 @@ internal static class EngineeringQuantityAliases
{
{ new(Length: 2), ["Area"] },
{ new(Length: 3), ["Volume"] },
+ { new(Length: 1, Time: -1), ["Speed"] },
{ new(Time: -1), ["Frequency", "AngularVelocity"] },
+ { new(Length: 1, Mass: 1, Time: -2), ["Force"] },
{ new(Length: -3, Mass: 1), ["Density"] },
{ new(Length: -1, Mass: 1, Time: -2), ["Pressure", "Stress", "YoungsModulus"] },
{ new(Length: 2, Mass: 1, Time: -2), ["Energy", "Torque"] },