Mercurial > projects > mde
view mde/mergetag/parse/parseTo.d @ 70:7fc0a8295c83
Moved my parseTo and parseFrom modules from tango.scrapple to mde in order to reduce dependencies.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 04 Jul 2008 19:04:16 +0100 |
parents | |
children |
line wrap: on
line source
/************************************************************************************************** * copyright: Copyright (c) 2007-2008 Diggory Hardy. * * author: Diggory Hardy, diggory.hardy@gmail.com * * license: BSD style: $(LICENSE) * * This contains templates for converting a char[] to various data-types. * * parseTo is roughly the inverse of $(B parseFrom) and should read any data output by $(B parseFrom). * It is also available in tango.scrapple. * * This module basically implements the following templated function for most basic D types: * bool, byte, short, int, long, ubyte, ushort, uint, ulong, float, double, real, char. * It also supports arrays and associative arrays of any supported type (including of other arrays) * and has special handling for strings (char[]) and binary (ubyte[]) data-types. * ----------------------------- * T parseTo(T) (char[] source); * ----------------------------- * * $(I source) is the string to parse, and data of the templated type that is read from the string * is returned. See the examples to get a better idea of its use. * * Syntax: * The syntax for parsing $(I source) is mostly the same used by D without any prefixes/suffixes * (except 0x, 0b & 0o base specifiers). Also a special ubyte[] syntax is supported; see examples. * The following escape sequences are supported for strings and characters: \' \" \\ * \a \b \f \n \r \t \v . Associative array literals use the same syntax as D, described here: * $(LINK http://www.digitalmars.com/d/2.0/expression.html#AssocArrayLiteral). All whitespace is * ignored (except of course within strings). * * There are also some public utility functions with their own documentation. * * Throws: * On errors, a ParseException or a UnicodeException (both extend TextException) is thrown with a * suitable message. No other exceptions should be thrown. * * Remarks: * There is currently no support for reading wchar/dchar strings. There are, however, unicode * conversions for converting UTF-8 to UTF-16/32. Be careful if converting on a char-by-char basis; * such conversions cannot be used for non-ascii characters. * * Examples: * ------------------------------------------------------------------------------------------------ * // Basic examples: * ulong a = parseTo!(ulong) ("20350"); * float d = parseTo!(float) (" 1.2e-9 "); * int[] b = parseTo!(int[]) ("[0,1,2,3]"); * * // String and char[] syntax: * char[] c = parseTo!(char[]) ("\"A string\""); * char[] e = parseTo!(char[]) ("['a','n','o','t','h','e','r', ' ' ,'s','t','r','i','n','g']"); * * // These be used interchangably; here's a more complex example of an associative array: * bool[char[]] f = parseTo!(bool[char[]]) ("[ \"one\":true, ['t','w','o']:false, \"three\":1, \"four\":000 ]"); * * // There is also a special notation for ubyte[] types: * // The digits following 0x must be in pairs and each specify one ubyte. * assert ( parseTo!(ubyte[]) (`0x01F2AC`) == parseTo!(ubyte[]) (`[01 ,0xF2, 0xAC]`) ); * * // There's no limit to the complexity! * char[char[][][][char]][bool] z = ...; // don't expect me to write this! * ------------------------------------------------------------------------------------------------ *************************************************************************************************/ module mde.mergetag.parse.parseTo; // tango imports import tango.core.Exception : TextException, UnicodeException; import cInt = tango.text.convert.Integer; import cFloat = tango.text.convert.Float; import Utf = tango.text.convert.Utf; import Util = tango.text.Util; /** * Base class for parseTo exceptions. */ class ParseException : TextException { this( char[] msg ) { super( msg ); } } //BEGIN parseTo templates // Associative arrays const char[] AA_ERR = "Invalid associative array: "; T[S] parseTo(T : T[S], S) (char[] src) { src = Util.trim(src); if (src.length < 2 || src[0] != '[' || src[$-1] != ']') throw new ParseException (AA_ERR ~ "not [ ... ]"); // bad braces. T[S] ret; foreach (char[] pair; split (src[1..$-1])) { uint i = 0; while (i < pair.length) { // advance to the ':' char c = pair[i]; if (c == ':') break; if (c == '\'' || c == '"') { // string or character ++i; while (i < pair.length && pair[i] != c) { if (pair[i] == '\\') { if (i+2 >= pair.length) throw new ParseException (AA_ERR ~ "unfinished escape sequence within string/char"); ++i; // escape seq. } ++i; } if (i == pair.length) { throw new ParseException (AA_ERR ~ "encountered [ ... KEY] (missing :DATA)"); } } ++i; } if (i == pair.length) { throw new ParseException (AA_ERR ~ "encountered [ ... KEY:] (missing DATA)"); } ret[parseTo!(S) (pair[0..i])] = parseTo!(T) (pair[i+1..$]); } return ret; } debug (UnitTest) unittest { char[][char] X = parseTo!(char[][char]) (`['a':"animal", 'b':['b','u','s']]`); char[][char] Y = ['a':cast(char[])"animal", 'b':['b','u','s']]; //FIXME: when the compiler's fixed: http://d.puremagic.com/issues/show_bug.cgi?id=1671 // just assert (X == Y) assert (X.length == Y.length); assert (X.keys == Y.keys); assert (X.values == Y.values); //X.rehash; Y.rehash; // doesn't make a difference //assert (X == Y); // fails (compiler bug) } // Arrays T[] parseTo(T : T[]) (char[] src) { src = Util.trim(src); if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T[]) (src); throw new ParseException ("Invalid array: not [x, ..., z]"); } // String (array special case) T parseTo(T : char[]) (char[] src) { src = Util.trim(src); if (src.length >= 2 && src[0] == '"' && src[$-1] == '"') { src = src[1..$-1]; T ret; ret.length = src.length; // maximum length; retract to actual length later uint i = 0; for (uint t = 0; t < src.length;) { // process a block of non-escaped characters uint s = t; while (t < src.length && src[t] != '\\') ++t; // non-escaped characters uint j = i + t - s; ret[i..j] = src[s..t]; // copy a block i = j; // process a block of escaped characters while (t < src.length && src[t] == '\\') { t++; if (t == src.length) throw new ParseException ("Invalid string: ends \\\" !"); // next char is " ret[i++] = replaceEscapedChar (src[t++]); // throws if it's invalid } } return ret[0..i]; } else if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T) (src); throw new ParseException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])"); } // Unicode conversions for strings: T parseTo(T : wchar[]) (char[] src) { // May throw a UnicodeException; don't bother catching and rethrowing: return Utf.toString16 (parseTo!(char[]) (src)); } T parseTo(T : dchar[]) (char[] src) { // May throw a UnicodeException; don't bother catching and rethrowing: return Utf.toString32 (parseTo!(char[]) (src)); } // Binary (array special case) T parseTo(T : ubyte[]) (char[] src) { src = Util.trim(src); // Standard case: if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T) (src); // Special case: sequence of hex digits, each pair of which is a ubyte if (src.length >= 2 && src[0..2] == "0x") { src = src[2..$]; // strip down to actual digits // Must be in pairs: if (src.length % 2 == 1) throw new ParseException ("Invalid binary: odd number of chars"); T ret; ret.length = src.length / 2; // exact for (uint i, pos; pos + 1 < src.length; ++i) { ubyte x = readHexChar(src, pos) << 4; x |= readHexChar(src, pos); ret[i] = x; } return ret; } else throw new ParseException ("Invalid ubyte[]: not an array and doesn't start 0x"); } debug (UnitTest) unittest { assert (parseTo!(double[]) (`[1.0,1.0e-10]`) == [1.0, 1.0e-10]); // generic array stuff assert (parseTo!(double[]) (`[ ]`) == cast(double[]) []); // empty array // char[] and char conversions, with commas, escape sequences and multichar UTF8 characters: assert (parseTo!(char[][]) (`[ ".\"", [',','\''] ,"!\b€" ]`) == [ ".\"".dup, [',','\''] ,"!\b€" ]); // wchar[] and dchar[] conversions: // The characters were pretty-much pulled at random from unicode tables. // The last few cause some wierd (display only) effects in my editor. assert (parseTo!(wchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"w); assert (parseTo!(dchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"d); assert (parseTo!(ubyte[]) (`0x01F2AC`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]); // ubyte[] special notation assert (parseTo!(ubyte[]) (`[01 ,0xF2, 0xAC]`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]); // ubyte[] std notation } // Basic types // Char T parseTo(T : char) (char[] src) { src = Util.trim(src); if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'') throw new ParseException ("Invalid char: not quoted (e.g. 'c')"); if (src[1] != '\\' && src.length == 3) return src[1]; // Either non escaped if (src.length == 4) return replaceEscapedChar (src[2]); // Or escaped // Report various errors; warnings for likely and difficult to tell cases: // Warn in case it's a multibyte UTF-8 character: if (src[1] & 0xC0u) throw new UnicodeException ("Invalid char: too long (non-ASCII UTF-8 characters cannot be read as a single character)", 1); throw new ParseException ("Invalid char: too long"); } /* Basic unicode convertions for wide-chars. * NOTE: c > 127 signals the start of a multibyte UTF-8 sequence which must be converted for * UTF-16/32. But since we don't know what the next bytes are we can't do the conversion. */ const char[] WIDE_CHAR_ERROR = "Error: unicode non-ascii character cannot be converted from a single UTF-8 char"; T parseTo(T : wchar) (char[] src) { char c = parseTo!(char) (src); if (c <= 127u) return cast(wchar) c; // this char can be converted else throw new UnicodeException (WIDE_CHAR_ERROR, 1); } T parseTo(T : dchar) (char[] src) { char c = parseTo!(char) (src); if (c <= 127u) return cast(dchar) c; // this char can be converted else throw new UnicodeException (WIDE_CHAR_ERROR, 1); } debug (UnitTest) unittest { assert (parseTo!(char) ("\'\\\'\'") == '\''); assert (parseTo!(wchar) ("'X'") == 'X'); assert (parseTo!(dchar) ("'X'") == 'X'); } // Bool T parseTo(T : bool) (char[] src) { src = Util.trim(src); if (src == "true") return true; if (src == "false") return false; uint pos; while (src.length > pos && src[pos] == '0') ++pos; // skip leading zeros if (src.length == pos && pos > 0) return false; if (src.length == pos + 1 && src[pos] == '1') return true; throw new ParseException ("Invalid bool: not true or false and doesn't evaluate to 0 or 1"); } debug (UnitTest) unittest { assert (parseTo!(bool[]) (`[true,false,01,00]`) == cast(bool[]) [1,0,1,0]); } // Ints T parseTo(T : byte) (char[] src) { return toTInt!(T) (src); } T parseTo(T : short) (char[] src) { return toTInt!(T) (src); } T parseTo(T : int) (char[] src) { return toTInt!(T) (src); } T parseTo(T : long) (char[] src) { return toTInt!(T) (src); } T parseTo(T : ubyte) (char[] src) { return toTInt!(T) (src); } T parseTo(T : ushort) (char[] src) { return toTInt!(T) (src); } T parseTo(T : uint) (char[] src) { return toTInt!(T) (src); } T parseTo(T : ulong) (char[] src) { return toTInt!(T) (src); } debug (UnitTest) unittest { assert (parseTo!(byte) ("-5") == cast(byte) -5); // annoyingly, octal syntax differs from D (blame tango): assert (parseTo!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]); } // Floats T parseTo(T : float) (char[] src) { return toTFloat!(T) (src); } T parseTo(T : double) (char[] src) { return toTFloat!(T) (src); } T parseTo(T : real) (char[] src) { return toTFloat!(T) (src); } debug (UnitTest) unittest { assert (parseTo!(float) ("0.0") == 0.0f); assert (parseTo!(double) ("-1e25") == -1e25); assert (parseTo!(real) ("5.24e-269") == cast(real) 5.24e-269); } //END parseTo templates //BEGIN Utility funcs /** Trims whitespace at ends of string and checks for and removes array brackets: [] * * Throws: * ParseException if brackets aren't end non-whitespace characters. * * Returns: * String without brackets (and whitespace outside those brackets). Useful for passing to split. */ char[] stripBrackets (char[] src) { src = Util.trim(src); if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return src[1..$-1]; throw new ParseException ("Invalid bracketed string: not [...]"); } /** Splits a string into substrings separated by '$(B ,)' with support for characters and strings * containing escape sequences and for embedded arrays ($(B [...])). * * Params: * src A string to separate on commas. Where used for parsing arrays, the brackets enclosing * the array should be removed before calling this function (stripBrackets can do this). * * Returns: * An array of substrings within src, excluding commas. Whitespace is not stripped and * empty strings may get returned. * * Remarks: * This function is primarily intended for as a utility function for use by the templates * parsing arrays and associative arrays, but it may be useful in other cases too. Hence the * fact no brackets are stripped from src. */ char[][] split (char[] src) { src = Util.trim (src); if (src == "") return []; // empty array: no elements when no data uint depth = 0; // surface depth (embedded arrays) char[][] ret; ret.length = src.length / 3; // unlikely to need a longer array uint k = 0; // current split piece uint i = 0, j = 0; // current read location, start of current piece while (i < src.length) { char c = src[i]; if (c == '\'' || c == '"') { // string or character ++i; while (i < src.length && src[i] != c) { if (src[i] == '\\') ++i; // escape seq. ++i; } // Doesn't throw if no terminal quote at end of src, but this should be caught later. } else if (c == '[') ++depth; else if (c == ']') { if (depth) --depth; else throw new ParseException ("Invalid array literal: closes before end of data item."); } else if (c == ',' && depth == 0) { // only if not an embedded array if (ret.length <= k) ret.length = ret.length * 2; ret[k++] = src[j..i]; // add this piece and increment k j = i + 1; } ++i; } if (ret.length <= k) ret.length = k + 1; ret[k] = src[j..i]; // add final piece (i >= j) return ret[0..k+1]; } /* Templated read-int function to read (un)signed 1-4 byte integers. * * Actually a reimplementation of tango.text.convert.Integer toLong and parse functions. */ private TInt toTInt(TInt) (char[] src) { const char[] INT_OUT_OF_RANGE = "Integer out of range"; bool sign; uint radix, ate, ate2; // Trim off whitespace. // NOTE: Cannot use tango.text.convert.Integer.trim to trim leading whitespace since it doesn't // treat new-lines, etc. as whitespace which for our purposes is whitespace. src = Util.trim (src); ate = cInt.trim (src, sign, radix); if (ate == src.length) throw new ParseException ("Invalid integer: no digits"); ulong val = cInt.convert (src[ate..$], radix, &ate2); ate += ate2; if (ate < src.length) throw new ParseException ("Invalid integer at marked character: \"" ~ src[0..ate] ~ "'" ~ src[ate] ~ "'" ~ src[ate+1..$] ~ "\""); if (val > TInt.max) throw new ParseException (INT_OUT_OF_RANGE); if (sign) { long sval = cast(long) -val; if (sval > TInt.min) return cast(TInt) sval; else throw new ParseException (INT_OUT_OF_RANGE); } return cast(TInt) val; } /* Basically a reimplementation of tango.text.convert.Float.toFloat which checks for * whitespace before throwing an exception for overlong input. */ private TFloat toTFloat(TFloat) (char[] src) { // NOTE: As for toTInt(), this needs to strip leading as well as trailing whitespace. src = Util.trim (src); if (src == "") throw new ParseException ("Invalid float: no digits"); uint ate; TFloat x = cFloat.parse (src, &ate); return x; } /* Throws an exception on invalid escape sequences. Supported escape sequences are the following * subset of those supported by D: \" \' \\ \a \b \f \n \r \t \v */ private char replaceEscapedChar (char c) { // This code was generated: if (c <= 'b') { if (c <= '\'') { if (c == '\"') { return '\"'; } else if (c == '\'') { return '\''; } } else { if (c == '\\') { return '\\'; } else if (c == 'a') { return '\a'; } else if (c == 'b') { return '\b'; } } } else { if (c <= 'n') { if (c == 'f') { return '\f'; } else if (c == 'n') { return '\n'; } } else { if (c == 'r') { return '\r'; } else if (c == 't') { return '\t'; } else if (c == 'v') { return '\v'; } } } // if we haven't returned: throw new ParseException ("Invalid escape sequence: \\"~c); } // Reads one hex char: [0-9A-Fa-f]. Otherwise throws an exception. Doesn't check src.length. private ubyte readHexChar (char[] src, inout uint pos) { ubyte x; if (src[pos] >= '0' && src[pos] <= '9') x = src[pos] - '0'; else if (src[pos] >= 'A' && src[pos] <= 'F') x = src[pos] - 'A' + 10; else if (src[pos] >= 'a' && src[pos] <= 'f') x = src[pos] - 'a' + 10; else throw new ParseException ("Invalid hex digit."); ++pos; return x; } // Generic array reader // Assumes input is of form "[xxxxx]" (i.e. first and last chars are '[', ']' and length >= 2). private T[] toArray(T : T[]) (char[] src) { T[] ret = new T[16]; // avoid unnecessary allocations uint i = 0; foreach (char[] element; split(src[1..$-1])) { if (i == ret.length) ret.length = ret.length * 2; ret[i] = parseTo!(T) (element); ++i; } return ret[0..i]; } debug (UnitTest) { import tango.io.Console; unittest { Cout ("Running unittest: parseTo ...").flush; assert (parseTo!(char[]) ("\"\\a\\b\\t\\n\\v\\f\\r\\\"\\\'\\\\\"") == "\a\b\t\n\v\f\r\"\'\\"); Cout (" complete").newline; } } //END Utility funcs