Skip to content

Commit c7e90ef

Browse files
wonbytePascalSenn
andauthoredMay 10, 2021
Add Longitude scalar (#3535)
Co-authored-by: PascalSenn <senn.pasc@gmail.com>
1 parent 2d119d6 commit c7e90ef

File tree

9 files changed

+761
-2
lines changed

9 files changed

+761
-2
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Text.RegularExpressions;
4+
using HotChocolate.Language;
5+
using static System.Math;
6+
7+
namespace HotChocolate.Types
8+
{
9+
/// <summary>
10+
/// The `LongitudeType` scalar represents a valid decimal degrees longitude number.
11+
/// <a href="https://en.wikipedia.org/wiki/Longitude">Read More</a>
12+
/// </summary>
13+
public class LongitudeType : ScalarType<double, StringValueNode>
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of <see cref="LongitudeType"/>
17+
/// </summary>
18+
public LongitudeType()
19+
: this(
20+
WellKnownScalarTypes.Longitude,
21+
ScalarResources.LongitudeType_Description)
22+
{
23+
}
24+
25+
/// <summary>
26+
/// Initializes a new instance of <see cref="LongitudeType"/>
27+
/// </summary>
28+
public LongitudeType(
29+
NameString name,
30+
string? description = null,
31+
BindingBehavior bind = BindingBehavior.Explicit)
32+
: base(name, bind)
33+
{
34+
Description = description;
35+
}
36+
37+
/// <inheritdoc />
38+
protected override bool IsInstanceOfType(double runtimeValue) =>
39+
Longitude.IsValid(runtimeValue);
40+
41+
/// <inheritdoc />
42+
public override IValueNode ParseResult(object? resultValue)
43+
{
44+
return resultValue switch
45+
{
46+
null => NullValueNode.Default,
47+
48+
string s when Longitude.TryDeserialize(s, out var runtimeValue) =>
49+
ParseValue(runtimeValue),
50+
51+
int i => ParseValue(i),
52+
53+
double d => ParseValue(d),
54+
55+
_ => throw ThrowHelper.LongitudeType_ParseValue_IsInvalid(this)
56+
};
57+
}
58+
59+
/// <inheritdoc />
60+
protected override double ParseLiteral(StringValueNode valueSyntax)
61+
{
62+
if (Longitude.TryDeserialize(valueSyntax.Value, out var runtimeValue))
63+
{
64+
return runtimeValue.Value;
65+
}
66+
67+
throw ThrowHelper.LongitudeType_ParseLiteral_IsInvalid(this);
68+
}
69+
70+
/// <inheritdoc />
71+
protected override StringValueNode ParseValue(double runtimeValue)
72+
{
73+
if (Longitude.TrySerialize(runtimeValue, out var s))
74+
{
75+
return new StringValueNode(s);
76+
}
77+
78+
throw ThrowHelper.LongitudeType_ParseValue_IsInvalid(this);
79+
}
80+
81+
/// <inheritdoc />
82+
public override bool TrySerialize(object? runtimeValue, out object? resultValue)
83+
{
84+
switch (runtimeValue)
85+
{
86+
case double d when Longitude.TrySerialize(d, out var serializedDouble):
87+
resultValue = serializedDouble;
88+
return true;
89+
90+
case int i when Longitude.TrySerialize(i, out var serializedInt):
91+
resultValue = serializedInt;
92+
return true;
93+
94+
default:
95+
resultValue = null;
96+
return false;
97+
}
98+
}
99+
100+
/// <inheritdoc />
101+
public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
102+
{
103+
if (resultValue is string s && Longitude.TryDeserialize(s, out var value))
104+
{
105+
runtimeValue = value;
106+
return true;
107+
}
108+
109+
runtimeValue = null;
110+
return false;
111+
}
112+
113+
private static class Longitude
114+
{
115+
private const double _min = -180.0;
116+
private const double _max = 180.0;
117+
private const int _maxPrecision = 8;
118+
119+
private const string _sexagesimalRegex =
120+
"^([0-9]{1,3})°\\s*([0-9]{1,3}(?:\\.(?:[0-9]{1,}))?)['′]\\s*(([0-9]{1,3}" +
121+
"(\\.([0-9]{1,}))?)[\"″]\\s*)?([NEOSW]?)$";
122+
123+
private static readonly Regex _validationPattern =
124+
new(_sexagesimalRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase);
125+
126+
public static bool IsValid(double value) => value is > _min and < _max;
127+
128+
public static bool TryDeserialize(
129+
string serialized,
130+
[NotNullWhen(true)] out double? value)
131+
{
132+
MatchCollection coords = _validationPattern.Matches(serialized);
133+
if (coords.Count > 0)
134+
{
135+
var minute = double.TryParse(coords[0].Groups[2].Value, out var min)
136+
? min / 60
137+
: 0;
138+
var second = double.TryParse(coords[0].Groups[4].Value, out var sec)
139+
? sec / 3600
140+
: 0;
141+
var degree = double.Parse(coords[0].Groups[1].Value);
142+
var result = degree + minute + second;
143+
144+
// Southern and western coordinates must be negative decimals
145+
var deserialized = coords[0].Groups[7].Value is "W" or "S" ? -result : result;
146+
147+
value = deserialized;
148+
return IsValid(deserialized);
149+
}
150+
151+
value = null;
152+
return false;
153+
}
154+
155+
public static bool TrySerialize(
156+
double runtimeValue,
157+
[NotNullWhen(true)] out string? resultValue)
158+
{
159+
if (IsValid(runtimeValue))
160+
{
161+
var degree = runtimeValue >= 0
162+
? Floor(runtimeValue)
163+
: Ceiling(runtimeValue);
164+
var degreeDecimals = runtimeValue - degree;
165+
166+
var minutesWhole = degreeDecimals * 60;
167+
var minutes = minutesWhole >= 0
168+
? Floor(minutesWhole)
169+
: Ceiling(minutesWhole);
170+
var minutesDecimal = minutesWhole - minutes;
171+
172+
var seconds =
173+
Round(minutesDecimal * 60, _maxPrecision, MidpointRounding.AwayFromZero);
174+
175+
string serializedLatitude = degree switch
176+
{
177+
>= 0 and < _max => $"{degree}° {minutes}' {seconds}\" E",
178+
< 0 and > _min => $"{Abs(degree)}° {Abs(minutes)}' {Abs(seconds)}\" W",
179+
_ => $"{degree}° {minutes}' {seconds}\""
180+
};
181+
182+
resultValue = serializedLatitude;
183+
return true;
184+
}
185+
186+
resultValue = null;
187+
return false;
188+
}
189+
}
190+
}
191+
}

