diff --git a/seq/array.go b/seq/array.go index 08479c2..01ca630 100644 --- a/seq/array.go +++ b/seq/array.go @@ -1,5 +1,7 @@ package seq +import "time" + // NewArray creates a new array. func NewArray(values ...float64) Array { return Array(values) @@ -17,3 +19,16 @@ func (a Array) Len() int { func (a Array) GetValue(index int) float64 { return a[index] } + +// ArrayOfTimes wraps an array of times as a sequence provider. +type ArrayOfTimes []time.Time + +// Len returns the length of the array. +func (aot ArrayOfTimes) Len() int { + return len(aot) +} + +// GetValue returns the time at the given index as a time.Time. +func (aot ArrayOfTimes) GetValue(index int) time.Time { + return aot[index] +} diff --git a/seq/provider.go b/seq/provider.go new file mode 100644 index 0000000..ce96d1f --- /dev/null +++ b/seq/provider.go @@ -0,0 +1,15 @@ +package seq + +import "time" + +// Provider is a provider for values for a seq. +type Provider interface { + Len() int + GetValue(int) float64 +} + +// TimeProvider is a provider for values for a seq. +type TimeProvider interface { + Len() int + GetValue(int) time.Time +} diff --git a/seq/random.go b/seq/random.go index 3d0768f..ea65084 100644 --- a/seq/random.go +++ b/seq/random.go @@ -11,7 +11,7 @@ func RandomValues(count int) []float64 { return Seq{NewRandom().WithLen(count)}.Array() } -// RandomValuesWithAverage returns an array of random values with a given average. +// RandomValuesWithMax returns an array of random values with a given average. func RandomValuesWithMax(count int, max float64) []float64 { return Seq{NewRandom().WithMax(max).WithLen(count)}.Array() } diff --git a/seq/sequence.go b/seq/seq.go similarity index 86% rename from seq/sequence.go rename to seq/seq.go index dfc369a..f95e6fc 100644 --- a/seq/sequence.go +++ b/seq/seq.go @@ -15,12 +15,6 @@ func Values(values ...float64) Seq { return Seq{Provider: Array(values)} } -// Provider is a provider for values for a seq. -type Provider interface { - Len() int - GetValue(int) float64 -} - // Seq is a utility wrapper for seq providers. type Seq struct { Provider @@ -28,12 +22,13 @@ type Seq struct { // Array enumerates the seq into a slice. func (s Seq) Array() (output []float64) { - if s.Len() == 0 { + slen := s.Len() + if slen == 0 { return } - output = make([]float64, s.Len()) - for i := 0; i < s.Len(); i++ { + output = make([]float64, slen) + for i := 0; i < slen; i++ { output[i] = s.GetValue(i) } return @@ -149,7 +144,43 @@ func (s Seq) Sort() Seq { return s } values := s.Array() - sort.Float64s(values) + sort.Slice(values, func(i, j int) bool { + return values[i] < values[j] + }) + return Seq{Provider: Array(values)} +} + +// SortDescending returns the seq sorted in descending order. +// This fully enumerates the seq. +func (s Seq) SortDescending() Seq { + if s.Len() == 0 { + return s + } + values := s.Array() + sort.Slice(values, func(i, j int) bool { + return values[i] > values[j] + }) + return Seq{Provider: Array(values)} +} + +// Reverse reverses the sequence's order. +func (s Seq) Reverse() Seq { + slen := s.Len() + if slen == 0 { + return s + } + + slen2 := slen >> 1 + values := s.Array() + + i := 0 + j := slen - 1 + for i < slen2 { + values[i], values[j] = values[j], values[i] + i++ + j-- + } + return Seq{Provider: Array(values)} } diff --git a/seq/sequence_test.go b/seq/seq_test.go similarity index 100% rename from seq/sequence_test.go rename to seq/seq_test.go diff --git a/seq/time.go b/seq/time.go index 79ef02a..fecab06 100644 --- a/seq/time.go +++ b/seq/time.go @@ -6,21 +6,12 @@ import ( "github.com/wcharczuk/go-chart/util" ) -// Time is a utility singleton with helper functions for time seq generation. -var Time timeSequence +// TimeUtil is a utility singleton with helper functions for time seq generation. +var TimeUtil timeUtil -type timeSequence struct{} +type timeUtil struct{} -// Days generates a seq of timestamps by day, from -days to today. -func (ts timeSequence) Days(days int) []time.Time { - var values []time.Time - for day := days; day >= 0; day-- { - values = append(values, time.Now().AddDate(0, 0, -day)) - } - return values -} - -func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { +func (tu timeUtil) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { var times []time.Time cursor := util.Date.On(marketOpen, from) toClose := util.Date.On(marketClose, to) @@ -41,7 +32,7 @@ func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose t return times } -func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { +func (tu timeUtil) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { var times []time.Time cursor := util.Date.On(marketOpen, from) toClose := util.Date.On(marketClose, to) @@ -62,7 +53,7 @@ func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, market return times } -func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { +func (tu timeUtil) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { var times []time.Time cursor := util.Date.On(marketOpen, from) toClose := util.Date.On(marketClose, to) @@ -78,7 +69,7 @@ func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClo return times } -func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { +func (tu timeUtil) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { var times []time.Time cursor := util.Date.On(marketOpen, from) toClose := util.Date.On(marketClose, to) @@ -94,7 +85,7 @@ func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen, return times } -func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { +func (tu timeUtil) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time { var times []time.Time cursor := util.Date.On(marketClose, from) toClose := util.Date.On(marketClose, to) @@ -109,7 +100,7 @@ func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, mar return times } -func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time { +func (tu timeUtil) Hours(start time.Time, totalHours int) []time.Time { times := make([]time.Time, totalHours) last := start @@ -122,13 +113,12 @@ func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time { } // HoursFilled adds zero values for the data bounded by the start and end of the xdata array. -func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { - start := Time.Start(xdata) - end := Time.End(xdata) +func (tu timeUtil) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { + start, end := Times(xdata...).MinAndMax() totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end)) - finalTimes := ts.Hours(start, totalHours+1) + finalTimes := tu.Hours(start, totalHours+1) finalValues := make([]float64, totalHours+1) var hoursFromStart int @@ -139,33 +129,3 @@ func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.T return finalTimes, finalValues } - -// Start returns the earliest (min) time in a list of times. -func (ts timeSequence) Start(times []time.Time) time.Time { - if len(times) == 0 { - return time.Time{} - } - - start := times[0] - for _, t := range times[1:] { - if t.Before(start) { - start = t - } - } - return start -} - -// Start returns the earliest (min) time in a list of times. -func (ts timeSequence) End(times []time.Time) time.Time { - if len(times) == 0 { - return time.Time{} - } - - end := times[0] - for _, t := range times[1:] { - if t.After(end) { - end = t - } - } - return end -} diff --git a/seq/time_seq.go b/seq/time_seq.go new file mode 100644 index 0000000..a7f0613 --- /dev/null +++ b/seq/time_seq.go @@ -0,0 +1,261 @@ +package seq + +import ( + "sort" + "time" +) + +var ( + // TimeZero is the zero time. + TimeZero = time.Time{} +) + +// Times returns a new time sequence. +func Times(values ...time.Time) TimeSeq { + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// TimeSeq is a sequence of times. +type TimeSeq struct { + TimeProvider +} + +// Array converts the sequence to times. +func (ts TimeSeq) Array() (output []time.Time) { + slen := ts.Len() + if slen == 0 { + return + } + + output = make([]time.Time, slen) + for i := 0; i < slen; i++ { + output[i] = ts.GetValue(i) + } + return +} + +// Each applies the `mapfn` to all values in the value provider. +func (ts TimeSeq) Each(mapfn func(int, time.Time)) { + for i := 0; i < ts.Len(); i++ { + mapfn(i, ts.GetValue(i)) + } +} + +// Map applies the `mapfn` to all values in the value provider, +// returning a new seq. +func (ts TimeSeq) Map(mapfn func(int, time.Time) time.Time) TimeSeq { + output := make([]time.Time, ts.Len()) + for i := 0; i < ts.Len(); i++ { + mapfn(i, ts.GetValue(i)) + } + return TimeSeq{ArrayOfTimes(output)} +} + +// FoldLeft collapses a seq from left to right. +func (ts TimeSeq) FoldLeft(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) { + tslen := ts.Len() + if tslen == 0 { + return TimeZero + } + + if tslen == 1 { + return ts.GetValue(0) + } + + v0 = ts.GetValue(0) + for i := 1; i < tslen; i++ { + v0 = mapfn(i, v0, ts.GetValue(i)) + } + return +} + +// FoldRight collapses a seq from right to left. +func (ts TimeSeq) FoldRight(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) { + tslen := ts.Len() + if tslen == 0 { + return TimeZero + } + + if tslen == 1 { + return ts.GetValue(0) + } + + v0 = ts.GetValue(tslen - 1) + for i := tslen - 2; i >= 0; i-- { + v0 = mapfn(i, v0, ts.GetValue(i)) + } + return +} + +// Sort returns the seq in ascending order. +func (ts TimeSeq) Sort() TimeSeq { + if ts.Len() == 0 { + return ts + } + + values := ts.Array() + sort.Slice(values, func(i, j int) bool { + return values[i].Before(values[j]) + }) + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// SortDescending returns the seq in descending order. +func (ts TimeSeq) SortDescending() TimeSeq { + if ts.Len() == 0 { + return ts + } + + values := ts.Array() + sort.Slice(values, func(i, j int) bool { + return values[i].After(values[j]) + }) + return TimeSeq{TimeProvider: ArrayOfTimes(values)} +} + +// Min returns the minimum (or earliest) time in the sequence. +func (ts TimeSeq) Min() (min time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + min = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.Before(min) { + min = tv + } + } + return +} + +// Start is an alias to `Min`. +func (ts TimeSeq) Start() time.Time { + return ts.Min() +} + +// Max returns the maximum (or latest) time in the sequence. +func (ts TimeSeq) Max() (max time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + max = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.After(max) { + max = tv + } + } + return +} + +// End is an alias to `Max`. +func (ts TimeSeq) End() time.Time { + return ts.Max() +} + +// First returns the first value in the sequence. +func (ts TimeSeq) First() time.Time { + if ts.Len() == 0 { + return TimeZero + } + + return ts.GetValue(0) +} + +// Last returns the last value in the sequence. +func (ts TimeSeq) Last() time.Time { + if ts.Len() == 0 { + return TimeZero + } + + return ts.GetValue(ts.Len() - 1) +} + +// MinAndMax returns both the earliest and latest value from a sequence in one pass. +func (ts TimeSeq) MinAndMax() (min, max time.Time) { + tslen := ts.Len() + if tslen == 0 { + return + } + min = ts.GetValue(0) + max = ts.GetValue(0) + var tv time.Time + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + if tv.Before(min) { + min = tv + } + if tv.After(max) { + max = tv + } + } + return +} + +// MapDistinct maps values given a map function to their distinct outputs. +func (ts TimeSeq) MapDistinct(mapFn func(time.Time) time.Time) TimeSeq { + tslen := ts.Len() + if tslen == 0 { + return TimeSeq{} + } + + var output []time.Time + hourLookup := SetOfTime{} + + // add the initial value + tv := ts.GetValue(0) + tvh := mapFn(tv) + hourLookup.Add(tvh) + output = append(output, tvh) + + for i := 1; i < tslen; i++ { + tv = ts.GetValue(i) + tvh = mapFn(tv) + if !hourLookup.Has(tvh) { + hourLookup.Add(tvh) + output = append(output, tvh) + } + } + + return TimeSeq{ArrayOfTimes(output)} +} + +// Hours returns times in each distinct hour represented by the sequence. +func (ts TimeSeq) Hours() TimeSeq { + return ts.MapDistinct(ts.trimToHour) +} + +// Days returns times in each distinct day represented by the sequence. +func (ts TimeSeq) Days() TimeSeq { + return ts.MapDistinct(ts.trimToDay) +} + +// Months returns times in each distinct months represented by the sequence. +func (ts TimeSeq) Months() TimeSeq { + return ts.MapDistinct(ts.trimToMonth) +} + +// Years returns times in each distinc year represented by the sequence. +func (ts TimeSeq) Years() TimeSeq { + return ts.MapDistinct(ts.trimToYear) +} + +func (ts TimeSeq) trimToHour(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), tv.Day(), tv.Hour(), 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToDay(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), tv.Day(), 0, 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToMonth(tv time.Time) time.Time { + return time.Date(tv.Year(), tv.Month(), 1, 0, 0, 0, 0, tv.Location()) +} + +func (ts TimeSeq) trimToYear(tv time.Time) time.Time { + return time.Date(tv.Year(), 1, 1, 0, 0, 0, 0, tv.Location()) +} diff --git a/seq/time_seq_test.go b/seq/time_seq_test.go new file mode 100644 index 0000000..52f4057 --- /dev/null +++ b/seq/time_seq_test.go @@ -0,0 +1,60 @@ +package seq + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" +) + +func TestTimeSeqTimes(t *testing.T) { + assert := assert.New(t) + + seq := Times(time.Now(), time.Now(), time.Now()) + assert.Equal(3, seq.Len()) +} + +func parseTime(str string) time.Time { + tv, _ := time.Parse("2006-01-02 15:04:05", str) + return tv +} + +func TestTimeSeqSort(t *testing.T) { + assert := assert.New(t) + + seq := Times( + parseTime("2016-05-14 12:00:00"), + parseTime("2017-05-14 12:00:00"), + parseTime("2015-05-14 12:00:00"), + parseTime("2017-05-13 12:00:00"), + ) + + sorted := seq.Sort() + assert.Equal(4, sorted.Len()) + min, max := sorted.MinAndMax() + assert.Equal(parseTime("2015-05-14 12:00:00"), min) + assert.Equal(parseTime("2017-05-14 12:00:00"), max) + + first, last := sorted.First(), sorted.Last() + assert.Equal(min, first) + assert.Equal(max, last) +} + +func TestTimeSeqDays(t *testing.T) { + assert := assert.New(t) + + seq := Times( + parseTime("2017-05-10 12:00:00"), + parseTime("2017-05-10 16:00:00"), + parseTime("2017-05-11 12:00:00"), + parseTime("2015-05-12 12:00:00"), + parseTime("2015-05-12 16:00:00"), + parseTime("2017-05-13 12:00:00"), + parseTime("2017-05-14 12:00:00"), + ) + + days := seq.Days() + assert.Equal(5, days.Len()) + assert.Equal(10, days.First().Day()) + assert.Equal(14, days.Last().Day()) +} diff --git a/seq/time_test.go b/seq/time_test.go index 31da051..40bd83f 100644 --- a/seq/time_test.go +++ b/seq/time_test.go @@ -12,7 +12,7 @@ func TestTimeMarketHours(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) - mh := Time.MarketHours(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) + mh := TimeUtil.MarketHours(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) assert.Len(mh, 8) assert.Equal(util.Date.Eastern(), mh[0].Location()) } @@ -20,7 +20,7 @@ func TestTimeMarketHours(t *testing.T) { func TestTimeMarketHourQuarters(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) - mh := Time.MarketHourQuarters(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) + mh := TimeUtil.MarketHourQuarters(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday) assert.Len(mh, 4) assert.Equal(9, mh[0].Hour()) assert.Equal(30, mh[0].Minute()) @@ -39,9 +39,9 @@ func TestTimeHours(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC) - seq := Time.Hours(today, 24) + seq := TimeUtil.Hours(today, 24) - end := Time.End(seq) + end := Times(seq...).Max() assert.Len(seq, 24) assert.Equal(2016, end.Year()) assert.Equal(07, int(end.Month())) @@ -72,8 +72,8 @@ func TestSequenceHoursFill(t *testing.T) { 0.6, } - filledTimes, filledValues := Time.HoursFilled(xdata, ydata) - assert.Len(filledTimes, util.Date.DiffHours(Time.Start(xdata), Time.End(xdata))+1) + filledTimes, filledValues := TimeUtil.HoursFilled(xdata, ydata) + assert.Len(filledTimes, util.Date.DiffHours(Times(xdata...).Start(), Times(xdata...).End())+1) assert.Equal(len(filledValues), len(filledTimes)) assert.NotZero(filledValues[0]) @@ -93,7 +93,7 @@ func TestTimeStart(t *testing.T) { time.Now().AddDate(0, 0, -5), } - assert.InTimeDelta(Time.Start(times), times[4], time.Millisecond) + assert.InTimeDelta(Times(times...).Start(), times[4], time.Millisecond) } func TestTimeEnd(t *testing.T) { @@ -107,5 +107,5 @@ func TestTimeEnd(t *testing.T) { time.Now().AddDate(0, 0, -5), } - assert.InTimeDelta(Time.End(times), times[2], time.Millisecond) + assert.InTimeDelta(Times(times...).End(), times[2], time.Millisecond) } diff --git a/seq/util.go b/seq/util.go index 685a408..238537b 100644 --- a/seq/util.go +++ b/seq/util.go @@ -1,6 +1,11 @@ package seq -import "math" +import ( + "math" + "time" + + "github.com/wcharczuk/go-chart/util" +) func round(input float64, places int) (rounded float64) { if math.IsNaN(input) { @@ -30,3 +35,22 @@ func f64i(value float64) int { r := round(value, 0) return int(r) } + +// SetOfTime is a simple hash set for timestamps as float64s. +type SetOfTime map[float64]bool + +// Add adds the value to the hash set. +func (sot SetOfTime) Add(tv time.Time) { + sot[util.Time.ToFloat64(tv)] = true +} + +// Has returns if the set contains a given time. +func (sot SetOfTime) Has(tv time.Time) bool { + _, hasValue := sot[util.Time.ToFloat64(tv)] + return hasValue +} + +// Remove removes the value from the set. +func (sot SetOfTime) Remove(tv time.Time) { + delete(sot, util.Time.ToFloat64(tv)) +}