UUIDの長さを圧縮する

# 目的
UUIDは様々なシステムで広く使われていますが、URLのパラメータとして渡そうとする時はやはりサイズが長いと思っています。
UUIDのバイナリー本質は16バイトの文字列なので、その素のデータを渡せば、かなり短縮できると考えています。

# 方法
## For JS
### UUID <=> Base64

```js
const uuid = require('uuid');

// UUID => Binary Array => Base64 Str(URL-safe)
const binArray = uuid.parse('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b');
// Uint8Array(16) [
//   110, 192, 189, 127,  17,
//   192,  67, 218, 151,  94,
//    42, 138, 217, 235, 174,
//    11
// ]

function base64encode(binaryArray) {
  const b64Str = btoa([...binaryArray].map(n => String.fromCharCode(n)).join(""));

  return b64Str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}

console.log(base64encode(binArray));
// 'bsC9fxHAQ9qXXiqK2euuCw'


// Base64 Str(URL-safe) => Binary Array => UUID
function base64decode(base64Str) {
  let str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
  if (str.length % 4) str += '='.repeat(4 - str.length % 4); // pad with '='
  const binArr = atob(str).split('').map(c => c.charCodeAt(0));

  return new Uint8Array(binArr);
}

const uuidBin = base64decode('bsC9fxHAQ9qXXiqK2euuCw');
// Uint8Array(16) [
//   110, 192, 189, 127,  17,
//   192,  67, 218, 151,  94,
//    42, 138, 217, 235, 174,
//    11
// ]

console.log(uuid.stringify(uuidBin));
// '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'
```

## For Ruby
### UUID <=> Base64
```rb
# NPM package "uuid" の "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b" を例として
uuid = "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b" # 36文字

require "base64"
## UUID => BinaryStr => Base64 (URL Safe)
### uuidをBinaryStrに変換
bin_str = [uuid.gsub("-", "")].pack("H*")
# => "n\xC0\xBD\x7F\x11\xC0C\xDA\x97^*\x8A\xD9\xEB\xAE\v"

### Base64 (URL Safe) に変換
b64_str = Base64.urlsafe_encode64(bin_str, padding: false)
# => "bsC9fxHAQ9qXXiqK2euuCw" # 22文字、約40%圧縮

## Base64 (URL Safe) => ByteArray => UUID
### Base64 (URL Safe) からバイト列に変換
byte_arr = Base64.urlsafe_decode64("bsC9fxHAQ9qXXiqK2euuCw").unpack("C*")
# => [110, 192, 189, 127, 17, 192, 67, 218, 151, 94, 42, 138, 217, 235, 174, 11]

### バイト列を元にUUID文字列を生成
# positions = [[3, 2, 1, 0], [5, 4], [7, 6], [8, 9], 10..15] # C#のGUIDの仕様
positions = [0..3, 4..5, 6..7, 8..9, 10..15] # NPM package "uuid" の仕様 ("uuid.stringify()" に準拠)
uuid_str = positions.map {| a | a.map {| n | "%02x" % byte_arr[n] }.join }.join("-")
# => "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b"
```

### Base64版UUID(もどき)を直接発行(厳密ではない)

```rb
require "securerandom"
require "base64"
# SecureRandom.uuid を使わず、Base64版を直接発行
SecureRandom.urlsafe_base64(16, false)
# => "h8JcfNDRGzoPc3pNiV88Bw"

# Base64版をUUIDに変換
arr = Base64.urlsafe_decode64("h8JcfNDRGzoPc3pNiV88Bw").unpack("C*")
[0..3, 4..5, 6..7, 8..9, 10..15].map {| a | a.map {| n | "%02x" % arr[n] }.join }.join("-")
# => "87c25c7c-d0d1-1b3a-0f73-7a4d895f3c07"
```

## For Python
### UUID <=> Base64

```py
import base64
import uuid

uuid_str = "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b"
uuid_bytes = uuid.UUID(uuid_str).bytes

[int(byte) for byte in uuid_bytes]                   
# [110, 192, 189, 127, 17, 192, 67, 218, 151, 94, 42, 138, 217, 235, 174, 11]

base64_str = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
# bsC9fxHAQ9qXXiqK2euuCw

padding = '=' * ((4 - len(base64_str) % 4) % 4)
uuid_bytes = base64.urlsafe_b64decode(base64_str + padding)

uuid.UUID(bytes=uuid_bytes)
# UUID('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b')
```

### メソッド化
下記コードはGitHub Copilotより生成

```py
import base64
import uuid

# UUID => Binary Array => Base64 Str (URL-safe)
def uuid_to_base64(uuid_str):
    # Convert UUID string to bytes
    uuid_bytes = uuid.UUID(uuid_str).bytes
    # Encode bytes to Base64 (URL-safe)
    base64_str = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
    return base64_str

# Base64 Str (URL-safe) => Binary Array => UUID
def base64_to_uuid(base64_str):
    # Decode Base64 (URL-safe) to bytes
    padding = '=' * ((4 - len(base64_str) % 4) % 4)  # Add padding if necessary
    uuid_bytes = base64.urlsafe_b64decode(base64_str + padding)
    # Convert bytes to UUID string
    return str(uuid.UUID(bytes=uuid_bytes))

# Example usage
original_uuid = "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b"
print("Original UUID:", original_uuid)

# Convert UUID to Base64
base64_str = uuid_to_base64(original_uuid)
print("Base64 (URL-safe):", base64_str)

# Convert Base64 back to UUID
converted_uuid = base64_to_uuid(base64_str)
print("Converted UUID:", converted_uuid)
```



# あわせ読み
- [MySQL に UUID のデータを 16 byte で保存する](https://qiita.com/moaikids/items/f987b4d5c1736335d103)

# 参考
## JS
- [JS/TSでバイナリ⇔BASE64はライブラリを使わなくても簡単に出来る](https://qiita.com/dojyorin/items/0b99d7e2e9febfe192c1)
- [Convert UUID to/from binary in Node](https://stackoverflow.com/a/79013406/27159826)
- [Remove trailing "=" when base64 encoding](https://stackoverflow.com/a/7139207/27159826)

## Ruby
- [Base64.#urlsafe_encode64](https://docs.ruby-lang.org/ja/latest/method/Base64/m/urlsafe_encode64.html)
- [MDN Web Docs 用語集 > Base64](https://developer.mozilla.org/ja/docs/Glossary/Base64)
- [module SecureRandom](https://docs.ruby-lang.org/ja/latest/class/SecureRandom.html#S_UUID)
- [singleton method SecureRandom.uuid](https://docs.ruby-lang.org/ja/latest/method/SecureRandom/s/uuid.html)
- [How to create ruby uuid from bytes?](https://stackoverflow.com/a/49073784/27159826)
- [Convert a string of 0-F into a byte array in Ruby](https://stackoverflow.com/a/3772446/27159826)
- [instance method String#unpack](https://docs.ruby-lang.org/ja/latest/method/String/i/unpack.html)

## Python
- [uuid --- RFC 4122 に従った UUID オブジェクト](https://docs.python.org/ja/3.13/library/uuid.html)