From 9d42c99788d6a2b5219a2166bb24f9e58cd417b2 Mon Sep 17 00:00:00 2001 From: Nejc Cotic Date: Tue, 29 Oct 2024 06:53:24 +0100 Subject: [PATCH] update json --- composer.json | 3 +- composer.lock | 17 +- examples/json.php | 165 ++++++++++++++++++++ src/Composite/Json.php | 200 ++++++++++++++++++++++++ src/Encoding/Base64Encoding.php | 41 +++++ src/Encoding/GzipEncoding.php | 45 ++++++ src/Encoding/HuffmanEncoding.php | 233 ++++++++++++++++++++++++++++ src/Encoding/Node.php | 35 +++++ src/Interfaces/DecoderInterface.php | 21 +++ src/Interfaces/EncoderInterface.php | 20 +++ 10 files changed, 771 insertions(+), 9 deletions(-) create mode 100644 examples/json.php create mode 100644 src/Composite/Json.php create mode 100644 src/Encoding/Base64Encoding.php create mode 100644 src/Encoding/GzipEncoding.php create mode 100644 src/Encoding/HuffmanEncoding.php create mode 100644 src/Encoding/Node.php create mode 100644 src/Interfaces/DecoderInterface.php create mode 100644 src/Interfaces/EncoderInterface.php diff --git a/composer.json b/composer.json index ac6661e..e59f33c 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "require": { "php": "^8.2", "ext-bcmath": "*", - "ext-ctype": "*" + "ext-ctype": "*", + "ext-zlib": "*" }, "require-dev": { "phpunit/phpunit": "^11.4.2" diff --git a/composer.lock b/composer.lock index 7182f09..54fbc5f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "271ccda664646033d9a3393b06caecfc", + "content-hash": "1f565fe082028eca22f4d68bd2ec44d0", "packages": [], "packages-dev": [ { @@ -568,16 +568,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.4.2", + "version": "11.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805" + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1863643c3f04ad03dcb9c6996c294784cdda4805", - "reference": "1863643c3f04ad03dcb9c6996c294784cdda4805", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8e8ed1854de5d36c088ec1833beae40d2dedd76", + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76", "shasum": "" }, "require": { @@ -648,7 +648,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.3" }, "funding": [ { @@ -664,7 +664,7 @@ "type": "tidelift" } ], - "time": "2024-10-19T13:05:19+00:00" + "time": "2024-10-28T13:07:50+00:00" }, { "name": "sebastian/cli-parser", @@ -1648,7 +1648,8 @@ "platform": { "php": "^8.2", "ext-bcmath": "*", - "ext-ctype": "*" + "ext-ctype": "*", + "ext-zlib": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/examples/json.php b/examples/json.php new file mode 100644 index 0000000..b867136 --- /dev/null +++ b/examples/json.php @@ -0,0 +1,165 @@ + 'Create Json Instances', + 'description' => 'We create two Json objects with different user data.', + 'code' => "\$json1 = new Json(\$jsonData1);\n\$json2 = new Json(\$jsonData2);", + 'output' => "Json1: " . $json1->getJson() . "\nJson2: " . $json2->getJson(), + ]; + +// // 2. Compare Json instances +// $areEqual = $json1->compareWith($json2) ? 'Yes' : 'No'; +// $examples[] = [ +// 'title' => 'Compare Json Instances', +// 'description' => 'We compare json1 and json2 to check if they are identical.', +// 'code' => "\$areEqual = \$json1->compareWith(\$json2) ? 'Yes' : 'No';", +// 'output' => "Are Json1 and Json2 identical? " . $areEqual, +// ]; + + // 3. Serialize Json to Array + $array1 = $json1->toArray(); + $examples[] = [ + 'title' => 'Serialize Json1 to Array', + 'description' => 'We convert json1 to a PHP array.', + 'code' => "\$array1 = \$json1->toArray();", + 'output' => print_r($array1, true), + ]; + + // 4. Deserialize Array to Json + $jsonFromArray = Json::fromArray($array1); + $examples[] = [ + 'title' => 'Deserialize Array to Json', + 'description' => 'We create a new Json object from array1.', + 'code' => "\$jsonFromArray = Json::fromArray(\$array1);", + 'output' => "Json from Array: " . $jsonFromArray->getJson(), + ]; + +// // 5. Compress Json1 using HuffmanEncoding + $huffmanEncoder = new HuffmanEncoding(); + $compressed = $json1->compress($huffmanEncoder); + $examples[] = [ + 'title' => 'Compress Json1 using HuffmanEncoding', + 'description' => 'We compress json1 using HuffmanEncoding.', + 'code' => "\$huffmanEncoder = new HuffmanEncoding();\n\$compressed = \$json1->compress(\$huffmanEncoder);", + 'output' => "Compressed Json1 (hex): " . bin2hex($compressed), + ]; + +// // 6. Decompress the previously compressed data + $decompressedJson = $json1->decompress($huffmanEncoder, $compressed); + $examples[] = [ + 'title' => 'Decompress the Compressed Data', + 'description' => 'We decompress the previously compressed data to retrieve the original JSON.', + 'code' => "\$decompressedJson = \$json1->decompress(\$huffmanEncoder, \$compressed);", + 'output' => "Decompressed Json: " . $decompressedJson->getJson(), + ]; + + // 7. Verify decompressed data matches original + $isMatch = ($json1->toArray() === $decompressedJson->toArray()) ? 'Yes' : 'No'; + $examples[] = [ + 'title' => 'Verify Decompressed Data', + 'description' => 'We check if the decompressed JSON matches the original json1 data.', + 'code' => "\$isMatch = (\$json1->toArray() === \$decompressedJson->toArray()) ? 'Yes' : 'No';", + 'output' => "Does decompressed Json match original Json1? " . $isMatch, + ]; +// + // 8. Update Json1 by adding a new user + $updatedJson1 = $json1->update('users', array_merge($json1->toArray()['users'], [['id' => 5, 'name' => 'Eve']])); + $examples[] = [ + 'title' => 'Update Json1 by Adding a New User', + 'description' => 'We add a new user to json1.', + 'code' => "\$updatedJson1 = \$json1->update('users', array_merge(\$json1->toArray()['users'], [['id' => 5, 'name' => 'Eve']]));", + 'output' => "Updated Json1: " . $updatedJson1->getJson(), + ]; +// + // 9. Remove a user from updated Json1 + $modifiedJson1 = $updatedJson1->remove('users', 2); // Assuming remove method removes by 'id' or index + $examples[] = [ + 'title' => 'Remove a User from Updated Json1', + 'description' => 'We remove the user with ID 2 from updatedJson1.', + 'code' => "\$modifiedJson1 = \$updatedJson1->remove('users', 2);", + 'output' => "Modified Json1: " . $modifiedJson1->getJson(), + ]; + +} catch (InvalidArgumentException|JsonException $e) { + $errorMessage = $e->getMessage(); +} + +// Capture all outputs +$content = ob_get_clean(); + +?> + + + + + + + Json Class Test Example + + + + + + + + + + +
+

Json Class Test Example

+ + + + + + +
+
+

+

+ +
+ +
+

Code

+
+
+ +
+

Output

+
+
+
+
+
+ +
+ + diff --git a/src/Composite/Json.php b/src/Composite/Json.php new file mode 100644 index 0000000..a88db75 --- /dev/null +++ b/src/Composite/Json.php @@ -0,0 +1,200 @@ +validateJson($json); + $this->schema = $schema; + $this->json = $json; + } + + /** + * Validates if a string is valid JSON. + * + * @param string $json + * @throws InvalidArgumentException + */ + private function validateJson(string $json): void + { + try { + json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new InvalidArgumentException('Invalid JSON provided: ' . $e->getMessage()); + } + } + + + /** + * Serializes the JSON data to an array. + * + * @return array + * @throws JsonException + */ + public function toArray(): array + { + if ($this->data === null) { + $this->data = json_decode($this->json, true, 512, JSON_THROW_ON_ERROR); + } + + return $this->data; + } + + /** + * Serializes the JSON data to an object. + * + * @return object + * @throws JsonException + */ + public function toObject(): object + { + return json_decode($this->json, false, 512, JSON_THROW_ON_ERROR); + } + + /** + * Deserializes an array to a Json instance. + * + * @param array $data + * @param string|null $schema + * @return self + * @throws InvalidArgumentException + * @throws JsonException + */ + public static function fromArray(array $data, ?string $schema = null): self + { + $json = json_encode($data, JSON_THROW_ON_ERROR); + return new self($json, $schema); + } + + /** + * Deserializes an object to a Json instance. + * + * @param object $object + * @param string|null $schema + * @return self + * @throws InvalidArgumentException + * @throws JsonException + */ + public static function fromObject(object $object, ?string $schema = null): self + { + $json = json_encode($object, JSON_THROW_ON_ERROR); + return new self($json, $schema); + } + + /** + * Gets the JSON string. + * + * @return string + */ + public function getJson(): string + { + return $this->json; + } + + /** + * Compresses the JSON string using the provided encoder. + * + * @param EncoderInterface $encoder + * @return string The compressed string. + */ + public function compress(EncoderInterface $encoder): string + { + return $encoder->encode($this->json); + } + + /** + * Decompresses the string using the provided decoder. + * + * @param DecoderInterface $decoder + * @param string $compressed + * @return self + * @throws InvalidArgumentException + */ + public static function decompress(DecoderInterface $decoder, string $compressed): self + { + $json = $decoder->decode($compressed); + return new self($json); + } + + /** + * Merges this JSON with another Json instance. + * In case of conflicting keys, values from the other Json take precedence. + * + * @param Json $other + * @return self + * @throws JsonException + */ + public function merge(Json $other): self + { + $mergedData = array_merge_recursive($this->toArray(), $other->toArray()); + $mergedJson = json_encode($mergedData, JSON_THROW_ON_ERROR); + return new self($mergedJson, $this->schema); + } + + /** + * Updates the JSON data with a given key-value pair. + * + * @param string $key + * @param mixed $value + * @return self + * @throws JsonException + */ + public function update(string $key, mixed $value): self + { + $data = $this->toArray(); + $data[$key] = $value; + $updatedJson = json_encode($data, JSON_THROW_ON_ERROR); + return new self($updatedJson, $this->schema); + } + + /** + * Removes a key from the JSON data. + * + * @param string $key + * @return self + * @throws JsonException + */ + public function remove(string $key): self + { + $data = $this->toArray(); + unset($data[$key]); + $updatedJson = json_encode($data, JSON_THROW_ON_ERROR); + return new self($updatedJson, $this->schema); + } +} diff --git a/src/Encoding/Base64Encoding.php b/src/Encoding/Base64Encoding.php new file mode 100644 index 0000000..30e1c51 --- /dev/null +++ b/src/Encoding/Base64Encoding.php @@ -0,0 +1,41 @@ +buildFrequencyTable($data); + + // Step 2: Build Huffman Tree + $huffmanTree = $this->buildHuffmanTree($frequency); + + // Step 3: Generate Huffman Codes + $codes = []; + $this->generateCodes($huffmanTree, '', $codes); + + // Step 4: Encode data + $encodedData = ''; + for ($i = 0, $len = strlen($data); $i < $len; $i++) { + $char = $data[$i]; + $encodedData .= $codes[$char]; + } + + // Step 5: Serialize frequency table and encoded data + // Prefix the encoded data with the JSON-encoded frequency table and a separator + $serializedFrequency = json_encode($frequency); + if ($serializedFrequency === false) { + throw new InvalidArgumentException('Failed to serialize frequency table.'); + } + + // Use a unique separator (null byte) to split frequency table and encoded data + $separator = "\0"; + + // Convert bit string to byte string + $byteString = $this->bitsToBytes($encodedData); + + return $serializedFrequency . $separator . $byteString; + } + + /** + * Decodes the data using Huffman decoding. + * + * @param string $data The encoded data with serialized frequency table. + * @return string The original decoded data. + */ + public function decode(string $data): string + { + if ($data === '') { + throw new InvalidArgumentException('Cannot decode empty string.'); + } + + // Step 1: Split the frequency table and the encoded data + $separatorPos = strpos($data, "\0"); + if ($separatorPos === false) { + throw new InvalidArgumentException('Invalid encoded data format.'); + } + + $serializedFrequency = substr($data, 0, $separatorPos); + $encodedDataBytes = substr($data, $separatorPos + 1); + + // Step 2: Deserialize frequency table + $frequency = json_decode($serializedFrequency, true); + if (!is_array($frequency)) { + throw new InvalidArgumentException('Failed to deserialize frequency table.'); + } + + // Step 3: Rebuild Huffman Tree + $huffmanTree = $this->buildHuffmanTree($frequency); + + // Step 4: Convert bytes back to bit string + $encodedDataBits = $this->bytesToBits($encodedDataBytes); + + // Step 5: Decode bit string using Huffman Tree + $decodedData = ''; + $currentNode = $huffmanTree; + $totalBits = strlen($encodedDataBits); + for ($i = 0; $i < $totalBits; $i++) { + $bit = $encodedDataBits[$i]; + if ($bit === '0') { + $currentNode = $currentNode->left; + } else { + $currentNode = $currentNode->right; + } + + if ($currentNode->isLeaf()) { + $decodedData .= $currentNode->character; + $currentNode = $huffmanTree; + } + } + + return $decodedData; + } + + /** + * Builds a frequency table for the given data. + * + * @param string $data + * @return array Associative array with characters as keys and frequencies as values. + */ + private function buildFrequencyTable(string $data): array + { + $frequency = []; + for ($i = 0, $len = strlen($data); $i < $len; $i++) { + $char = $data[$i]; + if (isset($frequency[$char])) { + $frequency[$char]++; + } else { + $frequency[$char] = 1; + } + } + return $frequency; + } + + /** + * Builds the Huffman tree from the frequency table. + * + * @param array $frequency + * @return Node The root of the Huffman tree. + */ + private function buildHuffmanTree(array $frequency): Node + { + // Create a priority queue (min-heap) based on frequency + $pq = new \SplPriorityQueue(); + $pq->setExtractFlags(\SplPriorityQueue::EXTR_DATA); + + foreach ($frequency as $char => $freq) { + // Ensure $char is a string + $char = (string)$char; + // Since SplPriorityQueue is a max-heap, use negative frequency for min-heap behavior + $pq->insert(new Node($char, $freq), -$freq); + } + + // Edge case: Only one unique character + if ($pq->count() === 1) { + $onlyNode = $pq->extract(); + return new Node((string)$onlyNode->character, $onlyNode->frequency, $onlyNode, null); + } + + // Build the Huffman tree + while ($pq->count() > 1) { + $left = $pq->extract(); + $right = $pq->extract(); + $merged = new Node('', $left->frequency + $right->frequency, $left, $right); + $pq->insert($merged, -$merged->frequency); + } + + return $pq->extract(); + } + + /** + * Generates Huffman codes by traversing the tree. + * + * @param Node $node + * @param string $prefix + * @param array &$codes + * @return void + */ + private function generateCodes(Node $node, string $prefix, array &$codes): void + { + if ($node->isLeaf()) { + // Edge case: If there's only one unique character, assign '0' as its code + $codes[$node->character] = $prefix === '' ? '0' : $prefix; + return; + } + + if ($node->left !== null) { + $this->generateCodes($node->left, $prefix . '0', $codes); + } + + if ($node->right !== null) { + $this->generateCodes($node->right, $prefix . '1', $codes); + } + } + + /** + * Converts a bit string to a byte string. + * + * @param string $bits + * @return string + */ + private function bitsToBytes(string $bits): string + { + $bytes = ''; + $length = strlen($bits); + for ($i = 0; $i < $length; $i += 8) { + $byte = substr($bits, $i, 8); + if (strlen($byte) < 8) { + $byte = str_pad($byte, 8, '0'); // Pad with zeros if not enough bits + } + $bytes .= chr(bindec($byte)); + } + return $bytes; + } + + /** + * Converts a byte string back to a bit string. + * + * @param string $bytes + * @return string + */ + private function bytesToBits(string $bytes): string + { + $bits = ''; + $length = strlen($bytes); + for ($i = 0; $i < $length; $i++) { + $bits .= str_pad(decbin(ord($bytes[$i])), 8, '0', STR_PAD_LEFT); + } + return $bits; + } +} diff --git a/src/Encoding/Node.php b/src/Encoding/Node.php new file mode 100644 index 0000000..48a7cc4 --- /dev/null +++ b/src/Encoding/Node.php @@ -0,0 +1,35 @@ +character = $character; + $this->frequency = $frequency; + $this->left = $left; + $this->right = $right; + } + + /** + * Check if the node is a leaf node. + * + * @return bool + */ + public function isLeaf(): bool + { + return is_null($this->left) && is_null($this->right); + } +} diff --git a/src/Interfaces/DecoderInterface.php b/src/Interfaces/DecoderInterface.php new file mode 100644 index 0000000..ac8f8c5 --- /dev/null +++ b/src/Interfaces/DecoderInterface.php @@ -0,0 +1,21 @@ +