Skip to content

Commit

Permalink
Merge pull request #620 from stof/floating_points
Browse files Browse the repository at this point in the history
Consistently use floating point numbers
  • Loading branch information
stof committed Oct 21, 2022
2 parents bf99dcd + 4a40784 commit 957888e
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 322 deletions.
15 changes: 5 additions & 10 deletions src/Ast/Sass/Expression/NumberExpression.php
Expand Up @@ -14,6 +14,7 @@

use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\SassNumber;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;

/**
Expand All @@ -24,7 +25,7 @@
final class NumberExpression implements Expression
{
/**
* @var float|int
* @var float
* @readonly
*/
private $value;
Expand All @@ -41,20 +42,14 @@ final class NumberExpression implements Expression
*/
private $unit;

/**
* @param int|float $value
*/
public function __construct($value, FileSpan $span, ?string $unit = null)
public function __construct(float $value, FileSpan $span, ?string $unit = null)
{
$this->value = $value;
$this->span = $span;
$this->unit = $unit;
}

/**
* @return float|int
*/
public function getValue()
public function getValue(): float
{
return $this->value;
}
Expand All @@ -76,6 +71,6 @@ public function accept(ExpressionVisitor $visitor)

public function __toString(): string
{
return $this->value . ($this->unit ?? '');
return (string) SassNumber::create($this->value, $this->unit);
}
}
16 changes: 7 additions & 9 deletions src/Parser/Parser.php
Expand Up @@ -337,28 +337,26 @@ protected function string(): string
}

/**
* Consumes and returns a natural number (that is, a non-negative integer).
* Consumes and returns a natural number (that is, a non-negative integer) as a double.
*
* Doesn't support scientific notation.
*/
protected function naturalNumber(): int
protected function naturalNumber(): float
{
$first = $this->scanner->readChar();

if (!Character::isDigit($first)) {
$this->scanner->error('Expected digit.', $this->scanner->getPosition() - 1);
}

$number = $first;
$number = (float) intval($first);

$next = $this->scanner->peekChar();

while ($next !== null && Character::isDigit($next)) {
$number .= $this->scanner->readChar();
$next = $this->scanner->peekChar();
while (Character::isDigit($this->scanner->peekChar())) {
$number *= 10;
$number += intval($this->scanner->readChar());
}

return intval($number);
return $number;
}

/**
Expand Down
69 changes: 35 additions & 34 deletions src/Parser/StylesheetParser.php
Expand Up @@ -2779,19 +2779,24 @@ private function number(): NumberExpression
{
$start = $this->scanner->getPosition();
$first = $this->scanner->peekChar();
$sign = $first === '-' ? -1 : 1;

if ($first === '+' || $first === '-') {
$this->scanner->readChar();
}

$number = $this->scanner->peekChar() === '.' ? 0 : $this->naturalNumber();
if ($this->scanner->peekChar() !== '.') {
$this->consumeNaturalNumber();
}

// Don't complain about a dot after a number unless the number starts with a
// dot. We don't allow a plain ".", but we need to allow "1." so that
// "1..." will work as a rest argument.
$number += $this->tryDecimal($this->scanner->getPosition() !== $start);
$number *= $this->tryExponent();
$this->tryDecimal($this->scanner->getPosition() !== $start);
$this->tryExponent();

// Use PHP's built-in double parsing so that we don't accumulate
// floating-point errors for numbers with lots of digits.
$number = floatval($this->scanner->substring($start));

$unit = null;
if ($this->scanner->scanChar('%')) {
Expand All @@ -2800,32 +2805,41 @@ private function number(): NumberExpression
$unit = $this->identifier(false, true);
}

return new NumberExpression($sign * $number, $this->scanner->spanFrom($start), $unit);
return new NumberExpression($number, $this->scanner->spanFrom($start), $unit);
}

/**
* Consumes a natural number (that is, a non-negative integer).
*
* Doesn't support scientific notation.
*/
private function consumeNaturalNumber(): void
{
if (!Character::isDigit($this->scanner->readChar())) {
$this->scanner->error('Expected digit.', $this->scanner->getPosition() - 1);
}

while (Character::isDigit($this->scanner->peekChar())) {
$this->scanner->readChar();
}
}

/**
* Consumes the decimal component of a number and returns its value, or 0 if
* there is no decimal component.
* Consumes the decimal component of a number if it exists.
*
* If $allowTrailingDot is `false`, this will throw an error if there's a
* dot without any numbers following it. Otherwise, it will ignore the dot
* without consuming it.
*
* @param bool $allowTrailingDot
*
* @return int|float
*/
private function tryDecimal(bool $allowTrailingDot = false)
private function tryDecimal(bool $allowTrailingDot = false): void
{
$start = $this->scanner->getPosition();

if ($this->scanner->peekChar() !== '.') {
return 0;
return;
}

if (!Character::isDigit($this->scanner->peekChar(1))) {
if ($allowTrailingDot) {
return 0;
return;
}

$this->scanner->error('Expected digit.', $this->scanner->getPosition() + 1);
Expand All @@ -2835,34 +2849,26 @@ private function tryDecimal(bool $allowTrailingDot = false)
while (Character::isDigit($this->scanner->peekChar())) {
$this->scanner->readChar();
}

// Use PHP's built-in float parsing so that we don't accumulate
// floating-point errors for numbers with lots of digits.
return floatval($this->scanner->substring($start));
}

/**
* Consumes the exponent component of a number and returns its value, or 1 if
* there is no exponent component.
*
* @return int|float
* Consumes the exponent component of a number if it exists.
*/
private function tryExponent()
private function tryExponent(): void
{
$first = $this->scanner->peekChar();

if ($first !== 'e' && $first !== 'E') {
return 1;
return;
}

$next = $this->scanner->peekChar(1);

if (!Character::isDigit($next) && $next !== '-' && $next !== '+') {
return 1;
return;
}

$this->scanner->readChar();
$exponentSign = $next === '-' ? -1 : 1;
if ($next === '+' || $next === '-') {
$this->scanner->readChar();
}
Expand All @@ -2871,14 +2877,9 @@ private function tryExponent()
$this->scanner->error('Expected digit.');
}

$exponent = 0.0;

while (Character::isDigit($this->scanner->peekChar())) {
$exponent *= 10;
$exponent += \ord($this->scanner->readChar()) - \ord('0');
$this->scanner->readChar();
}

return pow(10, $exponentSign * $exponent);
}

/**
Expand Down
6 changes: 2 additions & 4 deletions src/Serializer/SerializeVisitor.php
Expand Up @@ -13,13 +13,11 @@
namespace ScssPhp\ScssPhp\Serializer;

use ScssPhp\ScssPhp\Ast\AstNode;
use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
use ScssPhp\ScssPhp\Ast\Css\CssComment;
use ScssPhp\ScssPhp\Ast\Css\CssDeclaration;
use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
use ScssPhp\ScssPhp\Ast\Css\CssNode;
use ScssPhp\ScssPhp\Ast\Css\CssParentNode;
use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
use ScssPhp\ScssPhp\Ast\Css\CssValue;
use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
Expand Down Expand Up @@ -934,9 +932,9 @@ public function visitNumber(SassNumber $value)
* Writes $number without exponent notation and with at most
* {@see SassNumber::PRECISION} digits after the decimal point.
*
* @param int|float $number
* @param float $number
*/
private function writeNumber($number): void
private function writeNumber(float $number): void
{
if (is_nan($number)) {
$this->buffer->write('NaN');
Expand Down

0 comments on commit 957888e

Please sign in to comment.