void adjustDecimalPrecision()

in source/shared/core_stmt.cpp [1882:2032]


void adjustDecimalPrecision(_Inout_ zval* param_z, _In_ SQLSMALLINT decimal_digits) 
{
    char* value = Z_STRVAL_P(param_z);
    int value_len = Z_STRLEN_P(param_z);
        
    // If the length is greater than maxDecimalStrLen, do not convert the string
    // 6 is derived from: 1 for the decimal point; 1 for sign of the number; 1 for 'e' or 'E' (scientific notation);
    //                    1 for sign of scientific exponent; 2 for length of scientific exponent
    const int MAX_DECIMAL_STRLEN = SQL_SERVER_DECIMAL_MAXIMUM_PRECISION + 6;
    if (value_len > MAX_DECIMAL_STRLEN) {
        return;
    }

    // If std::stold() succeeds, 'index' is the position of the first character after the numerical value
    long double d = 0;
    size_t index;
    try {
        d = std::stold(std::string(value), &index);
    }
    catch (const std::logic_error& ) {
        return;		// invalid input caused the conversion to throw an exception
    }
    if (index < value_len) {
        return;		// the input contains something else apart from the numerical value 
    }

    // Navigate to the first digit or the decimal point
    short is_negative = (d < 0) ? 1 : 0;
    char *src = value + is_negative;
    while (*src != DECIMAL_POINT && !isdigit(static_cast<unsigned int>(*src))) {
        src++;
    }

    // Check if the value is in scientific notation
    char *exp = strchr(src, 'E');
    if (exp == NULL) {
        exp = strchr(src, 'e');
    }

    // Find the decimal point
    char *pt = strchr(src, DECIMAL_POINT);

    char buffer[50] = "  ";             // A buffer with 2 blank spaces, as leeway
    int offset = 1 + is_negative;     // The position to start copying the original numerical value

    if (exp == NULL) {
		if (pt == NULL) {
			return;		// decimal point not found
		}

        int src_length = strnlen_s(src);
        int num_decimals = src_length - (pt - src) - 1;
		if (num_decimals <= decimal_digits) {
			return;     // no need to adjust number of decimals
		}

        memcpy_s(buffer + offset, src_length, src, src_length);
        round_up_decimal_numbers(buffer, (pt - src) + offset, decimal_digits, offset, src_length + offset);
    }
    else {
        int power = atoi(exp+1);
        if (abs(power) > SQL_SERVER_DECIMAL_MAXIMUM_PRECISION) {
            return;     // Out of range, so let the server handle this
        }

        int num_decimals = 0;
        if (power == 0) {
            // Simply chop off the exp part
            int length = (exp - src);
            memcpy_s(buffer + offset, length, src, length);

            if (pt != NULL) {
                // Adjust decimal places only if decimal point is found and number of decimals more than decimal_digits
                num_decimals = exp - pt - 1;
                if (num_decimals > decimal_digits) {
                    round_up_decimal_numbers(buffer, (pt - src) + offset, decimal_digits, offset, length + offset);
                }
            }            
        } else {
            int oldpos = 0;
            if (pt == NULL) {
                oldpos = exp - src;     // Decimal point not found, use the exp sign
            }
            else {
                oldpos = pt - src;
                num_decimals = exp - pt - 1;
                if (power > 0 && num_decimals <= power) {
                    return;             // The result will be a whole number, do nothing and return
                }
            }

            // Derive the new position for the decimal point in the buffer
            int newpos = oldpos + power;
            if (power > 0) {
                newpos = newpos + offset;
                if (num_decimals == 0) {
                    memset(buffer + offset + oldpos, '0', power);    // Fill parts of the buffer with zeroes first
                }
                else {
                    buffer[newpos] = DECIMAL_POINT;
                }
            }
            else {
                // The negative "power" part shows exactly how many places to move the decimal point.
                // Whether to pad zeroes depending on the original position of the decimal point pos.
                if (newpos <= 0) {
                    // If newpos is negative or zero, pad zeroes (size of '0.' + places to move) in the buffer
                    short numzeroes = 2 + abs(newpos);
                    memset(buffer + offset, '0', numzeroes);
                    newpos = offset + 1;                    // The new decimal position should be offset + '0'
                    buffer[newpos] = DECIMAL_POINT;			// Replace that '0' with the decimal point
                    offset = numzeroes + offset;            // Short offset now in the buffer
                }
                else {
                    newpos = newpos + offset;
                    buffer[newpos] = DECIMAL_POINT;
                }
            }

            // Start copying the content to the buffer until the exp sign or one more digit after decimal_digits 
            char *p = src;
            int idx = offset;
            int lastpos = newpos + decimal_digits + 1;
            while (p != exp && idx <= lastpos) {
                if (*p == DECIMAL_POINT) {
                    p++;
                    continue;
                }
                if (buffer[idx] == DECIMAL_POINT) {
                    idx++;
                }
                buffer[idx++] = *p;
                p++;
            }
            // Round up is required only when number of decimals is more than decimal_digits
            num_decimals = idx - newpos - 1;
            if (num_decimals > decimal_digits) {
                round_up_decimal_numbers(buffer, newpos, decimal_digits, offset, idx);
            }
        }      
    }

    // Set the minus sign if negative
    if (is_negative) {
        buffer[0] = '-';
    }

    zend_string* zstr = zend_string_init(buffer, strnlen_s(buffer), 0);
    zend_string_release(Z_STR_P(param_z));
    ZVAL_NEW_STR(param_z, zstr);
}