Skip to content

Commit

Permalink
satellite/emission: refactor dimension handling logic
Browse files Browse the repository at this point in the history
Make dimension handling more efficient.
It avoids the string manipulation and removes the need for simplify.

Resolves post-merge comment of #12318

Issue:
#6694

Change-Id: Ic5d802afbec4f9e92e35dd660b30c517e3af32b8
  • Loading branch information
VitaliiShpital authored and Storj Robot committed Feb 16, 2024
1 parent 11b4095 commit 7d3c13c
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 241 deletions.
253 changes: 106 additions & 147 deletions satellite/emission/dimen.go
Expand Up @@ -4,179 +4,138 @@
package emission

import (
"bytes"
"fmt"
"sort"
"strings"

"github.com/zeebo/errs"
"golang.org/x/exp/slices"
)

// Q is a Val constructor function without any dimension.
var Q = ValMaker("")
// Unit represents a set of unit dimensions.
// Unit{byte: 0, watt: 0, hour: 1, kilogram: 0} means hour (H).
// Unit{byte: 1, watt: 0, hour: -1, kilogram: 0} means byte/hour (B/H).
// Unit{byte: -1, watt: 0, hour: -1, kilogram: 1} means kg/byte-hour (kg/B*H).
type Unit struct {
byte int8
watt int8
hour int8
kilogram int8
}

