From 2627004ff05ffcbc80daad751552dd74f300aa24 Mon Sep 17 00:00:00 2001 From: khaelys Date: Fri, 5 Nov 2021 17:26:31 +0100 Subject: [PATCH 1/8] feat: Calculate worked period based on the inferred datetimev2 --- src/Clockify/Fill/EntryFillDialog.cs | 7 +- src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 68 +++- .../Recognizer/TimeSurveyBotLuisExTest.cs | 332 +++++++++++++++++- 3 files changed, 398 insertions(+), 9 deletions(-) diff --git a/src/Clockify/Fill/EntryFillDialog.cs b/src/Clockify/Fill/EntryFillDialog.cs index 916a7ed..3577e31 100644 --- a/src/Clockify/Fill/EntryFillDialog.cs +++ b/src/Clockify/Fill/EntryFillDialog.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Bot.Clockify.Client; using Bot.Clockify.Models; +using Bot.Common; using Bot.Common.ChannelData.Telegram; using Bot.Common.Recognizer; using Bot.Data; @@ -26,6 +27,7 @@ public class EntryFillDialog : ComponentDialog private readonly WorthAskingForTaskService _worthAskingForTask; private readonly UserState _userState; private readonly IClockifyMessageSource _messageSource; + private readonly IDateTimeProvider _dateTimeProvider; private readonly ILogger _logger; private const string TaskWaterfall = "TaskWaterfall"; @@ -41,7 +43,7 @@ public class EntryFillDialog : ComponentDialog public EntryFillDialog(ClockifyEntityRecognizer clockifyWorkableRecognizer, ITimeEntryStoreService timeEntryStoreService, WorthAskingForTaskService worthAskingForTask, UserState userState, IClockifyService clockifyService, ITokenRepository tokenRepository, - IClockifyMessageSource messageSource, ILogger logger) + IClockifyMessageSource messageSource, IDateTimeProvider dateTimeProvider, ILogger logger) { _clockifyWorkableRecognizer = clockifyWorkableRecognizer; _timeEntryStoreService = timeEntryStoreService; @@ -50,6 +52,7 @@ public EntryFillDialog(ClockifyEntityRecognizer clockifyWorkableRecognizer, _clockifyService = clockifyService; _tokenRepository = tokenRepository; _messageSource = messageSource; + _dateTimeProvider = dateTimeProvider; _logger = logger; AddDialog(new WaterfallDialog(TaskWaterfall, new List { @@ -78,7 +81,7 @@ private async Task PromptForTaskAsync(WaterfallStepContext ste await _clockifyWorkableRecognizer.RecognizeProject(luisResult.ProjectName(), clockifyToken); stepContext.Values["Project"] = recognizedProject; double minutes = luisResult.WorkedDurationInMinutes(); - var (start, end) = luisResult.WorkedPeriod(minutes); + var (start, end) = luisResult.WorkedPeriod(_dateTimeProvider, minutes); stepContext.Values["Start"] = start; stepContext.Values["End"] = end; string fullEntity = recognizedProject.Name; diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index 17447ec..d12a3fd 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -69,18 +69,78 @@ ex is InvalidOperationException } } - public (DateTime start, DateTime end) WorkedPeriod(double minutes, string culture = Culture.English) + public (DateTime start, DateTime end) WorkedPeriod(IDateTimeProvider dateTimeProvider, double minutes, + string culture = Culture.English) { var workedPeriodInstances = Entities._instance.datetime; if (workedPeriodInstances.Length > 1) { string? instance = workedPeriodInstances[1].Text; - var recognizedDateTime = DateTimeRecognizer.RecognizeDateTime(instance, culture).First(); + var refTime = dateTimeProvider.DateTimeUtcNow(); + var recognizedDateTime = + DateTimeRecognizer.RecognizeDateTime(instance, culture, refTime: refTime).First(); var resolvedPeriod = ((List>)recognizedDateTime.Resolution["values"])[0]; - // TODO: use resolvedPeriod to pick a (start, end) period + return RecognizedWorkedPeriod(refTime, resolvedPeriod, minutes); } - var thisMorning = DateTime.Today.AddHours(9); + + var thisMorning = dateTimeProvider.DateTimeUtcNow().Date.AddHours(9); return (thisMorning, thisMorning.AddMinutes(minutes)); } + + private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime refTime, + IReadOnlyDictionary periodData, double minutes) + { + string dateTimeType = periodData["type"]; + if (dateTimeType.Equals("date")) + { + var date = DateTime.Parse(periodData["value"]); + var start = new DateTime(date.Year, date.Month, date.Day, 9, 0, 0); + return (start, start.AddMinutes(minutes)); + } + + if (dateTimeType.Equals("datetime")) + { + var datetime = DateTime.Parse(periodData["value"]); + return (datetime, datetime.AddMinutes(minutes)); + } + + if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("before")) + { + var time = DateTime.Parse(periodData["end"]); + var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute, + time.Second); + return (datetime.Subtract(TimeSpan.FromMinutes(minutes)), datetime); + } + + if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("since")) + { + var time = DateTime.Parse(periodData["start"]); + var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute, + time.Second); + return (datetime, datetime.AddMinutes(minutes)); + } + + if (dateTimeType.Equals("timerange") && !periodData.ContainsKey("Mod")) + { + var timeStart = DateTime.Parse(periodData["start"]); + var datetimeStart = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeStart.Hour, + timeStart.Minute, timeStart.Second); + var timeEnd = DateTime.Parse(periodData["end"]); + var datetimeEnd = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeEnd.Hour, + timeEnd.Minute, timeEnd.Second); + + double minutesBetweenDates = datetimeEnd.Subtract(datetimeStart).TotalMinutes; + // Floating point comparison, we check that the difference is greater than one minute. + if (Math.Abs(minutesBetweenDates - minutes) > 1) + { + throw new InvalidWorkedPeriodException( + $"Worked period time span differs from the duration provided. Expected {minutes} but got {minutesBetweenDates}"); + } + + return (datetimeStart, datetimeEnd); + } + + throw new InvalidWorkedPeriodException($"Date time type {dateTimeType} is not allowed"); + } } } \ No newline at end of file diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index 5205e89..e3ce25d 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -1,7 +1,9 @@ using System; +using Bot.Common; using Bot.Common.Recognizer; using FluentAssertions; using Microsoft.Bot.Builder.AI.Luis; +using Moq; using Xunit; namespace Bot.Tests.Common.Recognizer @@ -9,7 +11,7 @@ namespace Bot.Tests.Common.Recognizer public class TimeSurveyBotLuisExTest { [Fact] - public void TimePeriod_ValidEntitiesInstance_ReturnsFirstDateTimeText() + public void WorkedDuration_ValidEntitiesInstance_ReturnsFirstDateTimeText() { const string timePeriod = "from 01 July to 10 July"; var instances = new TimeSurveyBotLuis._Entities._Instance @@ -41,7 +43,7 @@ public void TimePeriod_ValidEntitiesInstance_ReturnsFirstDateTimeText() } [Fact] - public void TimePeriod_NullOrEmptyDateTimeInstance_ThrowsException() + public void WorkedDuration_NullOrEmptyDateTimeInstance_ThrowsException() { var emptyDateTimeTextEntities = new TimeSurveyBotLuis._Entities._Instance { @@ -95,7 +97,7 @@ public void TimePeriod_NullOrEmptyDateTimeInstance_ThrowsException() } [Fact] - public void TimePeriodInMinutes_EightHoursPeriod_ReturnsMinutes() + public void WorkedDurationInMinutes_EightHoursPeriod_ReturnsMinutes() { var instances = new TimeSurveyBotLuis._Entities._Instance { @@ -121,5 +123,329 @@ public void TimePeriodInMinutes_EightHoursPeriod_ReturnsMinutes() luisResult.WorkedDurationInMinutes().Should().Be(expectedMinutes); } + [Fact] + public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); + var lastFridayStart = new DateTime(2021, 10, 29, 9, 0, 0); + var lastFridayEnd = new DateTime(2021, 10, 29, 11, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "last friday", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(lastFridayStart); + end.Should().Be(lastFridayEnd); + } + + [Fact] + public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); + var lastFridayStart = new DateTime(2021, 10, 29, 16, 0, 0); + var lastFridayEnd = new DateTime(2021, 10, 29, 18, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "last friday at 4pm", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(lastFridayStart); + end.Should().Be(lastFridayEnd); + } + + [Fact] + public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "till 6 pm", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(mondayFirstNovember); + end.Should().Be(expectedEnd); + } + + [Fact] + public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "from 4 pm", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(mondayFirstNovember); + end.Should().Be(expectedEnd); + } + + [Fact] + public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 9, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 11, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "from 9 am to 11 am", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(expectedStart); + end.Should().Be(expectedEnd); + } + + [Fact] + public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPeriodException() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "1 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "from 9 am to 11 am", + Type = "builtin.datetimeV2.datetime" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + Func<(DateTime, DateTime)> action = () => luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60); + + action.Should().ThrowExactly() + .WithMessage("Worked period time span differs from the duration provided. Expected 60 but got 120"); + } + + [Fact] + public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "1 hours", + Type = "builtin.datetimeV2.duration" + }, + new InstanceData + { + Text = "for 60 minutes", + Type = "builtin.datetimeV2.duration" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + Func<(DateTime, DateTime)> action = () => luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60); + + action.Should().ThrowExactly() + .WithMessage("Date time type duration is not allowed"); + } + + [Fact] + public void WorkedPeriod_NoHoursRange_ReturnsWorkedPeriodStartingFromNineAm() + { + var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 9, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 11, 0, 0); + + var mockDateTimeProvider = new Mock(); + mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) + .Returns(mondayFirstNovember); + + var instances = new TimeSurveyBotLuis._Entities._Instance + { + datetime = new[] + { + new InstanceData + { + Text = "2 hours", + Type = "builtin.datetimeV2.duration" + } + } + }; + + var luisResult = new TimeSurveyBotLuis + { + Entities = new TimeSurveyBotLuis._Entities + { + _instance = instances + } + }; + + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + + start.Should().Be(expectedStart); + end.Should().Be(expectedEnd); + } } } \ No newline at end of file From eedaeced58cb99d2abc0ced68df1c2826d261d13 Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 11:12:33 +0100 Subject: [PATCH 2/8] feat: Add time zone for default worked period --- src/Clockify/Fill/EntryFillDialog.cs | 2 +- src/Clockify/Fill/TimeEntryStoreService.cs | 1 + src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 15 ++-- .../Recognizer/TimeSurveyBotLuisExTest.cs | 89 ++++++++++--------- 4 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Clockify/Fill/EntryFillDialog.cs b/src/Clockify/Fill/EntryFillDialog.cs index 3577e31..dc98c84 100644 --- a/src/Clockify/Fill/EntryFillDialog.cs +++ b/src/Clockify/Fill/EntryFillDialog.cs @@ -81,7 +81,7 @@ private async Task PromptForTaskAsync(WaterfallStepContext ste await _clockifyWorkableRecognizer.RecognizeProject(luisResult.ProjectName(), clockifyToken); stepContext.Values["Project"] = recognizedProject; double minutes = luisResult.WorkedDurationInMinutes(); - var (start, end) = luisResult.WorkedPeriod(_dateTimeProvider, minutes); + var (start, end) = luisResult.WorkedPeriod(_dateTimeProvider, minutes, userProfile.TimeZone); stepContext.Values["Start"] = start; stepContext.Values["End"] = end; string fullEntity = recognizedProject.Name; diff --git a/src/Clockify/Fill/TimeEntryStoreService.cs b/src/Clockify/Fill/TimeEntryStoreService.cs index bff7047..770bcfa 100644 --- a/src/Clockify/Fill/TimeEntryStoreService.cs +++ b/src/Clockify/Fill/TimeEntryStoreService.cs @@ -36,6 +36,7 @@ public async Task AddTimeEntries(string clockifyToken, ProjectDo project await _clockifyService.AddTimeEntryAsync(clockifyToken, workspaceId, timeEntry); + // TODO timezone and separate total hours calculation var todayEntries = await _clockifyService.GetHydratedTimeEntriesAsync(clockifyToken, workspaceId, userId, new DateTimeOffset(DateTime.Today), new DateTimeOffset(DateTime.Today.AddDays(1))); diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index d12a3fd..cf62c72 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -69,26 +69,27 @@ ex is InvalidOperationException } } - public (DateTime start, DateTime end) WorkedPeriod(IDateTimeProvider dateTimeProvider, double minutes, + public (DateTime start, DateTime end) WorkedPeriod(IDateTimeProvider dateTimeProvider, double minutes, TimeZoneInfo timeZone, string culture = Culture.English) { var workedPeriodInstances = Entities._instance.datetime; + var userNow = TimeZoneInfo.ConvertTime(dateTimeProvider.DateTimeUtcNow(), timeZone); if (workedPeriodInstances.Length > 1) { string? instance = workedPeriodInstances[1].Text; - var refTime = dateTimeProvider.DateTimeUtcNow(); var recognizedDateTime = - DateTimeRecognizer.RecognizeDateTime(instance, culture, refTime: refTime).First(); + DateTimeRecognizer.RecognizeDateTime(instance, culture, refTime: userNow).First(); var resolvedPeriod = ((List>)recognizedDateTime.Resolution["values"])[0]; - return RecognizedWorkedPeriod(refTime, resolvedPeriod, minutes); + return RecognizedWorkedPeriod(userNow, resolvedPeriod, minutes, timeZone); } - var thisMorning = dateTimeProvider.DateTimeUtcNow().Date.AddHours(9); - return (thisMorning, thisMorning.AddMinutes(minutes)); + var thisMorning = userNow.Date.AddHours(9); + var thisMorningUtc = TimeZoneInfo.ConvertTimeToUtc(thisMorning); + return (thisMorningUtc, thisMorningUtc.AddMinutes(minutes)); } private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime refTime, - IReadOnlyDictionary periodData, double minutes) + IReadOnlyDictionary periodData, double minutes, TimeZoneInfo timeZone) { string dateTimeType = periodData["type"]; if (dateTimeType.Equals("date")) diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index e3ce25d..722e772 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Microsoft.Bot.Builder.AI.Luis; using Moq; +using TimeZoneConverter; using Xunit; namespace Bot.Tests.Common.Recognizer @@ -38,10 +39,10 @@ public void WorkedDuration_ValidEntitiesInstance_ReturnsFirstDateTimeText() _instance = instances } }; - + luisResult.WorkedDuration().Should().Be(timePeriod); } - + [Fact] public void WorkedDuration_NullOrEmptyDateTimeInstance_ThrowsException() { @@ -63,7 +64,7 @@ public void WorkedDuration_NullOrEmptyDateTimeInstance_ThrowsException() _instance = emptyDateTimeTextEntities } }; - + var nullDateTimeInstance = new TimeSurveyBotLuis._Entities._Instance { datetime = null @@ -82,12 +83,12 @@ public void WorkedDuration_NullOrEmptyDateTimeInstance_ThrowsException() _instance = new TimeSurveyBotLuis._Entities._Instance() } }; - - Func getDateTimeWithNullDateTimeEntities = () => lsEmptyDateTimeTextInstance.WorkedDuration(); + + Func getDateTimeWithNullDateTimeEntities = () => lsEmptyDateTimeTextInstance.WorkedDuration(); getDateTimeWithNullDateTimeEntities.Should().ThrowExactly() .WithMessage("No worked duration has been recognized"); - - Func getDateTimeWithEmptyEntities = () => lsEmptyInstance.WorkedDuration(); + + Func getDateTimeWithEmptyEntities = () => lsEmptyInstance.WorkedDuration(); getDateTimeWithEmptyEntities.Should().ThrowExactly() .WithMessage("No worked duration has been recognized"); @@ -119,21 +120,21 @@ public void WorkedDurationInMinutes_EightHoursPeriod_ReturnsMinutes() }; const double expectedMinutes = 480.00; - + luisResult.WorkedDurationInMinutes().Should().Be(expectedMinutes); } - + [Fact] public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() { var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); var lastFridayStart = new DateTime(2021, 10, 29, 9, 0, 0); var lastFridayEnd = new DateTime(2021, 10, 29, 11, 0, 0); - + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -159,23 +160,24 @@ public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(lastFridayStart); end.Should().Be(lastFridayEnd); } - + [Fact] public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); var lastFridayStart = new DateTime(2021, 10, 29, 16, 0, 0); var lastFridayEnd = new DateTime(2021, 10, 29, 18, 0, 0); - + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -201,22 +203,23 @@ public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(lastFridayStart); end.Should().Be(lastFridayEnd); } - + [Fact] public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); - + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -242,22 +245,23 @@ public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(mondayFirstNovember); end.Should().Be(expectedEnd); } - + [Fact] public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); - + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -283,23 +287,24 @@ public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(mondayFirstNovember); end.Should().Be(expectedEnd); } - + [Fact] public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); var expectedStart = new DateTime(2021, 11, 1, 9, 0, 0); var expectedEnd = new DateTime(2021, 11, 1, 11, 0, 0); - + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -325,12 +330,13 @@ public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); } - + [Fact] public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPeriodException() { @@ -339,7 +345,7 @@ public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPerio var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -365,12 +371,13 @@ public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPerio } }; - Func<(DateTime, DateTime)> action = () => luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60); + Func<(DateTime, DateTime)> action = () => + luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, TZConvert.GetTimeZoneInfo("Europe/Rome")); action.Should().ThrowExactly() .WithMessage("Worked period time span differs from the duration provided. Expected 60 but got 120"); } - + [Fact] public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() { @@ -379,7 +386,7 @@ public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -405,23 +412,24 @@ public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() } }; - Func<(DateTime, DateTime)> action = () => luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60); + Func<(DateTime, DateTime)> action = () => + luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, TZConvert.GetTimeZoneInfo("Europe/Rome")); action.Should().ThrowExactly() .WithMessage("Date time type duration is not allowed"); } - + [Fact] public void WorkedPeriod_NoHoursRange_ReturnsWorkedPeriodStartingFromNineAm() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 9, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 11, 0, 0); - + var expectedStart = new DateTime(2021, 11, 1, 8, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 10, 0, 0); + var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) .Returns(mondayFirstNovember); - + var instances = new TimeSurveyBotLuis._Entities._Instance { datetime = new[] @@ -442,7 +450,8 @@ public void WorkedPeriod_NoHoursRange_ReturnsWorkedPeriodStartingFromNineAm() } }; - var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120); + var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, + TZConvert.GetTimeZoneInfo("Europe/Rome")); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); From 91f6b4f56c44cf2fbb16a537bf18cfed6b0368bb Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 11:22:04 +0100 Subject: [PATCH 3/8] fix: Add time zone param on utc conversion --- src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index cf62c72..82ba03a 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -84,7 +84,7 @@ ex is InvalidOperationException } var thisMorning = userNow.Date.AddHours(9); - var thisMorningUtc = TimeZoneInfo.ConvertTimeToUtc(thisMorning); + var thisMorningUtc = TimeZoneInfo.ConvertTimeToUtc(thisMorning, timeZone); return (thisMorningUtc, thisMorningUtc.AddMinutes(minutes)); } From bab6c1b7b1d18b1ec560ece024ee1cc45df37a39 Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 11:43:06 +0100 Subject: [PATCH 4/8] feat: Add time zone for timerange worked period --- src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 14 ++++++++++---- .../Common/Recognizer/TimeSurveyBotLuisExTest.cs | 14 ++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index 82ba03a..0cce860 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -107,7 +107,7 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("before")) { - var time = DateTime.Parse(periodData["end"]); + var time = ParseToUtc(periodData["end"], timeZone); var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute, time.Second); return (datetime.Subtract(TimeSpan.FromMinutes(minutes)), datetime); @@ -115,7 +115,7 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("since")) { - var time = DateTime.Parse(periodData["start"]); + var time = ParseToUtc(periodData["start"], timeZone); var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute, time.Second); return (datetime, datetime.AddMinutes(minutes)); @@ -123,10 +123,10 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re if (dateTimeType.Equals("timerange") && !periodData.ContainsKey("Mod")) { - var timeStart = DateTime.Parse(periodData["start"]); + var timeStart = ParseToUtc(periodData["start"], timeZone); var datetimeStart = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeStart.Hour, timeStart.Minute, timeStart.Second); - var timeEnd = DateTime.Parse(periodData["end"]); + var timeEnd = ParseToUtc(periodData["end"], timeZone); var datetimeEnd = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeEnd.Hour, timeEnd.Minute, timeEnd.Second); @@ -143,5 +143,11 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re throw new InvalidWorkedPeriodException($"Date time type {dateTimeType} is not allowed"); } + + private static DateTime ParseToUtc(string localDateTimeString, TimeZoneInfo timeZone) + { + var localDt = DateTime.Parse(localDateTimeString); + return TimeZoneInfo.ConvertTimeToUtc(localDt, timeZone); + } } } \ No newline at end of file diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index 722e772..f03da9b 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -214,7 +214,8 @@ public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 15, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 17, 0, 0); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -248,7 +249,7 @@ public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, TZConvert.GetTimeZoneInfo("Europe/Rome")); - start.Should().Be(mondayFirstNovember); + start.Should().Be(expectedStart); end.Should().Be(expectedEnd); } @@ -256,7 +257,8 @@ public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 18, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 15, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 17, 0, 0); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -290,7 +292,7 @@ public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, TZConvert.GetTimeZoneInfo("Europe/Rome")); - start.Should().Be(mondayFirstNovember); + start.Should().Be(expectedStart); end.Should().Be(expectedEnd); } @@ -298,8 +300,8 @@ public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 9, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 11, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 8, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 10, 0, 0); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) From 2ee982bf8c20f0e2b73fb815c51aabbafbbce23d Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 11:45:40 +0100 Subject: [PATCH 5/8] feat: Add time zone for datetime worked period --- src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 2 +- .../Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index 0cce860..81a0d5a 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -101,7 +101,7 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re if (dateTimeType.Equals("datetime")) { - var datetime = DateTime.Parse(periodData["value"]); + var datetime = ParseToUtc(periodData["value"], timeZone); return (datetime, datetime.AddMinutes(minutes)); } diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index f03da9b..745f447 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -170,9 +170,9 @@ public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() [Fact] public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() { - var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); - var lastFridayStart = new DateTime(2021, 10, 29, 16, 0, 0); - var lastFridayEnd = new DateTime(2021, 10, 29, 18, 0, 0); + var mondayFirstNovember = new DateTime(2021, 11, 8, 15, 0, 0); + var lastFridayStart = new DateTime(2021, 11, 5, 15, 0, 0); + var lastFridayEnd = new DateTime(2021, 11, 5, 17, 0, 0); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) From 1124b149f3c0009da0901e07e904815b87811df8 Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 11:55:21 +0100 Subject: [PATCH 6/8] feat: Add time zone for date worked period --- src/Common/Recognizer/TimeSurveyBotLuisEx.cs | 3 ++- .../Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs index 81a0d5a..ae5ff04 100644 --- a/src/Common/Recognizer/TimeSurveyBotLuisEx.cs +++ b/src/Common/Recognizer/TimeSurveyBotLuisEx.cs @@ -96,7 +96,8 @@ private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime re { var date = DateTime.Parse(periodData["value"]); var start = new DateTime(date.Year, date.Month, date.Day, 9, 0, 0); - return (start, start.AddMinutes(minutes)); + var startUtc = TimeZoneInfo.ConvertTimeToUtc(start, timeZone); + return (startUtc, startUtc.AddMinutes(minutes)); } if (dateTimeType.Equals("datetime")) diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index 745f447..6383ac4 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -127,9 +127,9 @@ public void WorkedDurationInMinutes_EightHoursPeriod_ReturnsMinutes() [Fact] public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() { - var mondayFirstNovember = new DateTime(2021, 11, 1, 15, 0, 0); - var lastFridayStart = new DateTime(2021, 10, 29, 9, 0, 0); - var lastFridayEnd = new DateTime(2021, 10, 29, 11, 0, 0); + var mondayFirstNovember = new DateTime(2021, 11, 8, 15, 0, 0); + var lastFridayStart = new DateTime(2021, 11, 5, 8, 0, 0); + var lastFridayEnd = new DateTime(2021, 11, 5, 10, 0, 0); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) From 18eaa02dd9bd582696e637353eb97438243559b9 Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 12:06:49 +0100 Subject: [PATCH 7/8] test: Use Moscow as test timezone --- .../Recognizer/TimeSurveyBotLuisExTest.cs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs index 6383ac4..bcfefe2 100644 --- a/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs +++ b/tests/Bot.Tests/Common/Recognizer/TimeSurveyBotLuisExTest.cs @@ -128,8 +128,9 @@ public void WorkedDurationInMinutes_EightHoursPeriod_ReturnsMinutes() public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() { var mondayFirstNovember = new DateTime(2021, 11, 8, 15, 0, 0); - var lastFridayStart = new DateTime(2021, 11, 5, 8, 0, 0); - var lastFridayEnd = new DateTime(2021, 11, 5, 10, 0, 0); + var lastFridayStart = new DateTime(2021, 11, 5, 6, 0, 0); + var lastFridayEnd = new DateTime(2021, 11, 5, 8, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -161,7 +162,7 @@ public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(lastFridayStart); end.Should().Be(lastFridayEnd); @@ -171,8 +172,9 @@ public void WorkedPeriod_DateWithoutTime_ReturnsWorkedPeriodFromNineAm() public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 8, 15, 0, 0); - var lastFridayStart = new DateTime(2021, 11, 5, 15, 0, 0); - var lastFridayEnd = new DateTime(2021, 11, 5, 17, 0, 0); + var lastFridayStart = new DateTime(2021, 11, 5, 13, 0, 0); + var lastFridayEnd = new DateTime(2021, 11, 5, 15, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -204,7 +206,7 @@ public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(lastFridayStart); end.Should().Be(lastFridayEnd); @@ -214,8 +216,9 @@ public void WorkedPeriod_PeriodStartingFromDateTime_ReturnsWorkedPeriod() public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 15, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 17, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 13, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 15, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -247,7 +250,7 @@ public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); @@ -257,8 +260,9 @@ public void WorkedPeriod_TillSelectedHour_ReturnsWorkedPeriod() public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 15, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 17, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 13, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 15, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -290,7 +294,7 @@ public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); @@ -300,8 +304,9 @@ public void WorkedPeriod_FromSelectedHour_ReturnsWorkedPeriod() public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 8, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 10, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 6, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 8, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -333,7 +338,7 @@ public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); @@ -343,6 +348,7 @@ public void WorkedPeriod_FromToHoursRange_ReturnsWorkedPeriod() public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPeriodException() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -374,7 +380,7 @@ public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPerio }; Func<(DateTime, DateTime)> action = () => - luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, TZConvert.GetTimeZoneInfo("Europe/Rome")); + luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, timeZonePlusThree); action.Should().ThrowExactly() .WithMessage("Worked period time span differs from the duration provided. Expected 60 but got 120"); @@ -384,6 +390,7 @@ public void WorkedPeriod_HoursRangeMismatchWithDuration_ThrowsInvalidWorkedPerio public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -415,7 +422,7 @@ public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() }; Func<(DateTime, DateTime)> action = () => - luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, TZConvert.GetTimeZoneInfo("Europe/Rome")); + luisResult.WorkedPeriod(mockDateTimeProvider.Object, 60, timeZonePlusThree); action.Should().ThrowExactly() .WithMessage("Date time type duration is not allowed"); @@ -425,8 +432,9 @@ public void WorkedPeriod_DateTimeIsDuration_ThrowsInvalidWorkedPeriodException() public void WorkedPeriod_NoHoursRange_ReturnsWorkedPeriodStartingFromNineAm() { var mondayFirstNovember = new DateTime(2021, 11, 1, 16, 0, 0); - var expectedStart = new DateTime(2021, 11, 1, 8, 0, 0); - var expectedEnd = new DateTime(2021, 11, 1, 10, 0, 0); + var expectedStart = new DateTime(2021, 11, 1, 6, 0, 0); + var expectedEnd = new DateTime(2021, 11, 1, 8, 0, 0); + var timeZonePlusThree = TZConvert.GetTimeZoneInfo("Europe/Moscow"); var mockDateTimeProvider = new Mock(); mockDateTimeProvider.Setup(d => d.DateTimeUtcNow()) @@ -453,7 +461,7 @@ public void WorkedPeriod_NoHoursRange_ReturnsWorkedPeriodStartingFromNineAm() }; var (start, end) = luisResult.WorkedPeriod(mockDateTimeProvider.Object, 120, - TZConvert.GetTimeZoneInfo("Europe/Rome")); + timeZonePlusThree); start.Should().Be(expectedStart); end.Should().Be(expectedEnd); From 05ab53b1daed9362499f5fc0c9c511dfab2db602 Mon Sep 17 00:00:00 2001 From: khaelys Date: Mon, 8 Nov 2021 12:33:04 +0100 Subject: [PATCH 8/8] feat: Add timezone on entry fill --- src/Clockify/Fill/EntryFillDialog.cs | 6 ++++-- src/Clockify/Fill/ITimeEntryStoreService.cs | 3 ++- src/Clockify/Fill/TimeEntryStoreService.cs | 16 +++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Clockify/Fill/EntryFillDialog.cs b/src/Clockify/Fill/EntryFillDialog.cs index dc98c84..8dfc1e5 100644 --- a/src/Clockify/Fill/EntryFillDialog.cs +++ b/src/Clockify/Fill/EntryFillDialog.cs @@ -80,6 +80,7 @@ private async Task PromptForTaskAsync(WaterfallStepContext ste var recognizedProject = await _clockifyWorkableRecognizer.RecognizeProject(luisResult.ProjectName(), clockifyToken); stepContext.Values["Project"] = recognizedProject; + stepContext.Values["TimeZone"] = userProfile.TimeZone; double minutes = luisResult.WorkedDurationInMinutes(); var (start, end) = luisResult.WorkedPeriod(_dateTimeProvider, minutes, userProfile.TimeZone); stepContext.Values["Start"] = start; @@ -250,12 +251,13 @@ private async Task ClockifyTaskValidatorAsync(PromptValidatorContext AddEntryAndExit(DialogContext stepContext, + private async Task AddEntryAndExit(WaterfallStepContext stepContext, CancellationToken cancellationToken, string clockifyToken, ProjectDo recognizedProject, DateTime start, DateTime end, string fullEntity, TaskDo? task) { + var timeZone = (TimeZoneInfo) stepContext.Values["TimeZone"]; double current = - await _timeEntryStoreService.AddTimeEntries(clockifyToken, recognizedProject, task, start, end); + await _timeEntryStoreService.AddTimeEntries(clockifyToken, recognizedProject, task, start, end, timeZone); string messageText = string.Format(_messageSource.AddEntryFeedback, (end-start).TotalMinutes, fullEntity, current); string platform = stepContext.Context.Activity.ChannelId; diff --git a/src/Clockify/Fill/ITimeEntryStoreService.cs b/src/Clockify/Fill/ITimeEntryStoreService.cs index 2e71a98..85581dd 100644 --- a/src/Clockify/Fill/ITimeEntryStoreService.cs +++ b/src/Clockify/Fill/ITimeEntryStoreService.cs @@ -6,6 +6,7 @@ namespace Bot.Clockify.Fill { public interface ITimeEntryStoreService { - public Task AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, DateTime end); + public Task AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, + DateTime end, TimeZoneInfo timeZone); } } \ No newline at end of file diff --git a/src/Clockify/Fill/TimeEntryStoreService.cs b/src/Clockify/Fill/TimeEntryStoreService.cs index 770bcfa..e657ba3 100644 --- a/src/Clockify/Fill/TimeEntryStoreService.cs +++ b/src/Clockify/Fill/TimeEntryStoreService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Bot.Clockify.Client; using Bot.Clockify.Models; +using Bot.Common; using Microsoft.Extensions.Configuration; namespace Bot.Clockify.Fill @@ -12,14 +13,18 @@ public class TimeEntryStoreService : ITimeEntryStoreService { private readonly IClockifyService _clockifyService; private readonly string _tagName; + private readonly IDateTimeProvider _dateTimeProvider; - public TimeEntryStoreService(IClockifyService clockifyService, IConfiguration configuration) + public TimeEntryStoreService(IClockifyService clockifyService, IConfiguration configuration, + IDateTimeProvider dateTimeProvider) { _clockifyService = clockifyService; + _dateTimeProvider = dateTimeProvider; _tagName = configuration["Tag"]; } - public async Task AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, DateTime end) + public async Task AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, + DateTime end, TimeZoneInfo timeZone) { string? tagId = await _clockifyService.GetTagAsync(clockifyToken, project.WorkspaceId, _tagName); string userId = (await _clockifyService.GetCurrentUserAsync(clockifyToken)).Id; @@ -31,14 +36,15 @@ public async Task AddTimeEntries(string clockifyToken, ProjectDo project taskId: task?.Id, billable: project.Billable, end: end, - tagIds: (tagId != null) ? new List {tagId} : null + tagIds: tagId != null ? new List { tagId } : null ); await _clockifyService.AddTimeEntryAsync(clockifyToken, workspaceId, timeEntry); - // TODO timezone and separate total hours calculation + // TODO Extract total hours calculation into another method + var userToday = TimeZoneInfo.ConvertTime(_dateTimeProvider.DateTimeUtcNow(), timeZone).Date; var todayEntries = await _clockifyService.GetHydratedTimeEntriesAsync(clockifyToken, workspaceId, userId, - new DateTimeOffset(DateTime.Today), new DateTimeOffset(DateTime.Today.AddDays(1))); + new DateTimeOffset(userToday), new DateTimeOffset(userToday.AddDays(1))); return todayEntries .Where(entry => entry.TimeInterval.Start.HasValue && entry.TimeInterval.End.HasValue)