The ids package provides randomly generated ids in a number of different forms with different readability and sizes.
Random bytes
The random_id function generates random identifiers by generating bytes random bytes and converting to hexadecimal (so each byte becomes a pair of characters). Rather than use R’s random number stream we use the openssl package here.
ids::random_id()
## [1] "9aa4d8e7fd4fd2d2007c907f2f43262f"
All ids functions take n as the first argument to be the number of identifiers generated:
The default here is 16 bytes, each of which has 256 values (so 256^16 = 2^128 = 3.4e38 combinations). You can make these larger or smaller with the bytes argument:
The above look a lot like UUIDs but they are not actually UUIDs. The uuid package provides real UUIDs generated with libuuid, and the ids::uuid function provides an interface to that:
There are 1748 animal names and 8946 adjectives so each one you add increases the idenfier space by a factor of 8946. So for 1, 2, and 3 adjectives there are about 15.6 million, 140 billion and 1250 trillion possible combinations.
This is a much smaller space than the random identifiers above, but these are more readable and memorable.
Note that here, the random nunbers are coming from R’s random number stream so are affected by set.seed().
Because some of the animal and adjective names are very long (e.g. a quasiextraterritorial hexakosioihexekontahexaphobic queenalexandrasbirdwingbutterfly), in order to generate more readable/memorable identifiers it may be useful to restrict the length. Pass max_len in to do this.
The sentence function creates a sentence style identifier. This uses the approach described by Asana on their blog. This approach encodes 32 bits of information (so 2^32 ~= 4 billion possibilities) and in theory can be remapped to an integer if you really wanted to.
“proquints” are an identifier that tries to be information dense but still human readable and (somewhat) pronounceable; “proquint” stands for PRO-nouncable QUINT-uplets. They are introduced in https://arxiv.org/html/0901.4016
Proquints are formed by alternating consonant/vowel/consonant/vowel/consonant using a subset of both (16 consonants and 4 vowels). This yields 2^16 (65,536) possibilities per word. Words are always lower case and always separated by a hyphen. So with 4 words there are 2^64 combinations in 23 characters.
Proquints are also useful in that they can be tranlated with integers. The proquint kapop has integer value 25258
ids::proquint_to_int("kapop")
## [1] 25258
ids::int_to_proquint(25258)
## [1] "kapop"
This makes proquints suitable for creating human-pronouncable identifers out of things like ip addresses, integer primary keys, etc.
The function ids::int_to_proquint_word will translate between proquint words and integers (and are vectorised)
w <-ids::int_to_proquint_word(sample(2^16, 10) -1L)
w
whille ids::proquint_to_int and ids::int_to_proquint allows translation of multi-word proquints. Overflow is a real possibility; the maximum integer representable is only about r human_no(.Machine$integer.max) and the maximum floating point number of accuracy of 1 is about 9010 trillion – these are big numbers but fairly small proquints:
ids::int_to_proquint(.Machine$integer.max -1)
## [1] "luzuz-zuzuv"
ids::int_to_proquint(2 /.Machine$double.eps)
## [1] "babob-babab-babab-babab"
But if you had a 6 word proquint this would not work!
p <-ids::proquint(1, 6)
Too big for an integer:
ids::proquint_to_int(p)
## Error in proquint_combine(idx, len, as): Numeric overflow: cannot represent proquint as numeric
And too big for an numeric number:
ids::proquint_to_int(p, as ="numeric")
## Error in proquint_combine(idx, len, as): Numeric overflow: cannot represent proquint as numeric
To allow this, we use openssl’s bignum support:
ids::proquint_to_int(p, as ="bignum")
## [[1]]
## [b] 62023053643412691785669488225
This returns a list with one bignum (this is required to allow vectorisation).
Roll your own identifiers
The ids functions can build identifiers in the style of adjective_animal or sentence. It takes as input a list of strings. This works particularly well with the rcorpora package which includes lists of strings.
As a second example we can use the word lists in rcorpora to generate identifiers in the form <mood>_<scientist>, like “melancholic_darwin”. These are similar to the names of docker containers.
The scientists names contain spaces which is not going to work for us because ids won’t correctly translate all internal spaces to the requested style.
Which gives strings that are just letters (though there are a few non-ASCII characters here that may cause problems because string handling is just a big pile of awful)
ids/inst/doc/ids.Rmd 0000644 0001762 0000144 00000023437 13113306437 014010 0 ustar ligges users ---
title: "ids"
author: "Rich FitzJohn"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{ids}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
``` {r echo = FALSE, results = "hide"}
knitr::opts_chunk$set(error = FALSE)
human_no <- function(x) {
s <- log10(floor(x + 1))
p <- c(0, thousand = 3, million = 6, billion = 9, trillion = 12)
i <- s > p
j <- max(which(i))
str <- names(p)[j]
if (nzchar(str)) {
paste(signif(x / 10^p[[j]], 3), str)
} else {
as.character(x)
}
}
set.seed(1)
```
The `ids` package provides randomly generated ids in a number of
different forms with different readability and sizes.
## Random bytes
The `random_id` function generates random identifiers by generating
`bytes` random bytes and converting to hexadecimal (so each byte
becomes a pair of characters). Rather than use R's random number
stream we use the `openssl` package here.
``` {r }
ids::random_id()
```
All `ids` functions take `n` as the first argument to be the number
of identifiers generated:
``` {r }
ids::random_id(5)
```
The default here is 16 bytes, each of which has 256 values (so
256^16 = 2^128 = 3.4e38 combinations). You can make these larger or
smaller with the `bytes` argument:
``` {r }
ids::random_id(5, 8)
```
If `NULL` is provided as `n`, then a generating function is
returned (all ids functions do this):
``` {r }
f <- ids::random_id(NULL, 8)
f
```
This function sets all arguments except for `n`
``` {r }
f()
f(4)
```
## UUIDs
The above look a lot like UUIDs but they are not actually UUIDs.
The `uuid` package provides real UUIDs generated with libuuid, and
the `ids::uuid` function provides an interface to that:
``` {r }
ids::uuid()
```
As above, generate more than one UUID:
``` {r }
ids::uuid(4)
```
Generate time-based UUIDs:
``` {r }
ids::uuid(4, use_time = TRUE)
```
and optionally drop the hyphens:
``` {r }
ids::uuid(5, drop_hyphens = TRUE)
```
## Adjective animal
Generate (somewhat) human readable identifiers by combining one or
more adjectives with an animal name.
``` {r }
ids::adjective_animal()
```
The list of adjectives and animals comes from
[gfycat.com](http://gfycat.com), via
https://github.com/a-type/adjective-adjective-animal
Generate more than one identifier:
``` {r }
ids::adjective_animal(4)
```
Use more than one adjective for very long idenfiers
``` {r }
ids::adjective_animal(4, 3)
```
``` {r echo = FALSE, results = "hide"}
n1 <- length(ids:::gfycat_animals)
n2 <- length(ids:::gfycat_adjectives)
```
There are `r n1` animal names and `r n2` adjectives so each one you
add increases the idenfier space by a factor of `r n2`. So for 1,
2, and 3 adjectives there are about `r human_no(n1 * n2)`,
`r human_no(n1 * n2^2)` and `r human_no(n1 * n2^3)` possible combinations.
This is a much smaller space than the random identifiers above, but
these are more readable and memorable.
Note that here, the random nunbers are coming from R's random
number stream so are affected by `set.seed()`.
Because some of the animal and adjective names are very long
(e.g. a _quasiextraterritorial hexakosioihexekontahexaphobic
queenalexandrasbirdwingbutterfly_), in order to generate more
readable/memorable identifiers it may be useful to restrict the
length. Pass `max_len` in to do this.
``` {r }
ids::adjective_animal(4, max_len = 6)
```
A vector of length 2 here can be used to apply to the adjectives
and animal respectively:
``` {r }
ids::adjective_animal(20, max_len = c(5, Inf))
```
Note that this decreases the pool size and so increases the chance
of collisions.
In addition to snake_case, the default, the punctuation between
words can be changed to:
kebab-case:
``` {r }
ids::adjective_animal(1, 2, style = "kebab")
```
dot.case:
``` {r }
ids::adjective_animal(1, 2, style = "dot")
```
camel-case:
``` {r }
ids::adjective_animal(1, 2, style = "camel")
```
PascalCase:
``` {r }
ids::adjective_animal(1, 2, style = "pascal")
```
CONSTANT_CASE (aka SHOUTY_CASE)
``` {r }
ids::adjective_animal(1, 2, style = "constant")
```
or with spaces, lower case:
``` {r }
ids::adjective_animal(1, 2, style = "lower")
```
UPPER CASE
``` {r }
ids::adjective_animal(1, 2, style = "upper")
```
Sentence case
``` {r }
ids::adjective_animal(1, 2, style = "sentence")
```
Title Case
``` {r }
ids::adjective_animal(1, 2, style = "title")
```
Again, pass `n = NULL` here to create a generating function:
``` {r }
aa3 <- ids::adjective_animal(NULL, 3, style = "kebab", max_len = c(6, 8))
```
...which can be used to generate ids on demand.
``` {r }
aa3()
aa3(4)
```
## Random sentences
The `sentence` function creates a sentence style identifier. This
uses the approach described by Asana on [their
blog](https://blog.asana.com/2011/09/6-sad-squid-snuggle-softly).
This approach encodes 32 bits of information (so 2^32 ~= 4 billion
possibilities) and in theory can be remapped to an integer if you
really wanted to.
``` {r }
ids::sentence()
```
As with `adjective_animal`, the case can be changed:
``` {r }
ids::sentence(2, "dot")
```
If you would rather past tense for the verbs, then pass `past = TRUE`:
``` {r }
ids::sentence(4, past = TRUE)
```
## proquints
"proquints" are an identifier that tries to be information dense
but still human readable and (somewhat) pronounceable; "proquint"
stands for *PRO*-nouncable *QUINT*-uplets. They are introduced in
https://arxiv.org/html/0901.4016
`ids` can generate proquints:
``` {r }
ids::proquint(10)
```
By default it generates two-word proquints but that can be changed:
``` {r }
ids::proquint(5, 1)
ids::proquint(2, 4)
```
Proquints are formed by alternating
consonant/vowel/consonant/vowel/consonant using a subset of both
(16 consonants and 4 vowels). This yields 2^16 (65,536)
possibilities per word. Words are always lower case and always
separated by a hyphen. So with 4 words there are 2^64 combinations
in 23 characters.
Proquints are also useful in that they can be tranlated with
integers. The proquint `kapop` has integer value 25258
``` {r }
ids::proquint_to_int("kapop")
ids::int_to_proquint(25258)
```
This makes proquints suitable for creating human-pronouncable
identifers out of things like ip addresses, integer primary keys,
etc.
The function `ids::int_to_proquint_word` will translate between
proquint words and integers (and are vectorised)
``` {r }
w <- ids::int_to_proquint_word(sample(2^16, 10) - 1L)
w
```
and `ids::proquint_word_to_int` does the reverse
``` {r }
ids::proquint_word_to_int(w)
```
whille `ids::proquint_to_int` and `ids::int_to_proquint` allows
translation of multi-word proquints. Overflow is a real
possibility; the maximum integer representable is only about `r
human_no(.Machine$integer.max)` and the maximum floating point
number of accuracy of 1 is about `r human_no(2 /
.Machine$double.eps)` -- these are big numbers but fairly small
proquints:
``` {r }
ids::int_to_proquint(.Machine$integer.max - 1)
ids::int_to_proquint(2 / .Machine$double.eps)
```
But if you had a 6 word proquint this would not work!
``` {r }
p <- ids::proquint(1, 6)
```
Too big for an integer:
``` {r error = TRUE}
ids::proquint_to_int(p)
```
And too big for an numeric number:
``` {r error = TRUE}
ids::proquint_to_int(p, as = "numeric")
```
To allow this, we use `openssl`'s `bignum` support:
``` {r }
ids::proquint_to_int(p, as = "bignum")
```
This returns a *list* with one bignum (this is required to allow
vectorisation).
## Roll your own identifiers
The `ids` functions can build identifiers in the style of
`adjective_animal` or `sentence`. It takes as input a list of
strings. This works particularly well with the `rcorpora` package
which includes lists of strings.
Here is a list of Pokemon names:
``` {r }
pokemon <- tolower(rcorpora::corpora("games/pokemon")$pokemon$name)
length(pokemon)
```
...and here is a list of adjectives
``` {r }
adjectives <- tolower(rcorpora::corpora("words/adjs")$adjs)
length(adjectives)
```
So we have a total pool size of about `r human_no(length(adjectives) *
length(pokemon))`, which is not huge, but it is at least topical.
To generate one identifier:
``` {r }
ids::ids(1, adjectives, pokemon)
```
All the style-changing code is available:
``` {r }
ids::ids(10, adjectives, pokemon, style = "dot")
```
Better would be to wrap this so that the constants are not passed
around the whole time:
``` {r }
adjective_pokemon <- function(n = 1, style = "snake") {
pokemon <- tolower(rcorpora::corpora("games/pokemon")$pokemon$name)
adjectives <- tolower(rcorpora::corpora("words/adjs")$adjs)
ids::ids(n, adjectives, pokemon, style = style)
}
adjective_pokemon(10, "kebab")
```
As a second example we can use the word lists in rcorpora to
generate identifiers in the form `_`, like
"melancholic_darwin". These are similar to the names of
docker containers.
First the lists of names themselves:
``` {r }
moods <- tolower(rcorpora::corpora("humans/moods")$moods)
scientists <- tolower(rcorpora::corpora("humans/scientists")$scientists)
```
Moods include:
``` {r }
sample(moods, 10)
```
The scientists names contain spaces which is not going to work for
us because `ids` won't correctly translate all internal spaces to
the requested style.
``` {r }
sample(scientists, 10)
```
To hack around this we'll just take the last name from the list and
remove all hyphens:
``` {r }
scientists <- vapply(strsplit(sub("(-|jr\\.$)", "", scientists), " "),
tail, character(1), 1)
```
Which gives strings that are just letters (though there are a few
non-ASCII characters here that may cause problems because string
handling is just a big pile of awful)
``` {r }
sample(scientists, 10)
```
With the word lists, create an identifier:
``` {r }
ids::ids(1, moods, scientists)
```
Or pass `NULL` for `n` and create a function:
``` {r }
sci_id <- ids::ids(NULL, moods, scientists, style = "kebab")
```
which takes just the number of identifiers to generate as an argument
``` {r }
sci_id(10)
```
ids/inst/doc/ids.R 0000644 0001762 0000144 00000015127 13113306437 013464 0 ustar ligges users ## ----echo = FALSE, results = "hide"--------------------------------------
knitr::opts_chunk$set(error = FALSE)
human_no <- function(x) {
s <- log10(floor(x + 1))
p <- c(0, thousand = 3, million = 6, billion = 9, trillion = 12)
i <- s > p
j <- max(which(i))
str <- names(p)[j]
if (nzchar(str)) {
paste(signif(x / 10^p[[j]], 3), str)
} else {
as.character(x)
}
}
set.seed(1)
## ------------------------------------------------------------------------
ids::random_id()
## ------------------------------------------------------------------------
ids::random_id(5)
## ------------------------------------------------------------------------
ids::random_id(5, 8)
## ------------------------------------------------------------------------
f <- ids::random_id(NULL, 8)
f
## ------------------------------------------------------------------------
f()
f(4)
## ------------------------------------------------------------------------
ids::uuid()
## ------------------------------------------------------------------------
ids::uuid(4)
## ------------------------------------------------------------------------
ids::uuid(4, use_time = TRUE)
## ------------------------------------------------------------------------
ids::uuid(5, drop_hyphens = TRUE)
## ------------------------------------------------------------------------
ids::adjective_animal()
## ------------------------------------------------------------------------
ids::adjective_animal(4)
## ------------------------------------------------------------------------
ids::adjective_animal(4, 3)
## ----echo = FALSE, results = "hide"--------------------------------------
n1 <- length(ids:::gfycat_animals)
n2 <- length(ids:::gfycat_adjectives)
## ------------------------------------------------------------------------
ids::adjective_animal(4, max_len = 6)
## ------------------------------------------------------------------------
ids::adjective_animal(20, max_len = c(5, Inf))
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "kebab")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "dot")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "camel")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "pascal")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "constant")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "lower")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "upper")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "sentence")
## ------------------------------------------------------------------------
ids::adjective_animal(1, 2, style = "title")
## ------------------------------------------------------------------------
aa3 <- ids::adjective_animal(NULL, 3, style = "kebab", max_len = c(6, 8))
## ------------------------------------------------------------------------
aa3()
aa3(4)
## ------------------------------------------------------------------------
ids::sentence()
## ------------------------------------------------------------------------
ids::sentence(2, "dot")
## ------------------------------------------------------------------------
ids::sentence(4, past = TRUE)
## ------------------------------------------------------------------------
ids::proquint(10)
## ------------------------------------------------------------------------
ids::proquint(5, 1)
ids::proquint(2, 4)
## ------------------------------------------------------------------------
ids::proquint_to_int("kapop")
ids::int_to_proquint(25258)
## ------------------------------------------------------------------------
w <- ids::int_to_proquint_word(sample(2^16, 10) - 1L)
w
## ------------------------------------------------------------------------
ids::proquint_word_to_int(w)
## ------------------------------------------------------------------------
ids::int_to_proquint(.Machine$integer.max - 1)
ids::int_to_proquint(2 / .Machine$double.eps)
## ------------------------------------------------------------------------
p <- ids::proquint(1, 6)
## ----error = TRUE--------------------------------------------------------
ids::proquint_to_int(p)
## ----error = TRUE--------------------------------------------------------
ids::proquint_to_int(p, as = "numeric")
## ------------------------------------------------------------------------
ids::proquint_to_int(p, as = "bignum")
## ------------------------------------------------------------------------
pokemon <- tolower(rcorpora::corpora("games/pokemon")$pokemon$name)
length(pokemon)
## ------------------------------------------------------------------------
adjectives <- tolower(rcorpora::corpora("words/adjs")$adjs)
length(adjectives)
## ------------------------------------------------------------------------
ids::ids(1, adjectives, pokemon)
## ------------------------------------------------------------------------
ids::ids(10, adjectives, pokemon, style = "dot")
## ------------------------------------------------------------------------
adjective_pokemon <- function(n = 1, style = "snake") {
pokemon <- tolower(rcorpora::corpora("games/pokemon")$pokemon$name)
adjectives <- tolower(rcorpora::corpora("words/adjs")$adjs)
ids::ids(n, adjectives, pokemon, style = style)
}
adjective_pokemon(10, "kebab")
## ------------------------------------------------------------------------
moods <- tolower(rcorpora::corpora("humans/moods")$moods)
scientists <- tolower(rcorpora::corpora("humans/scientists")$scientists)
## ------------------------------------------------------------------------
sample(moods, 10)
## ------------------------------------------------------------------------
sample(scientists, 10)
## ------------------------------------------------------------------------
scientists <- vapply(strsplit(sub("(-|jr\\.$)", "", scientists), " "),
tail, character(1), 1)
## ------------------------------------------------------------------------
sample(scientists, 10)
## ------------------------------------------------------------------------
ids::ids(1, moods, scientists)
## ------------------------------------------------------------------------
sci_id <- ids::ids(NULL, moods, scientists, style = "kebab")
## ------------------------------------------------------------------------
sci_id(10)
ids/tests/ 0000755 0001762 0000144 00000000000 12612173545 012201 5 ustar ligges users ids/tests/testthat.R 0000644 0001762 0000144 00000000062 12612173545 014162 0 ustar ligges users library(testthat)
library(ids)
test_check("ids")
ids/tests/testthat/ 0000755 0001762 0000144 00000000000 13113501667 014036 5 ustar ligges users ids/tests/testthat/test-sentence.R 0000644 0001762 0000144 00000001627 13007061140 016735 0 ustar ligges users context("sentence")
test_that("basic", {
re <- "^[0-9]+(_[a-z]+){4}$"
res <- sentence()
expect_is(res, "character")
expect_equal(length(res), 1)
expect_match(res, re)
})
test_that("tense", {
verb <- vapply(strsplit(sentence(100), "_", fixed = TRUE),
function(x) x[[4L]], character(1))
expect_true(all(verb %in% asana_verbs_present))
verb <- vapply(strsplit(sentence(100, past = TRUE), "_", fixed = TRUE),
function(x) x[[4L]], character(1))
expect_true(all(verb %in% asana_verbs_past))
})
test_that("functional interface", {
f <- sentence(NULL, style = "kebab", past = TRUE)
expect_is(f, "function")
re <- "^[0-9]+(-[a-z]+){4}$"
expect_match(f(), re)
res <- f(100)
expect_true(all(grepl(re, res)))
verb <- vapply(strsplit(res, "-", fixed = TRUE),
function(x) x[[4L]], character(1))
expect_true(all(verb %in% asana_verbs_past))
})
ids/tests/testthat/test-adjective-animal.R 0000644 0001762 0000144 00000004371 13107317605 020340 0 ustar ligges users context("adjective animal")
test_that("data", {
expect_false(any(grepl("\\s", gfycat_animals)))
expect_false(any(grepl("\\s", gfycat_adjectives)))
})
test_that("basic", {
res <- adjective_animal()
expect_is(res, "character")
expect_equal(length(res), 1)
expect_match(res, "^[a-z]+_[a-z]+$")
})
test_that("length", {
expect_equal(length(adjective_animal(5)), 5)
expect_equal(adjective_animal(0), character(0))
})
test_that("style", {
expect_match(adjective_animal(style = "Pascal"), "^[A-Z][a-z]+[A-Z][a-z]+$")
expect_match(adjective_animal(style = "camel"), "^[a-z]+[A-Z][a-z]+$")
expect_match(adjective_animal(style = "snake"), "^[a-z]+_[a-z]+$")
expect_match(adjective_animal(style = "kebab"), "^[a-z]+-[a-z]+$")
expect_match(adjective_animal(style = "lower"), "^[a-z]+ [a-z]+$")
expect_match(adjective_animal(style = "upper"), "^[A-Z]+ [A-Z]+$")
expect_match(adjective_animal(style = "constant"), "^[A-Z]+_[A-Z]+$")
expect_match(adjective_animal(style = "title"), "^[A-Z][a-z]+ [A-Z][a-z]+$")
expect_match(adjective_animal(style = "sentence"), "^[A-Z][a-z]+ [a-z]+$")
})
test_that("restrict length", {
tmp <- strsplit(adjective_animal(200, max_len = 5), "_", fixed = TRUE)
expect_lte(max(sapply(tmp, nchar)), 5)
tmp <- strsplit(adjective_animal(200, max_len = c(5, 10)), "_", fixed = TRUE)
tmp <- apply(sapply(tmp, nchar), 1, max)
expect_lte(tmp[[1]], 5)
expect_lte(tmp[[2]], 10)
expect_gt(tmp[[2]], tmp[[1]])
})
test_that("restrict length error cases", {
expect_error(adjective_animal(max_len = 2),
"max_len must be at least 3")
expect_error(adjective_animal(max_len = -2),
"max_len must be at least 3")
## This is ignored -- perhaps it should not be?
expect_silent(adjective_animal(max_len = numeric(0)))
expect_error(adjective_animal(max_len = c(5, 6, 7)),
"max_len must be length 1 or 2")
expect_error(adjective_animal(n_adjectives = 2, max_len = c(5, 6, 7)),
"max_len must be length 1 or 2")
})
test_that("functional interface", {
f <- adjective_animal(NULL, max_len = 10, style = "kebab", n_adjectives = 2)
expect_is(f, "function")
re <- "^[a-z]{2,10}-[a-z]{2,10}-[a-z]{2,10}$"
expect_match(f(), re)
x <- f(100)
expect_true(all(grepl(re, x)))
})
ids/tests/testthat/test-ids.R 0000644 0001762 0000144 00000001241 13007061140 015700 0 ustar ligges users context("ids")
test_that("animals", {
re_snake2 <- "^([a-z]+)_([a-z]+)$"
re_camel2 <- "^([a-z]+)([A-Z][a-z]+)$"
re_snake3 <- "^([a-z]+)_([a-z]+)_([a-z]+)$"
res <- adjective_animal()
expect_match(res, re_snake2)
expect_match(adjective_animal(1, 2), re_snake3)
expect_match(adjective_animal(style = "camel"), re_camel2)
res2 <- adjective_animal(2, style = "camel")
expect_match(res2, re_camel2)
expect_true(all(sub(re_camel2, "\\1", res2) %in% gfycat_adjectives))
expect_true(all(tolower(sub(re_camel2, "\\2", res2)) %in% gfycat_animals))
## Smoke test:
for (s in names(cases())) {
expect_is(adjective_animal(style = s), "character")
}
})
ids/tests/testthat/test-proquint.R 0000644 0001762 0000144 00000026144 13113306233 017016 0 ustar ligges users context("proquint")
test_that("word conversions", {
i <- 10^(0:4)
## From Python:
cmp <- c("babad", "babap", "badoh", "bazom", "fisib")
## Ours
w <- int_to_proquint_word(i)
expect_identical(w, cmp)
j <- proquint_word_to_int(w, TRUE)
expect_equal(j, i)
expect_is(j, "integer")
k <- proquint_word_to_int(w, FALSE)
expect_identical(j, k)
expect_is(k, "integer")
})
## This is exhaustive, because it's not that slow to do everything:
test_that("word conversions - exhaustive", {
i <- seq_len(PROQUINT_WORD) - 1L
w <- int_to_proquint_word(i)
expect_equal(w, cache$proquint_words)
expect_equal(int_to_proquint_word(i, FALSE), cache$proquint_words)
## word -> index
expect_equal(proquint_word_to_int(w), i)
expect_equal(proquint_word_to_int(w, FALSE), i)
})
test_that("proquint conversions", {
## Then start on the multi-word cases
known <- c("babad" = 1,
"babap" = 10,
"badoh" = 100,
"bazom" = 1000,
"fisib" = 10000,
"babad-mipob" = 100000,
"babaz-hanab" = 1000000,
"bafim-nipab" = 10000000,
"biluj-vahab" = 100000000,
"govip-somab" = 1000000000)
expect_equal(proquint_to_int(names(known), use_cache = TRUE),
unname(known))
expect_equal(proquint_to_int(names(known), use_cache = FALSE),
unname(known))
## Then the reverse:
expect_equal(int_to_proquint(unname(known), TRUE), names(known))
expect_equal(int_to_proquint(unname(known), FALSE), names(known))
})
## Around the transition:
test_that("1-2 word transition corner cases", {
i <- (PROQUINT_WORD - 2):(PROQUINT_WORD + 2)
expected <- c("zuzuv", "zuzuz", "babad-babab", "babad-babad", "babad-babaf")
expect_equal(int_to_proquint(i), expected)
expect_equal(int_to_proquint(i, FALSE), expected)
expect_equal(proquint_to_int(expected, use_cache = TRUE), i)
expect_equal(proquint_to_int(expected, use_cache = FALSE), i)
})
test_that("sampled words do not depend on cache", {
set.seed(1)
w1 <- proquint_sample_words(10, TRUE)
set.seed(1)
w2 <- proquint_sample_words(10, FALSE)
expect_identical(w1, w2)
})
test_that("generate openssl random numbers", {
i <- rand_i16(1, TRUE)
expect_is(i, "integer")
expect_gte(i, 0L)
expect_lt(i, 2^16)
j <- rand_i16(10, TRUE)
expect_is(j, "integer")
expect_equal(length(j), 10)
expect_true(all(j >= 0L))
expect_true(all(j < 2^16))
set.seed(1)
i <- rand_i16(100, TRUE)
set.seed(1)
j <- rand_i16(100, TRUE)
expect_false(identical(i, j))
i <- rand_i16(2^16 * 16, TRUE)
n <- tabulate(i + 1, 2^16)
expect_true(all(i >= 0L))
expect_true(all(i < 2^16))
})
test_that("openssl random identifiers", {
set.seed(1)
w1 <- proquint(100, use_openssl = TRUE)
set.seed(1)
w2 <- proquint(100, use_openssl = TRUE)
expect_false(identical(w1, w2))
})
test_that("bad type on conversion", {
expect_error(int_to_proquint("one"), "Invalid type for 'x'")
expect_error(int_to_proquint(TRUE), "Invalid type for 'x'")
expect_error(int_to_proquint(1+4i), "Invalid type for 'x'")
})
test_that("word -> int translation: missing values", {
i <- 12345L
w <- int_to_proquint_word(i)
expect_identical(proquint_word_to_int(NA), NA_integer_)
expect_identical(proquint_word_to_int(NA_character_), NA_integer_)
expect_identical(proquint_word_to_int(c(NA_character_, w)),
c(NA_integer_, i))
expect_identical(proquint_word_to_int(c(NA, w, w, NA)),
c(NA_integer_, i, i, NA_integer_))
})
test_that("int -> word translation: missing values", {
i <- 12345L
w <- int_to_proquint_word(i)
expect_identical(int_to_proquint_word(NA), NA_character_)
expect_identical(int_to_proquint_word(NA_integer_), NA_character_)
expect_identical(int_to_proquint_word(c(NA_integer_, i)),
c(NA_character_, w))
expect_identical(int_to_proquint_word(c(NA, i, i, NA)),
c(NA_character_, w, w, NA_character_))
})
test_that("identifier -> int translation: missing values", {
i <- 12345L
w <- int_to_proquint_word(i)
ib <- openssl::bignum(i)
expect_identical(proquint_to_int(NA, "integer"), NA_integer_)
expect_identical(proquint_to_int(NA, "numeric"), NA_real_)
expect_identical(proquint_to_int(NA, "bignum"), list(NULL))
expect_identical(proquint_to_int(NA_character_, "integer"), NA_integer_)
expect_identical(proquint_to_int(NA_character_, "numeric"), NA_real_)
expect_identical(proquint_to_int(NA_character_, "bignum"), list(NULL))
expect_identical(proquint_to_int(c(NA, w), "integer"), c(NA_integer_, i))
expect_identical(proquint_to_int(c(NA, w), "numeric"), c(NA_real_, i))
expect_identical(proquint_to_int(c(NA, w), "bignum"),
list(NULL, ib))
expect_identical(proquint_to_int(c(NA, w, w, NA), "integer"),
c(NA_integer_, i, i, NA_integer_))
expect_identical(proquint_to_int(c(NA, w, w, NA), "numeric"),
c(NA_real_, i, i, NA_real_))
expect_identical(proquint_to_int(c(NA, w, w, NA), "bignum"),
list(NULL, ib, ib, NULL))
})
test_that("int -> identifier translation: missing values", {
i <- 12345L
w <- int_to_proquint_word(i)
ib <- openssl::bignum(i)
expect_identical(int_to_proquint(NA), NA_character_)
expect_identical(int_to_proquint(NA_integer_), NA_character_)
expect_identical(int_to_proquint(NA_real_), NA_character_)
expect_identical(int_to_proquint(list(NULL)), NA_character_)
expect_identical(int_to_proquint(c(NA_integer_, i)), c(NA, w))
expect_identical(int_to_proquint(c(NA_real_, i)), c(NA, w))
expect_identical(int_to_proquint(list(NULL, ib)), c(NA, w))
expect_identical(int_to_proquint(c(NA_integer_, i, i, NA_integer_)),
c(NA_character_, w, w, NA_character_))
expect_identical(int_to_proquint(c(NA_real_, i, i, NA_real_)),
c(NA_character_, w, w, NA_character_))
expect_identical(int_to_proquint(list(NULL, ib, ib, NULL)),
c(NA_character_, w, w, NA_character_))
})
test_that("identifier: 0", {
expect_identical(proquint(0), character(0))
})
test_that("identifier: 1", {
x <- proquint(1)
expect_is(x, "character")
expect_equal(length(x), 1)
re <- sprintf("^%s-%s$", PROQUINT_RE_WORD, PROQUINT_RE_WORD)
expect_match(x, re)
})
test_that("identifier: n", {
n <- 10
x <- proquint(n)
expect_is(x, "character")
expect_equal(length(x), n)
re <- sprintf("^%s-%s$", PROQUINT_RE_WORD, PROQUINT_RE_WORD)
expect_match(x, re, all = TRUE)
})
test_that("vary word length", {
re3 <- sprintf("^%s-%s-%s$",
PROQUINT_RE_WORD, PROQUINT_RE_WORD, PROQUINT_RE_WORD)
expect_match(proquint(1, 3), re3)
expect_match(proquint(1, 1), PROQUINT_RE1)
})
test_that("functional interface", {
re3 <- sprintf("^%s-%s-%s$",
PROQUINT_RE_WORD, PROQUINT_RE_WORD, PROQUINT_RE_WORD)
f <- proquint(NULL, 3)
expect_is(f, "function")
expect_match(f(), re3)
expect_equal(length(f()), 1)
expect_equal(length(f(10)), 10)
})
## -- below here is all boring error handling stuff
## Validate words
test_that("invalid word -> int", {
expect_error(proquint_word_to_int("hello!"),
"Invalid proquint word: 'hello!'")
expect_error(proquint_word_to_int(c("hello!", "babad"),),
"Invalid proquint word: 'hello!'")
expect_error(proquint_word_to_int(c("hello!", "babad", "yo!"),),
"Invalid proquint word: 'hello!', 'yo!'")
expect_error(proquint_word_to_int(1),
"Invalid proquint word: '1'")
})
## Validate word indexes
test_that("invalid word -> int", {
expect_error(int_to_proquint_word(-1),
"Invalid proquint word index (out of range): -1",
fixed = TRUE)
expect_error(int_to_proquint_word(c(-1, 1)),
"Invalid proquint word index (out of range): -1",
fixed = TRUE)
expect_error(int_to_proquint_word(c(-1, 1, 65536)),
"Invalid proquint word index (out of range): -1, 65536",
fixed = TRUE)
expect_error(int_to_proquint_word("babad"),
"Invalid proquint word index (not numeric)",
fixed = TRUE)
})
test_that("zero length input", {
expect_equal(proquint_word_to_int(NULL), integer(0))
expect_equal(proquint_word_to_int(character(0)), integer(0))
## This is not terrific but it is what it is:
expect_equal(proquint_word_to_int(integer(0)), integer(0))
expect_equal(int_to_proquint_word(NULL), character(0))
expect_equal(int_to_proquint_word(integer(0)), character(0))
})
test_that("zero length input", {
expect_identical(proquint_to_int(NULL, "integer"), integer(0))
expect_identical(proquint_to_int(NULL, "numeric"), numeric(0))
expect_identical(proquint_to_int(NULL, "bignum"), list())
expect_identical(proquint_to_int(character(0), "integer"), integer(0))
expect_identical(proquint_to_int(character(0), "numeric"), numeric(0))
expect_identical(proquint_to_int(character(0), "bignum"), list())
})
test_that("zero length input", {
expect_identical(int_to_proquint(NULL), character(0))
expect_identical(int_to_proquint(logical(0)), character(0))
expect_identical(int_to_proquint(integer(0)), character(0))
expect_identical(int_to_proquint(numeric(0)), character(0))
expect_identical(int_to_proquint(list()), character(0))
})
test_that("invalid identifier", {
expect_error(proquint_to_int(TRUE), "Expected a character vector for 'p'")
expect_error(proquint_to_int("aaaaa"), "Invalid identifier: 'aaaaa'")
})
test_that("proquint to int, varying formats", {
i <- c(1, 10, 100, 1000, 10000)
w <- int_to_proquint_word(i)
expect_identical(proquint_to_int(w, "integer"), as.integer(i))
expect_identical(proquint_to_int(w, "numeric"), as.numeric(i))
expect_identical(proquint_to_int(w, "bignum"),
lapply(i, openssl::bignum))
})
test_that("int to proquint, varying formats", {
i <- c(1, 10, 100, 1000, 10000)
w <- int_to_proquint_word(i)
expect_identical(int_to_proquint(as.integer(i)), w)
expect_identical(int_to_proquint(as.numeric(i)), w)
expect_identical(int_to_proquint(lapply(i, openssl::bignum)), w)
})
test_that("integer overflow", {
big <- .Machine$integer.max * 2
pq <- int_to_proquint(big)
expect_is(pq, "character")
expect_error(proquint_to_int(pq, "integer"),
"Integer overflow: cannot represent proquint as integer")
expect_identical(proquint_to_int(pq, "numeric"), big)
expect_identical(proquint_to_int(pq, "bignum"),
list(openssl::bignum(big)))
})
test_that("numeric overflow", {
pow <- log2(2/.Machine$double.eps) + 1
big <- openssl::bignum(2)^pow + 1
pq <- int_to_proquint(big)
expect_is(pq, "character")
expect_error(proquint_to_int(pq, "integer"),
"Integer overflow: cannot represent proquint as integer")
expect_error(proquint_to_int(pq, "numeric"),
"Numeric overflow: cannot represent proquint as numeric")
expect_identical(proquint_to_int(pq, "bignum"),
list(openssl::bignum(big)))
})
## This supports my temporary as_integer_bignum function:
test_that("as_integer_bignum", {
x <- c(0L, 255L, 256L, 1000L, 200000L, .Machine$integer.max)
for (i in x) {
expect_identical(as_integer_bignum(openssl::bignum(i)), i)
}
})
ids/tests/testthat/test-random.R 0000644 0001762 0000144 00000001716 13007061140 016410 0 ustar ligges users context("random")
re_random <- function(len) {
sprintf("^[[:xdigit:]]{%s}", len)
}
test_that("uuid", {
expect_equal(random_id(0), character(0))
x1 <- random_id(1)
expect_equal(length(x1), 1)
expect_is(x1, "character")
expect_match(x1, re_random(32))
x10 <- random_id(10)
expect_equal(length(x10), 10)
expect_is(x10, "character")
expect_true(all(grepl(re_random(32), x10)))
})
test_that("bind args", {
f <- random_id(NULL, bytes = 5)
expect_is(f, "function")
expect_equal(as.list(formals(f)), list(n = 1))
expect_match(f(), re_random(10))
expect_equal(nchar(f()), 10)
})
test_that("r byte stream", {
set.seed(1)
id1 <- random_id()
set.seed(1)
expect_false(id1 == random_id())
set.seed(1)
expect_false(id1 == random_id(NULL)())
set.seed(1)
id1 <- random_id(use_openssl = FALSE)
set.seed(1)
expect_true(id1 == random_id(use_openssl = FALSE))
set.seed(1)
expect_true(id1 == random_id(NULL, use_openssl = FALSE)())
})
ids/tests/testthat/test-case.R 0000644 0001762 0000144 00000002534 13007061140 016042 0 ustar ligges users context("case")
test_that("style match", {
expect_equal(check_style("p")$name, "pascal")
expect_equal(check_style("pascal")$name, "pascal")
expect_equal(check_style("pascalcase")$name, "pascal")
expect_equal(check_style("PascalCase")$name, "pascal")
expect_equal(check_style("Pascal-Case")$name, "pascal")
expect_equal(check_style("Pascal_Case")$name, "pascal")
})
test_that("camel", {
expect_equal(toupper_camel(character(0)), character(0))
expect_equal(toupper_camel("aaa"), "aaa")
expect_equal(toupper_camel(c("aaa", "bbb")), c("aaa", "Bbb"))
expect_equal(toupper_camel(rbind(c("aaa", "bbb"),
c("ccc", "ddd"))),
rbind(c("aaa", "Bbb"),
c("ccc", "Ddd")))
})
test_that("sentence", {
tr <- make_combine("sentence")
expect_equal(tr(c("foo", "bar")), "Foo bar")
expect_equal(tr(rbind(c("foo", "bar"),
c("another", "sentence"))),
c("Foo bar",
"Another sentence"))
})
test_that("error cases", {
expect_error(check_style(NULL), "must be a character vector")
expect_error(check_style(1), "must be a character vector")
expect_error(check_style(character(0)), "must be a scalar")
expect_error(check_style(c("camel", "upper")), "must be a scalar")
expect_error(check_style("unknown"), "Invalid style 'unknown'")
})
ids/tests/testthat/test-uuid.R 0000644 0001762 0000144 00000001320 13007061140 016065 0 ustar ligges users context("uuid")
RE_UUID <- "^[[:xdigit:]]{8}-([[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$"
test_that("uuid", {
expect_equal(uuid(0), character(0))
x1 <- uuid(1)
expect_equal(length(x1), 1)
expect_is(x1, "character")
expect_match(x1, RE_UUID)
x10 <- uuid(10)
expect_equal(length(x10), 10)
expect_is(x10, "character")
expect_true(all(grepl(RE_UUID, x10)))
expect_true(all(grepl("^[[:xdigit:]]{32}$", uuid(10, drop_hyphens = TRUE))))
expect_match(uuid(use_time = TRUE), RE_UUID)
})
test_that("bind args", {
f <- uuid(NULL)
expect_is(f, "function")
expect_equal(as.list(formals(f)), list(n = 1))
expect_match(f(), RE_UUID)
g <- uuid(NULL, TRUE)
expect_match(g(), "^[[:xdigit:]]{32}$")
})
ids/NAMESPACE 0000644 0001762 0000144 00000000434 13113306233 012244 0 ustar ligges users # Generated by roxygen2: do not edit by hand
export(adjective_animal)
export(ids)
export(int_to_proquint)
export(int_to_proquint_word)
export(proquint)
export(proquint_to_int)
export(proquint_word_to_int)
export(random_id)
export(sentence)
export(uuid)
importFrom(uuid,UUIDgenerate)
ids/NEWS.md 0000644 0001762 0000144 00000000260 13113306233 012120 0 ustar ligges users # ids 1.1.0 (2017-05-22)
* Fix occasionally failing text (removes one animal from the pool)
* New identifier type "proquint"
# ids 1.0.0 (2016-11-03)
* Initial CRAN release
ids/R/ 0000755 0001762 0000144 00000000000 13113306233 011225 5 ustar ligges users ids/R/case.R 0000644 0001762 0000144 00000003372 13007061140 012265 0 ustar ligges users # PascalCase
# camelCase
# snake_case
# kebab-case
# Title Case
# lower case
# upper case
# CONSTANT_CASE
# Sentence case
check_style <- function(style) {
if (!is.character(style)) {
stop("style must be a character vector")
}
if (length(style) != 1L) {
stop("style must be a scalar")
}
pos <- cases()
i <- pmatch(sub("[_-]?case$", "", tolower(style)), names(pos))
if (is.na(i)) {
stop(sprintf("Invalid style '%s'", style))
}
ret <- pos[[i]]
ret$name <- names(pos)[[i]]
ret
}
make_combine <- function(style) {
dat <- check_style(style)
join <- dat$join
tr <- dat$tr
function(x) {
if (is.matrix(x)) {
apply(tr(x), 1, paste, collapse = join)
} else {
paste(tr(x), collapse = join)
}
}
}
toupper_initial <- function(x) {
substr(x, 1, 1) <- toupper(substr(x, 1, 1))
x
}
toupper_camel <- function(x) {
if (is.matrix(x)) {
x[, -1] <- toupper_initial(x[, -1])
} else {
x[-1] <- toupper_initial(x[-1])
}
x
}
toupper_sentence <- function(x) {
if (is.matrix(x)) {
x[, 1] <- toupper_initial(x[, 1])
} else {
x[1] <- toupper_initial(x[1])
}
x
}
toupper_pascal <- function(x) {
x[] <- toupper_initial(x)
x
}
cases <- function() {
list(snake = list(join = "_", tr = identity),
kebab = list(join = "-", tr = identity),
dot = list(join = ".", tr = identity),
camel = list(join = "", tr = toupper_camel),
pascal = list(join = "", tr = toupper_pascal),
lower = list(join = " ", tr = identity), # lower case already
upper = list(join = " ", tr = toupper),
title = list(join = " ", tr = toupper_pascal),
sentence = list(join = " ", tr = toupper_sentence),
constant = list(join = "_", tr = toupper))
}
ids/R/random.R 0000644 0001762 0000144 00000004211 13007061140 012623 0 ustar ligges users #' Random identifiers. By default this uses the \code{openssl}
#' package to produce a random set of bytes, and expresses that as a
#' hex character string. This does not affect R's random number
#' stream.
#'
#' @title Cryptographically generated random identifiers
#' @inheritParams ids
#'
#' @param bytes The number of bytes to include for each identifier.
#' The length of the returned identifiers will be twice this long
#' with each pair of characters representing a single byte.
#'
#' @param use_openssl A logical, indicating if we should use the
#' openssl for generating the random identifiers. The openssl
#' random bytestream is not affected by the state of the R random
#' number generator (e.g., via \code{\link{set.seed}}) so may not be
#' suitable for use where reproducibility is important. The speed
#' should be very similar for both approaches.
#'
#' @export
#' @author Rich FitzJohn
#' @examples
#' # Generate a random id:
#' random_id()
#'
#' # Generate 10 of them!
#' random_id(10)
#'
#' # Different length ids
#' random_id(bytes = 8)
#' # (note that the number of characters is twice the number of bytes)
#'
#' # The ids are not affected by R's RNG state:
#' set.seed(1)
#' (id1 <- random_id())
#' set.seed(1)
#' (id2 <- random_id())
#' # The generated identifiers are different, despite the seed being the same:
#' id1 == id2
#'
#' # If you need these identifiers to be reproducible, pass use_openssl = FALSE
#' set.seed(1)
#' (id1 <- random_id(use_openssl = FALSE))
#' set.seed(1)
#' (id2 <- random_id(use_openssl = FALSE))
#' # This time they are the same:
#' id1 == id2
#'
#' # Pass \code{n = NULL} to generate a function that binds your arguments:
#' id8 <- random_id(NULL, bytes = 8)
#' id8(10)
random_id <- function(n = 1, bytes = 16, use_openssl = TRUE) {
if (is.null(n)) {
force(bytes)
force(use_openssl)
function(n = 1) {
random_id(n, bytes, use_openssl)
}
} else {
rand_bytes <- if (use_openssl) openssl::rand_bytes else r_rand_bytes
apply(matrix(as.character(rand_bytes(n * bytes)), n), 1, paste,
collapse = "")
}
}
r_rand_bytes <- function(n) {
as.raw(sample(256L, n, TRUE) - 1L)
}
ids/R/adjective_animal.R 0000644 0001762 0000144 00000004577 13007061140 014641 0 ustar ligges users #' Ids based on a number of adjectives and an animal
#'
#' The list of adjectives and animals comes from
#' \url{https://github.com/a-type/adjective-adjective-animal}, and in
#' turn from \url{gfycat.com}
#'
#' @title Ids based on a number of adjectives and an animal
#' @param n_adjectives Number of adjectives to prefix the anmial with
#'
#' @param max_len The maximum length of a word part to include (this
#' may be useful because some of the names are rather long. This
#' stops you generating a
#' \code{hexakosioihexekontahexaphobic_queenalexandrasbirdwingbutterfly}).
#' A vector of length 2 can be passed in here in which case the
#' first element will apply to the adjectives (all of them) and the
#' second element will apply to the animals.
#'
#' @inheritParams ids
#' @export
#' @author Rich FitzJohn
#' @examples
#' # Generate a random identifier:
#' adjective_animal()
#'
#' # Generate a bunch all at once:
#' adjective_animal(5)
#'
#' # Control the style of punctuation with the style argument:
#' adjective_animal(style = "lower")
#' adjective_animal(style = "CONSTANT")
#' adjective_animal(style = "camel")
#' adjective_animal(style = "kebab")
#'
#' # Control the number of adjectives used
#' adjective_animal(n_adjectives = 3)
#'
#' # This can get out of hand quickly though:
#' adjective_animal(n_adjectives = 7)
#'
#' # Limit the length of adjectives and animals used:
#' adjective_animal(10, max_len = 6)
#'
#' # The lengths can be controlled for adjectives and animals
#' # separately, with Inf meaning no limit:
#' adjective_animal(10, max_len = c(6, Inf), n_adjectives = 2)
#'
#' # Pass n = NULL to bind arguments to a function
#' id <- adjective_animal(NULL, n_adjectives = 2, style = "dot", max_len = 6)
#' id()
#' id(10)
adjective_animal <- function(n = 1, n_adjectives = 1, style = "snake",
max_len = Inf) {
if (any(is.finite(max_len))) {
if (any(max_len < 3)) {
stop("max_len must be at least 3")
}
if (length(max_len) == 1L) {
max_len <- rep_len(max_len, 2)
} else if (length(max_len) != 2L) {
stop("max_len must be length 1 or 2")
}
gfycat_adjectives <-
gfycat_adjectives[nchar(gfycat_adjectives) <= max_len[1]]
gfycat_animals <- gfycat_animals[nchar(gfycat_animals) <= max_len[2]]
}
vals <- c(rep(list(gfycat_adjectives), n_adjectives), list(gfycat_animals))
ids(n, vals = vals, style = style)
}
ids/R/sysdata.rda 0000644 0001762 0000144 00000145164 13107317434 013410 0 ustar ligges users }kJ5|7#nfe}=-22K5
WkIDuJf#G
4=j[jkl??c6Y]TYX_,N1hL/CV6u>0}QY-u45c}6]}(r}cu]vy?#ʺ:;m|xV9wfZg?SVRmO<b}kg,6/cQT]VaȻs9";5cWď/9>;-x/f5gvcVge@|s0{Zv3~,uX=ӹk.y!\H;;kϬFZ7j*nSSw(3k_pdJ#36
K0^b'Vy,>?ܛc>A*βψОMWxhHs ZcYڢ=?'[O&иwD<ϪSkFSAs~x`cW`3;c;PԧB7Sor