@ 261102.1

261025.1 · 864d769d888e49621849c8ad616d794f099e713f

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".

reading declarations

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),

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):

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