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"] },