From dbf666de3a580621fd88ff2f7e814a652557c7fb Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Sun, 4 Feb 2018 16:09:23 +0100 Subject: [PATCH] Package fitbit: Fix parsing of sleep data. --- fitbit/sleep.go | 36 ++++-- fitbit/sleep_test.go | 313 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 258 insertions(+), 91 deletions(-) diff --git a/fitbit/sleep.go b/fitbit/sleep.go index d7da138..a912938 100644 --- a/fitbit/sleep.go +++ b/fitbit/sleep.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "sort" "time" "google.golang.org/appengine/log" @@ -43,29 +44,42 @@ type Sleep struct { Stages []SleepStage } +type byTime []SleepStage + +func (t byTime) Len() int { return len(t) } +func (t byTime) Less(i, j int) bool { return t[i].StartTime.Before(t[j].StartTime) } +func (t byTime) Swap(i, j int) { t[i], t[j] = t[j], t[i] } + func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, error) { type rawData struct { DateTime string Level string Seconds int64 } - var jsonSleep struct { - Levels struct { - Data []rawData - ShortData []rawData + var parsed struct { + Sleep []struct { + Levels struct { + Data []rawData + ShortData []rawData + } } } - if err := json.Unmarshal(data, &jsonSleep); err != nil { + if err := json.Unmarshal(data, &parsed); err != nil { return nil, err } - rawStages := jsonSleep.Levels.Data - if len(jsonSleep.Levels.ShortData) != 0 { - rawStages = append(rawStages, jsonSleep.Levels.ShortData...) + var allStages []rawData + for _, s := range parsed.Sleep { + if len(s.Levels.Data) != 0 { + allStages = append(allStages, s.Levels.Data...) + } + if len(s.Levels.ShortData) != 0 { + allStages = append(allStages, s.Levels.ShortData...) + } } var ret Sleep - for _, stg := range rawStages { + for _, stg := range allStages { tm, err := time.ParseInLocation("2006-01-02T15:04:05.999", stg.DateTime, loc) if err != nil { log.Warningf(ctx, "unable to parse time: %q", stg.DateTime) @@ -94,10 +108,12 @@ func parseSleep(ctx context.Context, data []byte, loc *time.Location) (*Sleep, e }) } - if len(ret.Stages) == 0 && len(jsonSleep.Levels.Data) != 0 { + if len(ret.Stages) == 0 && len(allStages) != 0 { return nil, fmt.Errorf("parsing sleep stages failed") } + sort.Sort(byTime(ret.Stages)) + return &ret, nil } diff --git a/fitbit/sleep_test.go b/fitbit/sleep_test.go index 5d83858..24b3ce4 100644 --- a/fitbit/sleep_test.go +++ b/fitbit/sleep_test.go @@ -2,7 +2,6 @@ package fitbit import ( "context" - "reflect" "testing" "time" ) @@ -10,91 +9,240 @@ import ( func TestParseSleep(t *testing.T) { ctx := context.Background() - input := `{ - "dateOfSleep": "2017-04-02", - "duration": 42000, - "efficiency": 42, - "isMainSleep": true, - "levels": { - "summary": { - "deep": { - "count": 42, - "minutes": 42, - "thirtyDayAvgMinutes": 42 - }, - "light": { - "count": 42, - "minutes": 42, - "thirtyDayAvgMinutes": 42 - }, - "rem": { - "count": 42, - "minutes": 42, - "thirtyDayAvgMinutes": 42 - }, - "wake": { - "count": 42, - "minutes": 42, - "thirtyDayAvgMinutes": 42 - } - }, - "data": [ - { - "datetime": "2017-04-01T23:58:30.000", - "level": "wake", - "seconds": 1080 - }, - { - "datetime": "2017-04-02T00:16:30.000", - "level": "rem", - "seconds": 2000 - } - ], - "shortData": [ - { - "datetime": "2017-04-01T23:58:30.000", - "level": "wake", - "seconds": 1080 - }, - { - "datetime": "2017-04-02T00:16:30.000", - "level": "deep", - "seconds": 100 - }, - { - "datetime": "2017-04-02T00:18:10.000", - "level": "rem", - "seconds": 1900 - } - ] - }, - "logId": 42, - "minutesAfterWakeup": 42, - "minutesAsleep": 42, - "minutesAwake": 42, - "minutesToFallAsleep": 0, - "startTime": "2017-04-01T23:58:30.000", - "timeInBed": 42, - "type": "stages" -}` + input := `{"sleep":[{"dateOfSleep":"2018-02-04","duration":30420000,"efficiency":95,"endTime":"2018-02-04T06:36:30.000","infoCode":0,"isMainSleep":true,"levels":{"data":[{"dateTime":"2018-02-03T22:09:30.000","level":"light","seconds":390},{"dateTime":"2018-02-03T22:16:00.000","level":"wake","seconds":840},{"dateTime":"2018-02-03T22:30:00.000","level":"light","seconds":1110},{"dateTime":"2018-02-03T22:48:30.000","level":"wake","seconds":870},{"dateTime":"2018-02-03T23:03:00.000","level":"light","seconds":120},{"dateTime":"2018-02-03T23:05:00.000","level":"wake","seconds":600},{"dateTime":"2018-02-03T23:15:00.000","level":"light","seconds":180},{"dateTime":"2018-02-03T23:18:00.000","level":"deep","seconds":360},{"dateTime":"2018-02-03T23:24:00.000","level":"light","seconds":390},{"dateTime":"2018-02-03T23:30:30.000","level":"deep","seconds":390},{"dateTime":"2018-02-03T23:37:00.000","level":"light","seconds":2580},{"dateTime":"2018-02-04T00:20:00.000","level":"rem","seconds":1080},{"dateTime":"2018-02-04T00:38:00.000","level":"wake","seconds":480},{"dateTime":"2018-02-04T00:46:00.000","level":"light","seconds":540},{"dateTime":"2018-02-04T00:55:00.000","level":"deep","seconds":1770},{"dateTime":"2018-02-04T01:24:30.000","level":"light","seconds":390},{"dateTime":"2018-02-04T01:31:00.000","level":"wake","seconds":630},{"dateTime":"2018-02-04T01:41:30.000","level":"light","seconds":480},{"dateTime":"2018-02-04T01:49:30.000","level":"rem","seconds":930},{"dateTime":"2018-02-04T02:05:00.000","level":"light","seconds":1920},{"dateTime":"2018-02-04T02:37:00.000","level":"deep","seconds":1830},{"dateTime":"2018-02-04T03:07:30.000","level":"light","seconds":630},{"dateTime":"2018-02-04T03:18:00.000","level":"rem","seconds":1740},{"dateTime":"2018-02-04T03:47:00.000","level":"wake","seconds":960},{"dateTime":"2018-02-04T04:03:00.000","level":"light","seconds":4860},{"dateTime":"2018-02-04T05:24:00.000","level":"rem","seconds":1890},{"dateTime":"2018-02-04T05:55:30.000","level":"light","seconds":2460}],"shortData":[{"dateTime":"2018-02-03T22:10:00.000","level":"wake","seconds":120},{"dateTime":"2018-02-03T22:13:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-03T22:39:00.000","level":"wake","seconds":60},{"dateTime":"2018-02-03T23:52:00.000","level":"wake","seconds":60},{"dateTime":"2018-02-03T23:56:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T00:04:30.000","level":"wake","seconds":90},{"dateTime":"2018-02-04T00:17:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T00:51:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T02:06:30.000","level":"wake","seconds":90},{"dateTime":"2018-02-04T02:17:00.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T02:24:00.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T03:07:00.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T03:23:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T03:42:00.000","level":"wake","seconds":60},{"dateTime":"2018-02-04T03:45:00.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T04:16:30.000","level":"wake","seconds":30},{"dateTime":"2018-02-04T04:21:30.000","level":"wake","seconds":60},{"dateTime":"2018-02-04T04:47:30.000","level":"wake","seconds":60},{"dateTime":"2018-02-04T06:29:00.000","level":"wake","seconds":30}],"summary":{"deep":{"count":4,"minutes":72,"thirtyDayAvgMinutes":81},"light":{"count":28,"minutes":255,"thirtyDayAvgMinutes":272},"rem":{"count":7,"minutes":92,"thirtyDayAvgMinutes":90},"wake":{"count":25,"minutes":88,"thirtyDayAvgMinutes":81}}},"logId":17122464961,"minutesAfterWakeup":0,"minutesAsleep":419,"minutesAwake":88,"minutesToFallAsleep":0,"startTime":"2018-02-03T22:09:30.000","timeInBed":507,"type":"stages"}],"summary":{"stages":{"deep":94,"light":155,"rem":78,"wake":71},"totalMinutesAsleep":419,"totalSleepRecords":1,"totalTimeInBed":507}}` want := &Sleep{ Stages: []SleepStage{ SleepStage{ - StartTime: time.Date(2017, time.April, 1, 23, 58, 30, 0, time.UTC), - EndTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC), + StartTime: time.Date(2018, time.February, 3, 22, 9, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 16, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 10, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 12, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 13, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 14, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 16, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 30, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 30, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 48, 30, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 39, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 22, 40, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 22, 48, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 3, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 3, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 5, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 5, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 15, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 15, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 18, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 18, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 24, 0, 0, time.UTC), + Level: SleepLevelDeep, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 24, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 30, 30, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 30, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 37, 0, 0, time.UTC), + Level: SleepLevelDeep, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 37, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 20, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 52, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 53, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 3, 23, 56, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 3, 23, 57, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 4, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 6, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 17, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 18, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 20, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 38, 0, 0, time.UTC), + Level: SleepLevelREM, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 38, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 46, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 46, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 55, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 51, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 0, 52, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 0, 55, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 1, 24, 30, 0, time.UTC), + Level: SleepLevelDeep, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 1, 24, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 1, 31, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 1, 31, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 1, 41, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 1, 41, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 1, 49, 30, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 1, 49, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 2, 5, 0, 0, time.UTC), + Level: SleepLevelREM, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 2, 5, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 2, 37, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 2, 6, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 2, 8, 0, 0, time.UTC), Level: SleepLevelWake, }, SleepStage{ - StartTime: time.Date(2017, time.April, 2, 0, 16, 30, 0, time.UTC), - EndTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC), + StartTime: time.Date(2018, time.February, 4, 2, 17, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 2, 17, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 2, 24, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 2, 24, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 2, 37, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 7, 30, 0, time.UTC), Level: SleepLevelDeep, }, SleepStage{ - StartTime: time.Date(2017, time.April, 2, 0, 18, 10, 0, time.UTC), - EndTime: time.Date(2017, time.April, 2, 0, 49, 50, 0, time.UTC), + StartTime: time.Date(2018, time.February, 4, 3, 7, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 7, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 7, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 18, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 18, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 47, 0, 0, time.UTC), Level: SleepLevelREM, }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 23, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 24, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 42, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 43, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 45, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 3, 45, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 3, 47, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 4, 3, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 4, 3, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 5, 24, 0, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 4, 16, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 4, 17, 0, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 4, 21, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 4, 22, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 4, 47, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 4, 48, 30, 0, time.UTC), + Level: SleepLevelWake, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 5, 24, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 5, 55, 30, 0, time.UTC), + Level: SleepLevelREM, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 5, 55, 30, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 6, 36, 30, 0, time.UTC), + Level: SleepLevelLight, + }, + SleepStage{ + StartTime: time.Date(2018, time.February, 4, 6, 29, 0, 0, time.UTC), + EndTime: time.Date(2018, time.February, 4, 6, 29, 30, 0, time.UTC), + Level: SleepLevelWake, + }, }, } @@ -103,14 +251,17 @@ func TestParseSleep(t *testing.T) { t.Errorf("parseSleep() = %v", err) } - for i, stg := range got.Stages { - t.Logf("got.Stages[%d] = %+v", i, stg) - } - for i, stg := range want.Stages { - t.Logf("want.Stages[%d] = %+v", i, stg) + if got, want := len(got.Stages), len(want.Stages); got != want { + t.Fatalf("parseSleep() = %d stages, want %d stages", got, want) } - if !reflect.DeepEqual(got, want) { - t.Errorf("parseSleep() = %+v, want %+v", got, want) + for i, gotStage := range got.Stages { + wantStage := want.Stages[i] + + if gotStage.Level != wantStage.Level || + !gotStage.StartTime.Equal(wantStage.StartTime) || + !gotStage.EndTime.Equal(wantStage.EndTime) { + t.Errorf("Stages[%d] differ; got %+v, want %+v", i, gotStage, wantStage) + } } } -- 2.11.0