Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consistently use floating point numbers #620

Merged
merged 1 commit into from Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -2754,19 +2754,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 @@ -2775,32 +2780,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 @@ -2810,34 +2824,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 @@ -2846,14 +2852,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