Add codecs for encoding and decoding unsigned integers

Introduced Uint16, Uint24, Uint32, Uint40, and Uint64 codecs with support for encoding/decoding in BigEndian format. Each codec includes range validation, getter/setter methods, and comprehensive unit tests to ensure correctness and reliability.
This commit is contained in:
2025-06-19 20:05:15 +01:00
parent eb0ba87ce6
commit 3b0fd72290
10 changed files with 611 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
package number
import (
"encoding/binary"
"io"
)
// Uint16 is a codec for encoding and decoding 16-bit unsigned integers.
type Uint16 struct {
value uint16
}
// Set sets the value as a uint16.
func (c *Uint16) Set(value uint16) {
c.value = value
}
// Get gets the value as a uint16.
func (c *Uint16) Get() uint16 {
return c.value
}
// SetInt sets the value as an int, converting it to uint16. Truncates values outside uint16 range (0-65535).
func (c *Uint16) SetInt(value int) {
c.value = uint16(value)
}
// GetInt gets the value as an int, converted from uint16.
func (c *Uint16) GetInt() int {
return int(c.value)
}
// MarshalWrite writes the uint16 value to the provided writer in BigEndian order.
func (c *Uint16) MarshalWrite(w io.Writer) error {
return binary.Write(w, binary.BigEndian, c.value)
}
// UnmarshalRead reads a uint16 value from the provided reader in BigEndian order.
func (c *Uint16) UnmarshalRead(r io.Reader) error {
return binary.Read(r, binary.BigEndian, &c.value)
}

View File

@@ -0,0 +1,66 @@
package number
import (
"bytes"
"math"
"testing"
"lukechampine.com/frand"
)
func TestUint16Codec(t *testing.T) {
// Helper function to generate random 16-bit integers
generateRandomUint16 := func() uint16 {
return uint16(frand.Intn(math.MaxUint16)) // math.MaxUint16 == 65535
}
for i := 0; i < 100; i++ { // Run test 100 times for random values
// Generate a random value
randomUint16 := generateRandomUint16()
randomInt := int(randomUint16)
// Create a new encodedUint16
encodedUint16 := new(Uint16)
// Test UInt16 setter and getter
encodedUint16.Set(randomUint16)
if encodedUint16.Get() != randomUint16 {
t.Fatalf("Get mismatch: got %d, expected %d", encodedUint16.Get(), randomUint16)
}
// Test GetInt setter and getter
encodedUint16.SetInt(randomInt)
if encodedUint16.GetInt() != randomInt {
t.Fatalf("GetInt mismatch: got %d, expected %d", encodedUint16.GetInt(), randomInt)
}
// Test encoding to []byte and decoding back
bufEnc := new(bytes.Buffer)
// MarshalWrite
err := encodedUint16.MarshalWrite(bufEnc)
if err != nil {
t.Fatalf("MarshalWrite failed: %v", err)
}
encoded := bufEnc.Bytes()
// Create a copy of encoded bytes before decoding
bufDec := bytes.NewBuffer(encoded)
// Decode back the value
decodedUint16 := new(Uint16)
err = decodedUint16.UnmarshalRead(bufDec)
if err != nil {
t.Fatalf("UnmarshalRead failed: %v", err)
}
if decodedUint16.Get() != randomUint16 {
t.Fatalf("Decoded value mismatch: got %d, expected %d", decodedUint16.Get(), randomUint16)
}
// Compare encoded bytes to ensure correctness
if !bytes.Equal(encoded, bufEnc.Bytes()) {
t.Fatalf("Byte encoding mismatch: got %v, expected %v", bufEnc.Bytes(), encoded)
}
}
}

View File

