Skip to content

Commit 77865b8

Browse files
committed
Type building metric values
1 parent 8853392 commit 77865b8

5 files changed

Lines changed: 115 additions & 34 deletions

File tree

src/PlateauResoniteLink/Application/Importing/BuildingAttributeModels.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23

34
namespace PlateauResoniteLink.Application.Importing;
@@ -49,27 +50,51 @@ internal enum PlateauBuildingStructure
4950
Other,
5051
}
5152

52-
internal enum BuildingMetricValueKind
53-
{
54-
Missing = 0,
55-
Known,
56-
Invalid,
57-
}
58-
5953
internal sealed record BuildingCodeValue<T>(T Value, string Code);
6054

61-
internal sealed record BuildingMetricValue(BuildingMetricValueKind Kind, double? Value, string? Raw)
55+
internal abstract record BuildingMetricValue
6256
{
63-
public static BuildingMetricValue Missing { get; } = new(BuildingMetricValueKind.Missing, null, null);
57+
public static BuildingMetricValue Missing { get; } = new MissingMetricValue();
6458

6559
public static BuildingMetricValue Known(double value)
6660
{
67-
return new BuildingMetricValue(BuildingMetricValueKind.Known, value, null);
61+
return new KnownMetricValue(value);
6862
}
6963

7064
public static BuildingMetricValue Invalid(string raw)
7165
{
72-
return new BuildingMetricValue(BuildingMetricValueKind.Invalid, null, raw);
66+
return new InvalidMetricValue(raw);
67+
}
68+
69+
internal sealed record MissingMetricValue : BuildingMetricValue
70+
{
71+
}
72+
73+
internal sealed record KnownMetricValue : BuildingMetricValue
74+
{
75+
public KnownMetricValue(double value)
76+
{
77+
ArgumentOutOfRangeException.ThrowIfNegative(value);
78+
79+
if (!double.IsFinite(value))
80+
{
81+
throw new ArgumentOutOfRangeException(nameof(value));
82+
}
83+
84+
Value = value;
85+
}
86+
87+
public double Value { get; }
88+
}
89+
90+
internal sealed record InvalidMetricValue : BuildingMetricValue
91+
{
92+
public InvalidMetricValue(string raw)
93+
{
94+
Raw = raw ?? throw new ArgumentNullException(nameof(raw));
95+
}
96+
97+
public string Raw { get; }
7398
}
7499
}
75100

src/PlateauResoniteLink/Application/Importing/BuildingAttributeParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ private static BuildingMetricValue ParseMetricValue(XElement? element, bool requ
166166
}
167167

168168
return double.TryParse(rawValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double value)
169+
&& double.IsFinite(value)
169170
&& value > 0.0
170171
? BuildingMetricValue.Known(value)
171172
: BuildingMetricValue.Invalid(rawValue);

src/PlateauResoniteLink/Application/Importing/BuildingAttributeQueries.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ internal static class BuildingAttributeQueries
99
{
1010
internal static int? TryGetKnownPositiveInteger(BuildingMetricValue metric)
1111
{
12-
if (metric.Kind != BuildingMetricValueKind.Known || !metric.Value.HasValue)
12+
if (metric is not BuildingMetricValue.KnownMetricValue knownMetric)
1313
{
1414
return null;
1515
}
1616

17-
int value = (int)Math.Round(metric.Value.Value, MidpointRounding.AwayFromZero);
18-
return Math.Abs(metric.Value.Value - value) < 1e-9
17+
int value = (int)Math.Round(knownMetric.Value, MidpointRounding.AwayFromZero);
18+
return Math.Abs(knownMetric.Value - value) < 1e-9
1919
&& (value == 0 || FacadeFloorMetrics.IsUsableFloorCount(value))
2020
? value
2121
: null;
2222
}
2323

2424
internal static double? TryGetKnownPositiveMetric(BuildingMetricValue metric)
2525
{
26-
return metric.Kind == BuildingMetricValueKind.Known && metric.Value is > 0.0
27-
? metric.Value
26+
return metric is BuildingMetricValue.KnownMetricValue { Value: > 0.0 } knownMetric
27+
? knownMetric.Value
2828
: null;
2929
}
3030

tests/PlateauResoniteLink.Tests/Profiles/BuildingAttributeParserTests.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Linq;
23
using System.Xml.Linq;
34

@@ -49,9 +50,9 @@ public void ParseTreatsPlateauMissingSentinelsAsMissingMetrics()
4950

5051
BuildingAttributeContext attributes = BuildingAttributeParser.Parse(element);
5152

52-
Assert.Equal(BuildingMetricValueKind.Missing, attributes.MeasuredHeightMeters.Kind);
53-
Assert.Equal(BuildingMetricValueKind.Missing, attributes.StoreysAboveGround.Kind);
54-
Assert.Equal(BuildingMetricValueKind.Missing, attributes.StoreysBelowGround.Kind);
53+
Assert.IsType<BuildingMetricValue.MissingMetricValue>(attributes.MeasuredHeightMeters);
54+
Assert.IsType<BuildingMetricValue.MissingMetricValue>(attributes.StoreysAboveGround);
55+
Assert.IsType<BuildingMetricValue.MissingMetricValue>(attributes.StoreysBelowGround);
5556
}
5657