‎src/HotChocolate/Core/src/Types.Scalars/ScalarResources.Designer.cs

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/HotChocolate/Core/src/Types.Scalars/ScalarResources.resx

+9
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@
232232
<data name="LocalCurrencyType_IsInvalid_ParseValue" xml:space="preserve">
233233
<value>LocalCurrencyType cannot parse the provided value. The provided value is not a valid local currency.</value>
234234
</data>
235+
<data name="LongitudeType_Description" xml:space="preserve">
236+
<value>The Longitude scalar type is a valid decimal degrees longitude number.</value>
237+
</data>
238+
<data name="LongitudeType_IsInvalid_ParseLiteral" xml:space="preserve">
239+
<value>LongitudeType cannot parse the provided literal. The provided value is not a valid longitude number.</value>
240+
</data>
241+
<data name="LongitudeType_IsInvalid_ParseValue" xml:space="preserve">
242+
<value>LongitudeType cannot parse the provided value. The provided value is not a valid longitude number.</value>
243+
</data>
235244
<data name="NegativeFloatType_Description" xml:space="preserve">
236245
<value>The NegativeFloat scalar type represents a double‐precision fractional value less than 0.</value>
237246
</data>

‎src/HotChocolate/Core/src/Types.Scalars/ThrowHelper.cs

