Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reopen "Fix DueDate.StringDate conversion to and from DateTime." #41

Merged
merged 14 commits into from
Nov 1, 2023

Conversation

AhmedZaki99
Copy link
Contributor

Fixes #36
All previous details of this request are discussed in #37

While I was checking this failing test, I noticed that the Roundtrip date conversion depends on information about timezones given in the DueDate.Date object itself, however, Todoist API always treats the DueDate.Date object as a UTC date and depends on the DueDate.Timezone to store timezone info.

I will try to fix this issue and update the pull request with a new commit shortly.

The class is used to change the time zone of the tests upon initialization, and resets it back when disposed.
The added tests ensure consistency in StringDate property setter and getter.
Date is now converted using the round-trip format without milliseconds.
Date is also no longer being converted to universal time before formatting.
DateTimeKind is switched to Unspecified instead of Utc since the due date should be treated as floating.
@AhmedZaki99
Copy link
Contributor Author

@olsh
I am not sure what is the proper way to solve this, since all timezone data is given in a separate parameter (DueDate.Timezone), what should happen when the user provides a local date in the DueDate constructor?
I know that we can use the old approach (converting the given date to universal), but wouldn't that cause some confusion to the users?

For example, when a user's code tries to create a floating due date with a local Date object, the time will be adjusted with the offset of whatever timezone the server is running in, resulting in inconsistent behavior if multiple server instances are running in different locations.

So, should we prevent Local date objects in the constructor (throw an ArgumentException), and assume that Unspecified dates are UTC?
Or should we ignore any Date.Kind given in the constructor and assume any given date as UTC?

@olsh
Copy link
Owner

olsh commented Oct 26, 2023

I'll take a look at the problem deeper.

BTW, here is the related issue
#1

The fix reverts back to depending on `DueDate.Timezone` property to determine the conversion formatting.
However, it doesn't convert the `DueDate.Date` value to universal time unless the `DueDate` is intended to be fixed.
Timezone info was missing for a fixed date.
Since floating due dates are now converted to `Unspecified` date time, the method `ToLocalTime` is no longer used because it assumes the given date as `UTC`.
@AhmedZaki99
Copy link
Contributor Author

@olsh
After some thinking, I figured out a way to handle different kinds of given dates.

First of all, for floating due dates, we ignore any timezone info given in the DateTime.Kind property and only use the time data given to us:

private const string FloatingEventDateFormat = "yyyy-MM-ddTHH:mm:ss";

...

if (string.IsNullOrEmpty(Timezone))
{
    return Date.Value.ToString(FloatingEventDateFormat);
}

That is because floating dates only represent a time in the user's timezone, no matter where he is:

Due date always represent an event in current user's timezone.

So, the code makes sure that whatever date is given should be the same date we find in the task without conversion.

Second, for fixed due dates, we should provide time in UTC since all timezone info is stored in DueDate.Timezone property:

Due date is stored in UTC.

That's why any dates given with the kind Unspecified are assumed UTC, but only dates given with the kind Local are converted to UTC based on the timezone of the machine running the code:

private const string FixedEventDateFormat = "yyyy-MM-ddTHH:mm:ssZ";

...

if (Date.Value.Kind == DateTimeKind.Local)
{
    return Date.Value.ToUniversalTime().ToString(FixedEventDateFormat);
}

return Date.Value.ToString(FixedEventDateFormat); // Unspecified dates are assuemd UTC.

Conversion of local DateTime objects is helpful in cases where the code is being used by a local client application (not a hosted server), where the user would expect to find the task date matching with the local date provided if the fixed timezone is, in fact, the one he lives in.

Commits with these changes have been added, only one simple extra change is made to the failing test:

Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date);

Since floating due dates are now converted to Unspecified date time, the method ToLocalTime is no longer used because it assumes the given date as UTC, but the conversion of floating dates is not necessary because it doesn't represent any timezone.

I hope this approach solves the problem.

@olsh
Copy link
Owner

olsh commented Oct 27, 2023

@AhmedZaki99 thank you for the contribution. These changes totally make sense to me.
I'd also suggest removing the public constructors and creating three template methods for each date type, something like:

DueDate.CreateFullDay(...)
DueDate.CreateFloating(...)
DueDate.CreateFixedTimeZone(...)

With only necessary parameters. I think the API is more explicit and easy to understand.

Also, it's worth adding comments to the methods from the Todoist docs. Because the due date concept is pretty hard to understand.

@AhmedZaki99
Copy link
Contributor Author

@olsh Great suggestion!

Should I create another pull request for it or should I just add the commits here?

@olsh
Copy link
Owner

olsh commented Oct 27, 2023

Let's add the changes to this PR.

AhmedZaki99 and others added 4 commits October 27, 2023 23:14
Methods include detailed documentation comments.
Static methods that are used to instantiate DueDates are now in charge of handling different kinds of dates.
@AhmedZaki99
Copy link
Contributor Author

Three commits are added:

  1. DueDate public constructors have been replaced by four static methods to create new instances (detailed documentation comments are provided):
DueDate FromText(string text, [Language language = null]);
DueDate CreateFullDay(DateTime date);
DueDate CreateFloating(DateTime dateTime);
DueDate CreateFixedTimeZone(DateTime dateTime, string timezone);
  1. Static methods are now handling different DateTime kinds, so the DueDate.StringDate property getter has been reverted to just do Roundtrip date conversion:
private const string DefaultEventDateFormat = "yyyy-MM-ddTHH:mm:ssK";

...

if (IsFullDay)
{
    return Date.Value.ToString(FullDayEventDateFormat);
}

return Date.Value.ToString(DefaultEventDateFormat);
  1. Tests have been updated with the new static methods replacing the public constructors, for example:
var item = new Item("demo task") { DueDate = DueDate.FromText("22 Dec 2021", Language.English) };

@AhmedZaki99
Copy link
Contributor Author

P.S. Don't forget to run all tests, I am not sure what might break after all these changes.

Copy link
Owner

@olsh olsh left a comment

Choose a reason for hiding this comment

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

Great job! Let's do the last changes and merge the PR.

@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 1 Code Smell

No Coverage information No Coverage information
0.0% 0.0% Duplication

@AhmedZaki99 AhmedZaki99 requested a review from olsh October 31, 2023 11:41
@olsh olsh merged commit a90eb4f into olsh:master Nov 1, 2023
@olsh
Copy link
Owner

olsh commented Nov 1, 2023

Thank you! 👍🏻

@AhmedZaki99 AhmedZaki99 deleted the patch-4 branch November 1, 2023 17:15
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.

Inconsistency in conversion between the DueDate.StringDate and DueDate.Date properties
2 participants