├── go.mod ├── .github └── workflows │ └── test.yml ├── example_test.go ├── go.sum ├── README.md ├── LICENSE ├── base36_test.go └── base36.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/martinlindhe/base36 2 | 3 | go 1.21 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.21.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Test 18 | run: go test -v ./... -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package base36_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/martinlindhe/base36" 7 | 8 | ) 9 | 10 | func ExampleEncode() { 11 | fmt.Println(base36.Encode(5481594952936519619)) 12 | // Output: 15N9Z8L3AU4EB 13 | } 14 | 15 | func ExampleDecode() { 16 | fmt.Println(base36.Decode("15N9Z8L3AU4EB")) 17 | // Output: 5481594952936519619 18 | } 19 | 20 | func ExampleEncodeBytes() { 21 | fmt.Println(base36.EncodeBytes([]byte{1, 2, 3, 4})) 22 | // Output: A2F44 23 | } 24 | 25 | func ExampleDecodeToBytes() { 26 | fmt.Println(base36.DecodeToBytes("A2F44")) 27 | // Output: [1 2 3 4] 28 | } 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | [![GoDoc](https://godoc.org/github.com/martinlindhe/base36?status.svg)](https://godoc.org/github.com/martinlindhe/base36) 4 | 5 | Implements Base36 encoding and decoding, which is useful to represent 6 | large integers in a case-insensitive alphanumeric way. 7 | 8 | ## Examples 9 | 10 | ```go 11 | import "github.com/martinlindhe/base36" 12 | 13 | fmt.Println(base36.Encode(5481594952936519619)) 14 | // Output: 15N9Z8L3AU4EB 15 | 16 | fmt.Println(base36.Decode("15N9Z8L3AU4EB")) 17 | // Output: 5481594952936519619 18 | 19 | fmt.Println(base36.EncodeBytes([]byte{1, 2, 3, 4})) 20 | // Output: A2F44 21 | 22 | fmt.Println(base36.DecodeToBytes("A2F44")) 23 | // Output: [1 2 3 4] 24 | ``` 25 | 26 | ## Notice 27 | 28 | For basic base 36 conversion, you can use [strconv.FormatUint()](https://pkg.go.dev/strconv#FormatUint) from the stdlib. 29 | 30 | ## License 31 | 32 | Under [MIT](LICENSE) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 Martin Lindhe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base36_test.go: -------------------------------------------------------------------------------- 1 | package base36 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var raw = []uint64{0, 50, 100, 999, 1000, 1111, 5959, 99999, 12 | 123456789, 5481594952936519619, math.MaxInt64 / 2048, math.MaxInt64 / 512, 13 | math.MaxInt64, math.MaxUint64} 14 | 15 | var encoded = []string{"0", "1E", "2S", "RR", "RS", "UV", "4LJ", "255R", 16 | "21I3V9", "15N9Z8L3AU4EB", "18CE53UN18F", "4XDKKFEK4XR", 17 | "1Y2P0IJ32E8E7", "3W5E11264SGSF"} 18 | 19 | func TestEncode(t *testing.T) { 20 | for i, v := range raw { 21 | assert.Equal(t, encoded[i], Encode(v)) 22 | } 23 | } 24 | 25 | func TestDecode(t *testing.T) { 26 | for i, v := range encoded { 27 | assert.Equal(t, raw[i], Decode(v)) 28 | assert.Equal(t, raw[i], Decode(strings.ToLower(v))) 29 | } 30 | } 31 | 32 | func TestDecodeToBytes(t *testing.T) { 33 | // ensure that lowercase encoded string decodes correctly 34 | // https://github.com/martinlindhe/base36/issues/8 35 | 36 | in := []byte("Hello World") 37 | 38 | encoded := EncodeBytes(in) 39 | 40 | decoded := DecodeToBytes(encoded) 41 | assert.Equal(t, decoded, in) 42 | 43 | decodedLower := DecodeToBytes(strings.ToLower(encoded)) 44 | assert.Equal(t, decodedLower, in) 45 | } 46 | 47 | func BenchmarkEncode(b *testing.B) { 48 | for i := 0; i < b.N; i++ { 49 | Encode(5481594952936519619) 50 | } 51 | } 52 | 53 | func BenchmarkEncodeBytesAsBytes(b *testing.B) { 54 | data := []byte{ 55 | 0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B, 0x48, 0x4F, 0x2A, 0x48, 0x4F, 0x2A, 56 | } 57 | for i := 0; i < b.N; i++ { 58 | EncodeBytesAsBytes(data) 59 | } 60 | } 61 | 62 | func BenchmarkDecode(b *testing.B) { 63 | for i := 0; i < b.N; i++ { 64 | Decode("1Y2P0IJ32E8E7") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /base36.go: -------------------------------------------------------------------------------- 1 | package base36 2 | 3 | import ( 4 | "math/big" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | base36 = []byte{ 10 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 11 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 12 | 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 13 | 'U', 'V', 'W', 'X', 'Y', 'Z'} 14 | 15 | //index = map[byte]int{ 16 | // '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, 17 | // '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 18 | // 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 19 | // 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, 20 | // 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 21 | // 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, 22 | // 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 23 | // 'Z': 35, 24 | // 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 25 | // 'f': 15, 'g': 16, 'h': 17, 'i': 18, 'j': 19, 26 | // 'k': 20, 'l': 21, 'm': 22, 'n': 23, 'o': 24, 27 | // 'p': 25, 'q': 26, 'r': 27, 's': 28, 't': 29, 28 | // 'u': 30, 'v': 31, 'w': 32, 'x': 33, 'y': 34, 29 | // 'z': 35, 30 | //} 31 | uint8Index = []uint64{ 32 | 0, 33 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 38 | 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 39 | 0, 0, 0, 0, 10, 11, 12, 13, 14, 40 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 41 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 42 | 35, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 43 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 44 | 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 45 | 34, 35, 0, 0, 0, 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58 | 0, 0, 0, 0, 0, // 256 59 | } 60 | pow36Index = []uint64{ 61 | 1, 36, 1296, 46656, 1679616, 60466176, 62 | 2176782336, 78364164096, 2821109907456, 63 | 101559956668416, 3656158440062976, 64 | 131621703842267136, 4738381338321616896, 65 | 9223372036854775808, 66 | } 67 | ) 68 | 69 | // Encode encodes a number to base36. 70 | func Encode(value uint64) string { 71 | var res [16]byte 72 | var i int 73 | for i = len(res) - 1; ; i-- { 74 | res[i] = base36[value%36] 75 | value /= 36 76 | if value == 0 { 77 | break 78 | } 79 | } 80 | 81 | return string(res[i:]) 82 | } 83 | 84 | // Decode decodes a base36-encoded string. 85 | func Decode(s string) uint64 { 86 | if len(s) > 13 { 87 | s = s[:12] 88 | } 89 | res := uint64(0) 90 | l := len(s) - 1 91 | for idx := 0; idx < len(s); idx++ { 92 | c := s[l-idx] 93 | res += uint8Index[c] * pow36Index[idx] 94 | } 95 | return res 96 | } 97 | 98 | var bigRadix = big.NewInt(36) 99 | var bigZero = big.NewInt(0) 100 | 101 | // EncodeBytesAsBytes encodes a byte slice to base36. 102 | func EncodeBytesAsBytes(b []byte) []byte { 103 | x := new(big.Int) 104 | x.SetBytes(b) 105 | 106 | answer := make([]byte, 0, len(b)*136/100) 107 | for x.Cmp(bigZero) > 0 { 108 | mod := new(big.Int) 109 | x.DivMod(x, bigRadix, mod) 110 | answer = append(answer, base36[mod.Int64()]) 111 | } 112 | 113 | // leading zero bytes 114 | for _, i := range b { 115 | if i != 0 { 116 | break 117 | } 118 | answer = append(answer, base36[0]) 119 | } 120 | 121 | // reverse 122 | alen := len(answer) 123 | for i := 0; i < alen/2; i++ { 124 | answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i] 125 | } 126 | 127 | return answer 128 | } 129 | 130 | // EncodeBytes encodes a byte slice to base36 string. 131 | func EncodeBytes(b []byte) string { 132 | return string(EncodeBytesAsBytes(b)) 133 | } 134 | 135 | // DecodeToBytes decodes a base36 string to a byte slice, using alphabet. 136 | func DecodeToBytes(b string) []byte { 137 | b = strings.ToUpper(b) 138 | alphabet := string(base36) 139 | answer := big.NewInt(0) 140 | j := big.NewInt(1) 141 | 142 | for i := len(b) - 1; i >= 0; i-- { 143 | tmp := strings.IndexAny(alphabet, string(b[i])) 144 | if tmp == -1 { 145 | return []byte("") 146 | } 147 | idx := big.NewInt(int64(tmp)) 148 | tmp1 := big.NewInt(0) 149 | tmp1.Mul(j, idx) 150 | 151 | answer.Add(answer, tmp1) 152 | j.Mul(j, bigRadix) 153 | } 154 | 155 | tmpval := answer.Bytes() 156 | 157 | var numZeros int 158 | for numZeros = 0; numZeros < len(b); numZeros++ { 159 | if b[numZeros] != alphabet[0] { 160 | break 161 | } 162 | } 163 | flen := numZeros + len(tmpval) 164 | val := make([]byte, flen, flen) 165 | copy(val[numZeros:], tmpval) 166 | 167 | return val 168 | } 169 | --------------------------------------------------------------------------------