+22
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,28 @@ public static SerializationException LocalTimeType_ParseLiteral_IsInvalid(IType
244244
type);
245245
}
246246

247+
public static SerializationException LongitudeType_ParseValue_IsInvalid(IType type)
248+
{
249+
return new SerializationException(
250+
ErrorBuilder.New()
251+
.SetMessage(ScalarResources.LongitudeType_IsInvalid_ParseValue)
252+
.SetCode(ErrorCodes.Scalars.InvalidRuntimeType)
253+
.SetExtension("actualType", WellKnownScalarTypes.Longitude)
254+
.Build(),
255+
type);
256+
}
257+
258+
public static SerializationException LongitudeType_ParseLiteral_IsInvalid(IType type)
259+
{
260+
return new SerializationException(
261+
ErrorBuilder.New()
262+
.SetMessage(ScalarResources.LongitudeType_IsInvalid_ParseLiteral)
263+
.SetCode(ErrorCodes.Scalars.InvalidSyntaxFormat)
264+
.SetExtension("actualType", WellKnownScalarTypes.Longitude)
265+
.Build(),
266+
type);
267+
}
268+
247269
public static SerializationException MacAddressType_ParseValue_IsInvalid(IType type)
248270
{
249271
return new SerializationException(

‎src/HotChocolate/Core/src/Types.Scalars/WellKnownScalarTypes.cs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal static class WellKnownScalarTypes
1313
public const string LocalDate = nameof(LocalDate);
1414
public const string LocalCurency = nameof(LocalCurency);
1515
public const string LocalTime = nameof(LocalTime);
16+
public const string Longitude = nameof(Longitude);
1617
public const string MacAddress = nameof(MacAddress);
1718
public const string NegativeFloat = nameof(NegativeFloat);
1819
public const string NegativeInt = nameof(NegativeInt);

‎src/HotChocolate/Core/test/Types.Scalars.Tests/LongitudeTypeTests.cs

+496
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"data": {
3+
"test": "0° 0' 0\" E"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
schema {
2+
query: Query
3+
}
4+
5+
type Query {
6+
scalar: Longitude
7+
}
8+
9+
"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`."
10+
directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT
11+
12+
"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`."
13+
directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD
14+
15+
"The Longitude scalar type is a valid decimal degrees longitude number."
16+
scalar Longitude

‎website/src/docs/hotchocolate/defining-a-schema/scalars.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,9 @@ services
422422
| Hsla | The `Hsla` scalar type represents a valid a CSS HSLA color as defined [here](https://developer.mozilla.org/en-US/docs/Web/CSS/) color_value#hsl_colors.
423423
| IPv4 | The `IPv4` scalar type represents a valid a IPv4 address as defined [here](https://en.wikipedia.org/wiki/) IPv4.
424424
| IPv6 | The `IPv6` scalar type represents a valid a IPv6 address as defined here [RFC8064](https://tools.ietf.org/html/rfc8064).
425-
| Isbn | The `ISBN` scalar type is a ISBN-10 or ISBN-13 number: https:\/\/en.wikipedia.org\/wiki\/International_Standard_Book_Number.
426-
| Latitude | The `Latitude` scalar type is a valid decimal degrees latitude number.
425+
| Isbn | The `ISBN` scalar type is a ISBN-10 or ISBN-13 number: https:\/\/en.wikipedia.org\/wiki\/International_Standard_Book_Number.
426+
| Latitude | The `Latitude` scalar type is a valid decimal degrees latitude number.
427+
| Longitude | The `Longitude` scalar type is a valid decimal degrees longitude number.
427428
| LocalCurrency | The `LocalCurrency` scalar type is a currency string.
428429
| LocalDate | The `LocalDate` scalar type represents a ISO date string, represented as UTF-8 character sequences yyyy-mm-dd. The scalar follows the specification defined in RFC3339.
429430
| LocalTime | The `LocalTime` scalar type is a local time string (i.e., with no associated timezone) in 24-hr `HH:mm:ss]`.

0 commit comments

Comments
 (0)
Please sign in to comment.