Pr397 - exchange set specification update#406
Conversation
Whilst testing found that a number of items were not geeting the data from the catalogue file dur to the scehme versoins being incorrect and the element tags were not correct
…ins. Error was in the PT_Locale
There was a problem hiding this comment.
Pull request overview
Updates the exchange set catalogue model and parsing to align more closely with the evolving S-100 Exchange Catalogue schema, and propagates stronger date/enum types into the Viewer’s exchange set header display and related tests.
Changes:
- Switched multiple catalogue/viewer “date” fields from
stringtoDateOnly?and updated Viewer tests accordingly. - Expanded
EncDotNet.S100.ExchangeSetscatalogue model (new enums/types likePurpose,NavigationPurpose,TemporalExtent,PT_Locale, etc.) and updatedExchangeCatalogueReaderto parse additional elements. - Updated
.gitignoreto exclude additional Visual Studio artifacts.
Reviewed changes
Copilot reviewed 19 out of 23 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/EncDotNet.S100.Viewer.Tests/ExchangeSetServiceLoaderTests.cs | Updates assertions to use DateOnly for exchange set header issue date. |
| tests/EncDotNet.S100.Viewer.Tests/DatasetsViewModelExchangeSetHeaderTests.cs | Updates test calls/assertions for RegisterExchangeSetHeader to use DateOnly. |
| tests/EncDotNet.S100.Viewer.Tests/DatasetsPanelTabsTests.cs | Updates exchange set header registration to pass DateOnly. |
| tests/EncDotNet.S100.ExchangeSets.Tests/ExchangeCatalogueReaderTests.cs | Updates expectations for typed purpose/date and the new DefaultLocale object. |
| src/EncDotNet.S100.Viewer/ViewModels/ExchangeSetHeader.cs | Changes IssueDate to DateOnly? and threads through metadata summary generation. |
| src/EncDotNet.S100.Viewer/ViewModels/DatasetsViewModel.cs | Updates RegisterExchangeSetHeader signature to accept DateOnly?. |
| src/EncDotNet.S100.Viewer/Services/ExchangeSetService.cs | Updates “latest issue date” resolution to work with DateOnly?. |
| src/EncDotNet.S100.ExchangeSets/TemporalExtent.cs | Adds a temporal extent model type for catalogue parsing. |
| src/EncDotNet.S100.ExchangeSets/Purpose.cs | Adds typed purpose enum for dataset discovery metadata. |
| src/EncDotNet.S100.ExchangeSets/PT_Locale.cs | Adds a locale model type used by catalogue/default locale parsing. |
| src/EncDotNet.S100.ExchangeSets/ProductSpecification.cs | Types product specification date and compliancy category. |
| src/EncDotNet.S100.ExchangeSets/NavigationPurpose.cs | Adds typed navigation purpose enum for dataset discovery metadata. |
| src/EncDotNet.S100.ExchangeSets/MaintenanceInformation.cs | Adds maintenance information model + frequency enum for catalogue parsing. |
| src/EncDotNet.S100.ExchangeSets/ExchangeCatalogueReader.cs | Expands catalogue parsing for new typed fields/locales/coverage/maintenance, plus date/time parsing helpers. |
| src/EncDotNet.S100.ExchangeSets/ExchangeCatalogue.cs | Replaces separate default-locale fields with PT_Locale + other locales list. |
| src/EncDotNet.S100.ExchangeSets/DatasetDiscoveryMetadata.cs | Adds new parsed fields and converts several fields to enums/date/time typed properties. |
| src/EncDotNet.S100.ExchangeSets/DataCoverage.cs | Extends data coverage model with additional fields (scales/resolution/temporal extent). |
| src/EncDotNet.S100.ExchangeSets/CompliancyCategory.cs | Adds compliancy category enum used by product specification parsing. |
| src/EncDotNet.S100.ExchangeSets/CatalogueDiscoveryMetadata.cs | Converts issue date to DateOnly? and replaces locale fields with PT_Locale + other locales. |
| .gitignore | Adds ignores for additional VS solution artifacts. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| .Elements(lan + "CountryCode") | ||
| .FirstOrDefault(); | ||
|
|
||
| var country = (string?)langCode?.Attribute("codeListValue"); |
| var charEncode = moreLocal? | ||
| .Elements(lan + "characterEncoding") | ||
| .FirstOrDefault()? | ||
| .Elements(lan + "MD_CharacterSetCode ") |
| .Elements(lan + "MD_CharacterSetCode ") | ||
| .FirstOrDefault(); | ||
|
|
||
| var encoding = (string?)langCode?.Attribute("codeListValue"); |
| return new DataCoverage | ||
| { | ||
| BoundingPolygon = element.Element(xc + "boundingPolygon")?.ToString(), | ||
| MaximumDisplayScale = int.TryParse(maxStr, CultureInfo.InvariantCulture, out var max) ? max : null, | ||
| MinimumDisplayScale = int.TryParse(minStr, CultureInfo.InvariantCulture, out var min) ? min : null, | ||
| OptimumDisplayScale = int.TryParse(optStr, CultureInfo.InvariantCulture, out var opt) ? opt : null, | ||
| ApproximateGridResolution = float.TryParse(resStr, CultureInfo.InvariantCulture, out var res) ? res : null, | ||
| }; |
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (purposeStr != null) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); | ||
|
|
||
|
|
||
| NavigationPurpose? navigationPurpose = null; | ||
| string? naxPurposeStr = (string?)element.Element(xc + "navigationPurpose"); | ||
| if (naxPurposeStr != null) | ||
| navigationPurpose = (NavigationPurpose)Enum.Parse(typeof(NavigationPurpose), naxPurposeStr); |
| if (!string.IsNullOrWhiteSpace(issueDate)) | ||
| if (issueDate != null) | ||
| { | ||
| parts.Add(string.Format(Strings.Pane_ExchangeSetHeader_Issued, issueDate)); |
| [Fact] | ||
| public void DefaultLocale_IsNull() | ||
| { | ||
| var catalogue = ReadTestCatalogue(); | ||
|
|
||
| Assert.Null(catalogue.DefaultLocaleLanguage); | ||
| Assert.Null(catalogue.DefaultLocaleCharacterEncoding); | ||
| Assert.Null(catalogue.DefaultLocale); | ||
| } |
| /// <summary>Catalogue-derived issue date string (the latest | ||
| /// <c>DatasetDiscoveryMetadata.IssueDate</c> across the set), or | ||
| /// <c>null</c> if unknown.</summary> | ||
| public string? IssueDate { get; } | ||
| public DateOnly? IssueDate { get; } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| var country = (string?)langCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; |
| var charEncode = moreLocal? | ||
| .Elements(lan + "characterEncoding") | ||
| .FirstOrDefault()? | ||
| .Elements(lan + "MD_CharacterSetCode ") | ||
| .FirstOrDefault(); | ||
|
|
||
| var encoding = (string?)langCode?.Attribute("codeListValue"); | ||
| if (encoding == null) | ||
| encoding = ""; |
| TemporalExtent? temporalExtent = ReadTempoalExtent(element, xc); | ||
|
|
||
| return new DataCoverage | ||
| { | ||
| BoundingPolygon = element.Element(xc + "boundingPolygon")?.ToString(), | ||
| MaximumDisplayScale = int.TryParse(maxStr, CultureInfo.InvariantCulture, out var max) ? max : null, | ||
| MinimumDisplayScale = int.TryParse(minStr, CultureInfo.InvariantCulture, out var min) ? min : null, | ||
| OptimumDisplayScale = int.TryParse(optStr, CultureInfo.InvariantCulture, out var opt) ? opt : null, | ||
| ApproximateGridResolution = float.TryParse(resStr, CultureInfo.InvariantCulture, out var res) ? res : null, | ||
| }; |
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// </summary> | ||
| private static string? ResolveLatestIssueDate( | ||
| private static DateOnly? ResolveLatestIssueDate( | ||
| IReadOnlyList<DatasetDiscoveryMetadata> datasets) |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| | ||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
| public enum NavigationPurpose | ||
| { | ||
| port = 1, | ||
| transit = 2, | ||
| overview = 3 | ||
| } | ||
| } |
| private static PT_Locale? ReadPTLocale(XElement? localeElement, XNamespace lan) | ||
| { | ||
| if (localeElement is null) return null; | ||
|
|
||
| XElement? moreLocal = localeElement.Element(lan + "PT_Locale"); |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| return new DataCoverage | ||
| { | ||
| BoundingPolygon = element.Element(xc + "boundingPolygon")?.ToString(), | ||
| MaximumDisplayScale = int.TryParse(maxStr, CultureInfo.InvariantCulture, out var max) ? max : null, | ||
| MinimumDisplayScale = int.TryParse(minStr, CultureInfo.InvariantCulture, out var min) ? min : null, | ||
| OptimumDisplayScale = int.TryParse(optStr, CultureInfo.InvariantCulture, out var opt) ? opt : null, | ||
| ApproximateGridResolution = float.TryParse(resStr, CultureInfo.InvariantCulture, out var res) ? res : null, | ||
| }; | ||
| } |
| var country = (string?)langCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; | ||
|
|
||
| var charEncode = moreLocal? | ||
| .Elements(lan + "characterEncoding") | ||
| .FirstOrDefault()? | ||
| .Elements(lan + "MD_CharacterSetCode ") | ||
| .FirstOrDefault(); | ||
|
|
||
| var encoding = (string?)langCode?.Attribute("codeListValue"); | ||
| if (encoding == null) | ||
| encoding = ""; |
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (purposeStr != null) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); | ||
|
|
||
|
|
||
| NavigationPurpose? navigationPurpose = null; | ||
| string? naxPurposeStr = (string?)element.Element(xc + "navigationPurpose"); | ||
| if (naxPurposeStr != null) | ||
| navigationPurpose = (NavigationPurpose)Enum.Parse(typeof(NavigationPurpose), naxPurposeStr); |
| DefaultLocale = ReadPTLocale(element.Element(xc + "defaultLocale"), lan), | ||
| OtherLocales = element.Elements(xc + "otherLocale").Select(e => ReadPTLocales(e, lan)).ToList(), | ||
| MetadataDateStamp = ParseDate((string?)element.Element(xc + "metadataDateStamp")), | ||
| ReplaceData = ParseBool(element, "replaceData", xc), |
| /// <summary>Catalogue-derived issue date string (the latest | ||
| /// <c>DatasetDiscoveryMetadata.IssueDate</c> across the set), or | ||
| /// <c>null</c> if unknown.</summary> | ||
| public string? IssueDate { get; } | ||
| public DateOnly? IssueDate { get; } |
| if (issueDate != null) | ||
| { | ||
| parts.Add(string.Format(Strings.Pane_ExchangeSetHeader_Issued, issueDate)); | ||
| } |
| /// <summary> | ||
| /// Returns the lexically-greatest non-null | ||
| /// <see cref="DatasetDiscoveryMetadata.IssueDate"/> across the | ||
| /// catalogue, or <c>null</c> if no dataset declared one. ISO-8601 | ||
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// </summary> | ||
| private static string? ResolveLatestIssueDate( | ||
| private static DateOnly? ResolveLatestIssueDate( |
|
Fixed review comments and merged |
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (!string.IsNullOrWhiteSpace(purposeStr) && Enum.TryParse<Purpose>(purposeStr, ignoreCase: true, out var parsedPurpose)) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); | ||
|
|
| NavigationPurpose? navigationPurpose = null; | ||
| string? naxPurposeStr = (string?)element.Element(xc + "navigationPurpose"); | ||
| if (naxPurposeStr != null) | ||
| navigationPurpose = (NavigationPurpose)Enum.Parse(typeof(NavigationPurpose), naxPurposeStr); |
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| MetadataDateStamp = ParseDate((string?)element.Element(xc + "metadataDateStamp")), | ||
| ReplaceData = ParseBool(element, "replaceData", xc), | ||
| NavigationPurpose = navigationPurpose, |
| MaintenanceFrequencyCode? maintFreq = null; | ||
| if (mfreq != null) | ||
| maintFreq = (MaintenanceFrequencyCode)Enum.Parse(typeof(MaintenanceFrequencyCode), mfreq); |
| var s = d.IssueDate; | ||
| if (string.IsNullOrEmpty(s)) continue; | ||
| if (latest is null || string.CompareOrdinal(s, latest) > 0) | ||
| if (s == null) continue; | ||
| if (latest is null || ((DateOnly)s).CompareTo((DateOnly)latest) > 0) | ||
| latest = s; |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 23 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (2)
src/EncDotNet.S100.ExchangeSets/ExchangeCatalogueReader.cs:95
compliancyCategoryis parsed viaEnum.Parse, which will throw if the XML contains an unexpected value or casing. For a reader that targets real-world catalogues, it’s safer to useEnum.TryParse(..., ignoreCase: true)and leave the valuenullwhen unrecognised.
}
return root
.Elements()
.Where(e => e.Name.LocalName.EndsWith(suffix, StringComparison.Ordinal));
src/EncDotNet.S100.ExchangeSets/ExchangeCatalogueReader.cs:113
- The
purposeparsing is redundant and can reintroduce case-sensitivity: it first does a case-insensitiveEnum.TryParse, but then assigns viaEnum.Parse(case-sensitive by default). This can throw on inputs likenewDatasetif the enum members are later renamed to PascalCase. Prefer assigning theparsedPurposereturned byTryParse.
City = ReadNestedCharacterString(addressEl, Cit + "city"),
AdministrativeArea = ReadNestedCharacterString(addressEl, Cit + "administrativeArea"),
PostalCode = ReadNestedCharacterString(addressEl, Cit + "postalCode"),
Country = ReadNestedCharacterString(addressEl, Cit + "country"),
};
| public enum MaintenanceFrequencyCode | ||
| { | ||
| asNeeded = 1, | ||
| irregular = 2 | ||
| }; | ||
|
|
| NavigationPurpose? navigationPurpose = null; | ||
| string? naxPurposeStr = (string?)element.Element(xc + "navigationPurpose"); | ||
| if (naxPurposeStr != null) | ||
| navigationPurpose = (NavigationPurpose)Enum.Parse(typeof(NavigationPurpose), naxPurposeStr); |
| if (issueDate != null) | ||
| { | ||
| parts.Add(string.Format(Strings.Pane_ExchangeSetHeader_Issued, issueDate)); | ||
| parts.Add(string.Format(Strings.Pane_ExchangeSetHeader_Issued, issueDate.Value.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture))); | ||
| } |
| public enum Purpose | ||
| { | ||
| newDataset = 1, | ||
| newEdition = 2, | ||
| update = 3, | ||
| reissue = 4, | ||
| cancellation = 5, | ||
| delta = 6, | ||
| } |
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (!string.IsNullOrWhiteSpace(purposeStr) && Enum.TryParse<Purpose>(purposeStr, ignoreCase: true, out var parsedPurpose)) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); | ||
|
|
| NavigationPurpose? navigationPurpose = null; | ||
| string? naxPurposeStr = (string?)element.Element(xc + "navigationPurpose"); | ||
| if (naxPurposeStr != null) | ||
| navigationPurpose = (NavigationPurpose)Enum.Parse(typeof(NavigationPurpose), naxPurposeStr); |
| MaintenanceFrequencyCode? maintFreq = null; | ||
| if (mfreq != null) | ||
| maintFreq = (MaintenanceFrequencyCode)Enum.Parse(typeof(MaintenanceFrequencyCode), mfreq); | ||
|
|
| var lang = (string?)langCode?.Attribute("codeListValue"); | ||
| if (lang == null) | ||
| lang = ""; | ||
|
|
| var encoding = (string?)charEncode?.Attribute("codeListValue"); | ||
| if (encoding == null) | ||
| encoding = ""; | ||
|
|
| // Permit dates are xs:date and may carry a trailing 'Z' (e.g. 2018-03-20Z). | ||
| string trimmed = value.Trim().TrimEnd('Z', 'z'); |
| public string? MetadataPointOfContact { get; init; } | ||
| public DateOnly? MetadataDateStamp { get; init; } | ||
|
|
||
| public bool? ReplaceData { get; set; } | ||
| /// <summary> |
| /// <summary> | ||
| /// Returns the lexically-greatest non-null | ||
| /// <see cref="DatasetDiscoveryMetadata.IssueDate"/> across the | ||
| /// catalogue, or <c>null</c> if no dataset declared one. ISO-8601 | ||
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// catalogue, or <c>null</c> if no dataset declared one. | ||
| /// </summary> |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (!string.IsNullOrWhiteSpace(purposeStr) && Enum.TryParse<Purpose>(purposeStr, ignoreCase: true, out var parsedPurpose)) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); |
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| } | ||
|
|
||
| private static TemporalExtent? ReadTempoalExtent(XElement element, XNamespace xc) | ||
| { | ||
| var timeInstantEl = element.Element(xc + "temporalExtent"); |
| /// <summary> | ||
| /// Returns the lexically-greatest non-null | ||
| /// <see cref="DatasetDiscoveryMetadata.IssueDate"/> across the | ||
| /// catalogue, or <c>null</c> if no dataset declared one. ISO-8601 | ||
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// catalogue, or <c>null</c> if no dataset declared one. | ||
| /// </summary> |
| public enum Purpose | ||
| { | ||
| newDataset = 1, | ||
| newEdition = 2, | ||
| update = 3, | ||
| reissue = 4, | ||
| cancellation = 5, | ||
| delta = 6, | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| Purpose? purpose = null; | ||
| string? purposeStr = (string?)element.Element(xc + "purpose"); | ||
| if (!string.IsNullOrWhiteSpace(purposeStr) && Enum.TryParse<Purpose>(purposeStr, ignoreCase: true, out var parsedPurpose)) | ||
| purpose = (Purpose)Enum.Parse(typeof(Purpose), purposeStr); | ||
|
|
| var numberStr = (string?)element.Element(xc + "number"); | ||
|
|
||
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null) | ||
| comp = (CompliancyCategory)Enum.Parse(typeof(CompliancyCategory), compStr); |
| MaintenanceFrequencyCode? maintFreq = null; | ||
| if (mfreq != null) | ||
| maintFreq = (MaintenanceFrequencyCode)Enum.Parse(typeof(MaintenanceFrequencyCode), mfreq); | ||
|
|
| var country = (string?)countryCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; | ||
|
|
| return null; | ||
| } | ||
|
|
||
| // Permit dates are xs:date and may carry a trailing 'Z' (e.g. 2018-03-20Z). |
| public enum Purpose | ||
| { | ||
| newDataset = 1, | ||
| newEdition = 2, | ||
| update = 3, | ||
| reissue = 4, | ||
| cancellation = 5, | ||
| delta = 6, | ||
| } |
| public enum NavigationPurpose | ||
| { | ||
| port = 1, | ||
| transit = 2, | ||
| overview = 3 | ||
| } |
| public enum CompliancyCategory | ||
| { | ||
| category1 = 1, | ||
| category2 = 2, | ||
| category3 = 3, | ||
| category4 = 4, | ||
| } |
| public enum MaintenanceFrequencyCode | ||
| { | ||
| asNeeded = 1, | ||
| irregular = 2, | ||
| } |
| @@ -150,25 +159,50 @@ | |||
| ExpectedHash = ReadExpectedHash(element), | |||
| Copyright = ParseBool(element, "copyright", xc), | |||
| Classification = ReadCodeListValue(element.Element(xc + "classification")), | |||
| Purpose = (string?)element.Element(xc + "purpose"), | |||
| Purpose = purpose, | |||
| NotForNavigation = ParseBool(element, "notForNavigation", xc), | |||
| SpecificUsage = ReadSpecificUsage(element.Element(xc + "specificUsage")), | |||
| EditionNumber = ParseInt(element, "editionNumber", xc), | |||
| UpdateNumber = ParseInt(element, "updateNumber", xc), | |||
| UpdateApplicationDate = (string?)element.Element(xc + "updateApplicationDate"), | |||
| IssueDate = (string?)element.Element(xc + "issueDate"), | |||
| UpdateApplicationDate = ParseDate((string?)element.Element(xc + "updateApplicationDate")), | |||
| ReferenceId = (string?)element.Element(xc + "referenceID"), | |||
| IssueDate = ParseDate((string?)element.Element(xc + "issueDate")), | |||
| IssueTime = ParseTime((string?)element.Element(xc + "issueTime")), | |||
| BoundingBox = ReadBoundingBox(element.Element(xc + "boundingBox")), | |||
| TemporalExtent = ReadTempoalExtent(element, xc), | |||
| ProductSpecification = ReadProductSpecification(element.Element(xc + "productSpecification"), xc), | |||
| ProducingAgency = ReadProducingAgency(element.Element(xc + "producingAgency")), | |||
| EncodingFormat = (string?)element.Element(xc + "encodingFormat"), | |||
| DataCoverages = element | |||
| .Elements(xc + "dataCoverage") | |||
| .Select(e => ReadDataCoverage(e, xc)) | |||
| .ToList(), | |||
| DefaultLocaleLanguage = ReadLocaleLanguage(defaultLocaleEl, lan), | |||
| DefaultLocaleCharacterEncoding = ReadLocaleCharacterEncoding(defaultLocaleEl, lan), | |||
| MetadataDateStamp = (string?)element.Element(xc + "metadataDateStamp"), | |||
| NavigationPurpose = (string?)element.Element(xc + "navigationPurpose"), | |||
| Comment = (string?)element.Element(xc + "comment"), | |||
| DefaultLocale = ReadPTLocale(element.Element(xc + "defaultLocale"), lan), | |||
| OtherLocales = element.Elements(xc + "otherLocale").Select(e => ReadPTLocales(e, lan)).ToList(), | |||
| MetadataDateStamp = ParseDate((string?)element.Element(xc + "metadataDateStamp")), | |||
| ReplaceData = ParseBool(element, "replaceData", xc), | |||
| NavigationPurpose = navigationPurpose, | |||
| ResourceMaintenance = ReadResourceMaintenance(element.Element(xc + "resourceMaintenance")) | |||
| }; | |||
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 23 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
src/EncDotNet.S100.ExchangeSets/MaintenanceInformation.cs:20
- This file currently mixes a file-scoped namespace with a block-scoped namespace, has an illegal trailing semicolon after the enum, and is missing the closing brace for the namespace/type block. As written it will not compile.
namespace EncDotNet.S100.ExchangeSets;
namespace EncDotNet.S100.ExchangeSets
{
public enum MaintenanceFrequencyCode
{
AsNeeded = 1,
Irregular = 2
};
public sealed class MaintenanceInformation
{
public MaintenanceFrequencyCode? MaintenanceAndUpdateFrequency { get; init; }
public DateOnly? MaintenanceDate { get; init; }
public string? UserDefinedMaintenanceFrequency { get; init; }
}
| CompliancyCategory? comp = null; | ||
| string? compStr = (string?)element.Element(xc + "compliancyCategory"); | ||
| if (compStr != null && Enum.TryParse(typeof(CompliancyCategory), compStr, out var parsedComp)) | ||
| comp = (CompliancyCategory)parsedComp; |
| MaintenanceFrequencyCode? maintFreq = null; | ||
| if (mfreq != null) | ||
| maintFreq = (MaintenanceFrequencyCode)Enum.Parse(typeof(MaintenanceFrequencyCode), mfreq); | ||
|
|
| return null; | ||
| } | ||
|
|
||
| // Permit dates are xs:date and may carry a trailing 'Z' (e.g. 2018-03-20Z). |
| var lang = (string?)langCode?.Attribute("codeListValue"); | ||
| if (lang == null) | ||
| lang = ""; | ||
|
|
||
|
|
||
| var countryCode = moreLocal? | ||
| .Elements(lan + "country") | ||
| .FirstOrDefault()? | ||
| .Elements(lan + "CountryCode") | ||
| .FirstOrDefault(); | ||
|
|
||
| var country = (string?)countryCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; | ||
|
|
||
| var charEncode = moreLocal? | ||
| .Elements(lan + "characterEncoding") | ||
| .FirstOrDefault()? | ||
| .Elements(lan + "MD_CharacterSetCode") | ||
| .FirstOrDefault(); | ||
|
|
||
| var encoding = (string?)charEncode?.Attribute("codeListValue"); | ||
| if (encoding == null) | ||
| encoding = ""; | ||
|
|
| public required string Language { get; init; } | ||
|
|
||
| public string? Country { get; init; } | ||
|
|
||
| public required string CharacterEncoding { get; init; } |
| /// <summary> | ||
| /// Returns the lexically-greatest non-null | ||
| /// <see cref="DatasetDiscoveryMetadata.IssueDate"/> across the | ||
| /// catalogue, or <c>null</c> if no dataset declared one. ISO-8601 | ||
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// catalogue, or <c>null</c> if no dataset declared one. | ||
| /// </summary> |
| public string? MetadataPointOfContact { get; init; } | ||
| public DateOnly? MetadataDateStamp { get; init; } | ||
|
|
||
| public bool? ReplaceData { get; set; } |
| public string? DefaultLocaleLanguage { get; init; } | ||
|
|
||
| public string? DefaultLocaleCharacterEncoding { get; init; } | ||
| public string? DataReplacement { get; init; } | ||
|
|
| /// <summary> | ||
| /// Returns the lexically-greatest non-null | ||
| /// <see cref="DatasetDiscoveryMetadata.IssueDate"/> across the | ||
| /// catalogue, or <c>null</c> if no dataset declared one. ISO-8601 | ||
| /// date strings sort correctly under ordinal comparison, so no | ||
| /// parsing is needed for the common case. | ||
| /// </summary> |
| var s = d.IssueDate; | ||
| if (string.IsNullOrEmpty(s)) continue; | ||
| if (latest is null || string.CompareOrdinal(s, latest) > 0) | ||
| if (s == null) continue; | ||
| if (latest is null || ((DateOnly)s).CompareTo((DateOnly)latest) > 0) | ||
| latest = s; |
| var lang = (string?)langCode?.Attribute("codeListValue"); | ||
| if (lang == null) | ||
| lang = ""; | ||
|
|
| var country = (string?)countryCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; | ||
|
|
| var encoding = (string?)charEncode?.Attribute("codeListValue"); | ||
| if (encoding == null) | ||
| encoding = ""; | ||
|
|
| return new PT_Locale | ||
| { | ||
| Language = lang, | ||
| Country = country, | ||
| CharacterEncoding =encoding, | ||
| }; |
| Comment = (string?)element.Element(xc + "comment"), | ||
| DefaultLocale = ReadPTLocale(element.Element(xc + "defaultLocale"), lan), | ||
| OtherLocales = element.Elements(xc + "otherLocale").Select(e => ReadPTLocales(e, lan)).ToList(), |
| public string? MetadataPointOfContact { get; init; } | ||
| public DateOnly? MetadataDateStamp { get; init; } | ||
|
|
||
| public bool? ReplaceData { get; init; } |
| public string? DefaultLocaleLanguage { get; init; } | ||
|
|
||
| public string? DefaultLocaleCharacterEncoding { get; init; } | ||
| public string? DataReplacement { get; init; } |
| var s = d.IssueDate; | ||
| if (string.IsNullOrEmpty(s)) continue; | ||
| if (latest is null || string.CompareOrdinal(s, latest) > 0) | ||
| if (s == null) continue; | ||
| if (latest is null || ((DateOnly)s).CompareTo(latest) > 0) | ||
| latest = s; |
| return null; | ||
| } | ||
|
|
||
| // Permit times are xs:date and may carry a trailing 'Z' (e.g. 13:45:30Z). |
| | ||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
| public sealed class TemporalExtent | ||
| { | ||
| public DateTime? TimeInstantBegin { get; init; } | ||
|
|
||
| public DateTime? TimeInstantEnd { get; init; } | ||
| } | ||
| } |
| | ||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
| public enum Purpose | ||
| { | ||
| NewDataset = 1, | ||
| NewEdition = 2, | ||
| Update = 3, | ||
| Reissue = 4, | ||
| Cancellation = 5, | ||
| Delta = 6, | ||
| } | ||
| } |
| | ||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
| public enum NavigationPurpose | ||
| { | ||
| Port = 1, | ||
| Transit = 2, | ||
| Overview = 3 | ||
| } | ||
| } |
|
|
||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
|
|
||
| public enum MaintenanceFrequencyCode | ||
| { | ||
| AsNeeded = 1, | ||
| Irregular = 2 | ||
| } | ||
|
|
||
| public sealed class MaintenanceInformation | ||
| { | ||
| public MaintenanceFrequencyCode? MaintenanceAndUpdateFrequency { get; init; } | ||
|
|
||
| public DateOnly? MaintenanceDate { get; init; } | ||
|
|
||
| public string? UserDefinedMaintenanceFrequency { get; init; } | ||
| } | ||
| } |
| | ||
|
|
||
| namespace EncDotNet.S100.ExchangeSets | ||
| { | ||
| public enum CompliancyCategory | ||
| { | ||
| Category1 = 1, | ||
| Category2 = 2, | ||
| Category3 = 3, | ||
| Category4 = 4, | ||
| } | ||
| } |
| var country = (string?)countryCode?.Attribute("codeListValue"); | ||
| if (country == null) | ||
| country = ""; | ||
|
|
| Assert.Equal("DSA", dataset.DigitalSignatureReference); | ||
| Assert.Equal("newDataset", dataset.Purpose); | ||
| Assert.Equal(Purpose.NewDataset, dataset.Purpose); | ||
| Assert.Equal(1, dataset.EditionNumber); | ||
| Assert.Equal("2023-01-16", dataset.IssueDate); | ||
| Assert.Equal(DateOnly.Parse("2023-01-16"), dataset.IssueDate); | ||
| Assert.Equal("ISO/IEC 8211", dataset.EncodingFormat); |
Summary
Spec alignment
Check each spec this PR touches and confirm the relevant skill was
consulted (
.github/skills/<spec>/SKILL.md):s100-framework)s101-enc)s102-bathymetry)s104-water-level)s111-surface-currents)s124-nav-warnings)s129-ukc)Spec section references cited in code/docs:
Tests
tests/SkippableFactdotnet test --configuration Releasepasses locallyDocumentation
src/<project>/README.mddocs/if user-facing behaviourchanged
Dependencies
Directory.Packages.props(not in the.csproj)gh-advisory-databasesecurity check run for any new dependencyBreaking changes