// Val represents a value which consists of the numeric value itself and it's dimensions e.g. 1 kW.
// It may be used to represent a really complex value e.g. 1 kW / H or 0.005 W * H / GB.
type Val struct {
Amount float64
Num []string
Denom []string
// Value creates new Val from existing Unit.
func (u *Unit) Value(v float64) Val {
return Val{Value: v, Unit: *u}
}

// ValMaker creates new Val constructor function by given string representation of the unit e.g. kg.
// By providing amount value to a constructor function we create a value instance.
// kg := ValMaker("kg") - kg is a constructor function here.
// kg(1) is a 1 kilogram Val.
func ValMaker(unit string) func(val float64) *Val {
if unit == "" {
return func(val float64) *Val {
return &Val{Amount: val}
}
}
return func(val float64) *Val {
return &Val{Amount: val, Num: []string{unit}}
}
// Mul multiplies existing Unit by a given one.
func (u *Unit) Mul(b Unit) {
u.byte += b.byte
u.watt += b.watt
u.hour += b.hour
u.kilogram += b.kilogram
}

// Maker creates a new Val constructor function from already existing Val.
// This is used to handle dimension factor differences.
// B := ValMaker("B") - B is a constructor function here.
// B(1) returns 1 byte Val.
// KB := B(1000).Maker() returns a construction function for a KB value.
// KB(1) returns 1 kilobyte Val but under the hood it's still 1000 B value.
func (v *Val) Maker() func(val float64) *Val {
return func(val float64) *Val {
return v.Mul(Q(val))
}
// Div divides existing Unit by a given one.
func (u *Unit) Div(b Unit) {
u.byte -= b.byte
u.watt -= b.watt
u.hour -= b.hour
u.kilogram -= b.kilogram
}

// Mul multiplies existing Val with a given one and returns new Val.
// It adjusts both the amount and the dimensions accordingly.
// Q := ValMaker("") - Q is a constructor function which has no dimension.
// Q(0.005) returns 0.005 Val.
// Q(0.005).Mul(W(1)) means 0.005 * 1 W = 0.005 W.
// Q(0.005).Mul(W(1)).Mul(H(1)) means 0.005 * 1 W * 1 H = 0.005 W * 1 H = 0.005 W * H.
func (v *Val) Mul(rhs *Val) *Val {
rv := &Val{Amount: v.Amount * rhs.Amount}
rv.Num = append(rv.Num, v.Num...)
rv.Num = append(rv.Num, rhs.Num...)
rv.Denom = append(rv.Denom, v.Denom...)
rv.Denom = append(rv.Denom, rhs.Denom...)
rv.simplify()
return rv
// String returns string representation of the Unit.
func (u *Unit) String() string {
var num bytes.Buffer
var div bytes.Buffer

a := func(prefix string, v int8) {
if v == 0 {
return
}

target := &num
if v < 0 {
target = &div
v = -v
}

switch v {
case 1:
target.WriteString(prefix)
case 2:
target.WriteString(prefix + "²")
case 3:
target.WriteString(prefix + "³")
default:
target.WriteString(fmt.Sprintf("%s^%d", prefix, v))
}
}

a("B", u.byte)
a("W", u.watt)
a("H", u.hour)
a("kg", u.kilogram)

n := num.String()
d := div.String()

switch {
case n == "" && d == "":
return ""
case d == "":
return n
case n == "":
return "1/" + d
default:
return n + "/" + d
}
}

// Div divides one Val by a given one and returns new Val.
// It adjusts both the amount and the dimensions accordingly.
// Q := ValMaker("") - Q is a constructor function which has no dimension.
// Q(0.005) returns 0.005 Val.
// Q(0.005).Mul(W(1)) means 0.005 * 1 W = 0.005 W.
// Q(0.005).Mul(W(1)).Div(H(1)) means 0.005 * 1 W / 1 H = 0.005 W / 1 H = 0.005 W / H.
func (v *Val) Div(rhs *Val) *Val {
rv := &Val{Amount: v.Amount / rhs.Amount}
rv.Num = append(rv.Num, v.Num...)
rv.Num = append(rv.Num, rhs.Denom...)
rv.Denom = append(rv.Denom, v.Denom...)
rv.Denom = append(rv.Denom, rhs.Num...)
rv.simplify()
return rv
// Val represents a value which consists of the numeric value itself and it's dimensions e.g. 1 W.
// It may be used to represent a really complex value e.g. 1 W / H or 0.005 W * H / B.
type Val struct {
Value float64
Unit Unit
}

// Add sums two Val instances with the same dimensions.
func (v *Val) Add(rhs *Val) (*Val, error) {
v.simplify()
rhs.simplify()
if !slices.Equal(v.Num, rhs.Num) {
return nil, errs.New(fmt.Sprintf("cannot add units %s, %s", v, rhs))
}
if !slices.Equal(v.Denom, rhs.Denom) {
return nil, errs.New(fmt.Sprintf("cannot add units %s, %s", v, rhs))
func (a Val) Add(b Val) (Val, error) {
if a.Unit != b.Unit {
return Val{}, errs.New(fmt.Sprintf("cannot add units %s, %s", a.Unit.String(), b.Unit.String()))
}
return &Val{
Amount: v.Amount + rhs.Amount,
Num: slices.Clone(v.Num),
Denom: slices.Clone(v.Denom),
}, nil
r := a
r.Value += b.Value
return r, nil
}

// Sub subtracts one Val from another if they have the same dimensions.
func (v *Val) Sub(rhs *Val) (*Val, error) {
v.simplify()
rhs.simplify()
if !slices.Equal(v.Num, rhs.Num) {
return nil, errs.New(fmt.Sprintf("cannot subtract units %s, %s", v, rhs))
}
if !slices.Equal(v.Denom, rhs.Denom) {
return nil, errs.New(fmt.Sprintf("cannot subtract units %s, %s", v, rhs))
func (a Val) Sub(b Val) (Val, error) {
if a.Unit != b.Unit {
return Val{}, errs.New(fmt.Sprintf("cannot subtract units %s, %s", a.Unit.String(), b.Unit.String()))
}
return &Val{
Amount: v.Amount - rhs.Amount,
Num: slices.Clone(v.Num),
Denom: slices.Clone(v.Denom),
}, nil
r := a
r.Value -= b.Value
return r, nil
}

// InUnits converts a Val into the specified units, if possible.
func (v *Val) InUnits(units *Val) (float64, error) {
x := v.Div(units)
if len(x.Num) != 0 {
return 0, errs.New(fmt.Sprintf("cannot convert %s to units %s", v, units))
}
if len(x.Denom) != 0 {
return 0, errs.New(fmt.Sprintf("cannot convert %s to units %s", v, units))
}
return x.Amount, nil
// Mul multiplies existing Val with a given one and returns new Val.
// It adjusts both the amount and the dimensions accordingly.
func (a Val) Mul(b Val) Val {
r := a
r.Unit.Mul(b.Unit)
r.Value *= b.Value
return r
}

// String returns string representation of the Val.
// Q := ValMaker("") - Q is a constructor function which has no dimension.
// Q(0.005).String is just 0.005.
// Q(0.005).Mul(W(1)).Mul(H(1)).Div(MB(1)).String() is 0.005 W * H / MB.
// KB(1).String() returns 1000 B because KB Val was created from a B Val.
func (v *Val) String() string {
var b strings.Builder
fmt.Fprintf(&b, "%v", v.Amount)
if len(v.Num) > 0 {
b.WriteByte(' ')
for i, num := range v.Num {
if i != 0 {
b.WriteByte('*')
}
b.WriteString(num)
}
}
if len(v.Denom) > 0 {
b.WriteString("/")
for i, num := range v.Denom {
if i != 0 {
b.WriteByte('/')
}
b.WriteString(num)
}
}
return b.String()
// Div divides one Val by a given one and returns new Val.
// It adjusts both the amount and the dimensions accordingly.
func (a Val) Div(b Val) Val {
r := a
r.Unit.Div(b.Unit)
r.Value /= b.Value
return r
}

func (v *Val) simplify() {
counts := map[string]int{}
for _, num := range v.Num {
counts[num]++
}
for _, denom := range v.Denom {
counts[denom]--
}
v.Num = v.Num[:0]
v.Denom = v.Denom[:0]
for name, count := range counts {
for count > 0 {
v.Num = append(v.Num, name)
count--
}
for count < 0 {
v.Denom = append(v.Denom, name)
count++
}
}
sort.Strings(v.Num)
sort.Strings(v.Denom)
// String returns string representation of the Val.
func (a Val) String() string {
return fmt.Sprintf("%f[%s]", a.Value, a.Unit.String())
}
39 changes: 39 additions & 0 deletions satellite/emission/dimen_test.go
@@ -0,0 +1,39 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

package emission

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestUnit_String(t *testing.T) {
cases := []struct {
unit Unit
expected string
}{
{unit: Unit{}, expected: ""},
{unit: Unit{byte: 1}, expected: "B"},
{unit: Unit{watt: 1}, expected: "W"},
{unit: Unit{hour: 1}, expected: "H"},
{unit: Unit{hour: -1}, expected: "1/H"},
{unit: Unit{kilogram: 1}, expected: "kg"},
{unit: Unit{kilogram: -1}, expected: "1/kg"},
{unit: Unit{byte: 1, watt: 1}, expected: "BW"},
{unit: Unit{byte: 1, watt: -1}, expected: "B/W"},
{unit: Unit{byte: 1, watt: -1, hour: -1}, expected: "B/WH"},
{unit: Unit{byte: 1, kilogram: 1, watt: -1, hour: -1}, expected: "Bkg/WH"},
{unit: Unit{byte: 2, kilogram: 1, watt: -2, hour: -1}, expected: "B²kg/W²H"},
{unit: Unit{byte: 3, watt: -1, hour: -2}, expected: "B³/WH²"},
{unit: Unit{byte: 2, watt: -4, hour: -1}, expected: "B²/W^4H"},
}

for _, c := range cases {
t.Run(fmt.Sprintf("expected:%s", c.expected), func(t *testing.T) {
require.Equal(t, c.unit.String(), c.expected)
})
}
}

0 comments on commit 7d3c13c

Please sign in to comment.