Skip to content

Commit 5e96716

Browse files
committed
Improve modular playground scenario
1 parent c3918c2 commit 5e96716

5 files changed

Lines changed: 158 additions & 23 deletions

File tree

UnitsNet.Modular/Samples/UnitsNet.Modular.Playground/ApplicationUnits.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@ internal interface PlaygroundDurationUnits;
1515
[UnitSet("MeterPerSecond", "KilometerPerHour", "MilePerHour")]
1616
internal interface PlaygroundSpeedUnits;
1717

18-
// The semantic ID connects this authoring type to GameScore.unitsnet.json.
19-
[QuantitySpec("UnitsNet.Modular.Playground.GameScore")]
20-
internal interface GameScoreSpec;
18+
[UnitSet("Joule", "KilowattHour")]
19+
internal interface PlaygroundEnergyUnits;
20+
21+
[UnitSet("Watt", "Kilowatt")]
22+
internal interface PlaygroundPowerUnits;
23+
24+
// The semantic ID connects this authoring type to ParcelCount.unitsnet.json.
25+
[QuantitySpec("UnitsNet.Modular.Playground.ParcelCount")]
26+
internal interface ParcelCountSpec;
2127

2228
// Add or remove selections here, then rebuild. Only these quantities are generated.
2329
[UnitsNetModule]
2430
internal interface PlaygroundUnits :
2531
IInclude<Catalog.LengthSpec, PlaygroundLengthUnits>,
2632
IInclude<Catalog.DurationSpec, PlaygroundDurationUnits>,
2733
IInclude<Catalog.SpeedSpec, PlaygroundSpeedUnits>,
28-
IInclude<GameScoreSpec>;
34+
IInclude<Catalog.EnergySpec, PlaygroundEnergyUnits>,
35+
IInclude<Catalog.PowerSpec, PlaygroundPowerUnits>,
36+
IInclude<ParcelCountSpec>;

