initial commit.

This commit is contained in:
Will Charczuk 2016-07-06 18:54:00 -07:00
commit 2dd44d3675
14 changed files with 930 additions and 0 deletions

210
chart.go Normal file
View File

@ -0,0 +1,210 @@
package chart
import (
"image/color"
"io"
"github.com/golang/freetype/truetype"
)
// Chart is what we're drawing.
/*
The chart box model is as follows:
0,0 width,0
cl,ct cr,ct
cl,cb cr,cb
0, height width,height
*/
type Chart struct {
Title string
TitleFontSize float64
Width int
Height int
Padding int
BackgroundColor color.RGBA
CanvasBackgroundColor color.RGBA
AxisShow bool
AxisStyle Style
AxisFontSize float64
CanvasBorderShow bool
CanvasBorderStyle Style
FinalValueLabelShow bool
FinalValueStyle Style
FontColor color.RGBA
Font *truetype.Font
Series []Series
}
// GetTitleFontSize calculates or returns the title font size.
func (c Chart) GetTitleFontSize() float64 {
if c.TitleFontSize != 0 {
if c.TitleFontSize > DefaultMinimumFontSize {
return c.TitleFontSize
}
}
fontSize := float64(c.Height >> 3)
if fontSize > DefaultMinimumFontSize {
return fontSize
}
return DefaultMinimumFontSize
}
// GetCanvasTop gets the top corner pixel.
func (c Chart) GetCanvasTop() int {
return c.Padding
}
// GetCanvasLeft gets the left corner pixel.
func (c Chart) GetCanvasLeft() int {
return c.Padding
}
// GetCanvasBottom gets the bottom corner pixel.
func (c Chart) GetCanvasBottom() int {
return c.Height - c.Padding
}
// GetCanvasRight gets the right corner pixel.
func (c Chart) GetCanvasRight() int {
return c.Width - c.Padding
}
// GetCanvasWidth returns the width of the canvas.
func (c Chart) GetCanvasWidth() int {
if c.Padding > 0 {
return c.Width - (c.Padding << 1)
}
return c.Width
}
// GetCanvasHeight returns the height of the canvas.
func (c Chart) GetCanvasHeight() int {
if c.Padding > 0 {
return c.Height - (c.Padding << 1)
}
return c.Height
}
// GetBackgroundColor returns the chart background color.
func (c Chart) GetBackgroundColor() color.RGBA {
if ColorIsZero(c.BackgroundColor) {
c.BackgroundColor = DefaultBackgroundColor
}
return c.BackgroundColor
}
// GetCanvasBackgroundColor returns the canvas background color.
func (c Chart) GetCanvasBackgroundColor() color.RGBA {
if ColorIsZero(c.CanvasBackgroundColor) {
c.CanvasBackgroundColor = DefaultCanvasColor
}
return c.CanvasBackgroundColor
}
// GetTextFont returns the text font.
func (c Chart) GetTextFont() (*truetype.Font, error) {
if c.Font != nil {
return c.Font, nil
}
return GetDefaultFont()
}
// GetTextFontColor returns the text font color.
func (c Chart) GetTextFontColor() color.RGBA {
if ColorIsZero(c.FontColor) {
c.FontColor = DefaultTextColor
}
return c.FontColor
}
// Render renders the chart with the given renderer to the given io.Writer.
func (c Chart) Render(provider RendererProvider, w io.Writer) error {
r := provider(c.Width, c.Height)
c.drawBackground(r)
c.drawCanvas(r)
c.drawAxes(r)
for _, series := range c.Series {
c.drawSeries(r, series)
}
err := c.drawTitle(r)
if err != nil {
return err
}
return r.Save(w)
}
func (c Chart) drawBackground(r Renderer) {
r.SetStrokeColor(c.GetBackgroundColor())
r.SetFillColor(c.GetBackgroundColor())
r.SetLineWidth(0)
r.MoveTo(0, 0)
r.LineTo(c.Width, 0)
r.LineTo(c.Width, c.Height)
r.LineTo(0, c.Height)
r.LineTo(0, 0)
r.FillStroke()
r.Close()
}
func (c Chart) drawCanvas(r Renderer) {
if !c.CanvasBorderStyle.IsZero() {
r.SetStrokeColor(c.CanvasBorderStyle.GetStrokeColor())
r.SetLineWidth(c.CanvasBorderStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(c.GetCanvasBackgroundColor())
r.SetLineWidth(0)
}
r.SetFillColor(c.GetCanvasBackgroundColor())
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.FillStroke()
r.Close()
}
func (c Chart) drawAxes(r Renderer) {
if c.AxisShow {
if !c.AxisStyle.IsZero() {
r.SetStrokeColor(c.AxisStyle.GetStrokeColor())
r.SetLineWidth(c.AxisStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(DefaultAxisColor)
r.SetLineWidth(DefaultLineWidth)
}
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
r.Stroke()
}
}
func (c Chart) drawSeries(r Renderer, s Series) {
}
func (c Chart) drawTitle(r Renderer) error {
if len(c.Title) > 0 {
font, err := c.GetTextFont()
if err != nil {
return err
}
r.SetFontColor(c.GetTextFontColor())
r.SetFont(font)
r.SetFontSize(c.GetTitleFontSize())
textWidth := r.MeasureText(c.Title)
titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.GetCanvasTop() + int(c.GetTitleFontSize()/2.0)
r.Text(c.Title, titleX, titleY)
}
return nil
}

30
chart_test.go Normal file
View File

@ -0,0 +1,30 @@
package chart
import (
"bytes"
"testing"
"time"
"github.com/blendlabs/go-assert"
)
func TestChartSingleSeries(t *testing.T) {
assert := assert.New(t)
now := time.Now()
c := Chart{
Title: "Hello!",
Width: 1024,
Height: 400,
Series: []Series{
TimeSeries{
Name: "Goog",
XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)},
YValues: []float64{1.0, 2.0, 3.0},
},
},
}
buffer := bytes.NewBuffer([]byte{})
c.Render(PNG, buffer)
assert.NotEmpty(buffer.Bytes())
}

69
defaults.go Normal file
View File

@ -0,0 +1,69 @@
package chart
import (
"image/color"
"sync"
"github.com/golang/freetype/truetype"
)
const (
// DefaultChartHeight is the default chart height.
DefaultChartHeight = 400
// DefaultChartWidth is the default chart width.
DefaultChartWidth = 200
// DefaultPadding is the default gap between the image border and
// chart content (referred to as the "canvas").
DefaultPadding = 10
// DefaultLineWidth is the default chart line width.
DefaultLineWidth = 2.0
//DefaultDPI is the default dots per inch for the chart.
DefaultDPI = 120.0
// DefaultMinimumFontSize is the default minimum font size.
DefaultMinimumFontSize = 8.0
)
var (
// DefaultBackgroundColor is the default chart background color.
// It is equivalent to css color:white.
DefaultBackgroundColor = color.RGBA{R: 255, G: 255, B: 255, A: 255}
// DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white.
DefaultCanvasColor = color.RGBA{R: 255, G: 255, B: 255, A: 255}
// DefaultTextColor is the default chart text color.
// It is equivalent to #333333.
DefaultTextColor = color.RGBA{R: 51, G: 51, B: 51, A: 255}
// DefaultAxisColor is the default chart axis line color.
// It is equivalent to #333333.
DefaultAxisColor = color.RGBA{R: 51, G: 51, B: 51, A: 255}
// DefaultBorderColor is the default chart border color.
// It is equivalent to #efefef.
DefaultBorderColor = color.RGBA{R: 239, G: 239, B: 239, A: 255}
// DefaultLineColor is the default (1st) series line color.
// It is equivalent to #0074d9.
DefaultLineColor = color.RGBA{R: 0, G: 217, B: 116, A: 255}
// DefaultFillColor is the default fill color.
// It is equivalent to #0074d9.
DefaultFillColor = color.RGBA{R: 0, G: 217, B: 116, A: 255}
)
var (
_defaultFontLock sync.Mutex
_defaultFont *truetype.Font
)
// GetDefaultFont returns the default font (Roboto-Medium).
func GetDefaultFont() (*truetype.Font, error) {
if _defaultFont == nil {
_defaultFontLock.Lock()
defer _defaultFontLock.Unlock()
if _defaultFont == nil {
font, err := truetype.Parse(roboto)
if err != nil {
return nil, err
}
_defaultFont = font
}
}
return _defaultFont, nil
}

41
point.go Normal file
View File

@ -0,0 +1,41 @@
package chart
// Points are an array of points.
import (
"fmt"
"strings"
)
// Point represents a x,y coordinate.
type Point struct {
X float64
Y float64
}
// Points represents a group of points.
type Points []Point
// String returns a string representation of the points.
func (p Points) String() string {
var values []string
for _, v := range p {
values = append(values, fmt.Sprintf("%d,%d", int(v.X), int(v.Y)))
}
return strings.Join(values, "\n")
}
// Len returns the length of the points set.
func (p Points) Len() int {
return len(p)
}
// Swap swaps two elments.
func (p Points) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
// Less returns if the X value of one element is less than another.
// This is the default sort for charts where you plot by x values in order.
func (p Points) Less(i, j int) bool {
return p[i].X < p[j].X
}

97
range.go Normal file
View File

@ -0,0 +1,97 @@
package chart
import (
"math"
"time"
)
// Range is a type that translates values from a range to a domain.
type Range interface {
GetMin() interface{}
GetMax() interface{}
Translate(value interface{}) int
}
// NewRangeOfFloat64 returns a new Range
func NewRangeOfFloat64(domain int, values ...float64) Range {
min, max := MinAndMax(values...)
return &RangeOfFloat64{
MinValue: min,
MaxValue: max,
MinMaxDelta: max - min,
Domain: domain,
}
}
// RangeOfFloat64 represents a continuous range
// of float64 values mapped to a [0...WindowMaxValue]
// interval.
type RangeOfFloat64 struct {
MinValue float64
MaxValue float64
MinMaxDelta float64
Domain int
}
// GetMin implements the interface method.
func (r RangeOfFloat64) GetMin() interface{} {
return r.MinValue
}
// GetMax implements the interface method.
func (r RangeOfFloat64) GetMax() interface{} {
return r.MaxValue
}
// Translate maps a given value into the range space.
// An example would be a 600 px image, with a min of 10 and a max of 100.
// Translate(50) would yield (50.0/90.0)*600 ~= 333.33
func (r RangeOfFloat64) Translate(value interface{}) int {
if typedValue, isTyped := value.(float64); isTyped {
finalValue := ((r.MaxValue - typedValue) / r.MinMaxDelta) * float64(r.Domain)
return int(math.Floor(finalValue))
}
return 0
}
// NewRangeOfTime makes a new range of time with the given time values.
func NewRangeOfTime(domain int, values ...time.Time) Range {
min, max := MinAndMaxOfTime(values...)
r := &RangeOfTime{
MinValue: min,
MaxValue: max,
MinMaxDelta: max.Unix() - min.Unix(),
Domain: domain,
}
return r
}
// RangeOfTime represents a timeseries.
type RangeOfTime struct {
MinValue time.Time
MaxValue time.Time
MinMaxDelta int64 //unix time difference
Domain int
}
// GetMin implements the interface method.
func (r RangeOfTime) GetMin() interface{} {
return r.MinValue
}
// GetMax implements the interface method.
func (r RangeOfTime) GetMax() interface{} {
return r.MaxValue
}
// Translate maps a given value into the range space (of time).
// An example would be a 600 px image, with a min of jan-01-2016 and a max of jun-01-2016.
// Translate(may-01-2016) would yield ... something.
func (r RangeOfTime) Translate(value interface{}) int {
if typed, isTyped := value.(time.Time); isTyped {
valueDelta := r.MaxValue.Unix() - typed.Unix()
finalValue := (float64(valueDelta) / float64(r.MinMaxDelta)) * float64(r.Domain)
return int(math.Floor(finalValue))
}
return 0
}

39
range_test.go Normal file
View File

@ -0,0 +1,39 @@
package chart
import (
"testing"
"time"
"github.com/blendlabs/go-assert"
)
func TestRangeTranslate(t *testing.T) {
assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
r := NewRange(1000, values...)
assert.Equal(1.0, r.MinValue)
assert.Equal(8.0, r.MaxValue)
assert.Equal(7.0, r.MinMaxDelta)
assert.Equal(1000, r.Domain)
assert.Equal(428, r.Translate(5.0))
}
func TestRangeOfTimeTranslate(t *testing.T) {
assert := assert.New(t)
values := []time.Time{
time.Now().AddDate(0, 0, -1),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -5),
time.Now().AddDate(0, 0, -6),
time.Now().AddDate(0, 0, -7),
time.Now().AddDate(0, 0, -8),
}
r := NewRangeOfTime(1000, values...)
assert.Equal(values[7], r.MinValue)
assert.Equal(values[0], r.MaxValue)
assert.Equal(values[0].Unix()-values[7].Unix(), r.MinMaxDelta)
assert.Equal(1000, r.Domain)
assert.Equal(571, r.Translate(time.Now().AddDate(0, 0, -5)))
}

131
raster_renderer.go Normal file
View File

@ -0,0 +1,131 @@
package chart
import (
"image"
"image/color"
"image/png"
"io"
"golang.org/x/image/font"
"github.com/golang/freetype/truetype"
drawing "github.com/llgcode/draw2d/draw2dimg"
)
// PNG returns a new png/raster renderer.
func PNG(width, height int) Renderer {
i := image.NewRGBA(image.Rect(0, 0, width, height))
return &rasterRenderer{
i: i,
gc: drawing.NewGraphicContext(i),
}
}
// RasterRenderer renders chart commands to a bitmap.
type rasterRenderer struct {
i *image.RGBA
gc *drawing.GraphicContext
fc *font.Drawer
font *truetype.Font
fontColor color.RGBA
fontSize float64
}
// SetStrokeColor implements the interface method.
func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) {
rr.gc.SetStrokeColor(c)
}
// SetFillColor implements the interface method.
func (rr *rasterRenderer) SetFillColor(c color.RGBA) {
rr.gc.SetFillColor(c)
}
// SetLineWidth implements the interface method.
func (rr *rasterRenderer) SetLineWidth(width int) {
rr.gc.SetLineWidth(float64(width))
}
// MoveTo implements the interface method.
func (rr *rasterRenderer) MoveTo(x, y int) {
rr.gc.MoveTo(float64(x), float64(y))
}
// LineTo implements the interface method.
func (rr *rasterRenderer) LineTo(x, y int) {
rr.gc.LineTo(float64(x), float64(y))
}
// Close implements the interface method.
func (rr *rasterRenderer) Close() {
rr.gc.Close()
}
// Stroke implements the interface method.
func (rr *rasterRenderer) Stroke() {
rr.gc.Stroke()
}
// FillStroke implements the interface method.
func (rr *rasterRenderer) FillStroke() {
rr.gc.FillStroke()
}
// Circle implements the interface method.
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
xf := float64(x)
yf := float64(y)
rr.gc.MoveTo(xf-radius, yf) //9
rr.gc.QuadCurveTo(xf, yf, xf, yf-radius) //12
rr.gc.QuadCurveTo(xf, yf, xf+radius, yf) //3
rr.gc.QuadCurveTo(xf, yf, xf, yf+radius) //6
rr.gc.QuadCurveTo(xf, yf, xf-radius, yf) //9
rr.gc.Close()
rr.gc.FillStroke()
}
// SetFont implements the interface method.
func (rr *rasterRenderer) SetFont(f *truetype.Font) {
rr.font = f
rr.gc.SetFont(f)
}
// SetFontSize implements the interface method.
func (rr *rasterRenderer) SetFontSize(size float64) {
rr.fontSize = size
rr.gc.SetFontSize(size)
}
// SetFontColor implements the interface method.
func (rr *rasterRenderer) SetFontColor(c color.RGBA) {
rr.fontColor = c
rr.gc.SetStrokeColor(c)
}
// Text implements the interface method.
func (rr *rasterRenderer) Text(body string, x, y int) {
rr.gc.CreateStringPath(body, float64(x), float64(y))
}
// MeasureText implements the interface method.
func (rr *rasterRenderer) MeasureText(body string) int {
if rr.fc == nil && rr.font != nil {
rr.fc = &font.Drawer{
Face: truetype.NewFace(rr.font, &truetype.Options{
DPI: DefaultDPI,
Size: rr.fontSize,
}),
}
}
if rr.fc != nil {
dimensions := rr.fc.MeasureString(body)
return dimensions.Floor()
}
return 0
}
// Save implements the interface method.
func (rr *rasterRenderer) Save(w io.Writer) error {
return png.Encode(w, rr.i)
}

