diff --git a/strings.scad b/strings.scad index 42bef64..fe58557 100644 --- a/strings.scad +++ b/strings.scad @@ -1,5 +1,14 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: strings.scad +// String manipulation and formatting functions. +// To use, add the following lines to the beginning of your file: +// ``` +// use +// ``` +////////////////////////////////////////////////////////////////////// + + // Section: String Operations -//----------------------------------------------------------------------------- // Function: substr() // Usage: @@ -26,6 +35,7 @@ function _substr(str,pos,len,substr="") = len <= 0 || pos>=len(str) ? substr : _substr(str, pos+1, len-1, str(substr, str[pos])); + // Function suffix() // Usage: // suffix(str,len) @@ -50,6 +60,7 @@ function str_join(list,sep="",_i=0, _result="") = _i >= len(list)-1 ? (_i==len(list) ? _result : str(_result,list[_i])) : str_join(list,sep,_i+1,str(_result,list[_i],sep)); + // Function: downcase() // Usage: // downcase(str) @@ -57,12 +68,27 @@ function str_join(list,sep="",_i=0, _result="") = // Returns the string with the standard ASCII upper case letters A-Z replaced // by their lower case versions. // Arguments: -// str = string to convert +// str = String to convert. // Example: // downcase("ABCdef"); // Returns "abcdef" function downcase(str) = str_join([for(char=str) let(code=ord(char)) code>=65 && code<=90 ? chr(code+32) : char]); + +// Function: upcase() +// Usage: +// upcase(str) +// Description: +// Returns the string with the standard ASCII lower case letters a-z replaced +// by their upper case versions. +// Arguments: +// str = String to convert. +// Example: +// upcase("ABCdef"); // Returns "ABCDEF" +function upcase(str) = + str_join([for(char=str) let(code=ord(char)) code>=97 && code<=122 ? chr(code-32) : char]); + + // Function: str_int() // Usage: // str_int(str, [base]) @@ -71,8 +97,8 @@ function downcase(str) = // conversion fails. Digits above 9 are represented using letters A-F in either // upper case or lower case. // Arguments: -// str = string to convert -// base = base for conversion, from 2-16. Default: 10 +// str = String to convert. +// base = Base for conversion, from 2-16. Default: 10 // Example: // str_int("349"); // Returns 349 // str_int("-37"); // Returns -37 @@ -98,6 +124,7 @@ function _str_int_recurse(str,base,i) = ) i==0 ? last_digit : _str_int_recurse(str,base,i-1)*base + last_digit; + // Function: str_float() // Usage: // str_float(str) @@ -105,7 +132,7 @@ function _str_int_recurse(str,base,i) = // Converts a string to a floating point number. Returns NaN if the // conversion fails. // Arguments: -// str = string to convert +// str = String to convert. // Example: // str_float("44"); // Returns 44 // str_float("3.4"); // Returns 3.4 @@ -125,6 +152,7 @@ function str_float(str) = let( dsplit = str_split(str,["."])) str_int(dsplit[0])+str_int(dsplit[1])/pow(10,len(dsplit[1])); + // Function: str_frac() // Usage: // str_frac(str,[mixed],[improper],[signed]) @@ -138,7 +166,7 @@ function str_float(str) = // the numerator is smaller than the denominator. If you set `signed` to false then the leading sign character is not permitted. // The empty string evaluates to zero. Any invalid string evaluates to NaN. // Arguments: -// str = string to convert +// str = String to convert. // mixed = set to true to accept mixed fractions, false to reject them. Default: true // improper = set to true to accept improper fractions, false to reject them. Default: true // signed = set to true to accept a leading sign character, false to reject. Default: true @@ -177,6 +205,7 @@ function str_frac(str,mixed=true,improper=true,signed=true) = denominator<0 ? (0/0) : numerator/denominator ); + // Function: str_num() // Usage: // str_num(str) @@ -207,7 +236,7 @@ function str_num(str) = // If keep_nulls is true then the output will have length equal to `len(sep)+1`, possibly with trailing null strings // if the string runs out before the separator list. // Arguments -// str = string to split +// str = String to split. // sep = a string or list of strings to use for the separator // keep_nulls = boolean value indicating whether to keep null strings in the output list. Default: true // Example: @@ -219,17 +248,17 @@ function str_num(str) = // str_split("abc+def-qrs*iop",["-","+","*"]); // Returns ["abc+def", "qrs*iop", "", ""] function str_split(str,sep,keep_nulls=true) = !keep_nulls ? _remove_empty_strs(str_split(str,sep,keep_nulls=true)) : - is_list(sep) ? str_split_recurse(str,sep,i=0,result=[]) : + is_list(sep) ? _str_split_recurse(str,sep,i=0,result=[]) : let( cutpts = concat([-1],sort(flatten(search(sep, str,0))),[len(str)])) [for(i=[0:len(cutpts)-2]) substr(str,cutpts[i]+1,cutpts[i+1]-cutpts[i]-1)]; -function str_split_recurse(str,sep,i,result) = +function _str_split_recurse(str,sep,i,result) = i == len(sep) ? concat(result,[str]) : let( pos = search(sep[i], str), end = pos==[] ? len(str) : pos[0] ) - str_split_recurse( + _str_split_recurse( substr(str,end+1), sep, i+1, concat(result, [substr(str,0,end)]) @@ -253,6 +282,7 @@ function _str_cmp(str,sindex,pattern) = function _str_cmp_recurse(str,sindex,pattern,plen,pindex=0,) = pindex < plen && pattern[pindex]==str[sindex] ? _str_cmp_recurse(str,sindex+1,pattern,plen,pindex+1): (pindex==plen); + // Function: str_find() // Usage: // str_find(str,pattern,[last],[all],[start]) @@ -264,7 +294,7 @@ function _str_cmp_recurse(str,sindex,pattern,plen,pindex=0,) = // and `last` is true then the search will find the pattern if it begins at index `start`. If no match exists, returns undef. // If you set `all` to true then all str_find() returns all of the matches in a list, or an empty list if there are no matches. // Arguments: -// str = string to search +// str = String to search. // pattern = string pattern to search for // last = set to true to return the last match. Default: false // all = set to true to return all matches as a list. Overrides last. Default: false @@ -313,8 +343,8 @@ function _str_find_all(str,pattern) = // Returns true if the input string `str` starts with the specified string pattern, `pattern`. // Otherwise returns false. // Arguments: -// str = string to search -// pattern = string pattern to search for +// str = String to search. +// pattern = String pattern to search for. // Example: // starts_with("abcdef","abc"); // Returns true // starts_with("abcdef","def"); // Returns false @@ -329,8 +359,8 @@ function starts_with(str,pattern) = _str_cmp(str,0,pattern); // Returns true if the input string `str` ends with the specified string pattern, `pattern`. // Otherwise returns false. // Arguments: -// str = string to search -// pattern = string pattern to search for +// str = String to search. +// pattern = String pattern to search for. // Example: // ends_with("abcdef","def"); // Returns true // ends_with("abcdef","de"); // Returns false @@ -346,6 +376,7 @@ function _str_count_trailing(s,c,_i=0) = (_i>=len(s)||!in_list(s[len(s)-1-_i],[each c]))? _i : _str_count_trailing(s,c,_i=_i+1); + // Function: str_strip_leading() // Usage: // str_strip_leading(s,c); @@ -391,9 +422,9 @@ function str_strip_trailing(s,c) = substr(s,len=len(s)-_str_count_trailing(s,c)) function str_strip(s,c) = str_strip_trailing(str_strip_leading(s,c),c); -// Function: fmti() +// Function: fmt_int() // Usage: -// fmti(i, [mindigits]); +// fmt_int(i, [mindigits]); // Description: // Formats an integer number into a string. This can handle larger numbers than `str()`. // Arguments: @@ -401,10 +432,10 @@ function str_strip(s,c) = str_strip_trailing(str_strip_leading(s,c),c); // mindigits = If the number has fewer than this many digits, pad the front with zeros until it does. Default: 1. // Example: // str(123456789012345); // Returns "1.23457e+14" -// fmti(123456789012345); // Returns "123456789012345" -// fmti(-123456789012345); // Returns "-123456789012345" -function fmti(i,mindigits=1) = - i<0? str("-", fmti(-i)) : +// fmt_int(123456789012345); // Returns "123456789012345" +// fmt_int(-123456789012345); // Returns "-123456789012345" +function fmt_int(i,mindigits=1) = + i<0? str("-", fmt_int(-i)) : let(i=floor(i), e=floor(log(i))) i==0? "0" : str_join( @@ -415,9 +446,33 @@ function fmti(i,mindigits=1) = ); -// Function: fmtf() +// Function: fmt_fixed() // Usage: -// fmtf(f,[sig]); +// s = fmt_fixed(f, [digits]); +// Description: +// Given a floating point number, formats it into a string with the given number of digits after the decimal point. +// Arguments: +// f = The floating point number to format. +// digits = The number of digits after the decimal to show. Default: 6 +function fmt_fixed(f,digits=6) = + assert(is_int(digits)) + assert(digits>0) + is_list(f)? str("[",str_join(sep=", ", [for (g=f) fmt_fixed(g,digits=digits)]),"]") : + str(f)=="nan"? "nan" : + str(f)=="inf"? "inf" : + f<0? str("-",fmt_fixed(-f,digits=digits)) : + assert(is_num(f)) + let( + sc = pow(10,digits), + scaled = floor(f * sc + 0.5), + whole = floor(scaled/sc), + part = floor(scaled-(whole*sc)) + ) str(fmt_int(whole),".",fmt_int(part,digits)); + + +// Function: fmt_float() +// Usage: +// fmt_float(f,[sig]); // Description: // Formats the given floating point number `f` into a string with `sig` significant digits. // Strips trailing `0`s after the decimal point. Strips trailing decimal point. @@ -427,22 +482,22 @@ function fmti(i,mindigits=1) = // f = The floating point number to format. // sig = The number of significant digits to display. Default: 12 // Example: -// fmtf(PI,12); // Returns: "3.14159265359" -// fmtf([PI,-16.75],12); // Returns: "[3.14159265359, -16.75]" -function fmtf(f,sig=12) = +// fmt_float(PI,12); // Returns: "3.14159265359" +// fmt_float([PI,-16.75],12); // Returns: "[3.14159265359, -16.75]" +function fmt_float(f,sig=12) = assert(is_int(sig)) assert(sig>0) - is_list(f)? str("[",str_join(sep=", ", [for (g=f) fmtf(g,sig=sig)]),"]") : + is_list(f)? str("[",str_join(sep=", ", [for (g=f) fmt_float(g,sig=sig)]),"]") : f==0? "0" : str(f)=="nan"? "nan" : str(f)=="inf"? "inf" : - f<0? str("-",fmtf(-f,sig=sig)) : + f<0? str("-",fmt_float(-f,sig=sig)) : assert(is_num(f)) let( e = floor(log(f)), mv = sig - e - 1 - ) mv == 0? fmti(floor(f + 0.5)) : - (e<-sig/2||mv<0)? str(fmtf(f*pow(10,-e),sig=sig),"e",e) : + ) mv == 0? fmt_int(floor(f + 0.5)) : + (e<-sig/2||mv<0)? str(fmt_float(f*pow(10,-e),sig=sig),"e",e) : let( ff = f + pow(10,-mv)*0.5, whole = floor(ff), @@ -453,11 +508,194 @@ function fmtf(f,sig=12) = str_strip_trailing( str_join([ ".", - fmti(part, mindigits=mv) + fmt_int(part, mindigits=mv) ]), "0." ) ]); +// Function: escape_html() +// Usage: +// echo(escape_html(s)); +// Description: +// Converts "<", ">", "&", and double-quote chars to their entity encoding so that echoing the strong will show it verbatim. +function escape_html(s) = + str_join([ + for (c=s) + c=="<"? "<" : + c==">"? ">" : + c=="&"? "&" : + c=="\""? """ : + c + ]); + + +// Function: is_lower() +// Usage: +// x = is_lower(s); +// Description: +// Returns true if all the characters in the given string are lowercase letters. (a-z) +function is_lower(s) = + assert(is_string(s)) + s==""? false : + len(s)>1? all([for (v=s) is_lower(v)]) : + let(v = ord(s[0])) (v>=ord("a") && v<=ord("z")); + + +// Function: is_upper() +// Usage: +// x = is_upper(s); +// Description: +// Returns true if all the characters in the given string are uppercase letters. (A-Z) +function is_upper(s) = + assert(is_string(s)) + s==""? false : + len(s)>1? all([for (v=s) is_upper(v)]) : + let(v = ord(s[0])) (v>=ord("A") && v<=ord("Z")); + + +// Function: is_digit() +// Usage: +// x = is_digit(s); +// Description: +// Returns true if all the characters in the given string are digits. (0-9) +function is_digit(s) = + assert(is_string(s)) + s==""? false : + len(s)>1? all([for (v=s) is_digit(v)]) : + let(v = ord(s[0])) (v>=ord("0") && v<=ord("9")); + + +// Function: is_hexdigit() +// Usage: +// x = is_hexdigit(s); +// Description: +// Returns true if all the characters in the given string are valid hexadecimal digits. (0-9 or a-f or A-F)) +function is_hexdigit(s) = + assert(is_string(s)) + s==""? false : + len(s)>1? all([for (v=s) is_hexdigit(v)]) : + let(v = ord(s[0])) + (v>=ord("0") && v<=ord("9")) || + (v>=ord("A") && v<=ord("F")) || + (v>=ord("a") && v<=ord("f")); + + +// Function: is_letter() +// Usage: +// x = is_letter(s); +// Description: +// Returns true if all the characters in the given string are standard ASCII letters. (A-Z or a-z) +function is_letter(s) = + assert(is_string(s)) + s==""? false : + all([for (v=s) is_lower(v) || is_upper(v)]); + + +// Function: str_format() +// Usage: +// s = str_format(fmt, vals); +// Description: +// Given a format string and a list of values, inserts the values into the placeholders in the format string and returns it. +// Formatting placeholders have the following syntax: +// - A leading `{` character to show the start of the placeholder. +// - An integer index into the `vals` list to specify which value should be formatted at that place. If not given, the first placeholder will use index `0`, the second will use index `1`, etc. +// - An optional `:` separator to indicate that what follows if a formatting specifier. If not given, no formatting info follows. +// - An optional `-` character to indicate that the value should be left justified if the value needs field width padding. If not given, right justification is used. +// - An optional `0` character to indicate that the field should be padded with `0`s. If not given, spaces will be used for padding. +// - An optional integer field width, which the value should be padded to. If not given, no padding will be performed. +// - An optional `.` followed by an integer precision length, for specifying how many digits to display in numeric formats. If not give, 6 digits is assumed. +// - An optional letter to indicate the formatting style to use. If not given, `s` is assumed, which will do it's generic best to format any data type. +// - A trailing `}` character to show the end of the placeholder. +// Formatting styles, and their effects are as follows: +// - `s`: Converts the value to a string with `str()` to display. This is very generic. +// - `i` or `d`: Formats numeric values as integers. +// - `f`: Formats numeric values with the precision number of digits after the decimal point. NaN and Inf are shown as `nan` and `inf`. +// - `F`: Formats numeric values with the precision number of digits after the decimal point. NaN and Inf are shown as `NAN` and `INF`. +// - `g`: Formats numeric values with the precision number of total significant digits. NaN and Inf are shown as `nan` and `inf`. Mantissas are demarked by `e`. +// - `G`: Formats numeric values with the precision number of total significant digits. NaN and Inf are shown as `NAN` and `INF`. Mantissas are demarked by `E`. +// - `b`: If the value logically evaluates as true, it shows as `true`, otherwise `false`. +// - `B`: If the value logically evaluates as true, it shows as `TRUE`, otherwise `FALSE`. +// Arguments: +// fmt = The formatting string, with placeholders to format the values into. +// vals = The list of values to format. +// use_nbsp = Pad fields with HTML entity ` ` instead of spaces. +// Example(NORENDER): +// str_format("The value of {} is {:.14f}.", ["pi", PI]); // Returns: "The value of pi is 3.14159265358979." +// str_format("The value {1:f} is known as {0}.", ["pi", PI]); // Returns: "The value 3.141593 is known as pi." +// str_format("We use a very small value {1:.6g} as {0}.", ["EPSILON", EPSILON]); // Returns: "We use a ver small value 1e-9 as EPSILON." +// str_format("{:-5s}{:i}{:b}", ["foo", 12e3, 5]); // Returns: "foo 12000true" +// str_format("{:-10s}{:.3f}", ["plecostamus",27.43982]); // Returns: "plecostamus27.440" +// str_format("{:-10.9s}{:.3f}", ["plecostamus",27.43982]); // Returns: "plecostam 27.440" +function str_format(fmt, vals, use_nbsp=false) = + let( + parts = str_split(fmt,"{") + ) str_join([ + for(i = idx(parts)) + let( + found_brace = i==0 || [for (c=parts[i]) if(c=="}") c] != [], + err = assert(found_brace, "Unbalanced { in format string."), + p = i==0? [undef,parts[i]] : str_split(parts[i],"}"), + fmta = p[0], + raw = p[1] + ) each [ + assert(i<99) + is_undef(fmta)? "" : let( + fmtb = str_split(fmta,":"), + num = is_digit(fmtb[0])? str_int(fmtb[0]) : (i-1), + left = fmtb[1][0] == "-", + fmtb1 = default(fmtb[1],""), + fmtc = left? substr(fmtb1,1) : fmtb1, + zero = fmtc[0] == "0", + lch = fmtc==""? "" : fmtc[len(fmtc)-1], + hastyp = is_letter(lch), + typ = hastyp? lch : "s", + fmtd = hastyp? substr(fmtc,0,len(fmtc)-1) : fmtc, + fmte = str_split((zero? substr(fmtd,1) : fmtd), "."), + wid = str_int(fmte[0]), + prec = str_int(fmte[1]), + val = assert(num>=0&&num