UnitsNet.Modular/Samples/UnitsNet.Modular.Playground/GameScore.unitsnet.json renamed to UnitsNet.Modular/Samples/UnitsNet.Modular.Playground/ParcelCount.unitsnet.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
{
2-
"Name": "GameScore",
2+
"Name": "ParcelCount",
33
"Namespace": "UnitsNet.Modular.Playground",
4-
"BaseUnit": "Point",
4+
"BaseUnit": "Parcel",
55
"BaseDimensions": {},
66
"Units": [
77
{
8-
"SingularName": "Point",
9-
"PluralName": "Points",
8+
"SingularName": "Parcel",
9+
"PluralName": "Parcels",
1010
"FromUnitToBaseFunc": "{x}",
1111
"FromBaseToUnitFunc": "{x}",
1212
"Localization": [
1313
{
1414
"Culture": "en-US",
15-
"Abbreviations": [ "pt" ]
15+
"Abbreviations": [ "pkg" ]
1616
}
1717
]
1818
},
Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,143 @@
11
// Licensed under MIT No Attribution, see LICENSE file at the root.
22

3+
using System.Globalization;
4+
using System.Text.Json;
35
using UnitsNet;
6+
using UnitsNet.Modular;
7+
using UnitsNet.Modular.Generated;
48
using UnitsNet.Units;
9+
using CoreQuantity = UnitsNet.Core.IQuantity<double>;
510

611
namespace UnitsNet.Modular.Playground;
712

813
internal static class Program
914
{
15+
private static readonly CultureInfo Invariant = CultureInfo.InvariantCulture;
16+
1017
public static void Main()
1118
{
12-
// These types and their relationship operator are generated from the built-in selections.
13-
Length route = Length.FromKilometers(5);
14-
Duration elapsed = Duration.FromMinutes(24);
15-
Speed averageSpeed = route / elapsed;
19+
Console.WriteLine("Electric delivery-day planner");
20+
Console.WriteLine("Mixed-unit route inputs, parcel manifest, driving time, and charging plan.");
21+
22+
// This sample intentionally uses several APIs side by side to illustrate their capabilities.
23+
// Production code would normally choose the shortest, most natural API for each task instead
24+
// of exercising every available alternative in one workflow.
25+
26+
// APIs illustrated: Parse(), TryParse(), Sum(), Average(), As(), GetAbbreviation(), and static Convert().
27+
// Parse() handles required localized input, while TryParse() avoids exceptions for optional input.
28+
// Sum() and Average() aggregate mixed units; As() returns only a converted number. Static
29+
// Convert() is useful when neither the source nor result needs to be a quantity object.
30+
PrintSection("Read and normalize the route manifest");
31+
string[] routeInputs = ["48 km", "31 mi", "72 km"];
32+
Length[] routeLegs = routeInputs
33+
.Select(text => Length.Parse(text, Invariant))
34+
.ToArray();
35+
36+
Length depotDetour = Length.TryParse("12 km", Invariant, out Length parsedDetour)
37+
? parsedDetour
38+
: Length.Zero;
39+
Length totalRoute = routeLegs
40+
.Append(depotDetour)
41+
.Sum(LengthUnit.Kilometer);
42+
Length averageLeg = routeLegs.Average(LengthUnit.Kilometer);
43+
double totalRouteMiles = totalRoute.As(LengthUnit.Mile);
44+
string mileAbbreviation = Length.GetAbbreviation(LengthUnit.Mile, Invariant);
45+
46+
Console.WriteLine($"Route legs: {string.Join(", ", routeLegs.Select(FormatStoredValue))}");
47+
Console.WriteLine(
48+
$"Total: {totalRoute.ToString("0.0", Invariant)} / " +
49+
$"{totalRouteMiles.ToString("0.0", Invariant)} {mileAbbreviation}");
50+
Console.WriteLine($"Average: {averageLeg.ToString("0.0", Invariant)} per scheduled leg");
51+
52+
double localSpeedLimitMph = Speed.Convert(
53+
80,
54+
SpeedUnit.KilometerPerHour,
55+
SpeedUnit.MilePerHour);
56+
Console.WriteLine($"80 km/h local limit = {localSpeedLimitMph.ToString("F1", Invariant)} mph");
57+
58+
// APIs illustrated: same-quantity addition, cross-quantity division, ToUnit(), and ToString().
59+
// Ordinary arithmetic preserves the quantity type. Cross-quantity operators produce a different
60+
// type and are generated only when every participant is selected in ApplicationUnits.cs.
61+
// ToUnit() retains a strongly typed quantity for subsequent formatting or calculations.
62+
PrintSection("Estimate the driving day");
63+
Duration drivingTime = Duration.Parse("4 h", Invariant) + Duration.FromMinutes(25);
64+
Speed averageSpeed = totalRoute / drivingTime;
65+
Speed displaySpeed = averageSpeed.ToUnit(SpeedUnit.KilometerPerHour);
66+
67+
Console.WriteLine($"Driving time: {drivingTime.ToString("0.0", Invariant)}");
68+
Console.WriteLine($"Average speed: {displaySpeed.ToString("0.0", Invariant)}");
69+
70+
// APIs illustrated: cross-quantity multiplication and its inferred division operator.
71+
// The Power * Duration = Energy relationship handles anchor-unit conversions, so multiplying
72+
// kW by minutes works directly; dividing Energy by Power yields the corresponding Duration.
73+
PrintSection("Plan the charging stop");
74+
Power charger = Power.Parse("150 kW", Invariant);
75+
Duration chargingWindow = Duration.FromMinutes(22);
76+
Energy deliveredEnergy = charger * chargingWindow;
77+
Energy requestedEnergy = Energy.Parse("42 kWh", Invariant);
78+
Duration timeToAddRequestedEnergy = requestedEnergy / charger;
79+
Energy displayEnergy = deliveredEnergy.ToUnit(EnergyUnit.KilowattHour);
80+
Duration displayChargeTime = timeToAddRequestedEnergy.ToUnit(DurationUnit.Minute);
81+
82+
Console.WriteLine($"Charger: {charger.ToString("0", Invariant)}");
83+
Console.WriteLine($"Energy in 22m: {displayEnergy.ToString("0.0", Invariant)}");
84+
Console.WriteLine($"Time for 42kWh: {displayChargeTime.ToString("0.0", Invariant)}");
1685

17-
Console.WriteLine($"{route} in {elapsed} = {averageSpeed.ToUnit(SpeedUnit.KilometerPerHour)}");
86+
// APIs illustrated: a custom JSON-defined quantity using the same generated API as a built-in.
87+
// ParcelCount comes from ParcelCount.unitsnet.json and receives parsing, construction,
88+
// conversion, formatting, and arithmetic without hand-written quantity code.
89+
PrintSection("Count the cargo with a custom quantity");
90+
ParcelCount manifest = ParcelCount.Parse("2 doz", Invariant) + ParcelCount.FromParcels(6);
91+
ParcelCount parcelManifest = manifest.ToUnit(ParcelCountUnit.Parcel);
92+
ParcelCount dozenManifest = manifest.ToUnit(ParcelCountUnit.Dozen);
93+
Console.WriteLine(
94+
$"Manifest: {parcelManifest.ToString("0", Invariant)} " +
95+
$"({dozenManifest.ToString("0.00", Invariant)})");
1896

19-
// This type is generated from GameScore.unitsnet.json in this project.
20-
GameScore score = GameScore.FromDozens(2) + GameScore.FromPoints(6);
21-
Console.WriteLine($"{score.ToUnit(GameScoreUnit.Point)} = {score.ToUnit(GameScoreUnit.Dozen)}");
97+
// APIs illustrated: the legacy-shaped Quantity facade, modular registry, and UnitSystem policy.
98+
// Quantity.From(...) keeps a familiar call shape for configuration-driven code. Unlike legacy
99+
// UnitsNet, the facade is scoped to the selected module and returns IQuantity<double> from
100+
// UnitsNet.Core. The modular registry exposes that selected catalog without global mutation.
101+
PrintSection("Use the modular dynamic API");
102+
CoreQuantity configuredDistance = Quantity.From(15, "Length", "Mile");
103+
QuantityRegistry registry = GeneratedQuantityRegistry.Instance;
104+
IQuantityDescriptor lengthDescriptor = registry.Get("Length");
105+
double configuredKilometers = registry.Convert(15, "Length", "Mile", "Kilometer");
106+
107+
string formattedConfiguredDistance = lengthDescriptor.Format(configuredDistance, "0.0", Invariant);
108+
Console.WriteLine(
109+
$"Configured leg: {formattedConfiguredDistance} = " +
110+
$"{configuredKilometers.ToString("F1", Invariant)} km");
111+
Console.WriteLine($"Selected types: {string.Join(", ", registry.Names.Order())}");
112+
113+
// Modular unit-system policy is immutable and explicit. It resolves only among units selected
114+
// by this module instead of changing process-wide UnitsNetSetup defaults at runtime.
115+
Length siRoute = totalRoute.ToUnit(UnitSystem.SI);
116+
Console.WriteLine($"SI policy: {siRoute.ToString("0", Invariant)}");
117+
118+
// API illustrated: the System.Text.Json converter exposed by the generated registry.
119+
// It knows the selected concrete types without assembly scanning, keeping serialization
120+
// friendly to trimming and Native AOT.
121+
PrintSection("Persist a strongly typed result");
122+
var jsonOptions = new JsonSerializerOptions { WriteIndented = false };
123+
jsonOptions.Converters.Add(GeneratedQuantityRegistry.JsonConverter);
124+
string json = JsonSerializer.Serialize(displayEnergy, jsonOptions);
125+
Energy restoredEnergy = JsonSerializer.Deserialize<Energy>(json, jsonOptions);
126+
127+
Console.WriteLine($"JSON: {json}");
128+
Console.WriteLine($"Round trip:{restoredEnergy.ToString("0.0", Invariant),10}");
22129

23130
Console.WriteLine();
24-
Console.WriteLine("Try editing ApplicationUnits.cs, GameScore.unitsnet.json, or this file, then run:");
131+
Console.WriteLine("Try editing ApplicationUnits.cs, ParcelCount.unitsnet.json, or this file, then run:");
25132
Console.WriteLine(" dotnet run");
26133
}
134+
135+
private static string FormatStoredValue(Length length) => length.ToString("0.#", Invariant);
136+
137+
private static void PrintSection(string title)
138+
{
139+
Console.WriteLine();
140+
Console.WriteLine(title);
141+
Console.WriteLine(new string('-', title.Length));
142+
}
27143
}

UnitsNet.Modular/Samples/UnitsNet.Modular.Playground/README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
# UnitsNet.Modular playground
22

3-
This small console app runs the real UnitsNet.Modular source generator from the current repository
4-
checkout. The generated quantity structs and unit enums become part of this app at build time; no
5-
generated C# is checked in.
3+
This console app runs the real UnitsNet.Modular source generator from the current repository
4+
checkout. It plans a day for an electric delivery van using mixed-unit route inputs, a parcel
5+
manifest, driving estimates, and a charging stop. The generated quantity structs and unit enums
6+
become part of this app at build time; no generated C# is checked in.
7+
8+
The scenario is split into readable sections that exercise:
9+
10+
- parsing and defensive `TryParse` at input boundaries;
11+
- numeric conversion, `As`, `ToUnit`, and formatted `ToString` output;
12+
- arithmetic and collection aggregation across mixed units;
13+
- generated `Length / Duration = Speed` and `Power * Duration = Energy` relationships;
14+
- an application-specific `ParcelCount` generated from JSON;
15+
- the selected-module registry and the legacy-shaped `Quantity` facade;
16+
- explicit immutable `UnitSystem` policy and generated System.Text.Json support.
617

718
## Run it from VS Code
819

@@ -24,7 +35,7 @@ Start with any of these:
2435

2536
1. In `ApplicationUnits.cs`, add or remove a built-in `IInclude<...>` quantity selection.
2637
2. Change a `[UnitSet]` list and see which enum members remain available after rebuilding.
27-
3. In `GameScore.unitsnet.json`, add a unit or change a conversion expression.
38+
3. In `ParcelCount.unitsnet.json`, add a unit or change a conversion expression.
2839
4. In `Program.cs`, use the generated types, conversions, parsing, formatting, or operators.
2940

3041
The project enables `EmitCompilerGeneratedFiles`, so after a build you can also inspect the emitted

UnitsNet.Modular/Samples/UnitsNet.Modular.Playground/UnitsNet.Modular.Playground.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
1212
</PropertyGroup>
1313
<ItemGroup>
14-
<AdditionalFiles Include="GameScore.unitsnet.json"
14+
<AdditionalFiles Include="ParcelCount.unitsnet.json"
1515
UnitsNetDefinition="true" />
1616
</ItemGroup>
1717
<ItemGroup>

0 commit comments

Comments
 (0)