5758
[Fact]
@@ -70,12 +71,55 @@ public void ParseRejectsNonMeterHeightButKeepsAreaWithoutMeterRequirement()
7071

7172
BuildingAttributeContext attributes = BuildingAttributeParser.Parse(element);
7273

73-
Assert.Equal(BuildingMetricValueKind.Invalid, attributes.MeasuredHeightMeters.Kind);
74-
Assert.Equal("12", attributes.MeasuredHeightMeters.Raw);
75-
Assert.Equal(BuildingMetricValueKind.Known, attributes.BuildingFootprintArea.Kind);
76-
Assert.Equal(160.5, attributes.BuildingFootprintArea.Value);
77-
Assert.Equal(BuildingMetricValueKind.Known, attributes.BuildingHeight.Kind);
78-
Assert.Equal(9.25, attributes.BuildingHeight.Value);
74+
BuildingMetricValue.InvalidMetricValue invalidMeasuredHeight =
75+
Assert.IsType<BuildingMetricValue.InvalidMetricValue>(attributes.MeasuredHeightMeters);
76+
BuildingMetricValue.KnownMetricValue footprintArea =
77+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.BuildingFootprintArea);
78+
BuildingMetricValue.KnownMetricValue buildingHeight =
79+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.BuildingHeight);
80+
Assert.Equal("12", invalidMeasuredHeight.Raw);
81+
Assert.Equal(160.5, footprintArea.Value);
82+
Assert.Equal(9.25, buildingHeight.Value);
83+
}
84+
85+
[Fact]
86+
public void ParsePreservesBlankMetricElementsAsInvalidMetrics()
87+
{
88+
XElement element = XElement.Parse(
89+
"""
90+
<bldg:Building xmlns:bldg="urn:bldg">
91+
<bldg:measuredHeight uom="m"> </bldg:measuredHeight>
92+
</bldg:Building>
93+
""");
94+
95+
BuildingAttributeContext attributes = BuildingAttributeParser.Parse(element);
96+
97+
BuildingMetricValue.InvalidMetricValue invalidMeasuredHeight =
98+
Assert.IsType<BuildingMetricValue.InvalidMetricValue>(attributes.MeasuredHeightMeters);
99+
Assert.Equal(string.Empty, invalidMeasuredHeight.Raw);
100+
}
101+
102+
[Fact]
103+
public void ParsePreservesNonFiniteMetricTextAsInvalidMetrics()
104+
{
105+
XElement element = XElement.Parse(
106+
"""
107+
<bldg:Building xmlns:bldg="urn:bldg">
108+
<bldg:measuredHeight uom="m">Infinity</bldg:measuredHeight>
109+
<bldg:BuildingDetailAttribute>
110+
<bldg:buildingHeight uom="m">1e309</bldg:buildingHeight>
111+
</bldg:BuildingDetailAttribute>
112+
</bldg:Building>
113+
""");
114+
115+
BuildingAttributeContext attributes = BuildingAttributeParser.Parse(element);
116+
117+
BuildingMetricValue.InvalidMetricValue invalidMeasuredHeight =
118+
Assert.IsType<BuildingMetricValue.InvalidMetricValue>(attributes.MeasuredHeightMeters);
119+
BuildingMetricValue.InvalidMetricValue invalidBuildingHeight =
120+
Assert.IsType<BuildingMetricValue.InvalidMetricValue>(attributes.BuildingHeight);
121+
Assert.Equal("Infinity", invalidMeasuredHeight.Raw);
122+
Assert.Equal("1e309", invalidBuildingHeight.Raw);
79123
}
80124