@@ -0,0 +1,78 @@
package number
import (
"errors"
"io"
)
// MaxUint24 is the maximum value of a 24-bit unsigned integer: 2^24 - 1.
const MaxUint24 uint32 = 1<<24 - 1
// Uint24Codec is a codec for encoding and decoding 24-bit unsigned integers.
type Uint24Codec struct {
value uint32
}
// SetUint24 sets the value as a 24-bit unsigned integer.
// If the value exceeds the maximum allowable value for 24 bits, it returns an error.
func (c *Uint24Codec) SetUint24(value uint32) error {
if value > MaxUint24 {
return errors.New("value exceeds 24-bit range")
}
c.value = value
return nil
}
// Uint24 gets the value as a 24-bit unsigned integer.
func (c *Uint24Codec) Uint24() uint32 {
return c.value
}
// SetInt sets the value as an int, converting it to a 24-bit unsigned integer.
// If the value is out of the 24-bit range, it returns an error.
func (c *Uint24Codec) SetInt(value int) error {
if value < 0 || uint32(value) > MaxUint24 {
return errors.New("value exceeds 24-bit range")
}
c.value = uint32(value)
return nil
}
// Int gets the value as an int, converted from the 24-bit unsigned integer.
func (c *Uint24Codec) Int() int {
return int(c.value)
}
// MarshalWrite encodes the 24-bit unsigned integer and writes it directly to the provided io.Writer.
// The encoding uses 3 bytes in BigEndian order.
func (c *Uint24Codec) MarshalWrite(w io.Writer) error {
if c.value > MaxUint24 {
return errors.New("value exceeds 24-bit range")
}
// Write the 3 bytes (BigEndian order) directly to the writer
var buf [3]byte
buf[0] = byte((c.value >> 16) & 0xFF) // Most significant byte
buf[1] = byte((c.value >> 8) & 0xFF)
buf[2] = byte(c.value & 0xFF) // Least significant byte
_, err := w.Write(buf[:]) // Write all 3 bytes to the writer
return err
}
// UnmarshalRead reads 3 bytes directly from the provided io.Reader and decodes it into a 24-bit unsigned integer.
func (c *Uint24Codec) UnmarshalRead(r io.Reader) error {
// Read 3 bytes directly from the reader
var buf [3]byte
_, err := io.ReadFull(r, buf[:]) // Ensure exactly 3 bytes are read
if err != nil {
return err
}
// Decode the 3 bytes into a 24-bit unsigned integer
c.value = (uint32(buf[0]) << 16) |
(uint32(buf[1]) << 8) |
uint32(buf[2])
return nil
}

View File

@@ -0,0 +1,67 @@
package number
import (
"bytes"
"testing"
)
func TestUint24Codec(t *testing.T) {
tests := []struct {
name string
value uint32
expectedErr bool
}{
{"Minimum Value", 0, false},
{"Maximum Value", MaxUint24, false},
{"Value in Range", 8374263, false}, // Example value within the range
{"Value Exceeds Range", MaxUint24 + 1, true}, // Exceeds 24-bit limit
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
codec := new(Uint24Codec)
// Test SetUint24
err := codec.SetUint24(tt.value)
if tt.expectedErr {
if err == nil {
t.Errorf("expected error but got none")
}
return
} else if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Test Uint24 getter
if codec.Uint24() != tt.value {
t.Errorf("Uint24 mismatch: got %d, expected %d", codec.Uint24(), tt.value)
}
// Test MarshalWrite and UnmarshalRead
buf := new(bytes.Buffer)
// MarshalWrite directly to the buffer
if err := codec.MarshalWrite(buf); err != nil {
t.Fatalf("MarshalWrite failed: %v", err)
}
// Validate encoded size is 3 bytes
encoded := buf.Bytes()
if len(encoded) != 3 {
t.Fatalf("encoded size mismatch: got %d bytes, expected 3 bytes", len(encoded))
}
// Decode from the buffer
decodedCodec := new(Uint24Codec)
if err := decodedCodec.UnmarshalRead(buf); err != nil {
t.Fatalf("UnmarshalRead failed: %v", err)
}
// Validate decoded value
if decodedCodec.Uint24() != tt.value {
t.Errorf("Decoded value mismatch: got %d, expected %d", decodedCodec.Uint24(), tt.value)
}
})
}
}

View File

@@ -0,0 +1,42 @@
package number
import (
"encoding/binary"
"io"
)
// Uint32Codec is a codec for encoding and decoding 32-bit unsigned integers.
type Uint32Codec struct {
value uint32
}
// SetUint32 sets the value as a uint32.
func (c *Uint32Codec) SetUint32(value uint32) {
c.value = value
}
// Uint32 gets the value as a uint32.
func (c *Uint32Codec) Uint32() uint32 {
return c.value
}
// SetInt sets the value as an int, converting it to uint32.
// Values outside the range of uint32 (04294967295) will be truncated.
func (c *Uint32Codec) SetInt(value int) {
c.value = uint32(value)
}
// Int gets the value as an int, converted from uint32.
func (c *Uint32Codec) Int() int {
return int(c.value)
}
// MarshalWrite writes the uint32 value to the provided writer in BigEndian order.
func (c *Uint32Codec) MarshalWrite(w io.Writer) error {
return binary.Write(w, binary.BigEndian, c.value)
}
// UnmarshalRead reads a uint32 value from the provided reader in BigEndian order.
func (c *Uint32Codec) UnmarshalRead(r io.Reader) error {
return binary.Read(r, binary.BigEndian, &c.value)
}

View File

