diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index a63cad5b7c7a..b803aad8054c 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -293,6 +293,43 @@ public static function trim(int|float $number) return json_decode(json_encode($number)); } + /** + * Format the given number in scientific notation. + * + * @param int|float $number + * @param int $exponent + * @param int $precision + * @param string|null $locale + * @return string|false + */ + public static function scientific(int|float $number, int $exponent = null, int $precision = 2, ?string $locale = null) + { + static::ensureIntlExtensionIsInstalled(); + + if (is_infinite($number) || is_nan($number)) { + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::SCIENTIFIC); + + return $formatter->format($number); + } + + $formatter = new NumberFormatter($locale ?? static::$locale, NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $precision); + $formatter->setAttribute(NumberFormatter::GROUPING_USED, 0); + + $exponent ??= ($number == 0 || abs($number) == 1) ? 0 : floor(log10(abs($number))); + $mantissa = round($number / pow(10, $exponent), $precision); + + if ($precision > 0) { + $mantissa = $formatter->format($mantissa); + $decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + if (str_contains($mantissa, $decimalSeparator)) { + $mantissa = rtrim(rtrim($mantissa, '0'), $decimalSeparator); + } + } + + return $mantissa . $formatter->getSymbol(NumberFormatter::EXPONENTIAL_SYMBOL) . $formatter->format($exponent); + } + /** * Execute the given callback using the given locale. * diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 5f5e44a63b2e..a1351f399b54 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -322,4 +322,68 @@ public function testTrim() $this->assertSame(12.3456789, Number::trim(12.3456789)); $this->assertSame(12.3456789, Number::trim(12.34567890000)); } + + #[RequiresPhpExtension('intl')] + public function testScientific() + { + $this->assertSame('1E0', Number::scientific(1)); + $this->assertSame('1E0', Number::scientific(1, precision: 0)); + $this->assertSame('1.23E1', Number::scientific(12.34)); + $this->assertSame('0.12E2', Number::scientific(12.34, exponent: 2)); + $this->assertSame('123400E-4', Number::scientific(12.34, exponent: -4)); + $this->assertSame('0.0012E4', Number::scientific(12.34, exponent: 4, precision: 4)); + $this->assertSame('1.23E1', Number::scientific(12.34, locale: 'en')); + $this->assertSame('1,23E1', Number::scientific(12.34, locale: 'fr')); + $this->assertContains(Number::scientific(12.34, locale: 'ar'), ['١٫٢٣أس١', '١٫٢٣اس١', '1.23E1']); + + // Large numbers + $this->assertSame('1E6', Number::scientific(1000000)); + $this->assertSame('10E5', Number::scientific(1000000, exponent: 5)); + $this->assertSame('100E4', Number::scientific(1000000, exponent: 4, precision: 1)); + $this->assertSame('1.23E6', Number::scientific(1234567)); + $this->assertSame('1.2E6', Number::scientific(1234567, precision: 1)); + $this->assertSame('123.46E4', Number::scientific(1234567, exponent: 4)); + $this->assertSame('123456.7E1', Number::scientific(1234567, exponent: 1)); + $this->assertSame('12.3E5', Number::scientific(1234567, exponent:5, precision: 1)); + $this->assertSame('123.5E4', Number::scientific(1234567, exponent: 4, precision: 1)); + $this->assertSame('123,46E4', Number::scientific(1234567, exponent: 4, locale: 'fr')); + $this->assertContains(Number::scientific(1234567, locale: 'ar'), ['١٫٢٣أس٦', '١٫٢٣اس٦', '1.23E6']); + $this->assertSame('1.23E8', Number::scientific(123456789)); + $this->assertSame('0.12E9', Number::scientific(123456789, exponent: 9)); + $this->assertSame('123456.789E3', Number::scientific(123456789, exponent: 3, precision: 15)); + $this->assertSame('1.2E8', Number::scientific(123456789, precision: 1)); + $this->assertSame('1,23E8', Number::scientific(123456789, locale: 'fr')); + + // Small numbers + $this->assertSame('1E-3', Number::scientific(0.001)); + $this->assertSame('10E-4', Number::scientific(0.001, exponent: -4)); + $this->assertSame('20E-4', Number::scientific(0.002, exponent: -4)); + $this->assertSame('8.23E-3', Number::scientific(0.00823, exponent: -3)); + $this->assertSame('1,1E-3', Number::scientific(0.0011, exponent: -3, locale: 'fr')); + $this->assertSame('1.23E-6', Number::scientific(0.000001234)); + $this->assertSame('123.4E-8', Number::scientific(0.000001234, exponent: -8)); + $this->assertSame('12340E-10', Number::scientific(0.000001234, exponent: -10)); + $this->assertSame('1,23E-6', Number::scientific(0.000001234, locale: 'fr')); + + // Zero and special values + $this->assertSame('0E0', Number::scientific(0)); + $this->assertSame('0E12', Number::scientific(0.00, exponent: 12)); + + // Infinity and NaN + $this->assertSame('∞', Number::scientific(INF)); + $this->assertSame('NaN', Number::scientific(NAN)); + $this->assertStringContainsString("число", Number::scientific(NAN, locale: 'ru')); + $this->assertStringContainsString("ليس", Number::scientific(NAN, locale: 'ar')); + + // Negative numbers + $this->assertSame('-1E0', Number::scientific(-1)); + $this->assertSame('-0.01E2', Number::scientific(-1, exponent: 2)); + $this->assertSame('-100E-2', Number::scientific(-1, exponent: -2)); + $this->assertSame('-1.23E1', Number::scientific(-12.34)); + $this->assertSame('-0.12E2', Number::scientific(-12.34, exponent: 2)); + $this->assertSame('-1234E-2', Number::scientific(-12.34, exponent: -2)); + $this->assertSame('-123,4E-1', Number::scientific(-12.34, exponent: -1, locale: 'fr')); + $this->assertSame('-1.23E-3', Number::scientific(-0.00123)); + $this->assertSame('-12.3E-4', Number::scientific(-0.00123, exponent: -4)); + } }