"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);