@@ -0,0 +1,66 @@
package number
import (
"bytes"
"math"
"testing"
"lukechampine.com/frand"
)
func TestUint32Codec(t *testing.T) {
// Helper function to generate random 32-bit integers
generateRandomUint32 := func() uint32 {
return uint32(frand.Intn(math.MaxUint32)) // math.MaxUint32 == 4294967295
}
for i := 0; i < 100; i++ { // Run test 100 times for random values
// Generate a random value
randomUint32 := generateRandomUint32()
randomInt := int(randomUint32)
// Create a new codec
codec := new(Uint32Codec)
// Test UInt32 setter and getter
codec.SetUint32(randomUint32)
if codec.Uint32() != randomUint32 {
t.Fatalf("Uint32 mismatch: got %d, expected %d", codec.Uint32(), randomUint32)
}
// Test Int setter and getter
codec.SetInt(randomInt)
if codec.Int() != randomInt {
t.Fatalf("Int mismatch: got %d, expected %d", codec.Int(), randomInt)
}
// Test encoding to []byte and decoding back
bufEnc := new(bytes.Buffer)
// MarshalWrite
err := codec.MarshalWrite(bufEnc)
if err != nil {
t.Fatalf("MarshalWrite failed: %v", err)
}
encoded := bufEnc.Bytes()
// Create a copy of encoded bytes before decoding
bufDec := bytes.NewBuffer(encoded)
// Decode back the value
decodedCodec := new(Uint32Codec)
err = decodedCodec.UnmarshalRead(bufDec)
if err != nil {
t.Fatalf("UnmarshalRead failed: %v", err)
}
if decodedCodec.Uint32() != randomUint32 {
t.Fatalf("Decoded value mismatch: got %d, expected %d", decodedCodec.Uint32(), randomUint32)
}
// Compare encoded bytes to ensure correctness
if !bytes.Equal(encoded, bufEnc.Bytes()) {
t.Fatalf("Byte encoding mismatch: got %v, expected %v", bufEnc.Bytes(), encoded)
}
}
}

View File

@@ -0,0 +1,75 @@
package number
import (
"errors"
"io"
)
// MaxUint40 is the maximum value of a 40-bit unsigned integer: 2^40 - 1.
const MaxUint40 uint64 = 1<<40 - 1
// Uint40Codec is a codec for encoding and decoding 40-bit unsigned integers.
type Uint40Codec struct{ value uint64 }
// SetUint40 sets the value as a 40-bit unsigned integer.
// If the value exceeds the maximum allowable value for 40 bits, it returns an error.
func (c *Uint40Codec) SetUint40(value uint64) error {
if value > MaxUint40 {
return errors.New("value exceeds 40-bit range")
}
c.value = value
return nil
}
// Uint40 gets the value as a 40-bit unsigned integer.
func (c *Uint40Codec) Uint40() uint64 { return c.value }
// SetInt sets the value as an int, converting it to a 40-bit unsigned integer.
// If the value is out of the 40-bit range, it returns an error.
func (c *Uint40Codec) SetInt(value int) error {
if value < 0 || uint64(value) > MaxUint40 {
return errors.New("value exceeds 40-bit range")
}
c.value = uint64(value)
return nil
}
// Int gets the value as an int, converted from the 40-bit unsigned integer.
// Note: If the value exceeds the int range, it will be truncated.
func (c *Uint40Codec) Int() int { return int(c.value) }
// MarshalWrite encodes the 40-bit unsigned integer and writes it to the provided writer.
// The encoding uses 5 bytes in BigEndian order.
func (c *Uint40Codec) MarshalWrite(w io.Writer) (err error) {
if c.value > MaxUint40 {
return errors.New("value exceeds 40-bit range")
}
// Buffer for the 5 bytes
buf := make([]byte, 5)
// Write the upper 5 bytes (ignoring the most significant 3 bytes of uint64)
buf[0] = byte((c.value >> 32) & 0xFF) // Most significant byte
buf[1] = byte((c.value >> 24) & 0xFF)
buf[2] = byte((c.value >> 16) & 0xFF)
buf[3] = byte((c.value >> 8) & 0xFF)
buf[4] = byte(c.value & 0xFF) // Least significant byte
_, err = w.Write(buf)
return err
}
// UnmarshalRead reads 5 bytes from the provided reader and decodes it into a 40-bit unsigned integer.
func (c *Uint40Codec) UnmarshalRead(r io.Reader) (err error) {
// Buffer for the 5 bytes
buf := make([]byte, 5)
_, err = r.Read(buf)
if err != nil {
return err
}
// Decode the 5 bytes into a 40-bit unsigned integer
c.value = (uint64(buf[0]) << 32) |
(uint64(buf[1]) << 24) |
(uint64(buf[2]) << 16) |
(uint64(buf[3]) << 8) |
uint64(buf[4])
return nil
}

View File

