Skip to content

feat: add xsd:datetime codec and data type - #65

Closed
mark-dropbear wants to merge 3 commits into
mainfrom
datetime
Closed

mark-dropbear wants to merge 3 commits into
mainfrom
datetime

Conversation

@mark-dropbear

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @mark-dropbear, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the library's support for XSD 1.1 built-in datatypes by introducing the xsd:datetime type and its associated codec. The core XsdDateTime class provides robust handling for the complexities of XSD date-time values, including 'floating' times and timezone offsets, ensuring accurate interpretation and representation. This addition improves the library's compliance with the XSD specification and offers a more reliable way to work with date-time data.

Highlights

  • New Data Type and Codec: Introduced a new xsd:datetime data type and its corresponding codec, XsdDateTimeCodec, to handle XSD date-time values.
  • XsdDateTime Class: Added the XsdDateTime class, a wrapper around Dart's DateTime, designed to provide XSD-compliant handling of xsd:dateTime values, specifically addressing 'floating' (timezone-less) times and preserving original timezone offsets.
  • Documentation Updates: Updated CHANGELOG.md and README.md to reflect the new xsd:datetime support, including a note on how XsdDateTime manages timezones differently from Dart's native DateTime.
  • Comprehensive Testing: Included a new test file, xsd_datetime_test.dart, with extensive unit tests for the XsdDateTime class, covering parsing, serialization, equality, and comparison across various timezone scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the xsd:datetime data type by adding a new XsdDateTime class and a corresponding XsdDateTimeCodec. The implementation correctly handles floating, UTC, and offset-based date-time strings, and includes logic to preserve the original timezone offset for round-tripping. The code is well-structured and includes a comprehensive set of unit tests.

My review includes a few suggestions for improvement:

  • In xsd_datetime.dart, I've suggested improving the documentation for the parse method, optimizing performance by caching regular expressions, and refining the toString() method to better handle round-tripping of values without fractional seconds.
  • In README.md, I've proposed a clarification to the note about DateTime's timezone handling.
  • I've also noted that the tests for toString() will need to be updated if the implementation is changed as suggested.
  • Finally, a minor formatting fix for CHANGELOG.md is included.

Overall, this is a great addition to the library.

Comment thread CHANGELOG.md Outdated
Comment thread README.md Outdated
Comment on lines +30 to +35
/// Parses an XSD dateTime string.
///
/// Handles formats like:
/// - `2002-10-10T12:00:00` (Floating)
/// - `2002-10-10T12:00:00Z` (UTC)
/// - `2002-10-10T12:00:00-05:00` (Offset)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation for parse is helpful, but it would be even better to explicitly mention that it can throw a FormatException for invalid input, which is standard practice for parse methods in Dart.

  /// Parses an XSD dateTime string.
  ///
  /// Throws a [FormatException] if the [input] is not a valid representation.
  ///
  /// Handles formats like:
  /// - `2002-10-10T12:00:00` (Floating)
  /// - `2002-10-10T12:00:00Z` (UTC)
  /// - `2002-10-10T12:00:00-05:00` (Offset)

