Implement ascii_downcase and ascii_upcase in C (#3689)

Both were defined as explode | map(...) | implode, which took 4 seconds
on a 10 MB string, about 90 times as long as parsing it.

Change the case of A-Z or a-z a byte at a time in C, copying multibyte
UTF-8 sequences unchanged, so the output stays the same and the 10 MB
case now takes about 60 ms including parsing. On a non-string input the
error now names the function, "ascii_downcase input must be a string",
instead of "explode input must be a string".
This commit is contained in:
Michiel van Slobbe authored and GitHub committed 2026-10-25 14:18:21 +09:00
1 parent bd56a74562
commit 864d769d88
3 files changed
+50 -6

No files matched your search

+32
View File
@@ -1355,6 +1355,36 @@ static jv f_string_explode(jq_state *jq, jv a) {
return jv_string_explode(a);
}
// Only the letters from..to change case. Bytes of multibyte UTF-8 sequences
// are all >= 0x80, so they are copied unchanged.
static void ascii_case_copy(char *dst, const char *src, int len, char from,
char to) {
for (int i = 0; i < len; i++) {
char c = src[i];
dst[i] = (c >= from && c <= to) ? c ^ 0x20 : c; // flip case: 'A' <-> 'a'
}
}
static jv ascii_case(jv input, char from, char to, const char *errmsg) {
if (jv_get_kind(input) != JV_KIND_STRING)
return ret_error(input, jv_string(errmsg));
int len = jv_string_length_bytes(jv_copy(input));
char *result = jv_mem_alloc((size_t)len + 1);
ascii_case_copy(result, jv_string_value(input), len, from, to);
jv ret = jv_string_sized(result, len);
free(result);
jv_free(input);
return ret;
}
static jv f_ascii_downcase(jq_state *jq, jv input) {
return ascii_case(input, 'A', 'Z', "ascii_downcase input must be a string");
}
static jv f_ascii_upcase(jq_state *jq, jv input) {
return ascii_case(input, 'a', 'z', "ascii_upcase input must be a string");
}
static jv f_string_indexes(jq_state *jq, jv a, jv b) {
if (jv_get_kind(a) != JV_KIND_STRING) {
jv_free(b);
@@ -2015,6 +2045,8 @@ BINOPS
CFUNC(f_string_split, "split", 2),
CFUNC(f_string_explode, "explode", 1),
CFUNC(f_string_implode, "implode", 1),
CFUNC(f_ascii_downcase, "ascii_downcase", 1),
CFUNC(f_ascii_upcase, "ascii_upcase", 1),
CFUNC(f_string_indexes, "_strindices", 2),
CFUNC(f_string_trim, "trim", 1),
CFUNC(f_string_ltrim, "ltrim", 1),
-6
View File
@@ -184,12 +184,6 @@ def repeat(exp):
exp, _repeat;
_repeat;
def inputs: try repeat(input) catch if .=="break" then empty else error end;
# like ruby's downcase - only characters A to Z are affected
def ascii_downcase:
explode | map( if 65 <= . and . <= 90 then . + 32 else . end) | implode;
# like ruby's upcase - only characters a to z are affected
def ascii_upcase:
explode | map( if 97 <= . and . <= 122 then . - 32 else . end) | implode;
# Streaming utilities
def truncate_stream(stream):
+18
View File
@@ -1888,6 +1888,24 @@ ascii_upcase
"useful but not for é"
"USEFUL BUT NOT FOR é"
# Only A-Z and a-z change; the neighbouring bytes @ [ ` { and non-ASCII do not
[ascii_downcase, ascii_upcase]
"@AZ[`az{ ÀÉé Dž 日本 😀 0123456789 THE QUICK brown fox JUMPS over"
["@az[`az{ ÀÉé Dž 日本 😀 0123456789 the quick brown fox jumps over","@AZ[`AZ{ ÀÉé Dž 日本 😀 0123456789 THE QUICK BROWN FOX JUMPS OVER"]
[ascii_downcase, ascii_upcase]
""
["",""]
# An embedded NUL does not end the string
[ascii_downcase, ascii_upcase]
"A\u0000z"
["a\u0000z","A\u0000Z"]
[.[] | try ascii_downcase catch ., try ascii_upcase catch .]
[null, ["A"]]
["ascii_downcase input must be a string","ascii_upcase input must be a string","ascii_downcase input must be a string","ascii_upcase input must be a string"]
bsearch(0,1,2,3,4)
[1,2,3]
-1