Nothing
library(testit)
assert("tojson() works", {
(.tojson(NULL) %==% "null")
(.tojson(list()) %==% "{}")
(.tojson(NA) %==% 'null')
(.tojson(NA_character_) %==% 'null')
(.tojson(1:10) %==% "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]")
(.tojson(TRUE) %==% "true")
(.tojson(FALSE) %==% "false")
x = list(a = 1, b = list(c = 1:3, d = "abc"))
out = '{\n "a": 1,\n "b": {\n "c": [1, 2, 3],\n "d": "abc"\n }\n}'
(.tojson(x) %==% out)
x = list(c("a", "b"), 1:5, TRUE)
out = '[\n ["a", "b"],\n [1, 2, 3, 4, 5],\n true\n]'
(.tojson(x) %==% out)
(.tojson(list('"a b"' = 'quotes "\'')) %==% '{\n "\\"a b\\"": "quotes \\"\'"\n}')
# data frames
(.tojson(data.frame()) %==% '{}')
# by column for named data frames
x = data.frame(a = 1:3, b = c('x', 'y', 'z'))
(.tojson(x) %==% '{\n "a": [1, 2, 3],\n "b": ["x", "y", "z"]\n}')
# length 1 columns must be arrays, too
x = data.frame(a = 1, b = 'x')
(.tojson(x) %==% '{\n "a": [1],\n "b": ["x"]\n}')
# by row for unnamed data frames
x = unname(data.frame(a = 1:3, b = c('x', 'y', 'z')))
(.tojson(x) %==% '[\n [1, "x"],\n [2, "y"],\n [3, "z"]\n]')
x = list(a = 1:5, b = js("function() {return true;}"))
out = '{\n "a": [1, 2, 3, 4, 5],\n "b": function() {return true;}\n}'
(.tojson(x) %==% out)
res = tojson(list(NULL, 1:10, TRUE, FALSE))
# passing a json object through tojson() should return it unchanged
(tojson(res) %==% res)
# a nested json value is emitted raw, not re-quoted (tojson is composable)
(.tojson(list(a = tojson(1:3, pretty = FALSE)), pretty = FALSE) %==% '{"a":[1,2,3]}')
})
assert('json_vector() converts atomic vectors to JSON', {
(json_vector(c('a', 'b'), to_array = TRUE) %==% '["a", "b"]')
(json_vector(c('a', 'b'), to_array = FALSE) %==% c('"a"', '"b"'))
(json_vector(1:3, to_array = TRUE, quote = FALSE) %==% '[1, 2, 3]')
# NA becomes null (character and numeric)
(json_vector(c('a', NA_character_), to_array = TRUE) %==% '["a", null]')
(json_vector(c(1, NA, 3), to_array = TRUE, quote = FALSE) %==% '[1, null, 3]')
# Inf/-Inf become Infinity/-Infinity (valid JS)
(json_vector(c(1, Inf, -Inf), to_array = TRUE, quote = FALSE) %==% '[1, Infinity, -Infinity]')
(json_vector(c(Inf, NA), to_array = TRUE, quote = FALSE) %==% '[Infinity, null]')
})
assert('json_atomic() handles Date, POSIXct, and factor', {
d = as.Date('2024-01-15')
(json_atomic(d) %==% 'new Date("2024-01-15")')
# Date/POSIXt in data.frames (I() must not trigger format.AsIs truncation)
t = as.POSIXct(c('2024-01-15 09:30:00', '2024-01-15 17:45:00'), tz = 'UTC')
(.tojson(data.frame(t = t)) %==%
'{\n "t": [new Date("2024-01-15 09:30:00"), new Date("2024-01-15 17:45:00")]\n}')
# single-row Date data.frame still produces an array
(.tojson(data.frame(d = d)) %==% '{\n "d": [new Date("2024-01-15")]\n}')
f = factor(c('a', 'b', 'a'))
(json_atomic(f) %==% '["a", "b", "a"]')
})
assert('.tojson() handles arrays', {
# each matrix row is an array element
(.tojson(matrix(1:4, 2)) %==% '[\n [1, 3],\n [2, 4]\n]')
(.tojson(matrix(1:6, 2)) %==% '[\n [1, 3, 5],\n [2, 4, 6]\n]')
# single-column matrices still emit one array per row
(.tojson(matrix(1:3, 3)) %==% '[\n [1],\n [2],\n [3]\n]')
# single-row matrices produce a single array element
(.tojson(matrix(1:3, 1)) %==% '[\n [1, 2, 3]\n]')
# character matrices are quoted and NA becomes null
(.tojson(matrix(c('a', NA, 'c', 'd'), 2)) %==%
'[\n ["a", "c"],\n [null, "d"]\n]')
# Inf/-Inf in numeric matrices
(.tojson(matrix(c(1, Inf, -Inf, 2), 2)) %==%
'[\n [1, -Infinity],\n [Infinity, 2]\n]')
# higher-dimensional arrays recurse (nested one level deeper per dimension)
(.tojson(array(1:8, c(2, 2, 2))) %==% paste0(
'[\n [\n [1, 5],\n [3, 7]\n ],\n',
' [\n [2, 6],\n [4, 8]\n ]\n]'
))
})
assert('json_vector() escapes control characters in strings', {
# newline, backspace, formfeed, carriage return, tab must all be escaped
(json_vector('\n', to_array = FALSE) %==% '"\\n"')
(json_vector('\b', to_array = FALSE) %==% '"\\b"')
(json_vector('\f', to_array = FALSE) %==% '"\\f"')
(json_vector('\r', to_array = FALSE) %==% '"\\r"')
(json_vector('\t', to_array = FALSE) %==% '"\\t"')
# backslash and double-quote must be escaped (via quote_string)
(json_vector('a\\b', to_array = FALSE) %==% '"a\\\\b"')
(json_vector('say "hi"', to_array = FALSE) %==% '"say \\"hi\\""')
})
assert('quote_string() returns character(0) for empty input', {
(quote_string(character(0)) %==% character(0))
})
assert('quote_string() escapes </script to keep output inline-<script> safe', {
# a literal </script> would close an inline <script> block early
(quote_string('a</script>b') %==% '"a<\\/script>b"')
(json_vector('</script>', to_array = FALSE) %==% '"<\\/script>"')
# case-insensitive like the HTML parser, preserving the original case
(quote_string('</SCRIPT>') %==% '"<\\/SCRIPT>"')
# other </ sequences and bare / are left alone (e.g. a regex, </style>)
(quote_string('</style>') %==% '"</style>"')
(quote_string('if (a</b && /</.test(s))') %==% '"if (a</b && /</.test(s))"')
(quote_string('a/b') %==% '"a/b"')
})
assert('tojson(dict = ) dictionary-encodes repeated values when it is shorter', {
# off by default: plain array
x = rep(c('alpha', 'bravo'), 10)
(.tojson(x) %==% json_atomic(x))
# enabled: runnable JS expression, 0-based codes indexing unique values (in
# order of appearance), chosen because it is shorter than the plain form
d = .tojson(x, dict = 1)
(grepl('\\.map\\(i => \\["alpha", "bravo"\\]\\[i\\]\\)$', d))
(nchar(d) < nchar(json_atomic(x)))
# numeric vectors encode too
(grepl('\\.map\\(i => \\[45, 52\\]\\[i\\]\\)$', .tojson(rep(c(45, 52), 20), dict = 1)))
# NA is a normal dict entry (null in the unique array), so no `?? null` tail
d2 = .tojson(rep(c('a', NA, 'b'), 10), dict = 1)
(grepl('\\[i\\]\\)$', d2))
(!grepl('\\?\\?', d2))
(grepl('null', d2))
# near-unique vectors stay plain even when enabled (encoding would be longer)
u = as.character(1:50)
(.tojson(u, dict = 1) %==% json_atomic(u))
# a numeric threshold gates candidacy: 2 uniques / 20 rows = 0.1
(.tojson(x, dict = 0.05) %==% json_atomic(x))
(grepl('\\.map', .tojson(x, dict = 0.1)))
# threads through data-frame columns
df = data.frame(g = rep(c('alpha', 'bravo'), 20), stringsAsFactors = FALSE)
(grepl('"g": \\[0, 1,.*\\.map\\(i => \\["alpha", "bravo"\\]\\[i\\]\\)', .tojson(df, dict = 1)))
})
assert('tojson(mode = "json") emits pure static JSON', {
# Inf/-Inf become null (JSON has no Infinity literal)
(json_vector(c(1, Inf, -Inf), to_array = TRUE, quote = FALSE, mode = 'json') %==%
'[1, null, null]')
(json_vector(c(Inf, NA), to_array = TRUE, quote = FALSE, mode = 'json') %==% '[null, null]')
# dates/times become a plain quoted string, not new Date(...)
d = as.Date('2024-01-15')
(json_atomic(d, mode = 'json') %==% '"2024-01-15"')
t = as.POSIXct('2024-01-15 09:30:00', tz = 'UTC')
(json_atomic(t, mode = 'json') %==% '"2024-01-15 09:30:00"')
(.tojson(data.frame(d = d), mode = 'json') %==% '{\n "d": ["2024-01-15"]\n}')
# dict uses the valid-JSON tagged object (json_reviver rebuilds it), not .map
(.tojson(rep(c('a', 'b'), 10), dict = 1, mode = 'json') %==%
paste0('{"\\u0001u":["a", "b"],"\\u0001c":[', paste(rep(c(0, 1), 10), collapse = ', '), ']}'))
# no JS wrapper when nothing was packed
(as.character(tojson(c(1, Inf), mode = 'json')) %==% '[1, null]')
})
assert('tojson(pretty = FALSE) produces compact output', {
# arrays join with ',' (no space), objects have no line breaks or indentation
(.tojson(1:5, pretty = FALSE) %==% '[1,2,3,4,5]')
(.tojson(list(a = 1:3, b = c('x', 'y')), pretty = FALSE) %==%
'{"a":[1,2,3],"b":["x","y"]}')
# nested lists stay single-line
(.tojson(list(a = 1, b = list(c = 1:3, d = 'abc')), pretty = FALSE) %==%
'{"a":1,"b":{"c":[1,2,3],"d":"abc"}}')
# matrices and by-row data frames
(.tojson(matrix(1:6, 2), pretty = FALSE) %==% '[[1,3,5],[2,4,6]]')
(.tojson(unname(data.frame(x = 1:2, y = c('a', 'b'))), pretty = FALSE) %==%
'[[1,"a"],[2,"b"]]')
# dict encoding uses compact code/unique arrays
(.tojson(rep(c(45, 52), 20), dict = 1, pretty = FALSE) %==%
paste0('[', paste(rep(c(0, 1), 20), collapse = ','), '].map(i => [45,52][i])'))
# pretty = TRUE (default) is unchanged
(.tojson(1:5) %==% '[1, 2, 3, 4, 5]')
})
assert('tojson(pack) packs long integer vectors as tagged base64', {
# width selection at the boundaries + little-endian base64
(pack_ints(c(0L, 255L)) %==% '{"\\u0001u8":"AP8="}')
(pack_ints(c(0L, 256L)) %==% '{"\\u0001u16":"AAAAAQ=="}')
(pack_ints(c(0L, 65535L)) %==% '{"\\u0001u16":"AAD//w=="}')
(pack_ints(c(0L, 65536L)) %==% '{"\\u0001u32":"AAAAAAAAAQA="}')
# negatives use signed widths
(pack_ints(c(-128L, 127L)) %==% '{"\\u0001i8":"gH8="}')
(pack_ints(c(-129L, 0L)) %==% '{"\\u0001i16":"f/8AAA=="}')
(pack_ints(c(-32769L, 0L)) %==% '{"\\u0001i32":"/3///wAAAAA="}')
x = rep(c(0L, 1L), 100)
# mode 'json': a packed vector is the bare valid-JSON tagged object
(.tojson(x, pack = 100, mode = 'json') %==% pack_ints(x))
(nchar(.tojson(x, pack = 100, mode = 'json')) < nchar(.tojson(x)))
# off by default, below threshold, NA, factors, doubles
(.tojson(x) %==% json_atomic(x))
(.tojson(x, pack = 1000) %==% json_atomic(x))
(.tojson(c(x, NA), pack = 100) %==% json_atomic(c(x, NA)))
(.tojson(factor(x), pack = 100) %==% json_atomic(factor(x)))
(.tojson(as.numeric(x), pack = 100) %==% json_atomic(as.numeric(x)))
# tojson(pack): a number is the length threshold
(as.character(tojson(x, pack = 100, mode = 'json')) %==% pack_ints(x))
(.tojson(list(a = x), pack = 100, pretty = FALSE, mode = 'json') %==%
paste0('{"a":', pack_ints(x), '}'))
# dict + pack in 'json' mode: pack the codes into the tagged object
y = rep(c('a', 'b'), 100)
(.tojson(y, dict = 1, pack = 100, mode = 'json') %==%
paste0('{"\\u0001u":["a", "b"],"\\u0001c":', pack_ints(rep(0:1, 100)), '}'))
# mode 'js' (default): a self-contained expression with an unpack() call
(.tojson(x, pack = 100) %==% paste0('unpack(', pack_ints(x), ')'))
(grepl('unpack(', tojson(x, pack = 100), fixed = TRUE))
(grepl('Array.from(unpack(', .tojson(y, dict = 1, pack = 100), fixed = TRUE))
(grepl(json_unpack, json_reviver, fixed = TRUE))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.