Comment on lines +36 to +71
static XsdDateTime parse(String input) {
// Regex to detect timezone presence.
// Matches Z or +/-HH:MM at the end.
final hasTimezone = RegExp(r'(Z|[+-]\d{2}:\d{2})$').hasMatch(input);

if (!hasTimezone) {
// Floating time. Parse as if it were UTC to avoid local timezone interference.
// We append 'Z' to force DateTime.parse to treat it as UTC, then strip it conceptually.
// Actually, DateTime.parse handles '2002-10-10T12:00:00' as local.
// To ensure we store the exact values provided without local conversion,
// we can append 'Z' to the input string before parsing.
final utcInput = '${input}Z';
final dt = DateTime.parse(utcInput);
return XsdDateTime(dt, isFloating: true);
} else {
final dt = DateTime.parse(input);
Duration? offset;

if (input.endsWith('Z')) {
offset = Duration.zero;
} else {
// Extract offset manually because DateTime doesn't expose the original offset
// if it converts to UTC/Local.
// Format is +/-HH:MM
final match = RegExp(r'([+-])(\d{2}):(\d{2})$').firstMatch(input);
if (match != null) {
final sign = match.group(1) == '+' ? 1 : -1;
final hours = int.parse(match.group(2)!);
final minutes = int.parse(match.group(3)!);
offset = Duration(hours: hours, minutes: minutes) * sign;
}
}

return XsdDateTime(dt, isFloating: false, originalOffset: offset);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expressions on lines 39 and 60 are recompiled on every call to parse. For better performance, you can define them as static final fields on the XsdDateTime class to compile them only once.

Example:

class XsdDateTime implements Comparable<XsdDateTime> {
  static final _timezoneRegex = RegExp(r'(Z|[+-]\d{2}:\d{2})$');
  static final _offsetRegex = RegExp(r'([+-])(\d{2}):(\d{2})$');

  // ... other members

  static XsdDateTime parse(String input) {
    final hasTimezone = _timezoneRegex.hasMatch(input);
    // ...
  }
  // ...
}

Comment on lines +79 to +111
String toString() {
if (isFloating) {
// Return ISO string without 'Z' or offset.
// value is UTC, so toIso8601String() returns ...Z
return value.toIso8601String().replaceAll('Z', '');
}

if (originalOffset != null && originalOffset != Duration.zero) {
// We need to shift the UTC value back to the original offset for display.
final localTime = value.add(originalOffset!);
// iso is ...Z or ... depending on how it was created, but since we added offset manually
// to a UTC date, the result is technically "what the time would be in that zone".
// However, DateTime.toIso8601String() always ends in Z if isUtc is true,
// or nothing if isUtc is false.
// Since 'value' is UTC, 'localTime' is also UTC (just shifted).
// We want to strip the Z and append the offset.

// Remember that DateTime.add returns a DateTime with the same isUtc property.
// So localTime is still UTC.
// Example: 12:00Z + 5 hours = 17:00Z.
// We want to print 17:00+05:00.
final bareIso = localTime.toIso8601String().replaceAll('Z', '');

final sign = originalOffset!.isNegative ? '-' : '+';
final absOffset = originalOffset!.abs();
final hours = absOffset.inHours.toString().padLeft(2, '0');
final minutes = (absOffset.inMinutes % 60).toString().padLeft(2, '0');

return '$bareIso$sign$hours:$minutes';
}

return value.toIso8601String();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current toString() implementation uses toIso8601String() which always includes milliseconds, even if they are zero (e.g., .000). This means that parsing a string like 2002-10-10T12:00:00 and then converting it back to a string results in 2002-10-10T12:00:00.000, which is not identical to the original input. While valid, this can be undesirable for round-tripping lexical representations.

To better preserve the original format, consider modifying toString() to omit the fractional part if it's zero. You could introduce a helper function that checks if millisecond and microsecond are zero and, if so, removes the .000 part from the toIso8601String() output before further processing.

Comment on lines +1 to +69
import 'package:test/test.dart';
import 'package:xsd/src/types/xsd_datetime.dart';

void main() {
group('XsdDateTime', () {
test('parses floating time', () {
final dt = XsdDateTime.parse('2002-10-10T12:00:00');
expect(dt.isFloating, isTrue);
expect(dt.value.isUtc, isTrue);
expect(dt.value.year, 2002);
expect(dt.value.month, 10);
expect(dt.value.day, 10);
expect(dt.value.hour, 12);
expect(dt.value.minute, 0);
expect(dt.value.second, 0);
expect(dt.originalOffset, isNull);
expect(dt.toString(), '2002-10-10T12:00:00.000');
});

test('parses UTC time', () {
final dt = XsdDateTime.parse('2002-10-10T12:00:00Z');
expect(dt.isFloating, isFalse);
expect(dt.value.isUtc, isTrue);
expect(dt.originalOffset, Duration.zero);
expect(dt.toString(), '2002-10-10T12:00:00.000Z');
});

test('parses offset time', () {
final dt = XsdDateTime.parse('2002-10-10T12:00:00-05:00');
expect(dt.isFloating, isFalse);
expect(dt.value.isUtc, isTrue);
// 12:00 -05:00 is 17:00 UTC
expect(dt.value.hour, 17);
expect(dt.originalOffset, const Duration(hours: -5));
expect(dt.toString(), '2002-10-10T12:00:00.000-05:00');
});

test('parses positive offset time', () {
final dt = XsdDateTime.parse('2002-10-10T12:00:00+05:30');
expect(dt.isFloating, isFalse);
expect(dt.value.isUtc, isTrue);
// 12:00 +05:30 is 06:30 UTC
expect(dt.value.hour, 6);
expect(dt.value.minute, 30);
expect(dt.originalOffset, const Duration(hours: 5, minutes: 30));
expect(dt.toString(), '2002-10-10T12:00:00.000+05:30');
});

test('equality', () {
final dt1 = XsdDateTime.parse('2002-10-10T12:00:00');
final dt2 = XsdDateTime.parse('2002-10-10T12:00:00');
final dt3 = XsdDateTime.parse('2002-10-10T12:00:00Z');

expect(dt1, equals(dt2));
expect(dt1.hashCode, equals(dt2.hashCode));
expect(dt1, isNot(equals(dt3))); // Floating != Zoned
expect(dt1.hashCode, isNot(equals(dt3.hashCode)));
});

test('compareTo', () {
final dt1 = XsdDateTime.parse(
'2002-10-10T12:00:00',
); // Floating (treated as UTC 12:00)
final dt2 = XsdDateTime.parse('2002-10-10T13:00:00Z'); // UTC 13:00

expect(dt1.compareTo(dt2), lessThan(0));
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertions for toString() (e.g., on lines 17, 25, 35, 46) expect the output to include .000 for fractional seconds, even when the input string did not have them. This is tied to the current toString() implementation in XsdDateTime.

As noted in a separate comment on xsd_datetime.dart, this behavior could be improved for better round-tripping of lexical values by omitting zero-valued fractional seconds. If that change is made, these tests will need to be updated to reflect the new expected output (e.g., 2002-10-10T12:00:00 instead of 2002-10-10T12:00:00.000).

mark-dropbear and others added 2 commits November 26, 2025 18:24
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@mark-dropbear
mark-dropbear deleted the datetime branch November 27, 2025 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant