1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-27 13:48:49 +08:00

More validation for the scales.

This commit is contained in:
Jakub Sobon 2019-01-08 23:56:05 -05:00
parent bdd0290c98
commit a3bed458d6
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
5 changed files with 111 additions and 9 deletions

View File

@ -85,7 +85,10 @@ func (y *Y) Details(cvsHeight int, maxWidth int) (*YDetails, error) {
if req := y.RequiredWidth(); maxWidth < req {
return nil, fmt.Errorf("the received maxWidth %d is smaller than the reported required width %d", maxWidth, req)
}
scale := NewYScale(y.min.Value, y.max.Value, cvsHeight, nonZeroDecimals)
scale, err := NewYScale(y.min.Value, y.max.Value, cvsHeight, nonZeroDecimals)
if err != nil {
return nil, err
}
// See how the labels would look like on the entire maxWidth.
maxLabelWidth := maxWidth - yAxisWidth

View File

@ -65,7 +65,7 @@ func TestY(t *testing.T) {
maxWidth: 2,
want: &YDetails{
Width: 2,
Scale: NewYScale(0, 3, 2, nonZeroDecimals),
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
Labels: []*Label{
{NewValue(0, nonZeroDecimals), image.Point{0, 1}},
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
@ -81,7 +81,7 @@ func TestY(t *testing.T) {
maxWidth: 5,
want: &YDetails{
Width: 5,
Scale: NewYScale(0, 3, 2, nonZeroDecimals),
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
Labels: []*Label{
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
@ -97,7 +97,7 @@ func TestY(t *testing.T) {
maxWidth: 6,
want: &YDetails{
Width: 5,
Scale: NewYScale(0, 3, 2, nonZeroDecimals),
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
Labels: []*Label{
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},

View File

@ -98,7 +98,10 @@ func TestYLabels(t *testing.T) {
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
scale := NewYScale(tc.min, tc.max, tc.cvsHeight, nonZeroDecimals)
scale, err := NewYScale(tc.min, tc.max, tc.cvsHeight, nonZeroDecimals)
if err != nil {
t.Fatalf("NewYScale => unexpected error: %v", err)
}
t.Logf("scale step: %v", scale.Step.Rounded)
got, err := yLabels(scale, tc.labelWidth)
if (err != nil) != tc.wantErr {

View File

@ -41,7 +41,16 @@ type YScale struct {
// NewYScale calculates the scale of the Y axis, given the boundary values and
// the height of the canvas. The nonZeroDecimals dictates rounding of the
// calculated scale, see NewValue for details.
func NewYScale(min, max float64, cvsHeight, nonZeroDecimals int) *YScale {
// Max must be greater or equal to min. The cvsHeight must be a positive
// number.
func NewYScale(min, max float64, cvsHeight, nonZeroDecimals int) (*YScale, error) {
if max < min {
return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min)
}
if min := 1; cvsHeight < min {
return nil, fmt.Errorf("cvsHeight cannot be less than %d, got %d", min, cvsHeight)
}
brailleHeight := cvsHeight * braille.RowMult
usablePixels := brailleHeight - 1 // One pixel reserved for value zero.
@ -53,7 +62,7 @@ func NewYScale(min, max float64, cvsHeight, nonZeroDecimals int) *YScale {
Step: step,
CvsHeight: cvsHeight,
brailleHeight: brailleHeight,
}
}, nil
}
// PixelToValue given a Y coordinate of the pixel, returns its value according
@ -88,6 +97,9 @@ func (ys *YScale) ValueToPixel(v float64) (int, error) {
if min, max := ys.Min.Value, ys.Max.Rounded; v < min || v > max {
return 0, fmt.Errorf("invalid value %v, must be in range %v <= v <= %v", v, min, max)
}
if ys.Step.Rounded == 0 {
return 0, nil
}
if ys.Min.Value < 0 {
diff := -1 * ys.Min.Rounded
@ -137,6 +149,8 @@ type XScale struct {
// points in the series and the width on the canvas that is available to the X
// axis. The nonZeroDecimals dictates rounding of the calculated scale, see
// NewValue for details.
// The numPoints must be zero or positive number. The axisWidth must be a
// positive number.
func NewXScale(numPoints int, axisWidth, nonZeroDecimals int) (*XScale, error) {
if numPoints < 0 {
return nil, fmt.Errorf("numPoints cannot be negative, got %d", numPoints)
@ -150,6 +164,9 @@ func NewXScale(numPoints int, axisWidth, nonZeroDecimals int) (*XScale, error) {
const min float64 = 0
max := float64(numPoints - 1)
if max < 0 {
max = 0
}
diff := max - min
step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
return &XScale{
@ -188,6 +205,9 @@ func (xs *XScale) ValueToPixel(v int) (int, error) {
if min, max := xs.Min.Value, xs.Max.Rounded; fv < min || fv > max {
return 0, fmt.Errorf("invalid value %v, must be in range %v <= v <= %v", v, min, max)
}
if xs.Step.Rounded == 0 {
return 0, nil
}
return int(numbers.Round(fv / xs.Step.Rounded)), nil
}

View File

@ -21,6 +21,24 @@ import (
"github.com/kylelemons/godebug/pretty"
)
// mustNewYScale returns a new YScale or panics.
func mustNewYScale(min, max float64, cvsHeight, nonZeroDecimals int) *YScale {
s, err := NewYScale(min, max, cvsHeight, nonZeroDecimals)
if err != nil {
panic(err)
}
return s
}
// mustNewXScale returns a new XScale or panics.
func mustNewXScale(numPoints int, axisWidth, nonZeroDecimals int) *XScale {
s, err := NewXScale(numPoints, axisWidth, nonZeroDecimals)
if err != nil {
panic(err)
}
return s
}
func TestYScale(t *testing.T) {
tests := []struct {
desc string
@ -29,7 +47,24 @@ func TestYScale(t *testing.T) {
cvsHeight int
nonZeroDecimals int
want *YScale
wantErr bool
}{
{
desc: "fails when max is less than min",
min: 0,
max: -1,
cvsHeight: 4,
nonZeroDecimals: 2,
wantErr: true,
},
{
desc: "fails when canvas height too small",
min: 0,
max: 1,
cvsHeight: 0,
nonZeroDecimals: 2,
wantErr: true,
},
{
desc: "min and max are zero",
min: 0,
@ -90,7 +125,13 @@ func TestYScale(t *testing.T) {
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := NewYScale(tc.min, tc.max, tc.cvsHeight, tc.nonZeroDecimals)
got, err := NewYScale(tc.min, tc.max, tc.cvsHeight, tc.nonZeroDecimals)
if (err != nil) != tc.wantErr {
t.Errorf("NewYScale => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("NewYScale => unexpected diff (-want, +got):\n%s", diff)
}
@ -176,6 +217,22 @@ func TestYScaleMethods(t *testing.T) {
{4, nil, true},
},
},
{
desc: "works without data points",
min: 0,
max: 0,
cvsHeight: 1,
nonZeroDecimals: 2,
pixelToValueTests: []pixelToValueTest{
{0, 0, false},
},
valueToPixelTests: []valueToPixelTest{
{0, 0, false},
},
cellLabelTests: []cellLabelTest{
{0, NewValue(0, 2), false},
},
},
{
desc: "integer scale",
min: 0,
@ -302,7 +359,11 @@ func TestYScaleMethods(t *testing.T) {
}
for _, test := range tests {
scale := NewYScale(test.min, test.max, test.cvsHeight, test.nonZeroDecimals)
scale, err := NewYScale(test.min, test.max, test.cvsHeight, test.nonZeroDecimals)
if err != nil {
t.Fatalf("NewYScale => unexpected error: %v", err)
}
t.Run(fmt.Sprintf("PixelToValue:%s", test.desc), func(t *testing.T) {
for _, tc := range test.pixelToValueTests {
got, err := scale.PixelToValue(tc.pixel)
@ -415,6 +476,21 @@ func TestXScaleMethods(t *testing.T) {
{2, nil, true},
},
},
{
desc: "works without data points",
numPoints: 0,
axisWidth: 1,
nonZeroDecimals: 2,
pixelToValueTests: []pixelToValueTest{
{0, 0, false},
},
valueToPixelTests: []valueToPixelTest{
{0, 0, false},
},
cellLabelTests: []cellLabelTest{
{0, NewValue(0, 2), false},
},
},
{
desc: "integer scale, all points fit",
numPoints: 6,