60
renderer.go Normal file
View File

@ -0,0 +1,60 @@
package chart
import (
"image/color"
"io"
"github.com/golang/freetype/truetype"
)
// RendererProvider is a function that returns a renderer.
type RendererProvider func(int, int) Renderer
// Renderer represents the basic methods required to draw a chart.
type Renderer interface {
// SetStrokeColor sets the current stroke color.
SetStrokeColor(color.RGBA)
// SetFillColor sets the current fill color.
SetFillColor(color.RGBA)
// SetLineWidth sets the stroke line width.
SetLineWidth(width int)
// MoveTo moves the cursor to a given point.
MoveTo(x, y int)
// LineTo both starts a shape and draws a line to a given point
// from the previous point.
LineTo(x, y int)
// Close finalizes a shape as drawn by LineTo.
Close()
// Stroke draws the 'stroke' or line component of a shape.
Stroke()
// FillStroke draws the 'stroke' and 'fills' a shape.
FillStroke()
// Circle draws a circle at the given coords with a given radius.
Circle(radius float64, x, y int)
// SetFont sets a font for a text field.
SetFont(*truetype.Font)
// SetFontColor sets a font's color
SetFontColor(color.RGBA)
// SetFontSize sets the font size for a text field.
SetFontSize(size float64)
// Text draws a text blob.
Text(body string, x, y int)
// MeasureText measures text.
MeasureText(body string) int
// Save writes the image to the given writer.
Save(w io.Writer) error
}

5
roboto.go Normal file

File diff suppressed because one or more lines are too long

92
series.go Normal file
View File

@ -0,0 +1,92 @@
package chart
import "time"
// Series is a entity data set.
type Series interface {
GetName() string
GetStyle() Style
Len() int
GetValue(index int) Point
GetXRange(domain int) Range
GetYRange(domain int) Range
}
// TimeSeries is a line on a chart.
type TimeSeries struct {
Name string
Style Style
XValues []time.Time
YValues []float64
}
// GetName returns the name of the time series.
func (ts TimeSeries) GetName() string {
return ts.Name
}
// GetStyle returns the line style.
func (ts TimeSeries) GetStyle() Style {
return ts.Style
}
// Len returns the number of elements in the series.
func (ts TimeSeries) Len() int {
return len(ts.XValues)
}
// GetXRange returns the x range.
func (ts TimeSeries) GetXRange(domain int) Range {
return NewRangeOfTime(domain, ts.XValues...)
}
// GetYRange returns the x range.
func (ts TimeSeries) GetYRange(domain int) Range {
return NewRangeOfFloat64(domain, ts.YValues...)
}
// GetValue gets a value at a given index.
func (ts TimeSeries) GetValue(index int) Point {
return Point{X: float64(ts.XValues[index].Unix()), Y: ts.YValues[index]}
}
// ContinousSeries represents a line on a chart.
type ContinousSeries struct {
Name string
Style Style
XValues []float64
YValues []float64
}
// GetName returns the name of the time series.
func (cs ContinousSeries) GetName() string {
return cs.Name
}
// GetStyle returns the line style.
func (cs ContinousSeries) GetStyle() Style {
return cs.Style
}
// Len returns the number of elements in the series.
func (cs ContinousSeries) Len() int {
return len(cs.XValues)
}
// GetValue gets a value at a given index.
func (cs ContinousSeries) GetValue(index int) Point {
return Point{X: cs.XValues[index], Y: cs.YValues[index]}
}
// GetXRange returns the x range.
func (cs ContinousSeries) GetXRange(domain int) Range {
return NewRangeOfFloat64(domain, cs.XValues...)
}
// GetYRange returns the x range.
func (cs ContinousSeries) GetYRange(domain int) Range {
return NewRangeOfFloat64(domain, cs.YValues...)
}

