    "The [[Float]] value of the given
     [[string representation|string]] of a decimal floating
     point number, or `null` if the string does not represent a
     decimal floating point number.

     If the given string representation contains more digits
     than can be represented by a `Float`, then the least
     significant digits are ignored.

     The syntax accepted by this method is the same as the
     syntax for a `Float` literal in the Ceylon language
     except that it may optionally begin with a sign
     character (`+` or `-`) and may not contain grouping
     underscore characters."
    see (`function formatFloat`,
         `function parseInteger`)
    tagged("Numbers", "Basic types")
    shared Float? parseFloat(String string) {

        // parse the sign first
        Integer sign;
        String unsignedPart;
        if (string.startsWith("-")) {
            sign = -1;
            unsignedPart = string[1...];
        }
        else if (string.startsWith("+")) {
            sign = +1;
            unsignedPart = string[1...];
        }
        else {
            sign = +1;
            unsignedPart = string;
        }
        // split into three main parts
        String wholePart;
        String fractionalPart;
        String? rest;
        if (exists dot = unsignedPart.firstOccurrence('.')) {
            wholePart = unsignedPart[...dot-1];
            String afterWholePart = unsignedPart[dot+1...];
            if (exists mag
                = afterWholePart.firstIndexWhere(Character.letter)) {
                fractionalPart = afterWholePart[...mag-1];
                rest = afterWholePart[mag...];
            }
            else {
                fractionalPart = afterWholePart;
                rest = null;
            }
        }
        else {
            if (exists mag
                = unsignedPart.firstIndexWhere(Character.letter)) {
                wholePart = unsignedPart[...mag-1];
                rest = unsignedPart[mag...];
            }
            else {
                wholePart = unsignedPart;
                rest = null;
            }
            fractionalPart = "0";
        }

        if (!wholePart.every(Character.digit) ||
            !fractionalPart.every(Character.digit)) {
            return null;
        }

        value usableWholePart
                = wholePart[0:maximumIntegerExponent];
        value usableFractionalPart
                = fractionalPart[0:
                    maximumIntegerExponent
                        - usableWholePart.size];

        value digits = usableWholePart + usableFractionalPart;
        value shift
                = usableFractionalPart.empty
                then usableWholePart.size - wholePart.size
                else usableFractionalPart.size;

        Integer exponent;
        if (exists rest) {
            if (exists magnitude
                    = parseFloatExponent(rest)) {
                exponent = magnitude - shift;
            }
            else {
                return null;
            }
        }
        else {
            exponent = -shift;
        }

        if (exists unsigned = parseInteger(digits)) {
            Float signed
                    = unsigned == 0
                    then 0 * sign.float //preserve sign of -0.0
                    else (sign * unsigned).nearestFloat;
            value exponentMagnitude = exponent.magnitude;
            if (exponentMagnitude == 0) {
                return signed;
            }
            else if (exponentMagnitude<=maximumIntegerExponent) {
                value scale = 10^exponentMagnitude;
                return exponent<0
                then signed / scale
                else signed * scale;
            }
            else {
                //scale can't be represented as
                //an integer, resulting in some
                //rounding error
                return signed * 10.0^exponent;
            }
        }

        return null;
    }

    //TODO: replace with a native implementation
    "The maximum number of decimal digits that can be
     represented by an [[Integer]]."
    Integer maximumIntegerExponent
            = smallest(runtime.maxIntegerValue.string.size,
                       runtime.minIntegerValue.string.size-1)
                - 1;

    Integer? parseFloatExponent(String string) {
        switch (string)
        case ("k") {
            return 3;
        }
        case ("M") {
            return 6;
        }
        case ("G") {
            return 9;
        }
        case ("T") {
            return 12;
        }
        case ("P") {
            return 15;
        }
        case ("m") {
            return -3;
        }
        case ("u") {
            return -6;
        }
        case ("n") {
            return -9;
        }
        case ("p") {
            return -12;
        }
        case ("f") {
            return -15;
        }
        else {
            if (string.lowercased.startsWith("e") &&
                string.rest.every(digitOrSign)) {
                return parseInteger(string.rest);
            }
            else {
                return null;
            }
        }
    }

    Boolean(Character) digitOrSign
            = or(Character.digit, "+-".contains);