Skip to content

Commit

Permalink
refactor: replace most custom date util functions by lancet ones
Browse files Browse the repository at this point in the history
refactor: add precision mode to missing intervals function
  • Loading branch information
muety committed Mar 25, 2022
1 parent 8a731a2 commit 5aae18e
Show file tree
Hide file tree
Showing 12 changed files with 653 additions and 768 deletions.
1,112 changes: 556 additions & 556 deletions coverage/coverage.out

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion routes/compat/wakatime/v1/heartbeat.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1

import (
"github.com/duke-git/lancet/v2/datetime"
"net/http"
"time"

Expand Down Expand Up @@ -65,7 +66,7 @@ func (h *HeartbeatHandler) Get(w http.ResponseWriter, r *http.Request) {
}

timezone := user.TZ()
rangeFrom, rangeTo := utils.StartOfDay(date.In(timezone)), utils.EndOfDay(date.In(timezone))
rangeFrom, rangeTo := datetime.BeginOfDay(date.In(timezone)), datetime.EndOfDay(date.In(timezone))

heartbeats, err := h.heartbeatSrvc.GetAllWithin(rangeFrom, rangeTo, user)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion routes/compat/wakatime/v1/summaries.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1

import (
"errors"
"github.com/duke-git/lancet/v2/datetime"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -120,7 +121,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
// i.e. for wakatime, an interval 2021-04-29 - 2021-04-29 is actually 2021-04-29 - 2021-04-30,
// while for wakapi it would be empty
// see https://github.com/muety/wakapi/issues/192
end = utils.EndOfDay(end).Add(-1 * time.Second)
end = datetime.EndOfDay(end)

overallParams := &models.SummaryParams{
From: start,
Expand Down
3 changes: 2 additions & 1 deletion routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package routes

import (
"fmt"
"github.com/duke-git/lancet/v2/datetime"
"github.com/muety/wakapi/views"
"html/template"
"net/http"
Expand All @@ -28,7 +29,7 @@ func DefaultTemplateFuncs() template.FuncMap {
"simpledate": utils.FormatDate,
"simpledatetime": utils.FormatDateTime,
"duration": utils.FmtWakatimeDuration,
"floordate": utils.FloorDate,
"floordate": datetime.BeginOfDay,
"ceildate": utils.CeilDate,
"title": strings.Title,
"join": strings.Join,
Expand Down
6 changes: 3 additions & 3 deletions services/imports/wakatime.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/duke-git/lancet/v2/datetime"
"net/http"
"time"

"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
wakatime "github.com/muety/wakapi/models/compat/wakatime/v1"
"github.com/muety/wakapi/utils"
"go.uber.org/atomic"
"golang.org/x/sync/semaphore"
)
Expand Down Expand Up @@ -295,8 +295,8 @@ func mapHeartbeat(
func generateDays(from, to time.Time) []time.Time {
days := make([]time.Time, 0)

from = utils.StartOfDay(from)
to = utils.StartOfDay(to.AddDate(0, 0, 1))
from = datetime.BeginOfDay(from)
to = datetime.BeginOfDay(to.AddDate(0, 0, 1))

for d := from; d.Before(to); d = d.AddDate(0, 0, 1) {
days = append(days, d)
Expand Down
33 changes: 20 additions & 13 deletions services/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package services
import (
"errors"
"fmt"
"github.com/duke-git/lancet/v2/datetime"
"github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config"
Expand Down Expand Up @@ -113,7 +114,7 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filte
}

// Generate missing slots (especially before and after existing summaries) from durations (formerly raw heartbeats)
missingIntervals := srv.getMissingIntervals(from, to, summaries)
missingIntervals := srv.getMissingIntervals(from, to, summaries, false)
for _, interval := range missingIntervals {
if s, err := srv.Summarize(interval.Start, interval.End, user, filters); err == nil {
summaries = append(summaries, s)
Expand Down Expand Up @@ -368,7 +369,7 @@ func (srv *SummaryService) mergeSummaryItems(existing []*models.SummaryItem, new
return itemList
}

func (srv *SummaryService) getMissingIntervals(from, to time.Time, summaries []*models.Summary) []*models.Interval {
func (srv *SummaryService) getMissingIntervals(from, to time.Time, summaries []*models.Summary, precise bool) []*models.Interval {
if len(summaries) == 0 {
return []*models.Interval{{from, to}}
}
Expand All @@ -377,37 +378,43 @@ func (srv *SummaryService) getMissingIntervals(from, to time.Time, summaries []*

// Pre
if from.Before(summaries[0].FromTime.T()) {
intervals = append(intervals, &models.Interval{from, summaries[0].FromTime.T()})
intervals = append(intervals, &models.Interval{Start: from, End: summaries[0].FromTime.T()})
}

// Between
for i := 0; i < len(summaries)-1; i++ {
t1, t2 := summaries[i].ToTime.T(), summaries[i+1].FromTime.T()
if t1.Equal(t2) {
if t1.Equal(t2) || t1.Equal(to) || t1.After(to) {
continue
}

td1 := t1
td2 := t2

// round to end of day / start of day, assuming that summaries are always generated on a per-day basis
// we assume that, if summary for any time range within a day is present, no further heartbeats exist on that day before 'from' and after 'to' time of that summary
// this requires that a summary exists for every single day in a year and none is skipped, which shouldn't ever happen
td1 := time.Date(t1.Year(), t1.Month(), t1.Day()+1, 0, 0, 0, 0, t1.Location())
td2 := time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, t2.Location())

// we always want to jump to beginning of next day
// however, if left summary ends already at midnight, we would instead jump to beginning of second-next day -> go back again
if td1.Sub(t1) == 24*time.Hour {
td1 = td1.Add(-1 * time.Hour)
// non-precise mode is mainly for speed when fetching summaries over large intervals and trades speed for summary accuracy / comprehensiveness
if !precise {
td1 = datetime.BeginOfDay(t1).AddDate(0, 0, 1)
td2 = datetime.BeginOfDay(t2)

// we always want to jump to beginning of next day
// however, if left summary ends already at midnight, we would instead jump to beginning of second-next day -> go back again
if td1.Sub(t1) == 24*time.Hour {
td1 = td1.Add(-1 * time.Hour)
}
}

// one or more day missing in between?
if td1.Before(td2) {
intervals = append(intervals, &models.Interval{summaries[i].ToTime.T(), summaries[i+1].FromTime.T()})
intervals = append(intervals, &models.Interval{Start: summaries[i].ToTime.T(), End: summaries[i+1].FromTime.T()})
}
}

// Post
if to.After(summaries[len(summaries)-1].ToTime.T()) {
intervals = append(intervals, &models.Interval{summaries[len(summaries)-1].ToTime.T(), to})
intervals = append(intervals, &models.Interval{Start: summaries[len(summaries)-1].ToTime.T(), End: to})
}

return intervals
Expand Down
39 changes: 39 additions & 0 deletions services/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,45 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Filters() {
assert.Contains(suite.T(), effectiveFilters.Label, TestProjectLabel3)
}

func (suite *SummaryServiceTestSuite) TestSummaryService_getMissingIntervals() {
sut := NewSummaryService(suite.SummaryRepository, suite.DurationService, suite.AliasService, suite.ProjectLabelService)

from1, _ := time.Parse(time.RFC822, "25 Mar 22 11:00 UTC")
to1, _ := time.Parse(time.RFC822, "25 Mar 22 13:00 UTC")
from2, _ := time.Parse(time.RFC822, "25 Mar 22 15:00 UTC")
to2, _ := time.Parse(time.RFC822, "26 Mar 22 00:00 UTC")

summaries := []*models.Summary{
{FromTime: models.CustomTime(from1), ToTime: models.CustomTime(to1)},
{FromTime: models.CustomTime(from2), ToTime: models.CustomTime(to2)},
}

r1 := sut.getMissingIntervals(from1, to1, summaries, true)
assert.Empty(suite.T(), r1)

r2 := sut.getMissingIntervals(from1, from1, summaries, true)
assert.Empty(suite.T(), r2)

// non-precise mode will not return intra-day intervals
// we might want to change this ...
r3 := sut.getMissingIntervals(from1, to2, summaries, false)
assert.Len(suite.T(), r3, 0)

r4 := sut.getMissingIntervals(from1, to2, summaries, true)
assert.Len(suite.T(), r4, 1)
assert.Equal(suite.T(), to1, r4[0].Start)
assert.Equal(suite.T(), from2, r4[0].End)

r5 := sut.getMissingIntervals(from1.Add(-time.Hour), to2.Add(time.Hour), summaries, true)
assert.Len(suite.T(), r5, 3)
assert.Equal(suite.T(), from1.Add(-time.Hour), r5[0].Start)
assert.Equal(suite.T(), from1, r5[0].End)
assert.Equal(suite.T(), to1, r5[1].Start)
assert.Equal(suite.T(), from2, r5[1].End)
assert.Equal(suite.T(), to2, r5[2].Start)
assert.Equal(suite.T(), to2.Add(time.Hour), r5[2].End)
}

func filterDurations(from, to time.Time, durations models.Durations) models.Durations {
filtered := make([]*models.Duration, 0, len(durations))
for _, d := range durations {
Expand Down
5 changes: 3 additions & 2 deletions services/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package services

import (
"fmt"
"github.com/duke-git/lancet/v2/datetime"
"github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config"
Expand Down Expand Up @@ -100,9 +101,9 @@ func (srv *UserService) GetAllByReports(reportsEnabled bool) ([]*models.User, er
}

func (srv *UserService) GetActive(exact bool) ([]*models.User, error) {
minDate := time.Now().Add(-24 * time.Hour * time.Duration(srv.config.App.InactiveDays))
minDate := time.Now().AddDate(0, 0, -1*srv.config.App.InactiveDays)
if !exact {
minDate = utils.FloorDateHour(minDate)
minDate = datetime.BeginOfHour(minDate)
}

cacheKey := fmt.Sprintf("%s--active", minDate.String())
Expand Down
2 changes: 1 addition & 1 deletion testing/wakapi_api_tests.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@
" pm.expect(jsonData.timezone).to.eql(pm.collectionVariables.get('TZ'));",
" var date = new Date(\"2022-01-01T00:00:00+0100\")",
" pm.expect(new Date(jsonData.start)).to.eql(date);",
" pm.expect(new Date(jsonData.end)).to.eql(new Date(date.getTime() + 3600 * 1000 * 24));",
" pm.expect(new Date(jsonData.end)).to.eql(new Date(date.getTime() + 3600 * 1000 * 24 - 1000));",
" pm.expect(jsonData.data.length).to.eql(2);",
"});"
],
Expand Down
99 changes: 11 additions & 88 deletions utils/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,41 @@ package utils

import (
"fmt"
"github.com/duke-git/lancet/v2/datetime"
"time"
)

// TODO: replace these functions by github.com/duke-git/lancet/v2/datetime
// needs additional thoughts, though, as for "EndOfX" functions, we currently return the discrete next day,
// while the above lib returns the very last nanosecond of the current day, i.e.
// 2022-02-15 23:59:59.999 +0800 CST vs. 2022-02-16 00:00:00.000 +0800 CST
// -> need to revisit comparison logic, etc.

func StartOfDay(date time.Time) time.Time {
return FloorDate(date)
}

func StartOfToday(tz *time.Location) time.Time {
return StartOfDay(FloorDate(time.Now().In(tz)))
}

func EndOfDay(date time.Time) time.Time {
floored := FloorDate(date)
if floored == date {
date = date.Add(1 * time.Second)
}
return CeilDate(date)
}

func EndOfToday(tz *time.Location) time.Time {
return EndOfDay(time.Now().In(tz))
}

func StartOfThisWeek(tz *time.Location) time.Time {
return StartOfWeek(time.Now().In(tz))
}

func StartOfWeek(date time.Time) time.Time {
year, week := date.ISOWeek()
return firstDayOfISOWeek(year, week, date.Location())
}

func StartOfThisMonth(tz *time.Location) time.Time {
return StartOfMonth(time.Now().In(tz))
}

func StartOfMonth(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
func BeginOfToday(tz *time.Location) time.Time {
return datetime.BeginOfDay(time.Now().In(tz))
}

func StartOfThisYear(tz *time.Location) time.Time {
return StartOfYear(time.Now().In(tz))
func BeginOfThisWeek(tz *time.Location) time.Time {
return datetime.BeginOfWeek(time.Now().In(tz))
}

func StartOfYear(date time.Time) time.Time {
return time.Date(date.Year(), time.January, 1, 0, 0, 0, 0, date.Location())
func BeginOfThisMonth(tz *time.Location) time.Time {
return datetime.BeginOfMonth(time.Now().In(tz))
}

// FloorDate rounds date down to the start of the day and keeps the time zone
func FloorDate(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
}

// FloorDateHour rounds date down to the start of the current hour and keeps the time zone
func FloorDateHour(date time.Time) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), date.Hour(), 0, 0, 0, date.Location())
func BeginOfThisYear(tz *time.Location) time.Time {
return datetime.BeginOfYear(time.Now().In(tz))
}

// CeilDate rounds date up to the start of next day if date is not already a start (00:00:00)
func CeilDate(date time.Time) time.Time {
floored := FloorDate(date)
floored := datetime.BeginOfDay(date)
if floored == date {
return floored
}
return floored.AddDate(0, 0, 1)
}

// SetLocation resets the time zone information of a date without converting it, i.e. 19:00 UTC will result in 19:00 CET, for instance
func SetLocation(date time.Time, tz *time.Location) time.Time {
return time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, tz)
}

// WithOffset adds the time zone difference between Local and tz to a date, i.e. 19:00 UTC will result in 21:00 CET (or 22:00 CEST), for instance
func WithOffset(date time.Time, tz *time.Location) time.Time {
now := time.Now()
_, localOffset := now.Zone()
_, targetOffset := now.In(tz).Zone()
dateTz := date.Add(time.Duration((targetOffset - localOffset) * int(time.Second)))
return time.Date(dateTz.Year(), dateTz.Month(), dateTz.Day(), dateTz.Hour(), dateTz.Minute(), dateTz.Second(), dateTz.Nanosecond(), dateTz.Location()).In(tz)
}

// SplitRangeByDays creates a slice of intervals between from and to, each of which is at max of 24 hours length and has its split at midnight
func SplitRangeByDays(from time.Time, to time.Time) [][]time.Time {
intervals := make([][]time.Time, 0)

for t1 := from; t1.Before(to); {
t2 := StartOfDay(t1).AddDate(0, 0, 1)
t2 := datetime.BeginOfDay(t1).AddDate(0, 0, 1)
if t2.After(to) {
t2 = to
}
Expand All @@ -118,22 +60,3 @@ func LocalTZOffset() time.Duration {
_, offset := time.Now().Zone()
return time.Duration(offset * int(time.Second))
}

// https://stackoverflow.com/a/18632496
func firstDayOfISOWeek(year int, week int, timezone *time.Location) time.Time {
date := time.Date(year, 0, 0, 0, 0, 0, 0, timezone)
isoYear, isoWeek := date.ISOWeek()
for date.Weekday() != time.Monday { // iterate back to Monday
date = date.AddDate(0, 0, -1)
isoYear, isoWeek = date.ISOWeek()
}
for isoYear < year { // iterate forward to the first day of the first week
date = date.AddDate(0, 0, 1)
isoYear, isoWeek = date.ISOWeek()
}
for isoWeek < week { // iterate forward to the first day of the given week
date = date.AddDate(0, 0, 1)
isoYear, isoWeek = date.ISOWeek()
}
return date
}

0 comments on commit 5aae18e

Please sign in to comment.