From 864d769d888e49621849c8ad616d794f099e713f Mon Sep 17 00:00:00 2001 From: Michiel van Slobbe Date: Sun, 25 Oct 2026 06:18:21 +0100 Subject: [PATCH] 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". --- src/builtin.c | 32 ++++++++++++++++++++++++++++++++ src/builtin.jq | 6 ------ tests/jq.test | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/builtin.c b/src/builtin.c index 4261c30a..7d6f775d 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -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), diff --git a/src/builtin.jq b/src/builtin.jq index 78783143..d6ae891c 100644 --- a/src/builtin.jq +++ b/src/builtin.jq @@ -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): diff --git a/tests/jq.test b/tests/jq.test index 557432d9..6086f376 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -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