parsing time zones and new chart types

This commit is contained in:
Nils Kruthoff 2026-08-10 14:05:38 +02:00
parent 05ede0c615
commit 0af39012cd
No known key found for this signature in database
GPG Key ID: A5C0151B4DDB172C

View File

@ -45,7 +45,7 @@ public static class ChartBlockParser
if (!TryGetRequiredString(root, "type", out var typeText) if (!TryGetRequiredString(root, "type", out var typeText)
|| !TryParseType(typeText, out var type)) || !TryParseType(typeText, out var type))
return ChartBlockParseResult.Invalid(json, "type must be bar, stacked_bar, line, pie, or donut."); return ChartBlockParseResult.Invalid(json, "type must be bar, stacked_bar, line, pie, donut, heatmap, or time_series.");
if (!TryGetRequiredString(root, "title", out var title) || string.IsNullOrWhiteSpace(title)) if (!TryGetRequiredString(root, "title", out var title) || string.IsNullOrWhiteSpace(title))
return ChartBlockParseResult.Invalid(json, "title must be a non-empty string."); return ChartBlockParseResult.Invalid(json, "title must be a non-empty string.");
@ -66,7 +66,7 @@ public static class ChartBlockParser
if (!HasOnlyKnownProperties(data, DATA_PROPERTIES, out propertyError)) if (!HasOnlyKnownProperties(data, DATA_PROPERTIES, out propertyError))
return ChartBlockParseResult.Invalid(json, propertyError); return ChartBlockParseResult.Invalid(json, propertyError);
if (!TryReadCategories(data, out var categories, out var error)) if (!TryReadCategories(data, type, out var categories, out var error))
return ChartBlockParseResult.Invalid(json, error); return ChartBlockParseResult.Invalid(json, error);
if (!TryReadSeries(data, categories.Count, out var series, out error)) if (!TryReadSeries(data, categories.Count, out var series, out error))
@ -89,7 +89,11 @@ public static class ChartBlockParser
} }
} }
private static bool TryReadCategories(JsonElement data, out IReadOnlyList<string> categories, out string error) private static bool TryReadCategories(
JsonElement data,
ChartDefinitionType type,
out IReadOnlyList<string> categories,
out string error)
{ {
categories = []; categories = [];
error = string.Empty; error = string.Empty;
@ -100,6 +104,7 @@ public static class ChartBlockParser
} }
var values = new List<string>(); var values = new List<string>();
DateTimeOffset? previousTimestamp = null;
foreach (var item in element.EnumerateArray()) foreach (var item in element.EnumerateArray())
{ {
if (item.ValueKind is not JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString())) if (item.ValueKind is not JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
@ -108,7 +113,25 @@ public static class ChartBlockParser
return false; return false;
} }
values.Add(item.GetString()!); var value = item.GetString()!;
if (type is ChartDefinitionType.TIME_SERIES)
{
if (!HasExplicitTimeZone(value) || !item.TryGetDateTimeOffset(out var timestamp))
{
error = "Time series categories must be ISO 8601 timestamps with Z or an explicit UTC offset.";
return false;
}
if (previousTimestamp is not null && timestamp <= previousTimestamp.Value)
{
error = "Time series categories must be strictly increasing timestamps.";
return false;
}
previousTimestamp = timestamp;
}
values.Add(value);
if (values.Count > MAX_CATEGORIES) if (values.Count > MAX_CATEGORIES)
{ {
error = $"A chart can contain at most {MAX_CATEGORIES} categories."; error = $"A chart can contain at most {MAX_CATEGORIES} categories.";
@ -122,10 +145,33 @@ public static class ChartBlockParser
return false; return false;
} }
if (type is ChartDefinitionType.TIME_SERIES && values.Count < 2)
{
error = "A time series chart requires at least two timestamps.";
return false;
}
categories = values; categories = values;
return true; return true;
} }
private static bool HasExplicitTimeZone(string value)
{
if (value.EndsWith('Z'))
return true;
if (value.Length < 6)
return false;
var offsetSign = value[^6];
return offsetSign is '+' or '-'
&& char.IsAsciiDigit(value[^5])
&& char.IsAsciiDigit(value[^4])
&& value[^3] is ':'
&& char.IsAsciiDigit(value[^2])
&& char.IsAsciiDigit(value[^1]);
}
private static bool TryReadSeries(JsonElement data, int categoryCount, out IReadOnlyList<ChartDefinitionSeries> series, out string error) private static bool TryReadSeries(JsonElement data, int categoryCount, out IReadOnlyList<ChartDefinitionSeries> series, out string error)
{ {
series = []; series = [];
@ -247,10 +293,12 @@ public static class ChartBlockParser
"line" => ChartDefinitionType.LINE, "line" => ChartDefinitionType.LINE,
"pie" => ChartDefinitionType.PIE, "pie" => ChartDefinitionType.PIE,
"donut" => ChartDefinitionType.DONUT, "donut" => ChartDefinitionType.DONUT,
"heatmap" => ChartDefinitionType.HEATMAP,
"time_series" => ChartDefinitionType.TIME_SERIES,
_ => default, _ => default,
}; };
return value is "bar" or "stacked_bar" or "line" or "pie" or "donut"; return value is "bar" or "stacked_bar" or "line" or "pie" or "donut" or "heatmap" or "time_series";
} }
} }