diff --git a/Tests/ByteSliceTest.php b/Tests/ByteSliceTest.php new file mode 100644 index 0000000..e80050e --- /dev/null +++ b/Tests/ByteSliceTest.php @@ -0,0 +1,131 @@ +assertSame([10, 20, 255], $byteSlice->getValue()); + } + + /** + * Test creating a ByteSlice with invalid byte values. + */ + public function testCreateInvalidByteSlice(): void + { + $this->expectException(InvalidByteException::class); + $this->expectExceptionMessage("All elements must be valid bytes (0-255). Invalid value: -1"); + + new ByteSlice([10, -1, 255]); // -1 is out of valid byte range + } + + /** + * Test converting ByteSlice to hexadecimal representation. + */ + public function testConvertToHexadecimal(): void + { + $byteSlice = new ByteSlice([10, 20, 255]); + $this->assertSame("0A14FF", $byteSlice->toHex()); + } + + /** + * Test slicing a portion of ByteSlice. + */ + public function testSliceByteSlice(): void + { + $byteSlice = new ByteSlice([10, 20, 255, 30, 40]); + $sliced = $byteSlice->slice(1, 3); + $this->assertSame([20, 255, 30], $sliced->getValue()); + } + + /** + * Test merging two ByteSlices. + */ + public function testMergeByteSlices(): void + { + $byteSlice1 = new ByteSlice([10, 20, 255]); + $byteSlice2 = new ByteSlice([1, 2, 3]); + $merged = $byteSlice1->merge($byteSlice2); + + $this->assertSame([10, 20, 255, 1, 2, 3], $merged->getValue()); + } + + /** + * Test getting the count of bytes in a ByteSlice. + */ + public function testGetByteCount(): void + { + $byteSlice = new ByteSlice([10, 20, 255]); + $this->assertCount(3, $byteSlice); + } + + /** + * Test ArrayAccess implementation for accessing a byte at a specific index. + */ + public function testArrayAccessGet(): void + { + $byteSlice = new ByteSlice([10, 20, 255]); + $this->assertSame(20, $byteSlice[1]); + $this->assertNull($byteSlice[999]); // Accessing an invalid index returns null + } + + /** + * Test ArrayAccess prevents modification of ByteSlice. + */ + public function testArrayAccessSetThrowsException(): void + { + $this->expectException(InvalidByteException::class); + $this->expectExceptionMessage("Cannot modify an immutable ByteSlice."); + + $byteSlice = new ByteSlice([10, 20, 255]); + $byteSlice[1] = 100; // Should throw an exception + } + + /** + * Test ArrayAccess prevents unsetting a byte in ByteSlice. + */ + public function testArrayAccessUnsetThrowsException(): void + { + $this->expectException(InvalidByteException::class); + $this->expectExceptionMessage("Cannot unset a value in an immutable ByteSlice."); + + $byteSlice = new ByteSlice([10, 20, 255]); + unset($byteSlice[1]); // Should throw an exception + } + + /** + * Test iterating over ByteSlice with IteratorAggregate. + */ + public function testIterationOverByteSlice(): void + { + $byteSlice = new ByteSlice([10, 20, 255]); + $result = []; + + foreach ($byteSlice as $byte) { + $result[] = $byte; + } + + $this->assertSame([10, 20, 255], $result); + } + + /** + * Test an empty ByteSlice. + */ + public function testEmptyByteSlice(): void + { + $byteSlice = new ByteSlice([]); + $this->assertSame([], $byteSlice->getValue()); + $this->assertCount(0, $byteSlice); + } +} + diff --git a/Tests/FloatArrayTest.php b/Tests/FloatArrayTest.php new file mode 100644 index 0000000..79b68ec --- /dev/null +++ b/Tests/FloatArrayTest.php @@ -0,0 +1,160 @@ +assertSame([10.5, 20.1, 30.7], $floatArray->getValue()); + } + + /** + * Test creating a FloatArray with invalid float values. + */ + public function testCreateInvalidFloatArray(): void + { + $this->expectException(InvalidFloatException::class); + $this->expectExceptionMessage("All elements must be floats. Invalid value: 1"); + + new FloatArray([10.5, 1, 'not a float']); // Invalid non-float values + } + + /** + * Test adding floats to a FloatArray. + * @throws InvalidFloatException + */ + public function testAddFloats(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $newFloatArray = $floatArray->add(40.2, 50.3); + + $this->assertSame([10.5, 20.1, 30.7, 40.2, 50.3], $newFloatArray->getValue()); + } + + /** + * Test removing floats from a FloatArray. + * @throws InvalidFloatException + */ + public function testRemoveFloats(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7, 40.2]); + $modifiedFloatArray = $floatArray->remove(20.1, 30.7); + + $this->assertSame([10.5, 40.2], $modifiedFloatArray->getValue()); + } + + /** + * Test calculating the sum of floats. + * @throws InvalidFloatException + */ + public function testSumOfFloats(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $this->assertSame(61.3, $floatArray->sum()); + } + + /** + * Test calculating the average of floats. + * @throws InvalidFloatException + */ + public function testAverageOfFloats(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $this->assertSame(20.433333333333334, $floatArray->average()); + } + + /** + * Test calculating the average of an empty FloatArray. + */ + public function testAverageOfEmptyFloatArray(): void + { + $this->expectException(InvalidFloatException::class); + $this->expectExceptionMessage("Cannot calculate average of an empty array."); + + $floatArray = new FloatArray([]); + $floatArray->average(); // Should throw exception + } + + /** + * Test getting the count of floats in a FloatArray. + * @throws InvalidFloatException + */ + public function testGetFloatCount(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $this->assertCount(3, $floatArray); + } + + /** + * Test ArrayAccess implementation for accessing a float at a specific index. + * @throws InvalidFloatException + */ + public function testArrayAccessGet(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $this->assertSame(20.1, $floatArray[1]); + $this->assertNull($floatArray[999]); // Accessing an invalid index returns null + } + + /** + * Test ArrayAccess prevents modification of FloatArray. + */ + public function testArrayAccessSetThrowsException(): void + { + $this->expectException(InvalidFloatException::class); + $this->expectExceptionMessage("Cannot modify an immutable FloatArray."); + + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $floatArray[1] = 50.3; // Should throw exception + } + + /** + * Test ArrayAccess prevents unsetting a float in FloatArray. + */ + public function testArrayAccessUnsetThrowsException(): void + { + $this->expectException(InvalidFloatException::class); + $this->expectExceptionMessage("Cannot unset a value in an immutable FloatArray."); + + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + unset($floatArray[1]); // Should throw exception + } + + /** + * Test iterating over FloatArray with IteratorAggregate. + * @throws InvalidFloatException + */ + public function testIterationOverFloatArray(): void + { + $floatArray = new FloatArray([10.5, 20.1, 30.7]); + $result = []; + + foreach ($floatArray as $float) { + $result[] = $float; + } + + $this->assertSame([10.5, 20.1, 30.7], $result); + } + + /** + * Test creating an empty FloatArray. + * @throws InvalidFloatException + */ + public function testEmptyFloatArray(): void + { + $floatArray = new FloatArray([]); + $this->assertSame([], $floatArray->getValue()); + $this->assertCount(0, $floatArray); + } +} diff --git a/Tests/StringArrayTest.php b/Tests/StringArrayTest.php new file mode 100644 index 0000000..b275786 --- /dev/null +++ b/Tests/StringArrayTest.php @@ -0,0 +1,147 @@ +assertEquals(['apple', 'banana', 'cherry'], $array->getValue()); + } + + public function testInvalidStringArrayThrowsException(): void + { + $this->expectException(InvalidStringException::class); + new StringArray(['apple', 123, 'cherry']); // Integer value should throw an exception + } + + public function testAddStringToNewInstance(): void + { + $array = new StringArray(['apple', 'banana']); + $newArray = $array->add('cherry'); + + $this->assertNotSame($array, $newArray); // Ensure immutability + $this->assertEquals(['apple', 'banana'], $array->getValue()); + $this->assertEquals(['apple', 'banana', 'cherry'], $newArray->getValue()); + } + + public function testRemoveStringFromArray(): void + { + $array = new StringArray(['apple', 'banana', 'cherry']); + $newArray = $array->remove('banana'); + + $this->assertNotSame($array, $newArray); // Ensure immutability + $this->assertEquals(['apple', 'banana', 'cherry'], $array->getValue()); + $this->assertEquals(['apple', 'cherry'], $newArray->getValue()); + } + + public function testRemoveNonExistentStringDoesNothing(): void + { + $array = new StringArray(['apple', 'banana', 'cherry']); + $newArray = $array->remove('pear'); + + // Check that the values are still the same (immutability) + $this->assertEquals($array->getValue(), $newArray->getValue()); + } + + + public function testContains(): void + { + $array = new StringArray(['apple', 'banana', 'cherry']); + + $this->assertTrue($array->contains('banana')); + $this->assertFalse($array->contains('pear')); + } + + public function testCountStrings(): void + { + $array = new StringArray(['apple', 'banana', 'cherry']); + $this->assertEquals(3, $array->count()); + } + + public function testToString(): void + { + $array = new StringArray(['apple', 'banana', 'cherry']); + $this->assertEquals('apple, banana, cherry', $array->toString()); + $this->assertEquals('apple|banana|cherry', $array->toString('|')); + } + + public function testFilterByPrefix(): void + { + $array = new StringArray(['apple', 'banana', 'apricot']); + $filtered = $array->filterByPrefix('ap'); + + $this->assertEquals(['apple', 'apricot'], $filtered); + } + + public function testFilterBySubstring(): void + { + $array = new StringArray(['apple', 'banana', 'pineapple']); + $filtered = $array->filterBySubstring('apple'); + + $this->assertEquals(['apple', 'pineapple'], $filtered); + } + + public function testToUpperCase(): void + { + $array = new StringArray(['apple', 'banana']); + $newArray = $array->toUpperCase(); + + $this->assertNotSame($array, $newArray); // Ensure immutability + $this->assertEquals(['APPLE', 'BANANA'], $newArray->getValue()); + } + + public function testToLowerCase(): void + { + $array = new StringArray(['APPLE', 'BANANA']); + $newArray = $array->toLowerCase(); + + $this->assertNotSame($array, $newArray); // Ensure immutability + $this->assertEquals(['apple', 'banana'], $newArray->getValue()); + } + + public function testClearArray(): void + { + $array = new StringArray(['apple', 'banana']); + $clearedArray = $array->clear(); + + $this->assertNotSame($array, $clearedArray); // Ensure immutability + $this->assertEquals([], $clearedArray->getValue()); + } + + public function testArrayAccess(): void + { + $array = new StringArray(['apple', 'banana']); + $this->assertEquals('apple', $array[0]); + $this->assertEquals('banana', $array[1]); + $this->assertNull($array[2]); // Non-existent index + } + + public function testOffsetSetThrowsException(): void + { + $this->expectException(InvalidStringException::class); + $array = new StringArray(['apple', 'banana']); + $array[0] = 'orange'; // Should throw an exception + } + + public function testOffsetUnsetThrowsException(): void + { + $this->expectException(InvalidStringException::class); + $array = new StringArray(['apple', 'banana']); + unset($array[0]); // Should throw an exception + } + + public function testIterator(): void + { + $array = new StringArray(['apple', 'banana']); + foreach ($array as $key => $value) { + $this->assertEquals($array->get($key), $value); + } + } +} diff --git a/examples/byteslice.php b/examples/byteslice.php new file mode 100644 index 0000000..c49635f --- /dev/null +++ b/examples/byteslice.php @@ -0,0 +1,213 @@ +getValue()); +} catch (InvalidByteException $e) { + echo $e->getMessage(); +} +CODE; + ob_start(); + $byteSlice = new ByteSlice([10, 20, 255]); + print_r($byteSlice->getValue()); + $output1 = ob_get_clean(); + + $examples[] = [ + 'title' => 'Create a ByteSlice', + 'description' => 'We create a ByteSlice with some initial byte values.', + 'code' => $fullCode1, + 'output' => $output1, + ]; + + // Example 2: Convert to Hexadecimal + $fullCode2 = <<<'CODE' +toHex(); + echo "Hexadecimal: " . $hex; +} catch (InvalidByteException $e) { + echo $e->getMessage(); +} +CODE; + ob_start(); + $hex = $byteSlice->toHex(); + echo "Hexadecimal: " . $hex; + $output2 = ob_get_clean(); + + $examples[] = [ + 'title' => 'Convert to Hexadecimal', + 'description' => 'We convert the byte slice to its hexadecimal representation.', + 'code' => $fullCode2, + 'output' => $output2, + ]; + + // Example 3: Slice the ByteSlice + $fullCode3 = <<<'CODE' +slice(1, 2); + print_r($sliced->getValue()); +} catch (InvalidByteException $e) { + echo $e->getMessage(); +} +CODE; + ob_start(); + $sliced = $byteSlice->slice(1, 2); + print_r($sliced->getValue()); + $output3 = ob_get_clean(); + + $examples[] = [ + 'title' => 'Slice the ByteSlice', + 'description' => 'We slice a portion of the byte array, starting at index 1.', + 'code' => $fullCode3, + 'output' => $output3, + ]; + + // Example 4: Merge ByteSlices + $fullCode4 = <<<'CODE' +merge($otherByteSlice); + print_r($merged->getValue()); +} catch (InvalidByteException $e) { + echo $e->getMessage(); +} +CODE; + ob_start(); + $otherByteSlice = new ByteSlice([1, 2, 3]); + $merged = $byteSlice->merge($otherByteSlice); + print_r($merged->getValue()); + $output4 = ob_get_clean(); + + $examples[] = [ + 'title' => 'Merge with Another ByteSlice', + 'description' => 'We merge the current byte slice with another byte slice [1, 2, 3].', + 'code' => $fullCode4, + 'output' => $output4, + ]; + + // Example 5: Get Count of Bytes + $fullCode5 = <<<'CODE' +count(); +} catch (InvalidByteException $e) { + echo $e->getMessage(); +} +CODE; + ob_start(); + echo "Count of bytes: " . $byteSlice->count(); + $output5 = ob_get_clean(); + + $examples[] = [ + 'title' => 'Get the Count of Bytes', + 'description' => 'We get the number of bytes currently in the byte slice.', + 'code' => $fullCode5, + 'output' => $output5, + ]; + +} catch (InvalidByteException $e) { + $errorMessage = $e->getMessage(); +} + +?> + + + + + + + ByteSlice Example with Tailwind and Prism.js + + + + + + + + + + + + + + +
+

PHP ByteSlice Example: Step by Step

+
+ +
+

+ Below are examples demonstrating how to create, modify, and interact with a ByteSlice object. + Each code block is followed by its corresponding output. +

+ + + +
+
+

+

+
+ +
+

Full Code

+
+
+ +
+

Output

+
+
+
+
+
+ + + + +
+

Error

+

+
+ +
+ + diff --git a/examples/stringarray.php b/examples/stringarray.php new file mode 100644 index 0000000..c204615 --- /dev/null +++ b/examples/stringarray.php @@ -0,0 +1,171 @@ + 'Create a StringArray', + 'description' => 'We create a StringArray with some initial values.', + 'code' => "\$stringArray = new StringArray(['apple', 'banana', 'cherry']);", + 'output' => print_r($stringArray->getValue(), true), + ]; + + // Add new elements + $newStringArray = $stringArray->add('date', 'elderberry'); + $examples[] = [ + 'title' => 'Add New Elements', + 'description' => "We add two new elements: 'date' and 'elderberry'.", + 'code' => "\$newStringArray = \$stringArray->add('date', 'elderberry');", + 'output' => print_r($newStringArray->getValue(), true), + ]; + + // Remove 'banana' + $modifiedArray = $newStringArray->remove('banana'); + $examples[] = [ + 'title' => 'Remove an Element', + 'description' => "We remove 'banana' from the array.", + 'code' => "\$modifiedArray = \$newStringArray->remove('banana');", + 'output' => print_r($modifiedArray->getValue(), true), + ]; + + // Check if 'apple' exists + $containsApple = $modifiedArray->contains('apple') ? "Yes" : "No"; + $examples[] = [ + 'title' => "Check if 'apple' Exists", + 'description' => "We check if the array contains 'apple'.", + 'code' => "\$containsApple = \$modifiedArray->contains('apple') ? 'Yes' : 'No';", + 'output' => "Contains 'apple': " . $containsApple, + ]; + + // Convert to uppercase + $upperCaseArray = $modifiedArray->toUpperCase(); + $examples[] = [ + 'title' => 'Convert to Uppercase', + 'description' => "We convert all strings in the array to uppercase.", + 'code' => "\$upperCaseArray = \$modifiedArray->toUpperCase();", + 'output' => print_r($upperCaseArray->getValue(), true), + ]; + + // Filter by prefix 'ch' + $filteredByPrefix = $stringArray->filterByPrefix('ch'); + $examples[] = [ + 'title' => "Filter by Prefix 'ch'", + 'description' => "We filter the array to only include items starting with 'ch'.", + 'code' => "\$filteredByPrefix = \$stringArray->filterByPrefix('ch');", + 'output' => print_r($filteredByPrefix, true), + ]; + + // Filter by substring 'err' + $filteredBySubstring = $stringArray->filterBySubstring('err'); + $examples[] = [ + 'title' => "Filter by Substring 'err'", + 'description' => "We filter the array to only include items that contain 'err'.", + 'code' => "\$filteredBySubstring = \$stringArray->filterBySubstring('err');", + 'output' => print_r($filteredBySubstring, true), + ]; + + // Get count + $count = $newStringArray->count(); + $examples[] = [ + 'title' => 'Get the Count of Strings', + 'description' => "We get the number of strings currently in the array.", + 'code' => "\$count = \$newStringArray->count();", + 'output' => "Count of strings: " . $count, + ]; + + // Get string representation + $stringRepresentation = $newStringArray->toString(); + $examples[] = [ + 'title' => 'Get String Representation', + 'description' => "We convert the array to a comma-separated string.", + 'code' => "\$stringRepresentation = \$newStringArray->toString();", + 'output' => "String representation: " . $stringRepresentation, + ]; + + // Clear the array + $clearedArray = $newStringArray->clear(); + $examples[] = [ + 'title' => 'Clear the Array', + 'description' => "We clear all elements from the array.", + 'code' => "\$clearedArray = \$newStringArray->clear();", + 'output' => print_r($clearedArray->getValue(), true), + ]; + +} catch (InvalidStringException $e) { + $errorMessage = $e->getMessage(); +} + +?> + + + + + + + StringArray Example with Tailwind and Prism.js + + + + + + + + + + + + + + +
+

