diff --git a/Makefile b/Makefile index 1ecb433..0f8d424 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ all: test +tools: + @go get -u github.com/blend/go-sdk/_bin/coverage + @go get -u github.com/blend/go-sdk/_bin/profanity + test: @go test ./... @@ -8,9 +12,4 @@ profanity: @profanity -include="*.go,Makefile,README.md" cover: - @go test -short -covermode=set -coverprofile=profile.cov - @go tool cover -html=profile.cov - @rm profile.cov - -deps: - @go get -u github.com/blend/go-sdk/_bin/profanity + @coverage \ No newline at end of file diff --git a/linear_coefficient_provider.go b/linear_coefficient_provider.go new file mode 100644 index 0000000..9701f6b --- /dev/null +++ b/linear_coefficient_provider.go @@ -0,0 +1,42 @@ +package chart + +// LinearCoefficientProvider is a type that returns linear cofficients. +type LinearCoefficientProvider interface { + Coefficients() (m, b, stdev, avg float64) +} + +// LinearCoefficients returns a fixed linear coefficient pair. +func LinearCoefficients(m, b float64) LinearCoefficientSet { + return LinearCoefficientSet{ + M: m, + B: b, + } +} + +// NormalizedLinearCoefficients returns a fixed linear coefficient pair. +func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet { + return LinearCoefficientSet{ + M: m, + B: b, + StdDev: stdev, + Avg: avg, + } +} + +// LinearCoefficientSet is the m and b values for the linear equation in the form: +// y = (m*x) + b +type LinearCoefficientSet struct { + M float64 + B float64 + StdDev float64 + Avg float64 +} + +// Coefficients returns the coefficients. +func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) { + m = lcs.M + b = lcs.B + stdev = lcs.StdDev + avg = lcs.Avg + return +} diff --git a/linear_regression_series.go b/linear_regression_series.go index ebeb98b..ae5253c 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -9,9 +9,10 @@ import ( // Interface Assertions. var ( - _ Series = (*LinearRegressionSeries)(nil) - _ FirstValuesProvider = (*LinearRegressionSeries)(nil) - _ LastValuesProvider = (*LinearRegressionSeries)(nil) + _ Series = (*LinearRegressionSeries)(nil) + _ FirstValuesProvider = (*LinearRegressionSeries)(nil) + _ LastValuesProvider = (*LinearRegressionSeries)(nil) + _ LinearCoefficientProvider = (*LinearRegressionSeries)(nil) ) // LinearRegressionSeries is a series that plots the n-nearest neighbors @@ -31,6 +32,19 @@ type LinearRegressionSeries struct { stddevx float64 } +// Coefficients returns the linear coefficients for the series. +func (lrs LinearRegressionSeries) Coefficients() (m, b, stdev, avg float64) { + if lrs.IsZero() { + lrs.computeCoefficients() + } + + m = lrs.m + b = lrs.b + stdev = lrs.stddevx + avg = lrs.avgx + return +} + // GetName returns the name of the time series. func (lrs LinearRegressionSeries) GetName() string { return lrs.Name @@ -79,7 +93,7 @@ func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } - if lrs.m == 0 && lrs.b == 0 { + if lrs.IsZero() { lrs.computeCoefficients() } offset := lrs.GetOffset() @@ -94,7 +108,7 @@ func (lrs *LinearRegressionSeries) GetFirstValues() (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } - if lrs.m == 0 && lrs.b == 0 { + if lrs.IsZero() { lrs.computeCoefficients() } x, y = lrs.InnerSeries.GetValues(0) @@ -107,7 +121,7 @@ func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } - if lrs.m == 0 && lrs.b == 0 { + if lrs.IsZero() { lrs.computeCoefficients() } endIndex := lrs.GetEndIndex() @@ -116,6 +130,29 @@ func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) { return } +// Render renders the series. +func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + style := lrs.Style.InheritFrom(defaults) + Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs) +} + +// Validate validates the series. +func (lrs *LinearRegressionSeries) Validate() error { + if lrs.InnerSeries == nil { + return fmt.Errorf("linear regression series requires InnerSeries to be set") + } + return nil +} + +// IsZero returns if we've computed the coefficients or not. +func (lrs *LinearRegressionSeries) IsZero() bool { + return lrs.m == 0 && lrs.b == 0 +} + +// +// internal helpers +// + func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 { return (xvalue - lrs.avgx) / lrs.stddevx } @@ -151,17 +188,3 @@ func (lrs *LinearRegressionSeries) computeCoefficients() { lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx) lrs.b = (sumy / p) - (lrs.m * sumx / p) } - -// Render renders the series. -func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := lrs.Style.InheritFrom(defaults) - Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs) -} - -// Validate validates the series. -func (lrs *LinearRegressionSeries) Validate() error { - if lrs.InnerSeries == nil { - return fmt.Errorf("linear regression series requires InnerSeries to be set") - } - return nil -} diff --git a/linear_series.go b/linear_series.go new file mode 100644 index 0000000..89afa93 --- /dev/null +++ b/linear_series.go @@ -0,0 +1,119 @@ +package chart + +import ( + "fmt" +) + +// Interface Assertions. +var ( + _ Series = (*LinearSeries)(nil) + _ FirstValuesProvider = (*LinearSeries)(nil) + _ LastValuesProvider = (*LinearSeries)(nil) +) + +// LinearSeries is a series that plots a line in a given domain. +type LinearSeries struct { + Name string + Style Style + YAxis YAxisType + + XValues []float64 + InnerSeries LinearCoefficientProvider + + m float64 + b float64 + stdev float64 + avg float64 +} + +// GetName returns the name of the time series. +func (ls LinearSeries) GetName() string { + return ls.Name +} + +// GetStyle returns the line style. +func (ls LinearSeries) GetStyle() Style { + return ls.Style +} + +// GetYAxis returns which YAxis the series draws on. +func (ls LinearSeries) GetYAxis() YAxisType { + return ls.YAxis +} + +// Len returns the number of elements in the series. +func (ls LinearSeries) Len() int { + return len(ls.XValues) +} + +// GetEndIndex returns the effective limit end. +func (ls LinearSeries) GetEndIndex() int { + return len(ls.XValues) - 1 +} + +// GetValues gets a value at a given index. +func (ls *LinearSeries) GetValues(index int) (x, y float64) { + if ls.InnerSeries == nil || len(ls.XValues) == 0 { + return + } + if ls.IsZero() { + ls.computeCoefficients() + } + x = ls.XValues[index] + y = (ls.m * ls.normalize(x)) + ls.b + return +} + +// GetFirstValues computes the first linear regression value. +func (ls *LinearSeries) GetFirstValues() (x, y float64) { + if ls.InnerSeries == nil || len(ls.XValues) == 0 { + return + } + if ls.IsZero() { + ls.computeCoefficients() + } + x, y = ls.GetValues(0) + return +} + +// GetLastValues computes the last linear regression value. +func (ls *LinearSeries) GetLastValues() (x, y float64) { + if ls.InnerSeries == nil || len(ls.XValues) == 0 { + return + } + if ls.IsZero() { + ls.computeCoefficients() + } + x, y = ls.GetValues(ls.GetEndIndex()) + return +} + +// Render renders the series. +func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls) +} + +// Validate validates the series. +func (ls LinearSeries) Validate() error { + if ls.InnerSeries == nil { + return fmt.Errorf("linear regression series requires InnerSeries to be set") + } + return nil +} + +// IsZero returns if the linear series has computed coefficients or not. +func (ls LinearSeries) IsZero() bool { + return ls.m == 0 && ls.b == 0 +} + +// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`. +func (ls *LinearSeries) computeCoefficients() { + ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients() +} + +func (ls *LinearSeries) normalize(xvalue float64) float64 { + if ls.avg > 0 && ls.stdev > 0 { + return (xvalue - ls.avg) / ls.stdev + } + return xvalue +} diff --git a/market_hours_range.go b/market_hours_range.go deleted file mode 100644 index de32e8c..0000000 --- a/market_hours_range.go +++ /dev/null @@ -1,195 +0,0 @@ -package chart - -import ( - "fmt" - "time" - - "github.com/wcharczuk/go-chart/seq" - "github.com/wcharczuk/go-chart/util" -) - -// MarketHoursRange is a special type of range that compresses a time range into just the -// market (i.e. NYSE operating hours and days) range. -type MarketHoursRange struct { - Min time.Time - Max time.Time - - MarketOpen time.Time - MarketClose time.Time - - HolidayProvider util.HolidayProvider - - ValueFormatter ValueFormatter - - Descending bool - Domain int -} - -// IsDescending returns if the range is descending. -func (mhr MarketHoursRange) IsDescending() bool { - return mhr.Descending -} - -// GetTimezone returns the timezone for the market hours range. -func (mhr MarketHoursRange) GetTimezone() *time.Location { - return mhr.GetMarketOpen().Location() -} - -// IsZero returns if the range is setup or not. -func (mhr MarketHoursRange) IsZero() bool { - return mhr.Min.IsZero() && mhr.Max.IsZero() -} - -// GetMin returns the min value. -func (mhr MarketHoursRange) GetMin() float64 { - return util.Time.ToFloat64(mhr.Min) -} - -// GetMax returns the max value. -func (mhr MarketHoursRange) GetMax() float64 { - return util.Time.ToFloat64(mhr.GetEffectiveMax()) -} - -// GetEffectiveMax gets either the close on the max, or the max itself. -func (mhr MarketHoursRange) GetEffectiveMax() time.Time { - maxClose := util.Date.On(mhr.MarketClose, mhr.Max) - if maxClose.After(mhr.Max) { - return maxClose - } - return mhr.Max -} - -// SetMin sets the min value. -func (mhr *MarketHoursRange) SetMin(min float64) { - mhr.Min = util.Time.FromFloat64(min) - mhr.Min = mhr.Min.In(mhr.GetTimezone()) -} - -// SetMax sets the max value. -func (mhr *MarketHoursRange) SetMax(max float64) { - mhr.Max = util.Time.FromFloat64(max) - mhr.Max = mhr.Max.In(mhr.GetTimezone()) -} - -// GetDelta gets the delta. -func (mhr MarketHoursRange) GetDelta() float64 { - min := mhr.GetMin() - max := mhr.GetMax() - return max - min -} - -// GetDomain gets the domain. -func (mhr MarketHoursRange) GetDomain() int { - return mhr.Domain -} - -// SetDomain sets the domain. -func (mhr *MarketHoursRange) SetDomain(domain int) { - mhr.Domain = domain -} - -// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider. -func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider { - if mhr.HolidayProvider == nil { - return func(_ time.Time) bool { return false } - } - return mhr.HolidayProvider -} - -// GetMarketOpen returns the market open time. -func (mhr MarketHoursRange) GetMarketOpen() time.Time { - if mhr.MarketOpen.IsZero() { - return util.NYSEOpen() - } - return mhr.MarketOpen -} - -// GetMarketClose returns the market close time. -func (mhr MarketHoursRange) GetMarketClose() time.Time { - if mhr.MarketClose.IsZero() { - return util.NYSEClose() - } - return mhr.MarketClose -} - -// GetTicks returns the ticks for the range. -// This is to override the default continous ticks that would be generated for the range. -func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick { - times := seq.Time.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) - timesWidth := mhr.measureTimes(r, defaults, vf, times) - if timesWidth <= mhr.Domain { - return mhr.makeTicks(vf, times) - } - - times = seq.Time.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) - timesWidth = mhr.measureTimes(r, defaults, vf, times) - if timesWidth <= mhr.Domain { - return mhr.makeTicks(vf, times) - } - - times = seq.Time.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) - timesWidth = mhr.measureTimes(r, defaults, vf, times) - if timesWidth <= mhr.Domain { - return mhr.makeTicks(vf, times) - } - - times = seq.Time.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) - timesWidth = mhr.measureTimes(r, defaults, vf, times) - if timesWidth <= mhr.Domain { - return mhr.makeTicks(vf, times) - } - - times = seq.Time.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) - timesWidth = mhr.measureTimes(r, defaults, vf, times) - if timesWidth <= mhr.Domain { - return mhr.makeTicks(vf, times) - } - - return GenerateContinuousTicks(r, mhr, false, defaults, vf) - -} - -func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int { - defaults.GetTextOptions().WriteToRenderer(r) - var total int - for index, t := range times { - timeLabel := vf(t) - - labelBox := r.MeasureText(timeLabel) - total += labelBox.Width() - if index > 0 { - total += DefaultMinimumTickHorizontalSpacing - } - } - return total -} - -func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []Tick { - ticks := make([]Tick, len(times)) - for index, t := range times { - ticks[index] = Tick{ - Value: util.Time.ToFloat64(t), - Label: vf(t), - } - } - return ticks -} - -func (mhr MarketHoursRange) String() string { - return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(time.RFC3339), mhr.Max.Format(time.RFC3339), mhr.Domain) -} - -// Translate maps a given value into the ContinuousRange space. -func (mhr MarketHoursRange) Translate(value float64) int { - valueTime := util.Time.FromFloat64(value) - valueTimeEastern := valueTime.In(util.Date.Eastern()) - totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) - valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) - translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain)) - - if mhr.IsDescending() { - return mhr.Domain - translated - } - - return translated -} diff --git a/market_hours_range_test.go b/market_hours_range_test.go deleted file mode 100644 index 26ff06b..0000000 --- a/market_hours_range_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package chart - -import ( - "testing" - "time" - - assert "github.com/blend/go-sdk/assert" - "github.com/wcharczuk/go-chart/util" -) - -func TestMarketHoursRangeGetDelta(t *testing.T) { - assert := assert.New(t) - - r := &MarketHoursRange{ - Min: time.Date(2016, 07, 19, 9, 30, 0, 0, util.Date.Eastern()), - Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()), - MarketOpen: util.NYSEOpen(), - MarketClose: util.NYSEClose(), - HolidayProvider: util.Date.IsNYSEHoliday, - } - - assert.NotZero(r.GetDelta()) -} - -func TestMarketHoursRangeTranslate(t *testing.T) { - assert := assert.New(t) - - r := &MarketHoursRange{ - Min: time.Date(2016, 07, 18, 9, 30, 0, 0, util.Date.Eastern()), - Max: time.Date(2016, 07, 22, 16, 00, 0, 0, util.Date.Eastern()), - MarketOpen: util.NYSEOpen(), - MarketClose: util.NYSEClose(), - HolidayProvider: util.Date.IsNYSEHoliday, - Domain: 1000, - } - - weds := time.Date(2016, 07, 20, 9, 30, 0, 0, util.Date.Eastern()) - - assert.Equal(0, r.Translate(util.Time.ToFloat64(r.Min))) - assert.Equal(400, r.Translate(util.Time.ToFloat64(weds))) - assert.Equal(1000, r.Translate(util.Time.ToFloat64(r.Max))) -} - -func TestMarketHoursRangeGetTicks(t *testing.T) { - assert := assert.New(t) - - r, err := PNG(1024, 1024) - assert.Nil(err) - - f, err := GetDefaultFont() - assert.Nil(err) - - defaults := Style{ - Font: f, - FontSize: 10, - FontColor: ColorBlack, - } - - ra := &MarketHoursRange{ - Min: util.Date.On(util.NYSEOpen(), util.Date.Date(2016, 07, 18, util.Date.Eastern())), - Max: util.Date.On(util.NYSEClose(), util.Date.Date(2016, 07, 22, util.Date.Eastern())), - MarketOpen: util.NYSEOpen(), - MarketClose: util.NYSEClose(), - HolidayProvider: util.Date.IsNYSEHoliday, - Domain: 1024, - } - - ticks := ra.GetTicks(r, defaults, TimeValueFormatter) - assert.NotEmpty(ticks) - assert.Len(ticks, 5) - assert.NotEqual(util.Time.ToFloat64(ra.Min), ticks[0].Value) - assert.NotEmpty(ticks[0].Label) -} diff --git a/seq/sequence.go b/seq/seq.go similarity index 100% rename from seq/sequence.go rename to seq/seq.go 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..4d859a1 100644 --- a/seq/time.go +++ b/seq/time.go @@ -20,95 +20,6 @@ func (ts timeSequence) Days(days int) []time.Time { return values } -func (ts timeSequence) 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) - for cursor.Before(toClose) || cursor.Equal(toClose) { - todayOpen := util.Date.On(marketOpen, cursor) - todayClose := util.Date.On(marketClose, cursor) - isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) - - if (cursor.Equal(todayOpen) || cursor.After(todayOpen)) && (cursor.Equal(todayClose) || cursor.Before(todayClose)) && isValidTradingDay { - times = append(times, cursor) - } - if cursor.After(todayClose) { - cursor = util.Date.NextMarketOpen(cursor, marketOpen, isHoliday) - } else { - cursor = util.Date.NextHour(cursor) - } - } - return times -} - -func (ts timeSequence) 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) - for cursor.Before(toClose) || cursor.Equal(toClose) { - - isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) - - if isValidTradingDay { - todayOpen := util.Date.On(marketOpen, cursor) - todayNoon := util.Date.NoonOn(cursor) - today2pm := util.Date.On(util.Date.Time(14, 0, 0, 0, cursor.Location()), cursor) - todayClose := util.Date.On(marketClose, cursor) - times = append(times, todayOpen, todayNoon, today2pm, todayClose) - } - - cursor = util.Date.NextDay(cursor) - } - return times -} - -func (ts timeSequence) 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) - for cursor.Before(toClose) || cursor.Equal(toClose) { - isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - todayClose := util.Date.On(marketClose, cursor) - times = append(times, todayClose) - } - - cursor = util.Date.NextDay(cursor) - } - return times -} - -func (ts timeSequence) 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) - for cursor.Before(toClose) || cursor.Equal(toClose) { - isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - todayClose := util.Date.On(marketClose, cursor) - times = append(times, todayClose) - } - - cursor = cursor.AddDate(0, 0, 2) - } - return times -} - -func (ts timeSequence) 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) - - for cursor.Equal(toClose) || cursor.Before(toClose) { - isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - times = append(times, cursor) - } - cursor = util.Date.NextDayOfWeek(cursor, time.Monday) - } - return times -} - func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time { times := make([]time.Time, totalHours) @@ -123,49 +34,17 @@ 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) - - totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end)) + start, end := util.Time.StartAndEnd(xdata...) + totalHours := util.Time.DiffHours(start, end) finalTimes := ts.Hours(start, totalHours+1) finalValues := make([]float64, totalHours+1) var hoursFromStart int for i, xd := range xdata { - hoursFromStart = util.Date.DiffHours(start, xd) + hoursFromStart = util.Time.DiffHours(start, xd) finalValues[hoursFromStart] = ydata[i] } 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_test.go b/seq/time_test.go index ca1148f..b37fb84 100644 --- a/seq/time_test.go +++ b/seq/time_test.go @@ -8,40 +8,13 @@ import ( "github.com/wcharczuk/go-chart/util" ) -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) - assert.Len(mh, 8) - assert.Equal(util.Date.Eastern(), mh[0].Location()) -} - -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) - assert.Len(mh, 4) - assert.Equal(9, mh[0].Hour()) - assert.Equal(30, mh[0].Minute()) - assert.Equal(util.Date.Eastern(), mh[0].Location()) - - assert.Equal(12, mh[1].Hour()) - assert.Equal(00, mh[1].Minute()) - assert.Equal(util.Date.Eastern(), mh[1].Location()) - - assert.Equal(14, mh[2].Hour()) - assert.Equal(00, mh[2].Minute()) - assert.Equal(util.Date.Eastern(), mh[2].Location()) -} - 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) - end := Time.End(seq) + end := util.Time.End(seq...) assert.Len(seq, 24) assert.Equal(2016, end.Year()) assert.Equal(07, int(end.Month())) @@ -73,7 +46,7 @@ func TestSequenceHoursFill(t *testing.T) { } filledTimes, filledValues := Time.HoursFilled(xdata, ydata) - expected := util.Date.DiffHours(Time.Start(xdata), Time.End(xdata)) + 1 + expected := util.Time.DiffHours(util.Time.Start(xdata...), util.Time.End(xdata...)) + 1 assert.Len(filledTimes, expected) assert.Equal(len(filledValues), len(filledTimes)) @@ -94,7 +67,7 @@ func TestTimeStart(t *testing.T) { time.Now().AddDate(0, 0, -5), } - assert.InTimeDelta(Time.Start(times), times[4], time.Millisecond) + assert.InTimeDelta(util.Time.Start(times...), times[4], time.Millisecond) } func TestTimeEnd(t *testing.T) { @@ -108,5 +81,5 @@ func TestTimeEnd(t *testing.T) { time.Now().AddDate(0, 0, -5), } - assert.InTimeDelta(Time.End(times), times[2], time.Millisecond) + assert.InTimeDelta(util.Time.End(times...), times[2], time.Millisecond) } diff --git a/seq/times.go b/seq/times.go new file mode 100644 index 0000000..8c65ced --- /dev/null +++ b/seq/times.go @@ -0,0 +1,31 @@ +package seq + +import ( + "time" + + "github.com/wcharczuk/go-chart/util" +) + +// Assert types implement interfaces. +var ( + _ Provider = (*Times)(nil) +) + +// Times are an array of times. +// It wraps the array with methods that implement `seq.Provider`. +type Times []time.Time + +// Array returns the times to an array. +func (t Times) Array() []time.Time { + return []time.Time(t) +} + +// Len returns the length of the array. +func (t Times) Len() int { + return len(t) +} + +// GetValue returns a value at an index as a time. +func (t Times) GetValue(index int) float64 { + return util.Time.ToFloat64(t[index]) +} diff --git a/util/date.go b/util/date.go index 18b8603..11fff97 100644 --- a/util/date.go +++ b/util/date.go @@ -1,7 +1,6 @@ package util import ( - "sync" "time" ) @@ -45,192 +44,88 @@ var ( Epoch = time.Unix(0, 0) ) -var ( - _easternLock sync.Mutex - _eastern *time.Location -) - -// NYSEOpen is when the NYSE opens. -func NYSEOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) } - -// NYSEClose is when the NYSE closes. -func NYSEClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) } - -// NASDAQOpen is when NASDAQ opens. -func NASDAQOpen() time.Time { return Date.Time(9, 30, 0, 0, Date.Eastern()) } - -// NASDAQClose is when NASDAQ closes. -func NASDAQClose() time.Time { return Date.Time(16, 0, 0, 0, Date.Eastern()) } - -// NYSEArcaOpen is when NYSEARCA opens. -func NYSEArcaOpen() time.Time { return Date.Time(4, 0, 0, 0, Date.Eastern()) } - -// NYSEArcaClose is when NYSEARCA closes. -func NYSEArcaClose() time.Time { return Date.Time(20, 0, 0, 0, Date.Eastern()) } - -// HolidayProvider is a function that returns if a given time falls on a holiday. -type HolidayProvider func(time.Time) bool - -// defaultHolidayProvider implements `HolidayProvider` and just returns false. -func defaultHolidayProvider(_ time.Time) bool { return false } - -var ( - // Date contains utility functions that operate on dates. - Date = &date{} -) +// Date contains utility functions that operate on dates. +var Date date type date struct{} -// IsNYSEHoliday returns if a date was/is on a nyse holiday day. -func (d date) IsNYSEHoliday(t time.Time) bool { - te := t.In(d.Eastern()) - if te.Year() == 2013 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 21 - } else if te.Month() == 2 { - return te.Day() == 18 - } else if te.Month() == 3 { - return te.Day() == 29 - } else if te.Month() == 5 { - return te.Day() == 27 - } else if te.Month() == 7 { - return te.Day() == 4 - } else if te.Month() == 9 { - return te.Day() == 2 - } else if te.Month() == 11 { - return te.Day() == 28 - } else if te.Month() == 12 { - return te.Day() == 25 - } - } else if te.Year() == 2014 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 20 - } else if te.Month() == 2 { - return te.Day() == 17 - } else if te.Month() == 4 { - return te.Day() == 18 - } else if te.Month() == 5 { - return te.Day() == 26 - } else if te.Month() == 7 { - return te.Day() == 4 - } else if te.Month() == 9 { - return te.Day() == 1 - } else if te.Month() == 11 { - return te.Day() == 27 - } else if te.Month() == 12 { - return te.Day() == 25 - } - } else if te.Year() == 2015 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 19 - } else if te.Month() == 2 { - return te.Day() == 16 - } else if te.Month() == 4 { - return te.Day() == 3 - } else if te.Month() == 5 { - return te.Day() == 25 - } else if te.Month() == 7 { - return te.Day() == 3 - } else if te.Month() == 9 { - return te.Day() == 7 - } else if te.Month() == 11 { - return te.Day() == 26 - } else if te.Month() == 12 { - return te.Day() == 25 - } - } else if te.Year() == 2016 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 18 - } else if te.Month() == 2 { - return te.Day() == 15 - } else if te.Month() == 3 { - return te.Day() == 25 - } else if te.Month() == 5 { - return te.Day() == 30 - } else if te.Month() == 7 { - return te.Day() == 4 - } else if te.Month() == 9 { - return te.Day() == 5 - } else if te.Month() == 11 { - return te.Day() == 24 || te.Day() == 25 - } else if te.Month() == 12 { - return te.Day() == 26 - } - } else if te.Year() == 2017 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 16 - } else if te.Month() == 2 { - return te.Day() == 20 - } else if te.Month() == 4 { - return te.Day() == 15 - } else if te.Month() == 5 { - return te.Day() == 29 - } else if te.Month() == 7 { - return te.Day() == 4 - } else if te.Month() == 9 { - return te.Day() == 4 - } else if te.Month() == 11 { - return te.Day() == 23 - } else if te.Month() == 12 { - return te.Day() == 25 - } - } else if te.Year() == 2018 { - if te.Month() == 1 { - return te.Day() == 1 || te.Day() == 15 - } else if te.Month() == 2 { - return te.Day() == 19 - } else if te.Month() == 3 { - return te.Day() == 30 - } else if te.Month() == 5 { - return te.Day() == 28 - } else if te.Month() == 7 { - return te.Day() == 4 - } else if te.Month() == 9 { - return te.Day() == 3 - } else if te.Month() == 11 { - return te.Day() == 22 - } else if te.Month() == 12 { - return te.Day() == 25 +func (d date) MustEastern() *time.Location { + if eastern, err := d.Eastern(); err != nil { + panic(err) + } else { + return eastern + } +} + +// Eastern returns the eastern timezone. +func (d date) Eastern() (*time.Location, error) { + // Try POSIX + est, err := time.LoadLocation("America/New_York") + if err == nil { + // Try Windows + est, err = time.LoadLocation("EST") + if err == nil { + return nil, err } } - return false + return est, nil } -// IsNYSEArcaHoliday returns that returns if a given time falls on a holiday. -func (d date) IsNYSEArcaHoliday(t time.Time) bool { - return d.IsNYSEHoliday(t) +func (d date) MustPacific() *time.Location { + if pst, err := d.Pacific(); err != nil { + panic(err) + } else { + return pst + } } -// IsNASDAQHoliday returns if a date was a NASDAQ holiday day. -func (d date) IsNASDAQHoliday(t time.Time) bool { - return d.IsNYSEHoliday(t) +// Pacific returns the pacific timezone. +func (d date) Pacific() (*time.Location, error) { + // Try POSIX + pst, err := time.LoadLocation("America/Los_Angeles") + if err == nil { + // Try Windows + pst, err = time.LoadLocation("PST") + if err == nil { + return nil, err + } + } + return pst, nil +} + +// TimeUTC returns a new time.Time for the given clock components in UTC. +// It is meant to be used with the `OnDate` function. +func (d date) TimeUTC(hour, min, sec, nsec int) time.Time { + return time.Date(0, 0, 0, hour, min, sec, nsec, time.UTC) } // Time returns a new time.Time for the given clock components. +// It is meant to be used with the `OnDate` function. func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time { return time.Date(0, 0, 0, hour, min, sec, nsec, loc) } +// DateUTC returns a new time.Time for the given date comonents at (noon) in UTC. +func (d date) DateUTC(year, month, day int) time.Time { + return time.Date(year, time.Month(month), day, 12, 0, 0, 0, time.UTC) +} + +// DateUTC returns a new time.Time for the given date comonents at (noon) in a given location. func (d date) Date(year, month, day int, loc *time.Location) time.Time { return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc) } -// On returns the clock components of clock (hour,minute,second) on the date components of d. -func (d date) On(clock, cd time.Time) time.Time { - tzAdjusted := cd.In(clock.Location()) +// OnDate returns the clock components of clock (hour,minute,second) on the date components of d. +func (d date) OnDate(clock, date time.Time) time.Time { + tzAdjusted := date.In(clock.Location()) return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location()) } -// NoonOn is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date. -func (d date) NoonOn(cd time.Time) time.Time { +// NoonOnDate is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date. +func (d date) NoonOnDate(cd time.Time) time.Time { return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location()) } -// Optional returns a pointer reference to a given time. -func (d date) Optional(t time.Time) *time.Time { - return &t -} - // IsWeekDay returns if the day is a monday->friday. func (d date) IsWeekDay(day time.Weekday) bool { return !d.IsWeekendDay(day) @@ -253,116 +148,11 @@ func (d date) Before(before, reference time.Time) bool { return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day() } -// NextMarketOpen returns the next market open after a given time. -func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time { - afterLocalized := after.In(openTime.Location()) - todaysOpen := d.On(openTime, afterLocalized) - - if isHoliday == nil { - isHoliday = defaultHolidayProvider - } - - todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen) - - if (afterLocalized.Equal(todaysOpen) || afterLocalized.Before(todaysOpen)) && todayIsValidTradingDay { - return todaysOpen - } - - for cursorDay := 1; cursorDay < 7; cursorDay++ { - newDay := todaysOpen.AddDate(0, 0, cursorDay) - isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) - if isValidTradingDay { - return d.On(openTime, newDay) - } - } - panic("Have exhausted day window looking for next market open.") -} - -// NextMarketClose returns the next market close after a given time. -func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time { - afterLocalized := after.In(closeTime.Location()) - - if isHoliday == nil { - isHoliday = defaultHolidayProvider - } - - todaysClose := d.On(closeTime, afterLocalized) - if afterLocalized.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) { - return todaysClose - } - - if afterLocalized.Equal(todaysClose) { //rare but it might happen. - return todaysClose - } - - for cursorDay := 1; cursorDay < 6; cursorDay++ { - newDay := todaysClose.AddDate(0, 0, cursorDay) - if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) { - return d.On(closeTime, newDay) - } - } - panic("Have exhausted day window looking for next market close.") -} - -// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates. -func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) { - startEastern := start.In(d.Eastern()) - endEastern := end.In(d.Eastern()) - - startMarketOpen := d.On(marketOpen, startEastern) - startMarketClose := d.On(marketClose, startEastern) - - if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) { - if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) { - if endEastern.Before(startMarketClose) { - seconds += int64(endEastern.Sub(startEastern) / time.Second) - } else { - seconds += int64(startMarketClose.Sub(startEastern) / time.Second) - } - } - } - - cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday) - for d.Before(cursor, endEastern) { - if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) { - close := d.NextMarketClose(cursor, marketClose, isHoliday) - seconds += int64(close.Sub(cursor) / time.Second) - } - cursor = cursor.AddDate(0, 0, 1) - } - - finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday) - finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday) - if endEastern.After(finalMarketOpen) { - if endEastern.Before(finalMarketClose) { - seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second) - } else { - seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second) - } - } - - return -} - const ( _secondsPerHour = 60 * 60 _secondsPerDay = 60 * 60 * 24 ) -func (d date) DiffDays(t1, t2 time.Time) (days int) { - t1n := t1.Unix() - t2n := t2.Unix() - diff := t2n - t1n //yields seconds - return int(diff / (_secondsPerDay)) -} - -func (d date) DiffHours(t1, t2 time.Time) (hours int) { - t1n := t1.Unix() - t2n := t2.Unix() - diff := t2n - t1n //yields seconds - return int(diff / (_secondsPerHour)) -} - // NextDay returns the timestamp advanced a day. func (d date) NextDay(ts time.Time) time.Time { return ts.AddDate(0, 0, 1) diff --git a/util/date_posix.go b/util/date_posix.go deleted file mode 100644 index 1a5a80c..0000000 --- a/util/date_posix.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build !windows - -package util - -import "time" - -// Eastern returns the eastern timezone. -func (d date) Eastern() *time.Location { - if _eastern == nil { - _easternLock.Lock() - defer _easternLock.Unlock() - if _eastern == nil { - _eastern, _ = time.LoadLocation("America/New_York") - } - } - return _eastern -} diff --git a/util/date_test.go b/util/date_test.go index 333dd13..80bf0e8 100644 --- a/util/date_test.go +++ b/util/date_test.go @@ -33,10 +33,10 @@ func TestDateDate(t *testing.T) { assert.Equal(time.UTC, ts.Location()) } -func TestDateOn(t *testing.T) { +func TestDateOnDate(t *testing.T) { assert := assert.New(t) - ts := Date.On(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.Eastern())) + ts := Date.OnDate(Date.Time(5, 4, 3, 2, time.UTC), Date.Date(2016, 6, 7, Date.MustEastern())) assert.Equal(2016, ts.Year()) assert.Equal(6, ts.Month()) assert.Equal(7, ts.Day()) @@ -47,9 +47,9 @@ func TestDateOn(t *testing.T) { assert.Equal(time.UTC, ts.Location()) } -func TestDateNoonOn(t *testing.T) { +func TestDateNoonOnDate(t *testing.T) { assert := assert.New(t) - noon := Date.NoonOn(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC)) + noon := Date.NoonOnDate(time.Date(2016, 04, 03, 02, 01, 0, 0, time.UTC)) assert.Equal(2016, noon.Year()) assert.Equal(4, noon.Month()) @@ -77,101 +77,16 @@ func TestDateBeforeHandlesTimezones(t *testing.T) { tuesdayUTC := time.Date(2016, 8, 02, 22, 00, 0, 0, time.UTC) mondayUTC := time.Date(2016, 8, 01, 1, 00, 0, 0, time.UTC) - sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.Eastern()) + sundayEST := time.Date(2016, 7, 31, 22, 00, 0, 0, Date.MustEastern()) assert.True(Date.Before(sundayEST, tuesdayUTC)) assert.False(Date.Before(sundayEST, mondayUTC)) } -func TestNextMarketOpen(t *testing.T) { - assert := assert.New(t) - - beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Date.Eastern()) - todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()) - - afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Date.Eastern()) - tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern()) - - afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Date.Eastern()) - mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Date.Eastern()) - - weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern()) - - assert.True(todayOpen.Equal(Date.NextMarketOpen(beforeOpen, NYSEOpen(), Date.IsNYSEHoliday))) - assert.True(tomorrowOpen.Equal(Date.NextMarketOpen(afterOpen, NYSEOpen(), Date.IsNYSEHoliday))) - assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen(), Date.IsNYSEHoliday))) - assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen(), Date.IsNYSEHoliday))) - - assert.Equal(Date.Eastern(), todayOpen.Location()) - assert.Equal(Date.Eastern(), tomorrowOpen.Location()) - assert.Equal(Date.Eastern(), mondayOpen.Location()) - - testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern()) - shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern()) - - assert.True(shouldbe.Equal(Date.NextMarketOpen(testRegression, NYSEOpen(), Date.IsNYSEHoliday))) -} - -func TestNextMarketClose(t *testing.T) { - assert := assert.New(t) - - beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Date.Eastern()) - todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Date.Eastern()) - - afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Date.Eastern()) - tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Date.Eastern()) - - afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Date.Eastern()) - mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Date.Eastern()) - - weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern()) - - assert.True(todayClose.Equal(Date.NextMarketClose(beforeClose, NYSEClose(), Date.IsNYSEHoliday))) - assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose(), Date.IsNYSEHoliday))) - assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose(), Date.IsNYSEHoliday))) - assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose(), Date.IsNYSEHoliday))) - - assert.Equal(Date.Eastern(), todayClose.Location()) - assert.Equal(Date.Eastern(), tomorrowClose.Location()) - assert.Equal(Date.Eastern(), mondayClose.Location()) -} - -func TestCalculateMarketSecondsBetween(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()) - end := time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()) - - shouldbe := 5 * 6.5 * 60 * 60 - - assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday)) -} - -func TestCalculateMarketSecondsBetween1D(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2016, 07, 22, 9, 45, 0, 0, Date.Eastern()) - end := time.Date(2016, 07, 22, 15, 45, 0, 0, Date.Eastern()) - - shouldbe := 6 * 60 * 60 - - assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday)) -} - -func TestCalculateMarketSecondsBetweenLTM(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern()) - end := time.Date(2016, 07, 01, 9, 30, 0, 0, Date.Eastern()) - - shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year. - assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday)) -} - func TestDateNextHour(t *testing.T) { assert := assert.New(t) - start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern()) + start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.MustEastern()) next := Date.NextHour(start) assert.Equal(2015, next.Year()) assert.Equal(07, next.Month()) @@ -221,40 +136,3 @@ func TestDateNextDayOfWeek(t *testing.T) { assert.Equal(time.UTC, nextSunday.Location()) assert.Equal(time.UTC, nextMonday.Location()) } - -func TestDateIsNYSEHoliday(t *testing.T) { - assert := assert.New(t) - - cursor := time.Date(2013, 01, 01, 0, 0, 0, 0, time.UTC) - end := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - var holidays int - for Date.Before(cursor, end) { - if Date.IsNYSEHoliday(cursor) { - holidays++ - } - cursor = cursor.AddDate(0, 0, 1) - } - assert.Equal(holidays, 55) -} - -func TestDateDiffDays(t *testing.T) { - assert := assert.New(t) - - t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC) - t2 := time.Date(2017, 01, 10, 3, 0, 0, 0, time.UTC) - t3 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC) - - assert.Equal(48, Date.DiffDays(t2, t1)) - assert.Equal(2, Date.DiffDays(t3, t1)) // technically we should round down. -} - -func TestDateDiffHours(t *testing.T) { - assert := assert.New(t) - - t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC) - t2 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC) - t3 := time.Date(2017, 02, 28, 12, 0, 0, 0, time.UTC) - - assert.Equal(68, Date.DiffHours(t2, t1)) - assert.Equal(24, Date.DiffHours(t1, t3)) -} diff --git a/util/date_windows.go b/util/date_windows.go deleted file mode 100644 index c42a367..0000000 --- a/util/date_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build windows - -package util - -import "time" - -// Eastern returns the eastern timezone. -func (d date) Eastern() *time.Location { - if _eastern == nil { - _easternLock.Lock() - defer _easternLock.Unlock() - if _eastern == nil { - _eastern, _ = time.LoadLocation("EST") - } - } - return _eastern -} diff --git a/util/time.go b/util/time.go index 88e0c8b..cb07856 100644 --- a/util/time.go +++ b/util/time.go @@ -18,3 +18,77 @@ func (tu timeUtil) ToFloat64(t time.Time) float64 { func (tu timeUtil) FromFloat64(tf float64) time.Time { return time.Unix(0, int64(tf)) } + +func (tu timeUtil) DiffDays(t1, t2 time.Time) (days int) { + t1n := t1.Unix() + t2n := t2.Unix() + var diff int64 + if t1n > t2n { + diff = t2n - t1n //yields seconds + } else { + diff = t1n - t2n //yields seconds + } + return int(diff / (_secondsPerDay)) +} + +func (tu timeUtil) DiffHours(t1, t2 time.Time) (hours int) { + t1n := t1.Unix() + t2n := t2.Unix() + var diff int64 + if t1n > t2n { + diff = t1n - t2n + } else { + diff = t2n - t1n + } + return int(diff / (_secondsPerHour)) +} + +// Start returns the earliest (min) time in a list of times. +func (tu timeUtil) 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 (tu timeUtil) 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 +} + +// StartAndEnd returns the start and end of a given set of time in one pass. +func (tu timeUtil) StartAndEnd(values ...time.Time) (start time.Time, end time.Time) { + if len(values) == 0 { + return + } + + start = values[0] + end = values[0] + + for _, v := range values[1:] { + if end.Before(v) { + end = v + } + if start.After(v) { + start = v + } + } + return +} diff --git a/util/time_test.go b/util/time_test.go new file mode 100644 index 0000000..c4f9668 --- /dev/null +++ b/util/time_test.go @@ -0,0 +1,30 @@ +package util + +import ( + "testing" + "time" + + "github.com/blend/go-sdk/assert" +) + +func TestTimeDiffDays(t *testing.T) { + assert := assert.New(t) + + t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC) + t2 := time.Date(2017, 01, 10, 3, 0, 0, 0, time.UTC) + t3 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC) + + assert.Equal(48, Time.DiffDays(t2, t1)) + assert.Equal(2, Time.DiffDays(t3, t1)) // technically we should round down. +} + +func TestTimeDiffHours(t *testing.T) { + assert := assert.New(t) + + t1 := time.Date(2017, 02, 27, 12, 0, 0, 0, time.UTC) + t2 := time.Date(2017, 02, 24, 16, 0, 0, 0, time.UTC) + t3 := time.Date(2017, 02, 28, 12, 0, 0, 0, time.UTC) + + assert.Equal(68, Time.DiffHours(t2, t1)) + assert.Equal(24, Time.DiffHours(t1, t3)) +}