diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index 6c76e2c..530982b 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -88,7 +88,10 @@ private function encodeTable(array $data, array $path, array &$lines): void continue; } if (!is_array($value) || $this->isInlineArray($value)) { - $lines[] = $this->encodeKey((string)$key) . ' = ' . $this->encodeValue($value); + $keyPath = $this->options->dottedKeys && $path !== [] + ? $this->encodePath([...$path, (string)$key]) + : $this->encodeKey((string)$key); + $lines[] = $keyPath . ' = ' . $this->encodeValue($value); } } @@ -104,6 +107,8 @@ private function encodeTable(array $data, array $path, array &$lines): void $lines[] = '[[' . $this->encodePath($newPath) . ']]'; $this->encodeTable($item, $newPath, $lines); } + } elseif ($this->options->dottedKeys) { + $this->encodeTable($value, $newPath, $lines); } else { $lines[] = ''; $lines[] = '[' . $this->encodePath($newPath) . ']'; @@ -124,7 +129,7 @@ private function encodeValue(mixed $value): string } if (is_int($value)) { - return (string)$value; + return $this->encodeInteger($value); } if (is_float($value)) { @@ -1200,6 +1205,21 @@ private function endsWithNewline(string $value): bool return str_ends_with($value, "\n") || str_ends_with($value, "\r\n"); } + private function encodeInteger(int $value): string + { + if (!$this->options->integerGrouping) { + return (string)$value; + } + + $sign = $value < 0 ? '-' : ''; + $absolute = (string)abs($value); + + // Add underscores every 3 digits from the right + $grouped = preg_replace('/\B(?=(\d{3})+(?!\d))/', '_', $absolute); + + return $sign . $grouped; + } + private function encodeString(string $value): string { // Use basic string with escaping @@ -1222,7 +1242,9 @@ private function encodeArray(array $value): string } $items = array_map(fn ($v) => $this->encodeValue($v), $value); - return '[' . implode(', ', $items) . ']'; + $trailing = $this->options->trailingComma && $items !== [] ? ',' : ''; + + return '[' . implode(', ', $items) . $trailing . ']'; } /** diff --git a/src/Encoder/EncoderOptions.php b/src/Encoder/EncoderOptions.php index 5889a11..17d6d37 100644 --- a/src/Encoder/EncoderOptions.php +++ b/src/Encoder/EncoderOptions.php @@ -14,6 +14,9 @@ public function __construct( public DocumentFormattingMode $documentFormatting = DocumentFormattingMode::Normalized, public bool $skipNulls = false, public TomlVersion $version = TomlVersion::V11, + public bool $integerGrouping = false, + public bool $trailingComma = false, + public bool $dottedKeys = false, ) { } } diff --git a/tests/Encoder/EncoderTest.php b/tests/Encoder/EncoderTest.php index e7716de..4a7167e 100644 --- a/tests/Encoder/EncoderTest.php +++ b/tests/Encoder/EncoderTest.php @@ -321,4 +321,133 @@ public function testEncodeDocumentCanUseSourceAwareFormatting(): void $this->assertSame('count = 1' . "\n", $result); } + + public function testEncodeIntegerGrouping(): void + { + $encoder = new Encoder(new EncoderOptions(integerGrouping: true)); + + $result = $encoder->encode([ + 'small' => 42, + 'medium' => 1000, + 'large' => 1000000, + 'huge' => 1234567890, + 'negative' => -9876543, + ]); + + $this->assertStringContainsString('small = 42', $result); + $this->assertStringContainsString('medium = 1_000', $result); + $this->assertStringContainsString('large = 1_000_000', $result); + $this->assertStringContainsString('huge = 1_234_567_890', $result); + $this->assertStringContainsString('negative = -9_876_543', $result); + } + + public function testEncodeIntegerGroupingDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'large' => 1000000, + ]); + + $this->assertStringContainsString('large = 1000000', $result); + } + + public function testEncodeTrailingComma(): void + { + $encoder = new Encoder(new EncoderOptions(trailingComma: true)); + + $result = $encoder->encode([ + 'items' => [1, 2, 3], + ]); + + $this->assertStringContainsString('items = [1, 2, 3,]', $result); + } + + public function testEncodeTrailingCommaEmptyArray(): void + { + $encoder = new Encoder(new EncoderOptions(trailingComma: true)); + + $result = $encoder->encode([ + 'empty' => [], + ]); + + $this->assertStringContainsString('empty = []', $result); + } + + public function testEncodeTrailingCommaDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'items' => [1, 2, 3], + ]); + + $this->assertStringContainsString('items = [1, 2, 3]', $result); + $this->assertStringNotContainsString('[1, 2, 3,]', $result); + } + + public function testEncodeDottedKeys(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'database' => [ + 'host' => 'localhost', + 'port' => 5432, + ], + ]); + + $this->assertStringContainsString('database.host = "localhost"', $result); + $this->assertStringContainsString('database.port = 5432', $result); + $this->assertStringNotContainsString('[database]', $result); + } + + public function testEncodeDottedKeysDeepNesting(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'a' => [ + 'b' => [ + 'c' => 'value', + ], + ], + ]); + + $this->assertStringContainsString('a.b.c = "value"', $result); + $this->assertStringNotContainsString('[a]', $result); + $this->assertStringNotContainsString('[a.b]', $result); + } + + public function testEncodeDottedKeysWithArrayOfTables(): void + { + $encoder = new Encoder(new EncoderOptions(dottedKeys: true)); + + $result = $encoder->encode([ + 'servers' => [ + ['name' => 'alpha'], + ['name' => 'beta'], + ], + ]); + + // Array of tables still requires [[]] syntax + $this->assertStringContainsString('[[servers]]', $result); + $this->assertStringContainsString('name = "alpha"', $result); + $this->assertStringContainsString('name = "beta"', $result); + } + + public function testEncodeDottedKeysDisabledByDefault(): void + { + $encoder = new Encoder(new EncoderOptions()); + + $result = $encoder->encode([ + 'database' => [ + 'host' => 'localhost', + ], + ]); + + $this->assertStringContainsString('[database]', $result); + $this->assertStringContainsString('host = "localhost"', $result); + $this->assertStringNotContainsString('database.host', $result); + } }