1
series_test.go Normal file
View File

@ -0,0 +1 @@
package chart

39
style.go Normal file
View File

@ -0,0 +1,39 @@
package chart
import "image/color"
// Style is a simple style set.
type Style struct {
StrokeColor color.RGBA
FillColor color.RGBA
StrokeWidth int
}
// IsZero returns if the object is set or not.
func (s Style) IsZero() bool {
return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0
}
// GetStrokeColor returns the stroke color.
func (s Style) GetStrokeColor() color.RGBA {
if ColorIsZero(s.StrokeColor) {
return DefaultLineColor
}
return s.StrokeColor
}
// GetFillColor returns the fill color.
func (s Style) GetFillColor() color.RGBA {
if ColorIsZero(s.FillColor) {
return DefaultFillColor
}
return s.FillColor
}
// GetStrokeWidth returns the stroke width.
func (s Style) GetStrokeWidth() int {
if s.StrokeWidth == 0 {
return DefaultLineWidth
}
return s.StrokeWidth
}

50
util.go Normal file
View File

@ -0,0 +1,50 @@
package chart
import (
"image/color"
"time"
)
// ColorIsZero returns if a color.RGBA is unset or not.
func ColorIsZero(c color.RGBA) bool {
return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
}
// MinAndMax returns both the min and max in one pass.
func MinAndMax(values ...float64) (min float64, max float64) {
if len(values) == 0 {
return
}
min = values[0]
max = values[0]
for _, v := range values {
if max < v {
max = v
}
if min > v {
min = v
}
}
return
}
// MinAndMaxOfTime returns the min and max of a given set of times
// in one pass.
func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
if len(values) == 0 {
return
}
min = values[0]
max = values[0]
for _, v := range values {
if max.Before(v) {
max = v
}
if min.After(v) {
min = v
}
}
return
}

66
util_test.go Normal file
View File

@ -0,0 +1,66 @@
package chart
import (
"testing"
"time"
"github.com/blendlabs/go-assert"
)
func TestMinAndMax(t *testing.T) {
assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0}
min, max := MinAndMax(values...)
assert.Equal(1.0, min)
assert.Equal(4.0, max)
}
func TestMinAndMaxReversed(t *testing.T) {
assert := assert.New(t)
values := []float64{4.0, 2.0, 3.0, 1.0}
min, max := MinAndMax(values...)
assert.Equal(1.0, min)
assert.Equal(4.0, max)
}
func TestMinAndMaxEmpty(t *testing.T) {
assert := assert.New(t)
values := []float64{}
min, max := MinAndMax(values...)
assert.Equal(0.0, min)
assert.Equal(0.0, max)
}
func TestMinAndMaxOfTime(t *testing.T) {
assert := assert.New(t)
values := []time.Time{
time.Now().AddDate(0, 0, -1),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -4),
}
min, max := MinAndMaxOfTime(values...)
assert.Equal(values[3], min)
assert.Equal(values[0], max)
}
func TestMinAndMaxOfTimeReversed(t *testing.T) {
assert := assert.New(t)
values := []time.Time{
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -1),
}
min, max := MinAndMaxOfTime(values...)
assert.Equal(values[0], min)
assert.Equal(values[3], max)
}
func TestMinAndMaxOfTimeEmpty(t *testing.T) {
assert := assert.New(t)
values := []time.Time{}
min, max := MinAndMaxOfTime(values...)
assert.Equal(time.Time{}, min)
assert.Equal(time.Time{}, max)
}