in mysql_strings/decimal.cc [1647:1829]
int decimal_round(const decimal_t *from, decimal_t *to, int scale,
decimal_round_mode mode) {
int frac0 = scale > 0 ? ROUND_UP(scale) : (scale + 1) / DIG_PER_DEC1,
frac1 = ROUND_UP(from->frac), round_digit = 0,
intg0 = ROUND_UP(from->intg), error = E_DEC_OK, len = to->len;
dec1 *buf0 = from->buf, *buf1 = to->buf, x, y, carry = 0;
int first_dig;
sanity(to);
switch (mode) {
case HALF_UP:
case HALF_EVEN:
round_digit = 5;
break;
case CEILING:
round_digit = from->sign ? 10 : 0;
break;
case FLOOR:
round_digit = from->sign ? 0 : 10;
break;
case TRUNCATE:
round_digit = 10;
break;
default:
assert(0);
}
/*
For my_decimal we always use len == DECIMAL_BUFF_LENGTH == 9
For internal testing here (ifdef MAIN) we always use len == 100/4
*/
assert(from->len == to->len);
if (unlikely(frac0 + intg0 > len)) {
frac0 = len - intg0;
scale = frac0 * DIG_PER_DEC1;
error = E_DEC_TRUNCATED;
}
if (scale + from->intg < 0) {
decimal_make_zero(to);
return E_DEC_OK;
}
if (to != from) {
dec1 *p0 = buf0 + intg0 + std::max(frac1, frac0);
dec1 *p1 = buf1 + intg0 + std::max(frac1, frac0);
assert(p0 - buf0 <= len);
assert(p1 - buf1 <= len);
while (buf0 < p0) *(--p1) = *(--p0);
buf0 = to->buf;
buf1 = to->buf;
to->sign = from->sign;
to->intg = std::min(intg0, len) * DIG_PER_DEC1;
}
if (frac0 > frac1) {
buf1 += intg0 + frac1;
while (frac0-- > frac1) *buf1++ = 0;
goto done;
}
if (scale >= from->frac) goto done; /* nothing to do */
buf0 += intg0 + frac0 - 1;
buf1 += intg0 + frac0 - 1;
if (scale == frac0 * DIG_PER_DEC1) {
int do_inc = false;
assert(frac0 + intg0 >= 0);
switch (round_digit) {
case 0: {
dec1 *p0 = buf0 + (frac1 - frac0);
for (; p0 > buf0; p0--) {
if (*p0) {
do_inc = true;
break;
}
}
break;
}
case 5: {
x = buf0[1] / DIG_MASK;
do_inc =
(x > 5) ||
((x == 5) && (mode == HALF_UP || (frac0 + intg0 > 0 && *buf0 & 1)));
break;
}
default:
break;
}
if (do_inc) {
if (frac0 + intg0 > 0)
(*buf1)++;
else
*(++buf1) = DIG_BASE;
} else if (frac0 + intg0 == 0) {
decimal_make_zero(to);
return E_DEC_OK;
}
} else {
/* TODO - fix this code as it won't work for CEILING mode */
int pos = frac0 * DIG_PER_DEC1 - scale - 1;
assert(frac0 + intg0 > 0);
x = *buf1 / powers10[pos];
y = x % 10;
if (y > round_digit ||
(round_digit == 5 && y == 5 && (mode == HALF_UP || (x / 10) & 1)))
x += 10;
*buf1 = powers10[pos] * (x - y);
}
/*
In case we're rounding e.g. 1.5e9 to 2.0e9, the decimal_digit_t's inside
the buffer are as follows.
Before <1, 5e8>
After <2, 5e8>
Hence we need to set the 2nd field to 0.
The same holds if we round 1.5e-9 to 2e-9.
*/
if (frac0 < frac1) {
dec1 *buf = to->buf + ((scale == 0 && intg0 == 0) ? 1 : intg0 + frac0);
dec1 *end = to->buf + len;
while (buf < end) *buf++ = 0;
}
if (*buf1 >= DIG_BASE) {
carry = 1;
*buf1 -= DIG_BASE;
while (carry && --buf1 >= to->buf) ADD(*buf1, *buf1, 0, carry);
if (unlikely(carry)) {
/* shifting the number to create space for new digit */
if (frac0 + intg0 >= len) {
frac0--;
scale = frac0 * DIG_PER_DEC1;
error = E_DEC_TRUNCATED; /* XXX */
}
for (buf1 = to->buf + intg0 + std::max(frac0, 0); buf1 > to->buf;
buf1--) {
/* Avoid out-of-bounds write. */
if (buf1 < to->buf + len)
buf1[0] = buf1[-1];
else
error = E_DEC_OVERFLOW;
}
*buf1 = 1;
/* We cannot have more than 9 * 9 = 81 digits. */
if (to->intg < len * DIG_PER_DEC1)
to->intg++;
else
error = E_DEC_OVERFLOW;
}
} else {
for (;;) {
if (likely(*buf1)) break;
if (buf1-- == to->buf) {
/* making 'zero' with the proper scale */
dec1 *p0 = to->buf + frac0 + 1;
to->intg = 1;
to->frac = std::max(scale, 0);
to->sign = false;
for (buf1 = to->buf; buf1 < p0; buf1++) *buf1 = 0;
return E_DEC_OK;
}
}
}
/* Here we check 999.9 -> 1000 case when we need to increase intg */
first_dig = to->intg % DIG_PER_DEC1;
if (first_dig && (*buf1 >= powers10[first_dig])) to->intg++;
if (scale < 0) scale = 0;
done:
assert(to->intg <= (len * DIG_PER_DEC1));
to->frac = scale;
return error;
}