-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
668 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package odometer | ||
|
||
import "errors" | ||
|
||
var ( | ||
ErrInvalidLocation = errors.New("invalid location") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package odometer | ||
|
||
import ( | ||
"math/big" | ||
) | ||
|
||
type Odometer interface { | ||
Decrement() | ||
DecrementN(n *big.Int) | ||
First() | ||
GetLocation() *big.Int | ||
Increment() | ||
IncrementN(n *big.Int) | ||
Last() | ||
SetLocation(n *big.Int) error | ||
String() string | ||
Value() []int | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
package odometer | ||
|
||
import ( | ||
"math/big" | ||
"slices" | ||
"sync" | ||
|
||
"github.com/jedib0t/go-passwords/charset" | ||
) | ||
|
||
var ( | ||
biZero = big.NewInt(0) | ||
biOne = big.NewInt(1) | ||
) | ||
|
||
type odometer struct { | ||
base int | ||
baseBigInt *big.Int | ||
charset []rune | ||
length int | ||
location *big.Int | ||
locationMax *big.Int | ||
mutex sync.RWMutex | ||
rollover bool | ||
value []int | ||
valueInCharset []rune | ||
} | ||
|
||
func New(cs charset.Charset, length int, opts ...Option) Odometer { | ||
base := len(cs) | ||
maxValues := numValues(base, length) | ||
|
||
o := &odometer{ | ||
base: base, | ||
baseBigInt: big.NewInt(int64(base)), | ||
charset: []rune(cs), | ||
length: length, | ||
location: big.NewInt(1), | ||
locationMax: new(big.Int).Set(maxValues), | ||
value: make([]int, length), | ||
valueInCharset: make([]rune, length), | ||
} | ||
for _, opt := range opts { | ||
opt(o) | ||
} | ||
return o | ||
} | ||
|
||
func (o *odometer) Decrement() { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
// set the location | ||
if o.location.Cmp(biOne) == 0 { // at first | ||
if o.rollover { | ||
o.last() | ||
} | ||
return | ||
} | ||
|
||
// decrement value | ||
o.location.Sub(o.location, biOne) | ||
for idx := o.length - 1; idx >= 0; idx-- { | ||
if o.decrementAtIndex(idx) { | ||
return | ||
} | ||
} | ||
} | ||
|
||
func (o *odometer) DecrementN(n *big.Int) { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
o.location.Sub(o.location, n) | ||
if o.location.Cmp(biOne) < 0 { // less than min | ||
if !o.rollover { | ||
o.first() | ||
return | ||
} | ||
// move backwards from max; o.location is currently -ve --> so Add() | ||
for o.location.Cmp(biOne) < 0 { | ||
o.location.Add(o.locationMax, o.location) | ||
} | ||
} | ||
o.computeValue() | ||
} | ||
|
||
func (o *odometer) First() { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
o.first() | ||
} | ||
|
||
func (o *odometer) GetLocation() *big.Int { | ||
o.mutex.RLock() | ||
defer o.mutex.RUnlock() | ||
|
||
return new(big.Int).Set(o.location) | ||
} | ||
|
||
func (o *odometer) Increment() { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
// set the location | ||
if o.location.Cmp(o.locationMax) == 0 { // at max | ||
if o.rollover { | ||
o.first() | ||
} | ||
return | ||
} | ||
|
||
// increment value | ||
o.location.Add(o.location, biOne) | ||
for idx := o.length - 1; idx >= 0; idx-- { | ||
if o.incrementAtIndex(idx) { | ||
return | ||
} | ||
} | ||
} | ||
|
||
func (o *odometer) IncrementN(n *big.Int) { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
o.location.Add(o.location, n) | ||
if o.location.Cmp(o.locationMax) > 0 { // more than max | ||
if !o.rollover { | ||
o.last() | ||
return | ||
} | ||
// move forwards from zero | ||
for o.location.Cmp(o.locationMax) > 0 { | ||
o.location.Sub(o.location, o.locationMax) | ||
} | ||
} | ||
o.computeValue() | ||
} | ||
|
||
func (o *odometer) Last() { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
o.last() | ||
} | ||
|
||
func (o *odometer) SetLocation(n *big.Int) error { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
if n.Cmp(biOne) < 0 || n.Cmp(o.locationMax) > 0 { | ||
return ErrInvalidLocation | ||
} | ||
o.location.Set(n) | ||
o.computeValue() | ||
return nil | ||
} | ||
|
||
func (o *odometer) String() string { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
for idx := range o.valueInCharset { | ||
o.valueInCharset[idx] = o.charset[o.value[idx]] | ||
} | ||
return string(o.valueInCharset) | ||
} | ||
|
||
func (o *odometer) Value() []int { | ||
o.mutex.Lock() | ||
defer o.mutex.Unlock() | ||
|
||
return slices.Clone(o.value) | ||
} | ||
|
||
func (o *odometer) computeValue() { | ||
// base conversion: convert the value of location to a decimal with the | ||
// given base using continuous division and use all the remainders as the | ||
// values | ||
|
||
// prep the dividend, remainder and modulus | ||
dividend, remainder := new(big.Int).Sub(o.location, biOne), new(big.Int) | ||
// append values in reverse (from right to left) | ||
valIdx := o.length - 1 | ||
// append every remainder until dividend becomes zero | ||
for ; dividend.Cmp(biZero) != 0; valIdx-- { | ||
dividend, remainder = dividend.QuoRem(dividend, o.baseBigInt, remainder) | ||
o.value[valIdx] = int(remainder.Int64()) | ||
} | ||
// left-pad the remaining characters with 0 (==> 0th char in charset) | ||
for ; valIdx >= 0; valIdx-- { | ||
o.value[valIdx] = 0 | ||
} | ||
} | ||
|
||
func (o *odometer) decrementAtIndex(idx int) bool { | ||
if o.value[idx] > 0 { | ||
o.value[idx]-- | ||
return true | ||
} | ||
if o.value[idx] == 0 && idx > 0 { | ||
o.value[idx] = o.base - 1 | ||
o.decrementAtIndex(idx - 1) | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
func (o *odometer) first() { | ||
o.location.Set(biOne) | ||
for idx := range o.value { | ||
o.value[idx] = 0 | ||
} | ||
} | ||
|
||
func (o *odometer) incrementAtIndex(idx int) bool { | ||
if o.value[idx] < o.base-1 { | ||
o.value[idx]++ | ||
return true | ||
} | ||
if o.value[idx] == o.base-1 && idx > 0 { | ||
o.value[idx] = 0 | ||
o.incrementAtIndex(idx - 1) | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
func (o *odometer) last() { | ||
o.location.Set(o.locationMax) | ||
for idx := range o.value { | ||
o.value[idx] = o.base - 1 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package odometer | ||
|
||
import ( | ||
"math" | ||
"math/big" | ||
"math/rand" | ||
"testing" | ||
"time" | ||
|
||
"github.com/jedib0t/go-passwords/charset" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func BenchmarkOdometer_Big_Decrement(b *testing.B) { | ||
o := New(charset.AllChars, 256) | ||
o.Last() | ||
|
||
for i := 0; i < b.N; i++ { | ||
o.Decrement() | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_Big_Increment(b *testing.B) { | ||
o := New(charset.AllChars, 256) | ||
|
||
for i := 0; i < b.N; i++ { | ||
o.Increment() | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_Decrement(b *testing.B) { | ||
o := New(charset.Numbers, 8, WithRolloverEnabled(true)) | ||
|
||
for i := 0; i < b.N; i++ { | ||
o.Decrement() | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_DecrementN(b *testing.B) { | ||
o := New(charset.Numbers, 8, WithRolloverEnabled(true)) | ||
|
||
n := big.NewInt(5) | ||
for i := 0; i < b.N; i++ { | ||
o.DecrementN(n) | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_Increment(b *testing.B) { | ||
o := New(charset.Numbers, 8, WithRolloverEnabled(true)) | ||
|
||
for i := 0; i < b.N; i++ { | ||
o.Increment() | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_IncrementN(b *testing.B) { | ||
o := New(charset.Numbers, 8, WithRolloverEnabled(true)) | ||
|
||
n := big.NewInt(5) | ||
for i := 0; i < b.N; i++ { | ||
o.IncrementN(n) | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_SetLocation(b *testing.B) { | ||
o := New(charset.Numbers, 8, WithRolloverEnabled(true)) | ||
maxValues := int64(math.Pow(10, 8)) | ||
rng := rand.New(rand.NewSource(time.Now().Unix())) | ||
|
||
for i := 0; i < b.N; i++ { | ||
n := big.NewInt(rng.Int63n(maxValues)) | ||
err := o.SetLocation(n) | ||
assert.Nil(b, err) | ||
} | ||
} | ||
|
||
func BenchmarkOdometer_String(b *testing.B) { | ||
o := New(charset.Numbers, 12) | ||
|
||
for i := 0; i < b.N; i++ { | ||
_ = o.String() | ||
} | ||
} |
Oops, something went wrong.