Packages

A flexible library for generating human-readable, memorable identifiers

Current section

Files

Jump to
memorable_ids lib memorable_ids.ex
Raw

lib/memorable_ids.ex

defmodule MemorableIds do
@moduledoc """
Memorable ID Generator
A flexible library for generating human-readable, memorable identifiers.
Uses combinations of adjectives, nouns, verbs, adverbs, and prepositions
with optional numeric/custom suffixes.
"""
alias MemorableIds.Dictionary
@doc """
Generate a memorable ID
## Options
* `:components` - Number of word components (1-5, default: 2)
* `:suffix` - Suffix generator function (default: nil)
* `:separator` - Separator between parts (default: "-")
## Examples
iex> id = MemorableIds.generate()
iex> String.contains?(id, "-")
true
iex> id = MemorableIds.generate(%{components: 3})
iex> length(String.split(id, "-"))
3
iex> id = MemorableIds.generate(%{components: 2, suffix: &MemorableIds.default_suffix/0})
iex> String.match?(id, ~r/.*-\\d{3}$/)
true
iex> id = MemorableIds.generate(%{components: 2, separator: "_"})
iex> String.contains?(id, "_")
true
"""
def generate(options \\ %{}) do
components = Map.get(options, :components, 2)
suffix = Map.get(options, :suffix, nil)
separator = Map.get(options, :separator, "-")
if components < 1 or components > 5 do
raise ArgumentError, "Components must be between 1 and 5"
end
component_generators = [
# 0: adjective
fn -> Dictionary.random_word(:adjectives) end,
# 1: noun
fn -> Dictionary.random_word(:nouns) end,
# 2: verb
fn -> Dictionary.random_word(:verbs) end,
# 3: adverb
fn -> Dictionary.random_word(:adverbs) end,
# 4: preposition
fn -> Dictionary.random_word(:prepositions) end
]
# Generate requested number of components
parts =
0..(components - 1)
|> Enum.map(fn i -> Enum.at(component_generators, i).() end)
# Add suffix if provided
final_parts =
if suffix && is_function(suffix, 0) do
case suffix.() do
nil -> parts
suffix_value -> parts ++ [suffix_value]
end
else
parts
end
Enum.join(final_parts, separator)
end
@doc """
Default suffix generator - random 3-digit number
## Examples
iex> suffix = MemorableIds.default_suffix()
iex> String.match?(suffix, ~r/^\\d{3}$/)
true
iex> suffix = MemorableIds.default_suffix()
iex> String.length(suffix)
3
"""
def default_suffix do
(:rand.uniform(1000) - 1)
|> Integer.to_string()
|> String.pad_leading(3, "0")
end
@doc """
Parse a memorable ID back to its components
## Examples
iex> MemorableIds.parse("cute-rabbit-042")
%{components: ["cute", "rabbit"], suffix: "042"}
iex> MemorableIds.parse("large-fox-swim")
%{components: ["large", "fox", "swim"], suffix: nil}
iex> MemorableIds.parse("cute_rabbit_123", "_")
%{components: ["cute", "rabbit"], suffix: "123"}
"""
def parse(id, separator \\ "-") do
parts = String.split(id, separator)
# Last part is likely suffix if it's numeric
case List.last(parts) do
nil ->
%{components: [], suffix: nil}
last_part ->
if Regex.match?(~r/^\d+$/, last_part) do
components = Enum.drop(parts, -1)
%{components: components, suffix: last_part}
else
%{components: parts, suffix: nil}
end
end
end
@doc """
Calculate total possible combinations for given configuration
## Examples
iex> combinations = MemorableIds.calculate_combinations(2)
iex> stats = MemorableIds.Dictionary.stats()
iex> combinations == stats.adjectives * stats.nouns
true
iex> combinations = MemorableIds.calculate_combinations(2, 1000)
iex> stats = MemorableIds.Dictionary.stats()
iex> combinations == stats.adjectives * stats.nouns * 1000
true
iex> combinations = MemorableIds.calculate_combinations(3)
iex> stats = MemorableIds.Dictionary.stats()
iex> combinations == stats.adjectives * stats.nouns * stats.verbs
true
"""
def calculate_combinations(components \\ 2, suffix_range \\ 1) do
stats = Dictionary.stats()
component_sizes = [
stats.adjectives,
stats.nouns,
stats.verbs,
stats.adverbs,
stats.prepositions
]
total =
0..(components - 1)
|> Enum.reduce(1, fn i, acc ->
acc * Enum.at(component_sizes, i)
end)
total * suffix_range
end
@doc """
Calculate collision probability using Birthday Paradox
## Examples
iex> total_combinations = MemorableIds.calculate_combinations(2)
iex> prob = MemorableIds.calculate_collision_probability(total_combinations, 100)
iex> is_float(prob) and prob >= 0.0 and prob <= 1.0
true
iex> total_combinations = MemorableIds.calculate_combinations(3)
iex> prob = MemorableIds.calculate_collision_probability(total_combinations, 10000)
iex> is_float(prob) and prob >= 0.0 and prob <= 1.0
true
"""
def calculate_collision_probability(total_combinations, generated_ids) do
cond do
generated_ids >= total_combinations ->
1.0
generated_ids <= 1 ->
0.0
true ->
# Birthday paradox approximation: 1 - e^(-n²/2N)
exponent = -(generated_ids * generated_ids) / (2 * total_combinations)
1.0 - :math.exp(exponent)
end
end
@doc """
Get collision analysis for different ID generation scenarios
## Examples
iex> analysis = MemorableIds.get_collision_analysis(2)
iex> expected = MemorableIds.calculate_combinations(2)
iex> analysis.total_combinations == expected
true
iex> analysis = MemorableIds.get_collision_analysis(2)
iex> is_list(analysis.scenarios)
true
iex> analysis = MemorableIds.get_collision_analysis(2)
iex> length(analysis.scenarios) >= 0
true
"""
def get_collision_analysis(components \\ 2, suffix_range \\ 1) do
total = calculate_combinations(components, suffix_range)
test_sizes = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000]
scenarios =
test_sizes
# Only show realistic scenarios
|> Enum.filter(fn size -> size < total * 0.8 end)
|> Enum.map(fn size ->
probability = calculate_collision_probability(total, size)
percentage =
(probability * 100)
|> Float.round(2)
|> case do
float when is_float(float) ->
# Ensure we always have 2 decimal places
:io_lib.format("~.2f", [float])
|> List.to_string()
|> Kernel.<>("%")
_ ->
"0.00%"
end
%{
ids: size,
probability: probability,
percentage: percentage
}
end)
%{
total_combinations: total,
scenarios: scenarios
}
end
@doc """
Collection of predefined suffix generators
## Examples
iex> generators = MemorableIds.suffix_generators()
iex> is_map(generators)
true
iex> generators = MemorableIds.suffix_generators()
iex> is_function(generators.number, 0)
true
"""
def suffix_generators do
%{
# Random 3-digit number (000-999)
number: &default_suffix/0,
# Random 4-digit number (0000-9999)
number4: fn ->
(:rand.uniform(10000) - 1)
|> Integer.to_string()
|> String.pad_leading(4, "0")
end,
# Random 2-digit hex (00-ff)
hex: fn ->
(:rand.uniform(256) - 1)
|> Integer.to_string(16)
|> String.downcase()
|> String.pad_leading(2, "0")
end,
# Last 4 digits of current timestamp
timestamp: fn ->
System.system_time(:millisecond)
|> Integer.to_string()
|> String.slice(-4..-1)
end,
# Random lowercase letter (a-z)
letter: fn ->
(?a + :rand.uniform(26) - 1)
|> List.wrap()
|> List.to_string()
end
}
end
end