Skip to content

Commit

Permalink
initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasparada committed Feb 10, 2024
0 parents commit 70d9289
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 0 deletions.
15 changes: 15 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ISC License

Copyright (c) 2024 Nicolás Parada

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/nicolasparada/go-ordered-map.svg)](https://pkg.go.dev/github.com/nicolasparada/go-ordered-map)

# Golang Ordered Map

Golang Ordered Map is a `map` data structure that maintains the order of the keys.
It also supports JSON and YAML marshalling.

## Installation

```bash
go get github.com/nicolasparada/go-ordered-map
```

## Usage

```go
package main

import (
orderedmap "github.com/nicolasparada/go-ordered-map"
)

func main() {
data := []byte(`{ "name": "John", "age": 30, "active": true }`)

var unordered map[string]any{}
if err := json.Unmarshal(data, &unordered); err != nil {
panic(err)
}

var ordered orderedmap.OrderedMap[string, any]
if err := json.Unmarshal(data, &ordered); err != nil {
panic(err)
}

json.NewEncoder(os.Stdout).Encode(unordered) // will print in undefined order
json.NewEncoder(os.Stdout).Encode(ordered) // will always print: {"name":"John","age":30,"active":true}
}
```
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/nicolasparada/go-ordered-map

go 1.22.0

require (
github.com/alecthomas/assert/v2 v2.5.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/alecthomas/repr v0.3.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w=
github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=
github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
131 changes: 131 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package orderedmap

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
)

// MarshalJSON implements json.Marshaler interface
// to marshall a sorted list of key-value pairs into an object.
func (o OrderedMap[K, V]) MarshalJSON() ([]byte, error) {
if o == nil {
return []byte(`null`), nil
}

if len(o) == 0 {
return []byte(`{}`), nil
}

var buf bytes.Buffer
if err := buf.WriteByte('{'); err != nil {
return nil, err
}

enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
for i, p := range o {
if i != 0 {
if err := buf.WriteByte(','); err != nil {
return nil, err
}
}

if err := enc.Encode(p.Key); err != nil {
return nil, err
}

if err := buf.WriteByte(':'); err != nil {
return nil, err
}

if err := enc.Encode(p.Val); err != nil {
return nil, err
}
}

if err := buf.WriteByte('}'); err != nil {
return nil, err
}

return bytes.TrimRight(buf.Bytes(), "\n"), nil
}

// UnmarshalJSON implements json.Unmarshaler interface
// to unmarshal an object into a sorted list of key-value pairs.
func (o *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
var m map[K]V
err := json.Unmarshal(data, &m)
if err != nil {
return err
}

dec := json.NewDecoder(bytes.NewReader(data))
t, err := dec.Token()
if err != nil {
return err
}

if t != json.Delim('{') {
return errors.New("expected start of object")
}

for {
t, err := dec.Token()
if err != nil {
return err
}

if t == json.Delim('}') {
break
}

key, ok := t.(K)
if !ok {
return fmt.Errorf("expected object key to be %T, got %T", key, t)
}

*o = append(*o, Pair[K, V]{
Key: key,
Val: m[key],
})

// ignored value
if err := skipJSONValue(dec); err != nil {
if errors.Is(err, io.EOF) {
break
}

return err
}
}

return nil
}

var errJSONEnd = errors.New("invalid end of json array or object")

func skipJSONValue(dec *json.Decoder) error {
t, err := dec.Token()
if err != nil {
return err
}

switch t {
case json.Delim('['), json.Delim('{'):
for {
if err := skipJSONValue(dec); err != nil {
if errors.Is(err, errJSONEnd) {
break
}
return err
}
}
case json.Delim(']'), json.Delim('}'):
return errJSONEnd
}

return nil
}
36 changes: 36 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package orderedmap

import (
"encoding/json"
"testing"
"time"

"github.com/alecthomas/assert/v2"
)

func TestOrderedMap_MarshalJSON(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second)
got, err := json.Marshal(OrderedMap[string, any]{
{"name", "John"},
{"age", 30},
{"active", true},
{"last_access_time", now},
})
assert.NoError(t, err)
assert.Equal(t, `{"name":"John","age":30,"active":true,"last_access_time":"`+now.Format(time.RFC3339Nano)+`"}`, string(got))
}

func TestOrderedMap_UnmarshalJSON(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second)
nowStr := now.Format(time.RFC3339Nano)
var got OrderedMap[string, any]
err := json.Unmarshal([]byte(`{"name":"John","age":30,"active":true,"last_access_time":"`+nowStr+`"}`), &got)
assert.NoError(t, err)

assert.Equal(t, OrderedMap[string, any]{
{"name", "John"},
{"age", float64(30)}, // json always unmarshals numbers as float64
{"active", true},
{"last_access_time", nowStr}, // json doesn't detect time.Time
}, got)
}
92 changes: 92 additions & 0 deletions ordered_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package orderedmap

type OrderedMap[K comparable, V any] []Pair[K, V]

type Pair[K comparable, V any] struct {
Key K
Val V
}

func New[K comparable, V any]() OrderedMap[K, V] {
return OrderedMap[K, V]{}
}

func (m OrderedMap[K, V]) Has(key K) bool {
_, ok := m.Get(key)
return ok
}

func (m OrderedMap[K, V]) Get(key K) (V, bool) {
for _, p := range m {
if p.Key == key {
return p.Val, true
}
}
var zero V
return zero, false
}

func (m *OrderedMap[K, V]) Set(key K, val V) {
if m == nil {
*m = OrderedMap[K, V]{}
}

for i, p := range *m {
if p.Key == key {
(*m)[i].Val = val
return
}
}
*m = append(*m, Pair[K, V]{key, val})
}

func (m *OrderedMap[K, V]) Delete(key K) {
if m == nil || len(*m) == 0 {
return
}

for i, p := range *m {
if p.Key == key {
*m = append((*m)[:i], (*m)[i+1:]...)
return
}
}
}

func (m OrderedMap[K, V]) Keys() []K {
if m == nil {
return nil
}
keys := make([]K, len(m))
for i, p := range m {
keys[i] = p.Key
}
return keys
}

func (m OrderedMap[K, V]) Values() []V {
if m == nil {
return nil
}
values := make([]V, len(m))
for i, p := range m {
values[i] = p.Val
}
return values
}

func (m *OrderedMap[K, V]) Clear() {
if m == nil || len(*m) == 0 {
return
}

*m = []Pair[K, V]{}
}

func (m OrderedMap[K, V]) Copy() OrderedMap[K, V] {
if m == nil {
return nil
}

return append([]Pair[K, V]{}, m...)
}
Loading

0 comments on commit 70d9289

Please sign in to comment.