81125
[Fact]
@@ -93,4 +137,13 @@ public void ParseDropsBlankDirectCodeValues()
93137

94138
Assert.Equal(["402101"], attributes.CityGmlFunctionCodes.ToArray());
95139
}
140+
141+
[Fact]
142+
public void MetricValueFactoriesRejectInvalidPayloadShapes()
143+
{
144+
Assert.Throws<ArgumentOutOfRangeException>(() => BuildingMetricValue.Known(double.NaN));
145+
Assert.Throws<ArgumentOutOfRangeException>(() => BuildingMetricValue.Known(double.PositiveInfinity));
146+
Assert.Throws<ArgumentOutOfRangeException>(() => BuildingMetricValue.Known(-1.0));
147+
Assert.Throws<ArgumentNullException>(() => BuildingMetricValue.Invalid(null!));
148+
}
96149
}

tests/PlateauResoniteLink.Tests/Profiles/LocalCityGmlSourceFileParserStreamingTests.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,17 @@ public async Task SourceFilePipeline_StreamParsedCityObjectsAsync_PreservesFloor
171171
Assert.Contains(attributes.Structures, value => value.Value == PlateauBuildingStructure.Wood && value.Code == "601");
172172
Assert.Equal(["3001"], attributes.CityGmlClassCodes);
173173
Assert.Equal(["401"], attributes.CityGmlFunctionCodes);
174-
Assert.Equal(BuildingMetricValueKind.Known, attributes.MeasuredHeightMeters.Kind);
175-
Assert.Equal(BuildingMetricValueKind.Known, attributes.StoreysAboveGround.Kind);
176-
Assert.Equal(BuildingMetricValueKind.Missing, attributes.StoreysBelowGround.Kind);
177-
Assert.Equal(BuildingMetricValueKind.Known, attributes.BuildingFootprintArea.Kind);
178-
Assert.Equal(BuildingMetricValueKind.Missing, attributes.BuildingRoofEdgeArea.Kind);
179-
Assert.Equal(BuildingMetricValueKind.Known, attributes.BuildingHeight.Kind);
180-
Assert.Equal(BuildingMetricValueKind.Known, attributes.EaveHeight.Kind);
181-
Assert.InRange(attributes.BuildingFootprintArea.Value!.Value, 120.499999, 120.500001);
182-
Assert.InRange(attributes.EaveHeight.Value!.Value, 9.699999, 9.700001);
174+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.MeasuredHeightMeters);
175+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.StoreysAboveGround);
176+
Assert.IsType<BuildingMetricValue.MissingMetricValue>(attributes.StoreysBelowGround);
177+
BuildingMetricValue.KnownMetricValue footprintArea =
178+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.BuildingFootprintArea);
179+
Assert.IsType<BuildingMetricValue.MissingMetricValue>(attributes.BuildingRoofEdgeArea);
180+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.BuildingHeight);
181+
BuildingMetricValue.KnownMetricValue eaveHeight =
182+
Assert.IsType<BuildingMetricValue.KnownMetricValue>(attributes.EaveHeight);
183+
Assert.InRange(footprintArea.Value, 120.499999, 120.500001);
184+
Assert.InRange(eaveHeight.Value, 9.699999, 9.700001);
183185
}
184186

185187
private static async Task<ParsedCityObject> ParseSingleBuildingWithDetailAttributeAsync(string detailAttributeXml)

0 commit comments

Comments
 (0)