Skip to content

Commit 9345b40

Browse files
PHP-86 - Decimal __toString creates incorrect string representation (#92)
Also fixed defect where parsing a double resulted in a garbage Decimal object.
1 parent 109c528 commit 9345b40

File tree

3 files changed

+80
-91
lines changed

3 files changed

+80
-91
lines changed

ext/src/Decimal.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ to_mpf(mpf_t result, php_driver_numeric *decimal)
6666
static void
6767
from_double(php_driver_numeric *result, double value)
6868
{
69-
cass_int64_t raw = (cass_int64_t) value;
69+
cass_int64_t raw = *((cass_int64_t*) &value);
70+
7071
cass_int64_t mantissa = raw & DOUBLE_MANITSSA_MASK;
7172
cass_int64_t exponent = (raw >> DOUBLE_MANTISSA_BITS) & DOUBLE_EXPONENT_MASK;
7273
char mantissa_str[32];

ext/util/math.c

Lines changed: 56 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -424,27 +424,12 @@ php_driver_parse_decimal(char *in, int in_len, mpz_t *number, long *scale TSRMLS
424424
void
425425
php_driver_format_integer(mpz_t number, char **out, int *out_len)
426426
{
427-
size_t len;
428-
char *tmp;
429-
430-
len = mpz_sizeinbase(number, 10);
431-
if (mpz_sgn(number) < 0)
432-
len++;
433-
434-
tmp = (char*) emalloc((len + 1) * sizeof(char));
435-
mpz_get_str(tmp, 10, number);
436-
437-
if (tmp[len - 1] == '\0') {
438-
len--;
439-
} else {
440-
tmp[len] = '\0';
441-
}
442-
443-
*out = tmp;
444-
*out_len = len;
427+
/* Adding 2 ensures enough space for the null-terminator and negative sign */
428+
*out = (char*) emalloc(mpz_sizeinbase(number, 10) + 2);
429+
mpz_get_str(*out, 10, number);
430+
*out_len = strlen(*out);
445431
}
446432

447-
448433
void
449434
php_driver_format_decimal(mpz_t number, long scale, char **out, int *out_len)
450435
{
@@ -462,91 +447,96 @@ php_driver_format_decimal(mpz_t number, long scale, char **out, int *out_len)
462447
if (mpz_sgn(number) < 0)
463448
negative = 1;
464449

450+
// Ultimately, we want to return a string representation of this decimal. So allocate
451+
// a buffer that could hold this decimal in the worst possible conservative case.
452+
453+
// absolute length + negative sign + point sign + scale (in case we end up with a number with leading 0s) +
454+
// exponent modifier and sign.
455+
tmp = (char*) emalloc(len + negative + 1 + scale + 2);
456+
mpz_get_str(tmp, 10, number);
457+
458+
// Update len to be the true length of the string representation of |number|. mpz_sizeinbase
459+
// can return a higher result than the actual length.
460+
// NOTE: the length of the string includes the negative sign (if present); account for that.
461+
len = strlen(tmp) - negative;
462+
465463
point = len - scale;
466464

467-
if (scale >= 0 && (point - 1) >= -6) {
465+
// We only support numbers with scale >= 0.
466+
assert(scale >= 0);
467+
468+
if ((point - 1) >= -6) {
468469
if (point <= 0) {
469-
/* current position */
470+
// e.g. -0.002 and 0.002
471+
int shift_start = negative;
472+
473+
// current position
470474
int i = 0;
471-
/* absolute length + negative sign + point sign + leading zeroes */
472-
total = len + negative + 2 + (point * -1);
473-
tmp = (char*) emalloc((total + 1) * sizeof(char));
474475

476+
// Move the numeric part (skip leading minus if needed) of tmp right by enough bytes to make room for
477+
// 0.0000 (as many leading zeroes as necessary).
478+
memmove(&(tmp[shift_start + 2 - point]), &(tmp[shift_start]), len);
479+
480+
// This is a (possibly negative) number with a 0 integer part.
475481
if (negative)
476482
tmp[i++] = '-';
477483

478484
tmp[i++] = '0';
479485
tmp[i++] = '.';
480486

487+
// Add leading zeroes.
481488
while (point < 0) {
482489
tmp[i++] = '0';
483490
point++;
484491
}
485492

486-
mpz_get_str(&(tmp[i]), 10, number);
487-
488-
if (tmp[i + len + negative - 1] == '\0') {
489-
len--;
490-
total--;
491-
}
492-
493-
if (negative)
494-
memmove(&(tmp[i]), &(tmp[i + 1]), len);
495-
493+
total = i + len;
496494
tmp[total] = '\0';
497495
} else {
496+
// e.g. 1.2, -1.2
498497
/* absolute length + negative sign + point sign */
499498
total = len + negative + 1;
500-
point = point + negative;
501-
tmp = (char*) emalloc((total + 1) * sizeof(char));
502499

503-
mpz_get_str(tmp, 10, number);
500+
// Insert the decimal point at the right location in the string.
504501

505-
if (tmp[len + negative - 1] == '\0') {
506-
len--;
507-
total--;
508-
}
502+
// point is the index at which to insert the decimal point, but it assumes we have a positive
503+
// number. Move it to the right if we have a negative number.
504+
point += negative;
509505

510506
memmove(&(tmp[point + 1]), &(tmp[point]), total - point);
511507

512508
tmp[point] = '.';
513509
tmp[total] = '\0';
514510
}
515511
} else {
512+
// Very small positive or negative number that we want to express in scientific notation:
513+
// 0.000000004, -0.000000004
514+
516515
int exponent = -1;
517516
int exponent_size = -1;
518-
int i = 1;
519517

520-
/* absolute length + negative sign + exponent modifier and sign */
521-
total = len + negative + 2;
522-
/* (optional) point sign */
523-
if (len > 1)
524-
total++;
525-
526-
/* exponent value */
518+
// Calculate the exponent value and its size.
527519
exponent = point - 1;
528520
exponent_size = (int) ceil(log10(abs(exponent) + 2)) + 1;
529521

530-
total = total + exponent_size;
531-
tmp = (char*) emalloc((total + 1) * sizeof(char));
522+
// If we only have one significant digit, we want to produce a string like
523+
// 1E-9. If we have more significant digits, then 1.123E-9.
532524

533-
mpz_get_str(tmp, 10, number);
525+
if (len == 1) {
526+
// Simple case; tmp is already leading with our number as we want it. Append E(exp) to it
527+
// and we're done.
528+
sprintf(&(tmp[1 + negative]), "E%+d", exponent);
529+
total = 1 + negative + 1 + exponent_size;
530+
} else {
531+
// We have a more complex number. Insert a decimal point after the first digit.
532+
point = negative ? 2 : 1;
533+
memmove(&(tmp[point + 1]), &(tmp[point]), len-1);
534+
tmp[point] = '.';
534535

535-
if (tmp[len + negative - 1] == '\0') {
536-
len--;
537-
total--;
536+
// Now append the exponent to the end and we're done.
537+
sprintf(&(tmp[point + len]), "E%+d", exponent);
538+
total = point + len + 1 + exponent_size;
538539
}
539-
540-
if (negative)
541-
i++;
542-
543-
memmove(&(tmp[i + 1]), &(tmp[i]), len - i);
544-
tmp[i] = '.';
545-
tmp[len + i++] = 'E';
546-
547-
snprintf(&(tmp[len + i]), exponent_size, "%+d", exponent);
548-
549-
tmp[total] = '\0';
550540
}
551541

552542
*out = tmp;

tests/unit/Cassandra/DecimalTest.php

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ public function testThrowsWhenCreatingNotAnInteger()
3333
}
3434

3535
/**
36-
* @dataProvider validStrings
36+
* @dataProvider validStringsAndNumbers
3737
*/
38-
public function testCorrectlyParsesStrings($number, $value, $scale, $string)
38+
public function testCorrectlyParsesStringsAndNumbers($input, $value, $scale, $string)
3939
{
40-
$number = new Decimal($number);
40+
$number = new Decimal($input);
4141
$this->assertEquals($value, $number->value());
4242
$this->assertEquals($scale, $number->scale());
43+
// Test to_string
4344
$this->assertEquals($string, (string) $number);
45+
// Test to_double
46+
$this->assertLessThanOrEqual(0.01, abs((float)$string - (float)$number));
4447
}
4548

46-
public function validStrings()
49+
public function validStringsAndNumbers()
4750
{
4851
return array(
4952
array("123", "123", 0, "123"),
@@ -58,26 +61,21 @@ public function validStrings()
5861
array("123.1", "1231", 1, "123.1"),
5962
array("55.55", "5555", 2, "55.55"),
6063
array("-123.123", "-123123", 3, "-123.123"),
61-
array("0.5", "5", 1, "0.5")
62-
);
63-
}
64-
65-
/**
66-
* @dataProvider validNumbers
67-
*/
68-
public function testFromNumbers($number)
69-
{
70-
//$decimal = new Decimal($number);
71-
//$this->assertEquals((float)$number, (float)$decimal);
72-
//$this->assertEquals((float)$number, (float)(string)$decimal);
73-
//$this->assertEquals((int)$number, $decimal->toInt());
74-
}
75-
76-
public function validNumbers()
77-
{
78-
return array(
79-
array(0.123),
80-
array(123),
64+
array("0.5", "5", 1, "0.5"),
65+
array(0.123, "1229999999999999982236431605997495353221893310546875", 52, "0.123"),
66+
array(123, "123", 0, "123"),
67+
array(123.5, "1235", 1, "123.5"),
68+
array(-123, "-123", 0, "-123"),
69+
array("9.5", "95", 1, "9.5"),
70+
array("-9.5", "-95", 1, "-9.5"),
71+
array("0.00001", "1", 5, "0.00001"),
72+
array("-0.00001", "-1", 5, "-0.00001"),
73+
array("0.00000001", "1", 8, "1E-8"),
74+
array("-0.00000001", "-1", 8, "-1E-8"),
75+
array("0.000000095", "95", 9, "9.5E-8"),
76+
array("-0.000000095", "-95", 9, "-9.5E-8"),
77+
array("0.000000015", "15", 9, "1.5E-8"),
78+
array("-0.000000015", "-15", 9, "-1.5E-8"),
8179
);
8280
}
8381

0 commit comments

Comments
 (0)