time sequence stuff

This commit is contained in:
Will Charczuk 2017-05-14 12:21:30 -07:00
parent 73e3e439c5
commit 566d798b32
10 changed files with 438 additions and 72 deletions

View File

@ -1,5 +1,7 @@
package seq package seq
import "time"
// NewArray creates a new array. // NewArray creates a new array.
func NewArray(values ...float64) Array { func NewArray(values ...float64) Array {
return Array(values) return Array(values)
@ -17,3 +19,16 @@ func (a Array) Len() int {
func (a Array) GetValue(index int) float64 { func (a Array) GetValue(index int) float64 {
return a[index] 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]
}

15
seq/provider.go Normal file
View File

@ -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
}

View File

@ -11,7 +11,7 @@ func RandomValues(count int) []float64 {
return Seq{NewRandom().WithLen(count)}.Array() 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 { func RandomValuesWithMax(count int, max float64) []float64 {
return Seq{NewRandom().WithMax(max).WithLen(count)}.Array() return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
} }

View File

@ -15,12 +15,6 @@ func Values(values ...float64) Seq {
return Seq{Provider: Array(values)} 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. // Seq is a utility wrapper for seq providers.
type Seq struct { type Seq struct {
Provider Provider
@ -28,12 +22,13 @@ type Seq struct {
// Array enumerates the seq into a slice. // Array enumerates the seq into a slice.
func (s Seq) Array() (output []float64) { func (s Seq) Array() (output []float64) {
if s.Len() == 0 { slen := s.Len()
if slen == 0 {
return return
} }
output = make([]float64, s.Len()) output = make([]float64, slen)
for i := 0; i < s.Len(); i++ { for i := 0; i < slen; i++ {
output[i] = s.GetValue(i) output[i] = s.GetValue(i)
} }
return return
@ -149,7 +144,43 @@ func (s Seq) Sort() Seq {
return s return s
} }
values := s.Array() 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)} return Seq{Provider: Array(values)}
} }

View File

@ -6,21 +6,12 @@ import (
"github.com/wcharczuk/go-chart/util" "github.com/wcharczuk/go-chart/util"
) )
// Time is a utility singleton with helper functions for time seq generation. // TimeUtil is a utility singleton with helper functions for time seq generation.
var Time timeSequence var TimeUtil timeUtil
type timeSequence struct{} type timeUtil struct{}
// Days generates a seq of timestamps by day, from -days to today. func (tu timeUtil) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
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 {
var times []time.Time var times []time.Time
cursor := util.Date.On(marketOpen, from) cursor := util.Date.On(marketOpen, from)
toClose := util.Date.On(marketClose, to) toClose := util.Date.On(marketClose, to)
@ -41,7 +32,7 @@ func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose t
return times 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 var times []time.Time
cursor := util.Date.On(marketOpen, from) cursor := util.Date.On(marketOpen, from)
toClose := util.Date.On(marketClose, to) toClose := util.Date.On(marketClose, to)
@ -62,7 +53,7 @@ func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, market
return times 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 var times []time.Time
cursor := util.Date.On(marketOpen, from) cursor := util.Date.On(marketOpen, from)
toClose := util.Date.On(marketClose, to) toClose := util.Date.On(marketClose, to)
@ -78,7 +69,7 @@ func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClo
return times 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 var times []time.Time
cursor := util.Date.On(marketOpen, from) cursor := util.Date.On(marketOpen, from)
toClose := util.Date.On(marketClose, to) toClose := util.Date.On(marketClose, to)
@ -94,7 +85,7 @@ func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen,
return times 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 var times []time.Time
cursor := util.Date.On(marketClose, from) cursor := util.Date.On(marketClose, from)
toClose := util.Date.On(marketClose, to) toClose := util.Date.On(marketClose, to)
@ -109,7 +100,7 @@ func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, mar
return times 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) times := make([]time.Time, totalHours)
last := start 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. // 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) { func (tu timeUtil) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
start := Time.Start(xdata) start, end := Times(xdata...).MinAndMax()
end := Time.End(xdata)
totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end)) 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) finalValues := make([]float64, totalHours+1)
var hoursFromStart int var hoursFromStart int
@ -139,33 +129,3 @@ func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.T
return finalTimes, finalValues 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
}

261
seq/time_seq.go Normal file
View File

@ -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())
}

60
seq/time_seq_test.go Normal file
View File

@ -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())
}

View File

@ -12,7 +12,7 @@ func TestTimeMarketHours(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) 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.Len(mh, 8)
assert.Equal(util.Date.Eastern(), mh[0].Location()) assert.Equal(util.Date.Eastern(), mh[0].Location())
} }
@ -20,7 +20,7 @@ func TestTimeMarketHours(t *testing.T) {
func TestTimeMarketHourQuarters(t *testing.T) { func TestTimeMarketHourQuarters(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern()) 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.Len(mh, 4)
assert.Equal(9, mh[0].Hour()) assert.Equal(9, mh[0].Hour())
assert.Equal(30, mh[0].Minute()) assert.Equal(30, mh[0].Minute())
@ -39,9 +39,9 @@ func TestTimeHours(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC) 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.Len(seq, 24)
assert.Equal(2016, end.Year()) assert.Equal(2016, end.Year())
assert.Equal(07, int(end.Month())) assert.Equal(07, int(end.Month()))
@ -72,8 +72,8 @@ func TestSequenceHoursFill(t *testing.T) {
0.6, 0.6,
} }
filledTimes, filledValues := Time.HoursFilled(xdata, ydata) filledTimes, filledValues := TimeUtil.HoursFilled(xdata, ydata)
assert.Len(filledTimes, util.Date.DiffHours(Time.Start(xdata), Time.End(xdata))+1) assert.Len(filledTimes, util.Date.DiffHours(Times(xdata...).Start(), Times(xdata...).End())+1)
assert.Equal(len(filledValues), len(filledTimes)) assert.Equal(len(filledValues), len(filledTimes))
assert.NotZero(filledValues[0]) assert.NotZero(filledValues[0])
@ -93,7 +93,7 @@ func TestTimeStart(t *testing.T) {
time.Now().AddDate(0, 0, -5), 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) { func TestTimeEnd(t *testing.T) {
@ -107,5 +107,5 @@ func TestTimeEnd(t *testing.T) {
time.Now().AddDate(0, 0, -5), time.Now().AddDate(0, 0, -5),
} }
assert.InTimeDelta(Time.End(times), times[2], time.Millisecond) assert.InTimeDelta(Times(times...).End(), times[2], time.Millisecond)
} }

View File

@ -1,6 +1,11 @@
package seq package seq
import "math" import (
"math"
"time"
"github.com/wcharczuk/go-chart/util"
)
func round(input float64, places int) (rounded float64) { func round(input float64, places int) (rounded float64) {
if math.IsNaN(input) { if math.IsNaN(input) {
@ -30,3 +35,22 @@ func f64i(value float64) int {
r := round(value, 0) r := round(value, 0)
return int(r) 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))
}