PHP StringArray Example: Step by Step

+
+ +
+

+ Below are examples demonstrating how to create, modify, and interact with a StringArray object. + Each code block is followed by its corresponding output. +

+ + + +
+
+

+

+
+ +
+

Code

+ +
+
+
+ +
+

Output

+
+
+
+
+
+ + + + +
+

Error

+

+
+ +
+ + diff --git a/src/Composite/Arrays/ByteSlice.php b/src/Composite/Arrays/ByteSlice.php index c753020..939b036 100644 --- a/src/Composite/Arrays/ByteSlice.php +++ b/src/Composite/Arrays/ByteSlice.php @@ -1,20 +1,167 @@ The byte values (0-255). + */ private array $value; - public function __construct(array $value) { - foreach ($value as $item) { + /** + * Constructor for ByteSlice. + * + * @param array $value The array of byte values. + * @throws InvalidByteException If any value is not a valid byte. + */ + public function __construct(array $value) + { + $this->validateBytes($value); + $this->value = $value; + } + + /** + * Validate that all elements are valid bytes (0-255). + * + * @param array $array The array to validate. + * @throws InvalidByteException If any element is not a valid byte. + * @return void + */ + private function validateBytes(array $array): void + { + foreach ($array as $item) { if (!is_int($item) || $item < 0 || $item > 255) { - throw new \InvalidArgumentException("All elements must be valid bytes (0-255)."); + throw new InvalidByteException("All elements must be valid bytes (0-255). Invalid value: " . $item); } } - $this->value = $value; } - public function getValue(): array { + /** + * Get the array of byte values. + * + * @return array The byte array. + */ + public function getValue(): array + { return $this->value; } + + /** + * Get the byte at a specific index. + * + * @param int $index The index. + * @return int|null The byte value or null if index is out of bounds. + */ + public function getByte(int $index): ?int + { + return $this->value[$index] ?? null; + } + + /** + * Get the count of bytes in the array. + * + * @return int The number of bytes. + */ + public function count(): int + { + return count($this->value); + } + + /** + * Convert the byte slice to a hexadecimal string. + * + * @return string The hexadecimal representation. + */ + public function toHex(): string + { + return implode('', array_map(fn($byte) => sprintf('%02X', $byte), $this->value)); + } + + /** + * Slice a portion of the byte array. + * + * @param int $offset The start offset. + * @param int|null $length The length of the slice. + * @return ByteSlice The sliced byte array. + * @throws InvalidByteException + */ + public function slice(int $offset, ?int $length = null): self + { + return new self(array_slice($this->value, $offset, $length)); + } + + /** + * Merge the current byte array with another byte array. + * + * @param ByteSlice $other The other byte array to merge. + * @return ByteSlice A new ByteSlice instance containing the merged bytes. + * @throws InvalidByteException + */ + public function merge(ByteSlice $other): self + { + return new self(array_merge($this->value, $other->getValue())); + } + + /** + * ArrayAccess: Check if a byte exists at the given offset. + * + * @param int $offset The array offset. + * @return bool True if offset exists, false otherwise. + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->value[$offset]); + } + + /** + * ArrayAccess: Get the byte at the given offset. + * + * @param int $offset The array offset. + * @return mixed The byte value at the given offset. + */ + public function offsetGet(mixed $offset): mixed + { + return $this->value[$offset] ?? null; + } + + /** + * ArrayAccess: Prevent modification by throwing an exception. + * + * @param int $offset The array offset. + * @param mixed $value The value to set (not allowed). + * @throws InvalidByteException Always thrown since ByteSlice is immutable. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new InvalidByteException("Cannot modify an immutable ByteSlice."); + } + + /** + * ArrayAccess: Prevent unsetting by throwing an exception. + * + * @param int $offset The array offset. + * @throws InvalidByteException Always thrown since ByteSlice is immutable. + */ + public function offsetUnset(mixed $offset): void + { + throw new InvalidByteException("Cannot unset a value in an immutable ByteSlice."); + } + + /** + * Get an iterator for the byte array. + * + * @return Traversable An iterator for the byte array. + */ + public function getIterator(): Traversable + { + return new \ArrayIterator($this->value); + } } diff --git a/src/Composite/Arrays/FloatArray.php b/src/Composite/Arrays/FloatArray.php index 480d09d..50591d4 100644 --- a/src/Composite/Arrays/FloatArray.php +++ b/src/Composite/Arrays/FloatArray.php @@ -1,20 +1,189 @@ The float values. + */ private array $value; - public function __construct(array $value) { - foreach ($value as $item) { + /** + * Constructor for FloatArray. + * + * @param array $value The array of float values. + * @throws InvalidFloatException If any value is not a valid float. + */ + public function __construct(array $value) + { + $this->validateFloats($value); + $this->value = $value; + } + + /** + * Validate that all elements are floats. + * + * @param array $array The array to validate. + * @throws InvalidFloatException If any element is not a valid float. + * @return void + */ + private function validateFloats(array $array): void + { + foreach ($array as $item) { if (!is_float($item)) { - throw new \InvalidArgumentException("All elements must be floats."); + throw new InvalidFloatException("All elements must be floats. Invalid value: " . json_encode($item)); } } - $this->value = $value; } - public function getValue(): array { + /** + * Get the array of float values. + * + * @return array The float array. + */ + public function getValue(): array + { return $this->value; } + + /** + * Get the float at a specific index. + * + * @param int $index The index. + * @return float|null The float value or null if index is out of bounds. + */ + public function getFloat(int $index): ?float + { + return $this->value[$index] ?? null; + } + + /** + * Get the count of floats in the array. + * + * @return int The number of floats. + */ + public function count(): int + { + return count($this->value); + } + + /** + * Calculate the sum of the float array. + * + * @return float The sum of the floats. + */ + public function sum(): float + { + return array_sum($this->value); + } + + /** + * Calculate the average of the float array. + * + * @return float The average value of the floats. + * @throws InvalidFloatException If the array is empty. + */ + public function average(): float + { + if ($this->count() === 0) { + throw new InvalidFloatException("Cannot calculate average of an empty array."); + } + return $this->sum() / $this->count(); + } + + /** + * Add new floats to the array (returns a new instance). + * + * @param float ...$floats The floats to add. + * @return FloatArray The new FloatArray with added floats. + * @throws InvalidFloatException If any value is not a valid float. + */ + public function add(float ...$floats): self + { + $this->validateFloats($floats); + return new self(array_merge($this->value, $floats)); + } + + /** + * Remove specific floats from the array (returns a new instance). + * + * @param float ...$floats The floats to remove. + * @return FloatArray The new FloatArray with removed floats. + * @throws InvalidFloatException + */ + public function remove(float ...$floats): self + { + $newArray = $this->value; + foreach ($floats as $float) { + $index = array_search($float, $newArray, true); + if ($index !== false) { + unset($newArray[$index]); + } + } + return new self(array_values($newArray)); + } + + /** + * ArrayAccess: Check if a float exists at the given offset. + * + * @param int $offset The array offset. + * @return bool True if offset exists, false otherwise. + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->value[$offset]); + } + + /** + * ArrayAccess: Get the float at the given offset. + * + * @param int $offset The array offset. + * @return float|null The float value or null if index does not exist. + */ + public function offsetGet(mixed $offset): mixed + { + return $this->value[$offset] ?? null; + } + + /** + * ArrayAccess: Prevent modification by throwing an exception. + * + * @param mixed $offset The array offset. + * @param mixed $value The value to set (not allowed). + * @throws InvalidFloatException Always thrown since FloatArray is immutable. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new InvalidFloatException("Cannot modify an immutable FloatArray."); + } + + /** + * ArrayAccess: Prevent unsetting by throwing an exception. + * + * @param int $offset The array offset. + * @throws InvalidFloatException Always thrown since FloatArray is immutable. + */ + public function offsetUnset(mixed $offset): void + { + throw new InvalidFloatException("Cannot unset a value in an immutable FloatArray."); + } + + /** + * Get an iterator for the float array. + * + * @return Traversable An iterator for the float array. + */ + public function getIterator(): Traversable + { + return new \ArrayIterator($this->value); + } } diff --git a/src/Composite/Arrays/StringArray.php b/src/Composite/Arrays/StringArray.php index f11cd11..52aa079 100644 --- a/src/Composite/Arrays/StringArray.php +++ b/src/Composite/Arrays/StringArray.php @@ -1,20 +1,252 @@ validateArray($value); + $this->value = $value; + } + + /** + * Validates that the array consists only of strings. + * + * @param array $array + * @return void + * @throws InvalidStringException + */ + private function validateArray(array $array): void + { + foreach ($array as $item) { if (!is_string($item)) { - throw new \InvalidArgumentException("All elements must be strings."); + throw new InvalidStringException("All elements must be strings. Invalid value: " . json_encode($item)); } } - $this->value = $value; } - public function getValue(): array { + /** + * Get the array of string values. + * + * @return array + */ + public function getValue(): array + { return $this->value; } + + /** + * Add multiple strings to the array (returns a new instance). + * + * @param string ...$strings + * @return self New instance with added values. + * @throws InvalidStringException + */ + public function add(string ...$strings): self + { + $this->validateArray($strings); + return new self(array_merge($this->value, $strings)); + } + + /** + * Remove multiple strings from the array (returns a new instance). + * + * @param string ...$strings + * @return self New instance with removed values. + * @throws InvalidStringException + */ + public function remove(string ...$strings): self + { + $newArray = $this->value; + foreach ($strings as $string) { + $index = array_search($string, $newArray, true); + if ($index !== false) { + unset($newArray[$index]); + } + } + return new self(array_values($newArray)); // Re-index array + } + + /** + * Check if multiple strings exist in the array. + * + * @param string ...$strings + * @return bool True if all strings are found, false otherwise. + */ + public function contains(string ...$strings): bool + { + foreach ($strings as $string) { + if (!in_array($string, $this->value, true)) { + return false; + } + } + return true; + } + + /** + * Get the count of strings in the array. + * + * @return int + */ + public function count(): int + { + return count($this->value); + } + + /** + * Get the array as a comma-separated string or with custom separator. + * + * @param string $separator Separator to use between strings (default: ", "). + * @return string + */ + public function toString(string $separator = ', '): string + { + return implode($separator, $this->value); + } + + /** + * Find strings that start with a specific prefix. + * + * @param string $prefix + * @return array Array of strings that start with the given prefix. + */ + public function filterByPrefix(string $prefix): array + { + return array_values(array_filter($this->value, fn($str) => str_starts_with($str, $prefix))); + } + + + /** + * Find strings that contain a specific substring. + * + * @param string $substring + * @return array Array of strings that contain the substring. + */ + public function filterBySubstring(string $substring): array + { + return array_values(array_filter($this->value, fn($str) => str_contains($str, $substring))); + } + + + /** + * Get a string at a specific index. + * + * @param int $index + * @return string|null + */ + public function get(int $index): ?string + { + return $this->value[$index] ?? null; + } + + /** + * Convert all strings to uppercase (returns a new instance). + * + * @return self + * @throws InvalidStringException + */ + public function toUpperCase(): self + { + return new self(array_map('strtoupper', $this->value)); + } + + /** + * Convert all strings to lowercase (returns a new instance). + * + * @return self + * @throws InvalidStringException + */ + public function toLowerCase(): self + { + return new self(array_map('strtolower', $this->value)); + } + + /** + * Clear the array (returns a new empty instance). + * + * @return self + * @throws InvalidStringException + */ + public function clear(): self + { + return new self([]); + } + + /** + * ArrayAccess method to check if an offset exists. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->value[$offset]); + } + + /** + * ArrayAccess method to get an offset. + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->value[$offset] ?? null; + } + + /** + * ArrayAccess method to set an offset (immutable, returns a new instance). + * + * @param mixed $offset + * @param mixed $value + * @return void + * @throws InvalidStringException + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new InvalidStringException("Cannot modify immutable StringArray."); + } + + /** + * ArrayAccess method to unset an offset (immutable, returns a new instance). + * + * @param mixed $offset + * @return void + * @throws InvalidStringException + */ + public function offsetUnset(mixed $offset): void + { + throw new InvalidStringException("Cannot modify immutable StringArray."); + } + + /** + * Returns an iterator for traversing the array. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return new \ArrayIterator($this->value); + } } diff --git a/src/Exceptions/InvalidByteException.php b/src/Exceptions/InvalidByteException.php new file mode 100644 index 0000000..3b768ec --- /dev/null +++ b/src/Exceptions/InvalidByteException.php @@ -0,0 +1,31 @@ +code}]: {$this->message}\n"; + } +} diff --git a/src/Exceptions/InvalidFloatException.php b/src/Exceptions/InvalidFloatException.php new file mode 100644 index 0000000..9bcfff6 --- /dev/null +++ b/src/Exceptions/InvalidFloatException.php @@ -0,0 +1,31 @@ +code}]: {$this->message}\n"; + } +} diff --git a/src/Exceptions/InvalidStringException.php b/src/Exceptions/InvalidStringException.php new file mode 100644 index 0000000..60a8ef2 --- /dev/null +++ b/src/Exceptions/InvalidStringException.php @@ -0,0 +1,52 @@ +invalidValue = $invalidValue; + + // Generate a more detailed default message if none is provided. + $message = $message ?? "Invalid string value: " . json_encode($invalidValue); + + parent::__construct($message, $code, $previous); + } + + /** + * Get the invalid value that caused the exception. + * + * @return mixed The invalid value. + */ + public function getInvalidValue(): mixed + { + return $this->invalidValue; + } + + /** + * String representation of the exception, including the invalid value. + * + * @return string + */ + public function __toString(): string + { + return __CLASS__ . ": [{$this->code}]: {$this->message}. Invalid value: " . json_encode($this->invalidValue) . "\n"; + } +}