@@ -0,0 +1,68 @@
package number
import (
"bytes"
"testing"
)
func TestUint40Codec(t *testing.T) {
// Test cases for Uint40Codec
tests := []struct {
name string
value uint64
expectedErr bool
}{
{"Minimum Value", 0, false},
{"Maximum Value", MaxUint40, false},
{"Value in Range", 109951162777, false}, // Example value within the range
{"Value Exceeds Range", MaxUint40 + 1, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
codec := new(Uint40Codec)
// Test SetUint40
err := codec.SetUint40(tt.value)
if tt.expectedErr {
if err == nil {
t.Errorf("expected error but got none")
}
return
} else if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Test Uint40 getter
if codec.Uint40() != tt.value {
t.Errorf("Uint40 mismatch: got %d, expected %d", codec.Uint40(), tt.value)
}
// Test MarshalWrite and UnmarshalRead
buf := new(bytes.Buffer)
// Marshal to a buffer
if err = codec.MarshalWrite(buf); err != nil {
t.Fatalf("MarshalWrite failed: %v", err)
}
// Validate encoded size is 5 bytes
encoded := buf.Bytes()
if len(encoded) != 5 {
t.Fatalf("encoded size mismatch: got %d bytes, expected 5 bytes", len(encoded))
}
// Decode from the buffer
decodedCodec := new(Uint40Codec)
if err = decodedCodec.UnmarshalRead(buf); err != nil {
t.Fatalf("UnmarshalRead failed: %v", err)
}
// Validate decoded value
if decodedCodec.Uint40() != tt.value {
t.Errorf("Decoded value mismatch: got %d, expected %d", decodedCodec.Uint40(), tt.value)
}
})
}
}

View File

@@ -0,0 +1,42 @@
package number
import (
"encoding/binary"
"io"
)
// Uint64Codec is a codec for encoding and decoding 64-bit unsigned integers.
type Uint64Codec struct {
value uint64
}
// SetUint64 sets the value as a uint64.
func (c *Uint64Codec) SetUint64(value uint64) {
c.value = value
}
// Uint64 gets the value as a uint64.
func (c *Uint64Codec) Uint64() uint64 {
return c.value
}
// SetInt sets the value as an int, converting it to uint64.
// Values outside the range of uint64 are truncated.
func (c *Uint64Codec) SetInt(value int) {
c.value = uint64(value)
}
// Int gets the value as an int, converted from uint64. May truncate if the value exceeds the range of int.
func (c *Uint64Codec) Int() int {
return int(c.value)
}
// MarshalWrite writes the uint64 value to the provided writer in BigEndian order.
func (c *Uint64Codec) MarshalWrite(w io.Writer) error {
return binary.Write(w, binary.BigEndian, c.value)
}
// UnmarshalRead reads a uint64 value from the provided reader in BigEndian order.
func (c *Uint64Codec) UnmarshalRead(r io.Reader) error {
return binary.Read(r, binary.BigEndian, &c.value)
}

View File

@@ -0,0 +1,66 @@
package number
import (
"bytes"
"math"
"testing"
"lukechampine.com/frand"
)
func TestUint64Codec(t *testing.T) {
// Helper function to generate random 64-bit integers
generateRandomUint64 := func() uint64 {
return frand.Uint64n(math.MaxUint64) // math.MaxUint64 == 18446744073709551615
}
for i := 0; i < 100; i++ { // Run test 100 times for random values
// Generate a random value
randomUint64 := generateRandomUint64()
randomInt := int(randomUint64)
// Create a new codec
codec := new(Uint64Codec)
// Test UInt64 setter and getter
codec.SetUint64(randomUint64)
if codec.Uint64() != randomUint64 {
t.Fatalf("Uint64 mismatch: got %d, expected %d", codec.Uint64(), randomUint64)
}
// Test Int setter and getter
codec.SetInt(randomInt)
if codec.Int() != randomInt {
t.Fatalf("Int mismatch: got %d, expected %d", codec.Int(), randomInt)
}
// Test encoding to []byte and decoding back
bufEnc := new(bytes.Buffer)
// MarshalWrite
err := codec.MarshalWrite(bufEnc)
if err != nil {
t.Fatalf("MarshalWrite failed: %v", err)
}
encoded := bufEnc.Bytes()
// Create a buffer for decoding
bufDec := bytes.NewBuffer(encoded)
// Decode back the value
decodedCodec := new(Uint64Codec)
err = decodedCodec.UnmarshalRead(bufDec)
if err != nil {
t.Fatalf("UnmarshalRead failed: %v", err)
}
if decodedCodec.Uint64() != randomUint64 {
t.Fatalf("Decoded value mismatch: got %d, expected %d", decodedCodec.Uint64(), randomUint64)
}
// Compare encoded bytes to ensure correctness
if !bytes.Equal(encoded, bufEnc.Bytes()) {
t.Fatalf("Byte encoding mismatch: got %v, expected %v", bufEnc.Bytes(), encoded)
}
}
}