diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs
new file mode 100644
index 0000000..82117dc
--- /dev/null
+++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZone.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Reflection;
+
+namespace Todoist.Net.Tests.Helpers;
+
+///
+/// A helper class that changes the local timezone to a fake timezone provided
+/// at initialization, and resets the original local timezone when disposed.
+///
+///
+/// See this SO question for more details.
+///
+public sealed class FakeLocalTimeZone : IDisposable
+{
+
+ ///
+ /// The fake time zone info that has been set as local.
+ ///
+ public TimeZoneInfo FakeTimeZoneInfo { get; }
+
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private FakeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
+ {
+ FakeTimeZoneInfo = fakeTimeZoneInfo;
+
+ var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static);
+ var cachedData = info.GetValue(null);
+
+ var field = cachedData.GetType().GetField("_localTimeZone",
+ BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance);
+
+ field.SetValue(cachedData, fakeTimeZoneInfo);
+ }
+
+ public void Dispose()
+ {
+ TimeZoneInfo.ClearCachedData();
+ }
+
+
+ ///
+ /// Changes the local time zone to the given .
+ ///
+ ///
+ /// Disposal of the returned object resets the local time zone to the original one.
+ ///
+ /// The time zone to set as local until disposal.
+ ///
+ /// A instance that represents the time zone change,
+ /// and used to reset it back to original at disposal.
+ ///
+ public static FakeLocalTimeZone ChangeLocalTimeZone(TimeZoneInfo fakeTimeZoneInfo)
+ {
+ return new FakeLocalTimeZone(fakeTimeZoneInfo);
+ }
+
+ ///
+ /// Changes the local time zone to a custom time zone with a .
+ ///
+ ///
+ /// Disposal of the returned object resets the local time zone to the original one.
+ ///
+ /// UTC offset of the custom time zone.
+ ///
+ /// A instance that represents the time zone change,
+ /// and used to reset it back to original at disposal.
+ ///
+ public static FakeLocalTimeZone ChangeLocalTimeZone(TimeSpan baseUtcOffset)
+ {
+ var fakeId = "Fake TimeZone";
+ var fakeDisplayName = $"(UTC+{baseUtcOffset:hh':'mm})";
+
+ var fakeTimeZoneInfo = TimeZoneInfo.CreateCustomTimeZone(fakeId, baseUtcOffset, fakeDisplayName, fakeDisplayName);
+
+ return new FakeLocalTimeZone(fakeTimeZoneInfo);
+ }
+
+}
diff --git a/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs
new file mode 100644
index 0000000..ca5b3c9
--- /dev/null
+++ b/src/Todoist.Net.Tests/Helpers/FakeLocalTimeZoneTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Linq;
+
+using Todoist.Net.Tests.Extensions;
+
+using Xunit;
+
+namespace Todoist.Net.Tests.Helpers;
+
+[Trait(Constants.TraitName, Constants.UnitTraitValue)]
+public class FakeLocalTimeZoneTests
+{
+
+ [Fact]
+ public void FakeLocalTimeZone_ShouldChangeLocalTimeZoneWithinScope_AndResetItBackOutsideScope()
+ {
+ var fakeTimeZoneOffset = TimeZoneInfo.Local.BaseUtcOffset + TimeSpan.FromHours(2);
+ var fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(fakeTimeZoneOffset);
+
+ using (fakeLocalTimeZone)
+ {
+ Assert.Equal(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
+ }
+ Assert.NotEqual(fakeLocalTimeZone.FakeTimeZoneInfo, TimeZoneInfo.Local);
+ }
+
+}
diff --git a/src/Todoist.Net.Tests/Models/DueDateTests.cs b/src/Todoist.Net.Tests/Models/DueDateTests.cs
index 359ce82..e3d7dcd 100644
--- a/src/Todoist.Net.Tests/Models/DueDateTests.cs
+++ b/src/Todoist.Net.Tests/Models/DueDateTests.cs
@@ -2,19 +2,35 @@
using Todoist.Net.Models;
using Todoist.Net.Tests.Extensions;
+using Todoist.Net.Tests.Helpers;
+
using Xunit;
namespace Todoist.Net.Tests.Models
{
[Trait(Constants.TraitName, Constants.UnitTraitValue)]
- public class DueDateTests
+ public class DueDateTests : IDisposable
{
+
+ private readonly FakeLocalTimeZone _fakeLocalTimeZone;
+
+ public DueDateTests()
+ {
+ _fakeLocalTimeZone = FakeLocalTimeZone.ChangeLocalTimeZone(TimeSpan.FromHours(5));
+ }
+
+ public void Dispose()
+ {
+ _fakeLocalTimeZone?.Dispose();
+ }
+
+
[Fact]
public void DateTimeAssignment_FullDayEvent_Success()
{
- var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
+ var date = new DateTime(2018, 2, 5);
- var dueDate = new DueDate(date, true);
+ var dueDate = DueDate.CreateFullDay(date);
Assert.Equal("2018-02-05", dueDate.StringDate);
Assert.True(dueDate.IsFullDay);
@@ -23,23 +39,65 @@ public void DateTimeAssignment_FullDayEvent_Success()
[Fact]
public void DateTimeAssignment_FloatingDueDateEvent_Success()
{
- var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
+ var date = new DateTime(2018, 2, 5);
- var dueDate = new DueDate(date);
+ var dueDate = DueDate.CreateFloating(date);
Assert.Equal("2018-02-05T00:00:00", dueDate.StringDate);
Assert.False(dueDate.IsFullDay);
}
[Fact]
- public void DateTimeAssignment_FloatingDueDateWithTimezoneEvent_Success()
+ public void DateTimeAssignment_FixedTimeZoneDueDateEvent_Success()
{
- var date = new DateTime(2018, 2, 5, 0, 0, 0, DateTimeKind.Utc);
+ var date = new DateTime(2018, 2, 5);
- var dueDate = new DueDate(date, false, "Asia/Jakarta");
+ var dueDate = DueDate.CreateFixedTimeZone(date, "Asia/Jakarta");
Assert.Equal("2018-02-05T00:00:00Z", dueDate.StringDate);
Assert.False(dueDate.IsFullDay);
}
+
+
+ [Fact]
+ public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFullDayDate()
+ {
+ var dueDate = new DueDate();
+ string initialValue = "2016-12-01";
+
+ dueDate.StringDate = initialValue;
+ string returnedValue = dueDate.StringDate;
+ dueDate.StringDate = returnedValue;
+
+ Assert.Equal(returnedValue, dueDate.StringDate);
+ }
+
+ [Fact]
+ public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFloatingDate()
+ {
+ var dueDate = new DueDate();
+ string initialValue = "2016-12-03T12:00:00";
+
+ dueDate.StringDate = initialValue;
+ string returnedValue = dueDate.StringDate;
+ dueDate.StringDate = returnedValue;
+
+ Assert.Equal(returnedValue, dueDate.StringDate);
+ }
+
+ [Fact]
+ public void StringDateProperty_ShouldReturnExactAssignedValue_WhenValueIsFixedDate()
+ {
+ var dueDate = new DueDate();
+ string initialValue = "2016-12-06T13:00:00Z";
+
+ dueDate.StringDate = initialValue;
+ dueDate.Timezone = "Asia/Jakarta";
+
+ string returnedValue = dueDate.StringDate;
+ dueDate.StringDate = returnedValue;
+
+ Assert.Equal(returnedValue, dueDate.StringDate);
+ }
}
}
diff --git a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs
index 242d630..4054978 100644
--- a/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs
+++ b/src/Todoist.Net.Tests/Services/ItemsServiceTests.cs
@@ -89,7 +89,7 @@ public void CreateItemClearDueDateAndDelete_Success()
{
var client = TodoistClientFactory.Create(_outputHelper);
- var item = new Item("demo task") { DueDate = new DueDate("22 Dec 2021", language: Language.English) };
+ var item = new Item("demo task") { DueDate = DueDate.FromText("22 Dec 2021", Language.English) };
client.Items.AddAsync(item).Wait();
var itemInfo = client.Items.GetAsync(item.Id).Result;
@@ -113,7 +113,7 @@ public void CreateItem_InvalidPDueDate_ThrowsException()
{
var client = TodoistClientFactory.Create(_outputHelper);
var item = new Item("bad task");
- item.DueDate = new DueDate("Invalid date string");
+ item.DueDate = DueDate.FromText("Invalid date string");
var aggregateException = Assert.ThrowsAsync(
async () =>
@@ -133,7 +133,7 @@ public async Task MoveItemsToProject_Success()
var item = new Item("demo task");
client.Items.AddAsync(item).Wait();
- item.DueDate = new DueDate("every fri");
+ item.DueDate = DueDate.FromText("every fri");
await client.Items.UpdateAsync(item);
var project = new Project(Guid.NewGuid().ToString());
@@ -162,7 +162,7 @@ public void QuickAddAsync_Success()
Assert.NotNull(item);
- client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, new DueDate(DateTime.UtcNow.AddMonths(1)))).Wait();
+ client.Items.CompleteRecurringAsync(new CompleteRecurringItemArgument(item.Id, DueDate.CreateFloating(DateTime.UtcNow.AddMonths(1)))).Wait();
client.Items.CompleteRecurringAsync(item.Id).Wait();
client.Items.DeleteAsync(item.Id).Wait();
@@ -189,12 +189,12 @@ public void CreateNewItem_DueDateIsLocal_DueDateNotChanged()
{
var client = TodoistClientFactory.Create(_outputHelper);
- var item = new Item("New task") { DueDate = new DueDate(DateTime.Now.AddYears(1).Date) };
+ var item = new Item("New task") { DueDate = DueDate.CreateFloating(DateTime.Now.AddYears(1).Date) };
var taskId = client.Items.AddAsync(item).Result;
var itemInfo = client.Items.GetAsync(taskId).Result;
- Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date?.ToLocalTime());
+ Assert.Equal(item.DueDate.Date, itemInfo.Item.DueDate.Date);
client.Items.DeleteAsync(item.Id).Wait();
}
@@ -208,7 +208,7 @@ public void CreateItemClearDurationAndDelete_Success()
var item = new Item("duration task")
{
- DueDate = new DueDate("22 Dec 2021 at 9:15", language: Language.English),
+ DueDate = DueDate.FromText("22 Dec 2021 at 9:15", Language.English),
Duration = new Duration(45, DurationUnit.Minute)
};
client.Items.AddAsync(item).Wait();
diff --git a/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs b/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs
index bf3d8e6..a5e1414 100644
--- a/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs
+++ b/src/Todoist.Net.Tests/Services/ReminersServiceTests.cs
@@ -30,7 +30,7 @@ public async Task CreateDelete_Success()
var itemId = await transaction.Items.AddAsync(new Item("Temp")).ConfigureAwait(false);
var reminderId =
- await transaction.Reminders.AddAsync(new Reminder(itemId) { DueDate = new DueDate(DateTime.UtcNow.AddDays(1)) }).ConfigureAwait(false);
+ await transaction.Reminders.AddAsync(new Reminder(itemId) { DueDate = DueDate.CreateFloating(DateTime.UtcNow.AddDays(1)) }).ConfigureAwait(false);
await transaction.CommitAsync().ConfigureAwait(false);
var reminders = await client.Reminders.GetAsync().ConfigureAwait(false);
@@ -50,7 +50,7 @@ public async Task AddRelativeReminder_Success()
var item = new Item("Test")
{
- DueDate = new DueDate(DateTime.UtcNow.AddDays(1))
+ DueDate = DueDate.CreateFloating(DateTime.UtcNow.AddDays(1))
};
var taskId = await client.Items.AddAsync(item).ConfigureAwait(false);
diff --git a/src/Todoist.Net/Models/DueDate.cs b/src/Todoist.Net/Models/DueDate.cs
index ea43054..e0f437a 100644
--- a/src/Todoist.Net/Models/DueDate.cs
+++ b/src/Todoist.Net/Models/DueDate.cs
@@ -11,36 +11,7 @@ namespace Todoist.Net.Models
public class DueDate
{
private const string FullDayEventDateFormat = "yyyy-MM-dd";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The date time.
- /// if set to true then it's a full day event.
- /// The timezone.
- public DueDate(DateTime date, bool isFullDay = false, string timezone = null)
- {
- Date = date;
- IsFullDay = isFullDay;
- Timezone = timezone;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The text (every day; each monday)
- /// The date time. this can be used with 'text' parameter to create a recurring from specific date
- /// if set to true then it's a full day event.
- /// The timezone.
- /// The language.
- public DueDate(string text, DateTime? date = null, bool isFullDay = false, string timezone = null, Language language = null)
- {
- Text = text;
- Date = date;
- IsFullDay = isFullDay;
- Timezone = timezone;
- Language = language;
- }
+ private const string DefaultEventDateFormat = "yyyy-MM-ddTHH:mm:ssK";
internal DueDate()
{
@@ -124,13 +95,7 @@ internal string StringDate
return Date.Value.ToString(FullDayEventDateFormat);
}
- var date = Date.Value.ToUniversalTime();
- if (string.IsNullOrEmpty(Timezone))
- {
- return date.ToString("s");
- }
-
- return date.ToString("s") + "Z";
+ return Date.Value.ToString(DefaultEventDateFormat);
}
set
@@ -143,8 +108,98 @@ internal string StringDate
return;
}
- Date = DateTime.Parse(value, CultureInfo.InvariantCulture);
+ Date = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
}
}
+
+
+ ///
+ /// Creates a new instance of from text (every day; each Monday).
+ ///
+ ///
+ /// See Todoist documentation for more information.
+ ///
+ /// The human-readable representation of due date (every day; each Monday)
+ /// The language of the text.
+ /// The created instance.
+ public static DueDate FromText(string text, Language language = null) => new DueDate
+ {
+ Text = text,
+ Language = language
+ };
+
+ ///
+ /// Creates a full-day .
+ ///
+ /// The full-day date.
+ ///
+ ///
+ /// Only the date component of the given is used, and any time data is truncated.
+ ///
+ ///
+ /// See Todoist documentation for more information.
+ ///
+ ///
+ /// The created instance.
+ public static DueDate CreateFullDay(DateTime date) => new DueDate
+ {
+ Date = date.Date,
+ IsFullDay = true
+ };
+
+ ///
+ /// Creates a floating .
+ ///
+ /// The floating date.
+ ///
+ ///
+ /// The given is treated as , and any timezone data is truncated.
+ ///
+ ///
+ /// Floating due dates always represent an event in the current user's timezone.
+ ///
+ /// Note that it's not quite compatible with RFC 3339,
+ /// because the concept of timezone is not applicable to this object.
+ ///
+ ///
+ /// See Todoist documentation for more information.
+ ///
+ ///
+ /// The created instance.
+ public static DueDate CreateFloating(DateTime dateTime) => new DueDate
+ {
+ Date = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), // Floating dates are unspecified.
+ IsFullDay = false
+ };
+
+ ///
+ /// Creates a fixed timezone .
+ ///
+ /// The fixed timezone date.
+ /// The timezone of the due instance.
+ ///
+ ///
+ /// Fixed due date is stored in UTC, hence, of kind is assumed UTC,
+ ///
and of kind is converted to UTC based on the system timezone.
+ ///
+ ///
+ /// See Todoist documentation for more information.
+ ///
+ ///
+ /// The created instance.
+ public static DueDate CreateFixedTimeZone(DateTime dateTime, string timezone)
+ {
+ if (dateTime.Kind == DateTimeKind.Unspecified)
+ {
+ dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); // Unspecified dates are assumed UTC.
+ }
+ return new DueDate
+ {
+ Date = dateTime.ToUniversalTime(), // Local dates are converted to UTC.
+ IsFullDay = false,
+ Timezone = timezone
+ };
+ }
+
}
}