diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 72deb09..8dea16d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -35,12 +35,6 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- - - name: Delete composer lock file - id: composer-lock - if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} - run: | - rm composer.lock - - name: Install dependencies run: composer update --no-progress --prefer-dist --optimize-autoloader --ignore-platform-reqs diff --git a/.vscode/settings.json b/.vscode/settings.json index 292db7b..387c0b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "yaml.schemas": { - "storage/schema.json": "file:///c%3A/html/power-parser/storage/tests/blueprints/*.yaml" + "storage/schema.json": "storage/tests/blueprints/*.yaml" } } diff --git a/src/Blueprint/Components/Fields.php b/src/Blueprint/Components/Fields.php index f75082d..bffe5b9 100644 --- a/src/Blueprint/Components/Fields.php +++ b/src/Blueprint/Components/Fields.php @@ -4,7 +4,10 @@ use HusamAwadhi\PowerParser\Blueprint\BlueprintHelper; use HusamAwadhi\PowerParser\Blueprint\ComponentInterface; +use HusamAwadhi\PowerParser\Blueprint\FieldFormat; +use HusamAwadhi\PowerParser\Blueprint\FieldType; use HusamAwadhi\PowerParser\Blueprint\ValueObject\Field; +use HusamAwadhi\PowerParser\Blueprint\ValueObject\FieldFormat as FieldFormatObject; use HusamAwadhi\PowerParser\Dictionary; use HusamAwadhi\PowerParser\Exception\InvalidFieldException; use Iterator; @@ -31,7 +34,17 @@ protected function buildFields(array $fields): array { $objectFields = []; foreach ($fields as $field) { - $objectFields[] = Field::from($field['name'], $field['position']); + $objectFields[] = Field::from( + $field['name'], + $field['position'], + (array_key_exists('type', $field) ? FieldType::from($field['type']) : null), + (array_key_exists('format', $field) + ? FieldFormatObject::from( + FieldFormat::from(explode('%', $field['format'])[0]), + (int) explode('%', $field['format'])[1] + ) + : null), + ); } return $objectFields; @@ -49,6 +62,7 @@ public static function from(array $fields, BlueprintHelper $helper): self */ public static function validation(array &$fields): void { + $i = 0; foreach ($fields as $field) { if ( !array_key_exists('name', $field) || @@ -56,6 +70,7 @@ public static function validation(array &$fields): void ) { throw new InvalidFieldException('missing or invalid name'); } + if ( !isset($field['position']) || empty($field['position']) || @@ -63,6 +78,43 @@ public static function validation(array &$fields): void ) { throw new InvalidFieldException('missing or invalid position'); } + + if ( + isset($field['type']) && + !empty($field['type']) + ) { + if (!FieldType::tryFrom($field['type'])) { + throw new InvalidFieldException( + \sprintf( + 'Blueprint %s field has invalid value (%s). Acceptable value(s) [%s]', + "type (#$i)", + $field['type'], + implode(', ', array_column(FieldType::cases(), 'value')) + ) + ); + } + } + + if ( + isset($field['format']) && + !empty($field['format']) + ) { + $format = explode('%', $field['format']); + if ( + count($format) !== 2 || + !FieldFormat::tryFrom($format[0]) || + !is_numeric($format[1]) + ) { + throw new InvalidFieldException( + \sprintf( + 'Blueprint format field has invalid value (%s). Acceptable value(s) {%s}%%{digits}', + $field['format'], + implode(',', array_column(FieldFormat::cases(), 'value')) + ) + ); + } + } + ++$i; } } diff --git a/src/Blueprint/FieldFormat.php b/src/Blueprint/FieldFormat.php new file mode 100644 index 0000000..b40a600 --- /dev/null +++ b/src/Blueprint/FieldFormat.php @@ -0,0 +1,9 @@ +position - 1, $row)) { throw new InvalidFieldException("field {$field->name} does not exist in position #{$field->position}"); } - $filteredFields[$field->name] = $row[$field->position - 1]; + $fieldValue = $row[$field->position - 1]; + $filteredFields[$field->name] = $this->postProcessField($fieldValue, $field); } return $filteredFields; @@ -131,14 +134,31 @@ protected function getTable(Component $component, array $rows, int &$index): arr protected function matchCondition(Condition $condition, mixed $data): bool { - $return = match ($condition->keyword) { + return match ($condition->keyword) { ConditionKeyword::AnyOf => in_array($data, explode(',', $condition->value)), ConditionKeyword::Is => $condition->value === $data, ConditionKeyword::IsNot => $condition->value !== $data, ConditionKeyword::NoneOf => !in_array($data, explode(',', $condition->value)), }; + } + + protected function postProcessField(mixed $value, Field $field): mixed + { + if (null !== $field->type) { + $value = match ($field->type) { + FieldType::BOOL => strtolower((string) $value) == 'true' || $value == true || $value == '1', + FieldType::BOOL_STRICT => $value == true, + FieldType::INT => (int) $value, + FieldType::FLOAT => (float) $value, + }; + } elseif (null !== $field->format) { + $value = match ($field->format->type) { + FieldFormat::STRING => substr($value, 0, $field->format->argument), + FieldFormat::FLOAT => round((float) $value, $field->format->argument, PHP_ROUND_HALF_UP), + }; + } - return $return; + return $value; } public function getFiltered(): array diff --git a/storage/schema.json b/storage/schema.json index 709e9b2..126c949 100644 --- a/storage/schema.json +++ b/storage/schema.json @@ -102,6 +102,12 @@ }, "position": { "type": "integer" + }, + "type": { + "type": "string" + }, + "format": { + "type": "string" } }, "required": [ diff --git a/storage/tests/blueprints/valid_with_processors.yaml b/storage/tests/blueprints/valid_with_processors.yaml new file mode 100644 index 0000000..6013a51 --- /dev/null +++ b/storage/tests/blueprints/valid_with_processors.yaml @@ -0,0 +1,63 @@ +version: "1.0" +meta: + file: + extension: xlsx + name: "sample" +blueprint: + - name: header_info + mandatory: true + type: hit + conditions: + - column: [1] + is: "Cashier Number" + fields: + - name: currency + position: 6 + - name: cashier + position: 2 + type: int + - name: balance_info + type: hit + mandatory: true + conditions: + - column: [4] + is: "Open Balance" + fields: + - name: opening_balance + position: 6 + format: f%2 + - name: transaction_table + type: hit + mandatory: true + table: true + conditions: + - column: [1] + isNot: "{null}" + fields: + - name: date + position: 1 + - name: type + position: 2 + - name: document_number + position: 3 + type: int + - name: description + position: 4 + - name: reference_number + position: 5 + type: int + - name: credit + position: 6 + format: f%2 + - name: debit + position: 7 + format: f%2 + - name: total + type: next + fields: + - name: total_credit + position: 6 + format: f%2 + - name: total_debit + position: 7 + format: f%2 diff --git a/tests/Blueprint/Components/FieldsTest.php b/tests/Blueprint/Components/FieldsTest.php index 5e0b2cd..297617b 100644 --- a/tests/Blueprint/Components/FieldsTest.php +++ b/tests/Blueprint/Components/FieldsTest.php @@ -6,7 +6,10 @@ use HusamAwadhi\PowerParser\Blueprint\BlueprintHelper; use HusamAwadhi\PowerParser\Blueprint\Components\Fields; +use HusamAwadhi\PowerParser\Blueprint\FieldFormat as FieldFormatEnum; +use HusamAwadhi\PowerParser\Blueprint\FieldType; use HusamAwadhi\PowerParser\Blueprint\ValueObject\Field; +use HusamAwadhi\PowerParser\Blueprint\ValueObject\FieldFormat; use HusamAwadhi\PowerParser\Exception\InvalidFieldException; use PHPUnit\Framework\TestCase; @@ -29,10 +32,18 @@ public function validParametersDataProvider() [ ['name' => 'field1', 'position' => 2], ['name' => 'field2', 'position' => 3], + ['name' => 'field3', 'position' => 25, 'type' => 'int'], + ['name' => 'field4', 'position' => 4, 'type' => 'bool-strict'], + ['name' => 'field5', 'position' => 4, 'format' => 's%5'], + ['name' => 'field5', 'position' => 4, 'type' => 'bool', 'format' => 'f%2'], ], [ Field::from('field1', 2), Field::from('field2', 3), + Field::from('field3', 25, FieldType::INT), + Field::from('field4', 4, FieldType::BOOL_STRICT), + Field::from('field5', 4, null, FieldFormat::from(FieldFormatEnum::STRING, 5)), + Field::from('field5', 4, FieldType::BOOL, FieldFormat::from(FieldFormatEnum::FLOAT, 2)), ], ], ]; @@ -65,6 +76,22 @@ public function invalidParametersDataProvider() [['position' => 'two'],], InvalidFieldException::class, ], + [ + [['name' => 'field1', 'position' => 2, 'type' => 'single'],], + InvalidFieldException::class, + ], + [ + [['name' => 'field1', 'position' => 2, 'format' => 's'],], + InvalidFieldException::class, + ], + [ + [['name' => 'field1', 'position' => 2, 'format' => 's%x'],], + InvalidFieldException::class, + ], + [ + [['name' => 'field1', 'position' => 2, 'format' => 's% '],], + InvalidFieldException::class, + ], ]; } } diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 9aa33f8..bd212c2 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -544,7 +544,7 @@ public function parsedExcelContentDataProvider(): array "description" => "Return for Invoice No. 3599", "reference_number" => null, "credit" => null, - "debit" =>"992.0015", + "debit" => "992.0015", ], [ "date" => "21/09/2022", "type" => "Return", @@ -592,4 +592,256 @@ public function testThrowsExceptionOnMissingMandatoryComponent() $parser->parse()->getAsArray(); } + + /** + * @dataProvider parsedExcelContentWithProcessorsDataProvider + */ + public function testGettingParsedExcelContentAsJsonWithProcessors($expectedArray) + { + $builder = new BlueprintBuilder(new BlueprintHelper()); + $builder->load($this->blueprintsDirectory . 'valid_with_processors.yaml'); + $blueprint = $builder->build(); + + $parser = new Parser( + $blueprint, + \file_get_contents($this->excelFile), + [Spreadsheet::class => new Spreadsheet()] + ); + + $parser->parse(); + + $this->assertJsonStringEqualsJsonString( + json_encode($expectedArray), + json_encode($parser) + ); + } + + public function parsedExcelContentWithProcessorsDataProvider(): array + { + return [ + [[ + "header_info" => ["currency" => "SR", "cashier" => 1], + "balance_info" => ["opening_balance" => 9152.25], + "transaction_table" => [ + [ + "date" => "21/09/2022", + "type" => "Journal Entry", "document_number" => 30, + "description" => "Cash purchase invoices", + "reference_number" => 0, + "credit" => 23380.63, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Payment Voucher", + "document_number" => 331, + "description" => null, + "reference_number" => 0, + "credit" => 0, + "debit" => 580, + ], [ + "date" => "21/09/2022", + "type" => "Payment Voucher", + "document_number" => 332, + "description" => null, + "reference_number" => 0, + "credit" => 0, + "debit" => 980, + ], [ + "date" => "21/09/2022", + "type" => "Payment Voucher", + "document_number" => 333, + "description" => null, + "reference_number" => 0, + "credit" => 0, + "debit" => 170, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3627, + "description" => "Sales", + "reference_number" => 0, + "credit" => 640, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3628, + "description" => "Sales", + "reference_number" => 0, + "credit" => 45.01, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3629, + "description" => "Sales", + "reference_number" => 0, + "credit" => 460, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3630, + "description" => "Sales", + "reference_number" => 0, + "credit" => 28.01, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3631, + "description" => "Sales", + "reference_number" => 0, + "credit" => 227.00, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3632, + "description" => "Sales", + "reference_number" => 0, + "credit" => 28.01, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3633, + "description" => "Sales", + "reference_number" => 0, + "credit" => 280.00, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3634, + "description" => "Sales", + "reference_number" => 0, + "credit" => 105, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3635, + "description" => "Sales", + "reference_number" => 0, + "credit" => 220, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3636, + "description" => "Sales", + "reference_number" => 0, + "credit" => 140, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3637, + "description" => "Sales", + "reference_number" => 0, + "credit" => 708.01, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3638, + "description" => "Sales", + "reference_number" => 0, + "credit" => 1360.01, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3639, + "description" => "Sales", + "reference_number" => 0, + "credit" => 152.08, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3640, + "description" => "Sales", + "reference_number" => 0, + "credit" => 90.02, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3641, + "description" => "Sales", + "reference_number" => 0, + "credit" => 460, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3642, + "description" => "Sales", + "reference_number" => 0, + "credit" => 180.09, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Invoice", + "document_number" => 3643, + "description" => "Sales", + "reference_number" => 0, + "credit" => 432.33, + "debit" => 0, + ], [ + "date" => "21/09/2022", + "type" => "Return", + "document_number" => 248, + "description" => "Return for Invoice No. 3625", + "reference_number" => 0, + "credit" => 0, + "debit" => 270, + ], [ + "date" => "21/09/2022", + "type" => "Return", + "document_number" => 249, + "description" => "Return", + "reference_number" => 0, + "credit" => 0, + "debit" => 100.00, + ], [ + "date" => "21/09/2022", + "type" => "Return", + "document_number" => 250, + "description" => "Return for Invoice No. 3599", + "reference_number" => 0, + "credit" => 0, + "debit" => 992, + ], [ + "date" => "21/09/2022", + "type" => "Return", + "document_number" => 251, + "description" => "Return for Invoice No. 3631", + "reference_number" => 0, + "credit" => 0, + "debit" => 112, + ], [ + "date" => "21/09/2022", + "type" => "Purchase", + "document_number" => 127, + "description" => null, + "reference_number" => 0, + "credit" => 0, + "debit" => 1030.63, + ], [ + "date" => "21/09/2022", + "type" => "Purchase", + "document_number" => 128, + "description" => null, + "reference_number" => 0, + "credit" => 0, + "debit" => 22350, + ] + ], + "total" => ["total_credit" => 28936.21, "total_debit" => 26584.63] + ]] + ]; + } }