[
  {
    "path": ".gitattributes",
    "content": "/tests export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n.php-cs-fixer.php export-ignore\nphpcs.xml export-ignore\nphpstan.neon export-ignore\nphpunit.xml export-ignore\npsalm.xml export-ignore\n"
  },
  {
    "path": ".gitignore",
    "content": ".phpunit.cache/\nvendor/\ncomposer.lock\n.php-cs-fixer.cache\n"
  },
  {
    "path": ".php-cs-fixer.php",
    "content": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->exclude('vendor')\n    ->in(__DIR__);\n\n$config = new PhpCsFixer\\Config();\nreturn $config->setRules([\n        '@PSR12' => true,\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 AnourValar\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Office: Documents | Reports | Grids\n\n## Installation\n\n### Minimal\n\n```bash\ncomposer require anourvalar/office\n```\n\n### Phpspreadsheet is required to work with Excel (xlsx).\n\n```bash\ncomposer require phpoffice/phpspreadsheet \"^3.10\"\n```\n\n### Zipstream-php is required to work with Word (docx).\n\n```bash\ncomposer require maennchen/zipstream-php \"^3.2\"\n```\n\n### Mpdf is required to work with PDF.\n\n```bash\ncomposer require mpdf/mpdf: \"^8.1\"\n```\n\n\n## Generate a document from an XLSX (Excel) template\n\n### One-dimensional table (basic usage)\n\n**template1.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-10.png)\n\n```php\n$data = [\n    // scalar\n    'vat' => 'No',\n    'total' => [\n        'price' => 2004.14,\n        'qty' => 3,\n    ],\n\n    // one-dimensional table\n    'products' => [\n        [\n            'name' => 'Product #1',\n            'price' => 989,\n            'qty' => 1,\n            'date' => new \\DateTime('2022-03-30'),\n        ],\n        [\n            'name' => 'Product #2',\n            'price' => 1015.14,\n            'qty' => 2,\n            'date' => new \\DateTime('2022-03-31'),\n        ],\n    ],\n];\n\n// Save to the file\n(new \\AnourValar\\Office\\SheetsService())\n    ->generate(\n        'template1.xlsx', // template filename\n        $data // markers\n    )\n    ->saveAs(\n        'generated_document.xlsx', // filename\n        \\AnourValar\\Office\\Format::Xlsx // save format\n    );\n\n// Output to the browser\nheader('Content-type: ' . \\AnourValar\\Office\\Format::Xlsx->contentType());\nheader('Content-Disposition: attachment; filename=\"generated_document.xlsx\"');\necho (new \\AnourValar\\Office\\SheetsService())\n    ->generate('template1.xlsx', $data)\n    ->save(\\AnourValar\\Office\\Format::Xlsx);\n\n// Available formats:\n// \\AnourValar\\Office\\Format::Xlsx\n// \\AnourValar\\Office\\Format::Pdf\n// \\AnourValar\\Office\\Format::Html\n// \\AnourValar\\Office\\Format::Ods\n```\n\n**generated_document.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-11.png)\n\n\n**The same template with empty data**\n\n![Demo](https://anour.ru/resources/office-v1-12.png)\n\n\n### Two-dimensional table\n\n**template2.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-20.png)\n\n```php\n$data = [\n    'best_manager' => 'Sveta',\n\n    // two-dimensional table\n    'managers' => [\n        'titles' => [[ 'William', 'James', 'Sveta' ]],\n\n        'values' => [\n            [ // additional row\n                'month' => 'January',\n                'amount' => [700, 800, 900], // additional columns\n            ],\n            [\n                'month' => 'February',\n                'amount' => [7000, 8000, 9000],\n            ],\n            [\n                'month' => 'March',\n                'amount' => [70000, 80000, 90000],\n            ],\n        ],\n    ],\n];\n\n// Save as XLSX (Excel)\n(new \\AnourValar\\Office\\SheetsService())\n    ->generate('template2.xlsx', $data)\n    ->saveAs('generated_document.xlsx'); // second argument (format) is optional\n```\n\n**generated_document.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-21.png)\n\n### Additional Features\n\n**template3.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-30.png)\n\n```php\n$data = [\n    'foo' => 'Hello',\n\n    'bar' => function (SheetsInterface $driver, $column, $row) {\n        $driver->insertImage('logo.png', $cell, ['width' => 100, 'offset_y' => -45]);\n        return 'Logo!'; // replace marker \"[bar]\" with \"Logo!\"\n    }\n];\n\n(new \\AnourValar\\Office\\SheetsService())\n    ->hookValue(function (SheetsInterface $driver, $column, $row, $value, $sheetIndex) {\n        // Hook will be called for every cell which is changing\n\n        $value .= ' world';\n        return $value;\n    })\n    ->generate(\n        'template3.ods', // ods template\n        $data,\n        true // cells auto format instead of template setup\n    )\n    ->saveAs('generated_document.xlsx');\n\n// Available hooks:\n// hookLoad: Closure(SheetsInterface $driver, string $templateFile, Format $templateFormat)\n// hookBefore: Closure(SheetsInterface $driver, array &$data)\n// hookValue: Closure(SheetsInterface $driver, string $column, int $row, $value, int $sheetIndex)\n// hookAfter: Closure(SheetsInterface $driver)\n```\n\n**generated_document.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-31.png)\n\n### Dynamic templates\n\n```php\n$data = [\n    'group1' => [\n        'name' => 'Group 1',\n        'products' => [\n            ['name' => 'Product 1', 'stock' => 101],\n            ['name' => 'Product 2', 'stock' => 102],\n        ],\n    ],\n    'group2' => [\n        'name' => 'Group 2',\n        'products' => [\n            ['name' => 'Product 3', 'stock' => 103],\n            ['name' => 'Product 4', 'stock' => 104],\n        ],\n    ],\n];\n\n(new \\AnourValar\\Office\\SheetsService())\n    ->hookLoad(function ($driver, string $templateFile, $templateFormat) {\n        // create empty document instead of using existing\n        return $driver->create();\n    })\n    ->hookBefore(function ($driver, array &$data) {\n        // place markers on-fly\n        $row = 1;\n        foreach (array_keys($data) as $group) {\n            // group's title\n            $driver\n                ->setValue(\"A$row\", \"[{$group}.name]\")\n                ->mergeCells(\"A$row:B$row\")\n                ->setStyle(\"A$row\", ['align' => 'center', 'bold' => true]);\n            $row++;\n\n            // group's products\n            $driver\n                ->setValue(\"A$row\", \"[$group.products.name]\")\n                ->setValue(\"B$row\", \"[$group.products.stock]\");\n            $row++;\n        }\n    })\n    ->generate('', $data)\n    ->saveAs('generated_document.xlsx');\n```\n\n**Dynamic template overview**\n\n![Demo](https://anour.ru/resources/office-v1-61.png)\n\n**generated_document.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-62.png)\n\n### Merge (union) few documents to a single file\n\n```php\n$dataA = ['foo' => 'hello'];\n$dataB = ['foo' => 'world'];\n\n$documentA = (new \\AnourValar\\Office\\SheetsService())->generate('template.xlsx', $dataA);\n$documentB = (new \\AnourValar\\Office\\SheetsService())->generate('template.xlsx', $dataB);\n\n$mixer = new \\AnourValar\\Office\\Mixer();\n$mixer($documentA, $documentB)->saveAs('generated_document.xlsx');\n```\n\n### Access the PhpSpreadsheet directly (default driver)\n\n```php\n(new \\AnourValar\\Office\\SheetsService())\n    ->hookBefore(function (\\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver $driver, array &$data) {\n        $spreadsheet = $driver->spreadsheet;\n\n        // @see \\PhpOffice\\PhpSpreadsheet\\Spreadsheet\n        $spreadsheet->createSheet()->setTitle('Foo Bar'); // adding a new Worksheet\n    })\n    ->generate('template.xlsx', [])\n    ->saveAs('generated_document.xlsx');\n```\n\n\n## Generate a document from an DOCX (Word) template\n\n```php\n(new \\AnourValar\\Office\\DocumentService)\n    ->generate('template.docx', ['foo' => 'bar'])\n    ->saveAs('generated_document.docx');\n```\n\n**template.docx:**\n\n![Demo](https://anour.ru/resources/office-v1-70.png)\n\n**generated_document.docx:**\n\n![Demo](https://anour.ru/resources/office-v1-71.png)\n\n\n## Export table (Grid)\n\n### Simple usage\n\n```php\n$data = [\n    ['William', 3000],\n    ['James', 4000],\n    ['Sveta', 5000],\n];\n\n// Save as XLSX (Excel)\n(new \\AnourValar\\Office\\GridService())\n    ->generate(\n        ['Name', 'Sales'], // headers\n        $data // data\n    )\n    ->saveAs('generated_grid.xlsx');\n```\n\n**generated_grid.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-41.png)\n\n### Advanced usage (generators)\n\n```php\n$headers = [\n    ['title' => 'Name', 'width' => 30],\n    ['title' => 'Sales'],\n];\n\n$data = function () {\n    yield ['name' => 'William', 'sales' => 3000];\n    yield ['name' => 'James', 'sales' => 4000];\n    yield ['name' => 'Sveta', 'sales' => 5000];\n};\n\n// Save as XLSX (Excel)\n(new \\AnourValar\\Office\\GridService())\n    ->hookHeader(function (GridInterface $driver, mixed $header, $key, $column) {\n        if (isset($header['width'])) {\n            $driver->setWidth($column, $header['width']); // column with fixed width\n        } else {\n            $driver->autoWidth($column); // column with auto width\n        }\n\n        return $header['title'];\n    })\n    ->hookRow(function (GridInterface $driver, mixed $row, $key) {\n        return [\n            $row['name'],\n            $row['sales'],\n        ];\n    })\n    ->hookAfter(function (\n        GridInterface $driver,\n        string $headersRange,\n        string $dataRange,\n        string $totalRange,\n        array $columns\n    ) {\n        $driver->setSheetTitle('Foo');\n\n        $driver->setStyle(\n            $headersRange, // A1:B1\n            ['bold' => true, 'background_color' => 'EEEEEE']\n        );\n\n        $driver->setStyle(\n            $totalRange, // A1:B4\n            ['borders' => true, 'align' => 'left']\n        );\n    })\n    ->generate($headers, $data)\n    ->saveAs('generated_grid.xlsx');\n```\n\n**generated_grid.xlsx:**\n\n![Demo](https://anour.ru/resources/office-v1-51.png)\n\n### Performance\n\nBy default, GridService uses PhpSpreadsheetDriver which gives a lot of features and flexability.\nThe only cons are performance and memory consumtion.\n\nZipDriver as an alternative is simpler, but much more faster:\n\n```bash\ncomposer require maennchen/zipstream-php \"^3.2\"\n```\n\n```php\n$data = [\n    ['William', 3000],\n    ['James', 4000],\n    ['Sveta', 5000],\n];\n\n// Save as XLSX (Excel)\n(new \\AnourValar\\Office\\GridService(new \\AnourValar\\Office\\Drivers\\ZipDriver()))\n    ->generate(['Name', 'Sales'], $data)\n    ->saveAs('generated_grid.xlsx');\n```\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"anourvalar/office\",\n    \"description\": \"Generate documents from existing Excel & Word templates | Export tables to Excel (Grids)\",\n    \"keywords\": [\n        \"excel\", \"xls\", \"xlsx\", \"template\", \"view\", \"document\", \"contract\", \"report\", \"generate\", \"engine\", \"fill\",\n        \"markers\", \"replacers\", \"placeholder\", \"templater\", \"pdf\", \"ods\", \"grid\", \"export\", \"generator\", \"anourvalar\",\n        \"variables\", \"word\", \"doc\", \"docx\", \"table\"\n    ],\n    \"homepage\": \"https://github.com/AnourValar/office\",\n    \"license\": \"MIT\",\n    \"require\": {\n        \"php\": \"^8.1\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^11.0\",\n        \"phpstan/phpstan\": \"^2.0\",\n        \"friendsofphp/php-cs-fixer\": \"^3.26\",\n        \"squizlabs/php_codesniffer\": \"^3.7\",\n        \"vimeo/psalm\": \"^7.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\"AnourValar\\\\Office\\\\\": \"src/\"}\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\"AnourValar\\\\Office\\\\Tests\\\\\": \"tests/\"}\n    }\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<!-- @see https://pear.php.net/manual/en/package.php.php-codesniffer.annotated-ruleset.php -->\n<ruleset name=\"PHPCS Rules\">\n\n    <description>PHPCS ruleset</description>\n\n    <file>src</file>\n    <file>tests</file>\n    <exclude-pattern>src/resources</exclude-pattern>\n\n    <!-- Show progress of the run -->\n    <arg value= \"p\"/>\n\n    <!-- Show sniff codes in all reports -->\n    <arg value= \"s\"/>\n\n    <!-- Our base rule: set to PSR12 -->\n    <rule ref=\"PSR12\">\n        <exclude name=\"PSR12.Operators.OperatorSpacing.NoSpaceBefore\"/>\n        <exclude name=\"PSR12.Operators.OperatorSpacing.NoSpaceAfter\"/>\n        <exclude name=\"PSR12.Traits.UseDeclaration.MultipleImport\"/>\n        <exclude name=\"Generic.Files.LineLength.TooLong\"/>\n        <exclude name=\"PSR2.Methods.FunctionClosingBrace.SpacingBeforeClose\"/>\n        <exclude name=\"PSR12.Classes.OpeningBraceSpace.Found\"/>\n        <exclude name=\"Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen\"/>\n        <exclude name=\"Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose\"/>\n    </rule>\n\n    <rule ref=\"PSR1.Methods.CamelCapsMethodName.NotCamelCaps\">\n        <exclude-pattern>tests/</exclude-pattern>\n    </rule>\n\n    <rule ref=\"Internal.NoCodeFound\">\n        <exclude-pattern>tests/</exclude-pattern>\n    </rule>\n\n</ruleset>\n"
  },
  {
    "path": "phpstan.neon",
    "content": "parameters:\n\n    paths:\n        - src\n        - tests\n\n    # The level 10 is the highest level\n    level: 5\n\n    ignoreErrors:\n        - '#has an uninitialized readonly property#'\n        - '#Binary operation \\\"\\-\\\" between non\\-empty\\-string#'\n        - '#Call to an undefined method AnourValar\\\\Office\\\\Drivers\\\\SaveInterface\\:\\:getSheetCount\\(\\)#'\n        - '#Call to an undefined method AnourValar\\\\Office\\\\Drivers\\\\SaveInterface\\:\\:replace\\(\\)#'\n        - '#unknown class PhpOffice#'\n        - '#Class PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Csv not found#'\n        - '#Unsafe usage of new static#'\n        - '#Call to an undefined method AnourValar\\\\Office\\\\Drivers\\\\SaveInterface\\:\\:setGrid\\(\\)#'\n        - '#Parameter \\#1 \\$driver of method AnourValar\\\\Office\\\\GridService\\:\\:getGenerator\\(\\) expects AnourValar\\\\Office\\\\Drivers\\\\GridInterface#'\n        - '#Class AnourValar\\\\Office\\\\Tests\\\\SheetsParserTest has an uninitialized property \\$service#'\n        - '#has an uninitialized property \\$fileSystem#'\n        - '#\\(\\) on iterable\\.#'\n        - '#Instantiated class ZipStream#'\n        - '#unknown class ZipStream#'\n        - '#has an uninitialized property \\$sourceActiveSheetIndex#'\n        - '#\\$format is assigned outside of the constructor#'\n        - '#has invalid return type PhpOffice#'\n        - '#\\$spreadsheet is assigned outside of the constructor#'\n        - '#Binary operation \\\"\\+\\\" between non\\-empty\\-string#'\n        - '#Instantiated class PhpOffice#'\n        - '#expects string, int given#'\n        - '#has invalid type PhpOffice#'\n        - '#Match expression does not handle remaining value: mixed#'\n        - '#Access to an undefined property AnourValar\\\\Office\\\\Drivers\\\\MixInterface\\:\\:\\$spreadsheet#'\n        - '#Call to an undefined method AnourValar\\\\Office\\\\Drivers\\\\MixInterface\\:\\:sheet\\(\\)#'\n        - '#Call to an undefined method#'\n        - '#Offset numeric\\-string on list\\<string> in isset\\(\\) does not exist\\.#'\n        - '#SaveInterface given\\.#'\n        - '#has invalid return type Illuminate#'\n\n    excludePaths:\n\n    checkFunctionNameCase: true\n    checkInternalClassCaseSensitivity: true\n    reportMaybesInMethodSignatures: true\n    reportStaticMethodSignatures: true\n    checkUninitializedProperties: true\n    checkDynamicProperties: true\n    reportAlwaysTrueInLastCondition: true\n    reportWrongPhpDocTypeInVarTag: true\n    checkMissingCallableSignature: true\n    reportPossiblyNonexistentGeneralArrayOffset: true\n    reportPossiblyNonexistentConstantArrayOffset: true\n    reportAnyTypeWideningInVarTag: true\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  bootstrap=\"vendor/autoload.php\"\n  backupGlobals=\"false\"\n  colors=\"true\"\n  stopOnFailure=\"true\"\n  xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\"\n  cacheDirectory=\".phpunit.cache\"\n  backupStaticProperties=\"false\"\n>\n  <coverage/>\n  <testsuites>\n    <testsuite name=\"Office\">\n      <directory>tests</directory>\n    </testsuite>\n  </testsuites>\n  <php></php>\n  <source>\n    <include>\n      <directory suffix=\".php\">src/</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "psalm.xml",
    "content": "<?xml version=\"1.0\"?>\n<psalm\n    errorLevel=\"7\"\n    resolveFromConfigFile=\"true\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xmlns=\"https://getpsalm.org/schema/config\"\n    xsi:schemaLocation=\"https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd\"\n    findUnusedBaselineEntry=\"true\"\n    findUnusedCode=\"true\"\n    findUnusedPsalmSuppress=\"true\"\n    findUnusedVariablesAndParams=\"true\"\n>\n    <projectFiles>\n        <directory name=\"src\"/>\n        <directory name=\"tests\"/>\n        <ignoreFiles>\n            <directory name=\"vendor\"/>\n        </ignoreFiles>\n    </projectFiles>\n\n    <issueHandlers>\n      <ForbiddenCode errorLevel=\"error\" />\n      <UnusedClosureParam errorLevel=\"suppress\" />\n      <PossiblyUnusedMethod errorLevel=\"suppress\" />\n      <UnusedClass errorLevel=\"suppress\" />\n      <UndefinedClass errorLevel=\"suppress\" />\n      <PossiblyUnusedParam errorLevel=\"suppress\" />\n      <PossiblyUnusedProperty errorLevel=\"suppress\" />\n      <PossiblyUnusedReturnValue errorLevel=\"suppress\" />\n      <MissingTemplateParam errorLevel=\"suppress\" />\n      <UnsupportedPropertyReferenceUsage errorLevel=\"suppress\" />\n      <MissingOverrideAttribute errorLevel=\"suppress\" />\n      <MissingPureAnnotation errorLevel=\"suppress\" />\n      <MissingAbstractPureAnnotation errorLevel=\"suppress\" />\n      <MissingInterfaceImmutableAnnotation errorLevel=\"suppress\" />\n      <MissingImmutableAnnotation errorLevel=\"suppress\" />\n    </issueHandlers>\n\n    <forbiddenFunctions>\n        <function name=\"var_dump\" />\n        <function name=\"dd\" />\n        <function name=\"dump\" />\n        <function name=\"print_r\" />\n    </forbiddenFunctions>\n</psalm>\n"
  },
  {
    "path": "src/Buffer.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Buffer implements \\Stringable\n{\n    /**\n     * @var resource\n     */\n    protected readonly mixed $resource;\n\n    /**\n     * @var string\n     */\n    protected readonly string $filename;\n\n    /**\n     * Creates a temporary file from the buffer\n     *\n     * @param string $buffer\n     * @return void\n     */\n    public function __construct(string $buffer)\n    {\n        $this->resource = tmpfile();\n        fwrite($this->resource, $buffer);\n\n        $this->filename = stream_get_meta_data($this->resource)['uri'];\n    }\n\n    /**\n     * @see magic\n     *\n     * @return string\n     */\n    public function __toString(): string\n    {\n        return $this->filename;\n    }\n\n    /**\n     * @return void\n     * @psalm-suppress InaccessibleProperty\n     */\n    public function __destruct()\n    {\n        fclose($this->resource); // works with php-fpm, octane, queue\n    }\n}\n"
  },
  {
    "path": "src/DocumentService.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\DocumentInterface;\n\nclass DocumentService\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n\n    /**\n     * @var \\AnourValar\\Office\\Drivers\\DocumentInterface\n     */\n    protected \\AnourValar\\Office\\Drivers\\DocumentInterface $driver;\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\DocumentInterface $driver\n     * @return void\n     */\n    public function __construct(DocumentInterface $driver = new \\AnourValar\\Office\\Drivers\\ZipDriver())\n    {\n        $this->driver = $driver;\n    }\n\n    /**\n     * Generate a document from the template (document)\n     *\n     * @param string|\\Stringable $templateFile\n     * @param mixed $data\n     * @return \\AnourValar\\Office\\Generated\n     */\n    public function generate(string|\\Stringable $templateFile, mixed $data): Generated\n    {\n        // Handle with input data\n        $data = $this->canonizeData($data);\n\n        // Open the template\n        $templateFormat = Format::tryFrom(mb_strtolower(pathinfo($templateFile, PATHINFO_EXTENSION))) ?? Format::Docx;\n        $driver = $this->driver->load($templateFile, $templateFormat);\n\n        // Handle\n        $driver->replace($data);\n\n        // Return\n        return new Generated($driver);\n    }\n\n    /**\n     * @param mixed $data\n     * @return array\n     */\n    protected function canonizeData(mixed $data): array\n    {\n        $result = [];\n\n        if (is_object($data) && method_exists($data, 'toArray')) {\n            $data = $data->toArray();\n        }\n\n        foreach ($this->dot($data) as $key => $value) {\n            $result[\"[$key]\"] = $value;\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Drivers/DocumentInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface DocumentInterface extends SaveInterface, LoadInterface\n{\n    /**\n     * Replace markers with values\n     *\n     * @param array $data\n     * @return self\n     */\n    public function replace(array $data): self;\n}\n"
  },
  {
    "path": "src/Drivers/GridInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface GridInterface extends SaveInterface\n{\n    /**\n     * Create new document\n     *\n     * @return self\n     */\n    public function create(): self;\n\n    /**\n     * Set data\n     *\n     * @param iterable $data\n     * @return self\n     */\n    public function setGrid(iterable $data): self;\n}\n"
  },
  {
    "path": "src/Drivers/LoadInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface LoadInterface\n{\n    /**\n     * Load a template with specific format\n     *\n     * @param string $file\n     * @param \\AnourValar\\Office\\Format $format\n     * @return \\AnourValar\\Office\\Drivers\\SaveInterface\n     */\n    public function load(string $file, \\AnourValar\\Office\\Format $format): SaveInterface;\n}\n"
  },
  {
    "path": "src/Drivers/MixInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface MixInterface extends MultiSheetInterface\n{\n    /**\n     * Set title for an active sheet\n     *\n     * @param string $title\n     * @return self\n     */\n    public function setSheetTitle(string $title): self;\n\n    /**\n     * Get title of an active sheet\n     *\n     * @return string\n     */\n    public function getSheetTitle(): string;\n\n    /**\n     * Merge (union) a sheet from another instanceof of driver\n     *\n     * @param \\AnourValar\\Office\\Drivers\\MixInterface $driver\n     * @return self\n     */\n    public function mergeDriver(\\AnourValar\\Office\\Drivers\\MixInterface $driver): self;\n}\n"
  },
  {
    "path": "src/Drivers/MultiSheetInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface MultiSheetInterface\n{\n    /**\n     * Set active sheet\n     *\n     * @param int $index\n     * @return self\n     */\n    public function setSheet(int $index): self;\n\n    /**\n     * Get sheets count\n     *\n     * @return int\n     */\n    public function getSheetCount(): int;\n}\n"
  },
  {
    "path": "src/Drivers/PhpSpreadsheetDriver.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\nuse PhpOffice\\PhpSpreadsheet\\Cell\\DataType;\nuse PhpOffice\\PhpSpreadsheet\\IOFactory;\nuse PhpOffice\\PhpSpreadsheet\\Style\\Border;\n\nclass PhpSpreadsheetDriver implements SheetsInterface, GridInterface, MixInterface\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n\n    /**\n     * @see \\PhpOffice\\PhpSpreadsheet\\Style\\NumberFormat\n     *\n     * @var string\n     */\n    public const FORMAT_DATE = 'm/d/yyyy';\n    public const FORMAT_DATETIME = 'm/d/yyyy h:mm';\n\n    /**\n     * @var string\n     */\n    public const FORMAT_INT = '#,##0';\n    public const FORMAT_DOUBLE = '#,##0.00';\n    public const FORMAT_DOUBLE_10 = '#,##########0.0000000000';\n    public const FORMAT_PERCENTAGE = '0.00%';\n\n    /**\n     * @var \\PhpOffice\\PhpSpreadsheet\\Spreadsheet\n     */\n    public readonly \\PhpOffice\\PhpSpreadsheet\\Spreadsheet $spreadsheet;\n\n    /**\n     * @var int\n     */\n    protected int $sourceActiveSheetIndex;\n\n    /**\n     * @return \\PhpOffice\\PhpSpreadsheet\\Worksheet\\Worksheet\n     */\n    public function sheet(): \\PhpOffice\\PhpSpreadsheet\\Worksheet\\Worksheet\n    {\n        return $this->spreadsheet->getActiveSheet();\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\GridInterface::create()\n     * @psalm-suppress InaccessibleProperty\n     */\n    public function create(): self\n    {\n        $instance = new static();\n        $instance->spreadsheet = new \\PhpOffice\\PhpSpreadsheet\\Spreadsheet();\n        $instance->sourceActiveSheetIndex = 0;\n\n        $this->readConfiguration($instance);\n        return $instance;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\LoadInterface::load()\n     * @psalm-suppress InaccessibleProperty\n     */\n    public function load(string $file, \\AnourValar\\Office\\Format $format): self\n    {\n        $instance = new static();\n        $instance->spreadsheet = IOFactory::createReader($instance->getFormat($format))->load($file);\n        $instance->sourceActiveSheetIndex = $instance->spreadsheet->getActiveSheetIndex();\n\n        $this->readConfiguration($instance);\n        return $instance;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SaveInterface::save()\n     */\n    public function save(string $file, \\AnourValar\\Office\\Format $format): void\n    {\n        $writer = \\PhpOffice\\PhpSpreadsheet\\IOFactory::createWriter($this->spreadsheet, $this->getFormat($format));\n        $this->writeConfiguration($writer);\n\n        $count = $this->spreadsheet->getSheetCount();\n        for ($i = 0; $i < $count; $i++) {\n            $this->spreadsheet->getSheet($i)->setSelectedCells('A1');\n        }\n        $this->spreadsheet->setActiveSheetIndex($this->sourceActiveSheetIndex);\n\n        if (method_exists($writer, 'writeAllSheets')) {\n            $writer->writeAllSheets();\n        }\n\n        $writer->save($file);\n    }\n\n    /**\n     * Clean up\n     *\n     * @return void\n     */\n    public function __destruct()\n    {\n        if (isset($this->spreadsheet)) {\n            $this->spreadsheet->disconnectWorksheets();\n            gc_collect_cycles();\n        }\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\MultiSheetInterface::setSheet()\n     */\n    public function setSheet(int $index): self\n    {\n        $this->spreadsheet->setActiveSheetIndex($index);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\MultiSheetInterface::getSheetCount()\n     */\n    public function getSheetCount(): int\n    {\n        return $this->spreadsheet->getSheetCount();\n    }\n\n    /**\n     * Apply value to a cell\n     *\n     * @param string $cell\n     * @param mixed $value\n     * @param bool $autoCellFormat\n     * @return self\n     */\n    public function setValue(string $cell, $value, bool $autoCellFormat = true): self\n    {\n        if ($value instanceof \\DateTimeInterface) {\n\n            $this->sheet()->setCellValue($cell, \\PhpOffice\\PhpSpreadsheet\\Shared\\Date::PHPToExcel($value));\n            if ($autoCellFormat) {\n                $this->setCellFormat($cell, static::FORMAT_DATE);\n            }\n\n        } elseif (is_string($value) || is_null($value)) {\n\n            if (is_numeric($value)) {\n                $this->sheet()->getCell($cell)->setValueExplicit($value, DataType::TYPE_STRING);\n            } else {\n                $this->sheet()->setCellValue($cell, $value);\n            }\n\n        } else {\n\n            if ($autoCellFormat && is_double($value)) {\n                $this->setCellFormat($cell, static::FORMAT_DOUBLE);\n            } elseif ($autoCellFormat && is_integer($value)) {\n                $this->setCellFormat($cell, static::FORMAT_INT);\n            }\n\n            $this->sheet()->getCell($cell)->setValueExplicit($value, DataType::TYPE_NUMERIC);\n\n        }\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::setValues()\n     */\n    public function setValues(array $data, bool $autoCellFormat = true): self\n    {\n        foreach ($data as $row => $columns) {\n            foreach ($columns as $column => $value) {\n                $this->setValue($column.$row, $value, $autoCellFormat);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\GridInterface::setGrid()\n     */\n    public function setGrid(iterable $data): self\n    {\n        $row = 0;\n        foreach ($data as $values) {\n            $row++;\n            $column = 'A';\n\n            foreach ($values as $value) {\n                if ($value !== '' && $value !== null) {\n                    $this->setValue($column.$row, $value);\n                }\n\n                $column = $this->strIncrement($column);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * Get cell' value\n     *\n     * @param string $cell\n     * @return mixed\n     */\n    public function getValue(string $cell)\n    {\n        return $this->sheet()->getCell($cell)->getValue();\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::getValues()\n     */\n    public function getValues(?string $ceilRange): array\n    {\n        if (! $ceilRange) {\n            $ceilRange = sprintf('A1:%s%s', $this->sheet()->getHighestColumn(), $this->sheet()->getHighestRow());\n        }\n\n        return $this->sheet()->rangeToArray(\n            $ceilRange, // The worksheet range that we want to retrieve\n            null,       // Value that should be returned for empty cells\n            false,      // Should formulas be calculated (the equivalent of getCalculatedValue() for each cell)\n            false,      // Should values be formatted (the equivalent of getFormattedValue() for each cell)\n            true        // Should the array be indexed by cell row and cell column\n        );\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::getMergeCells()\n     */\n    public function getMergeCells(): array\n    {\n        return array_values($this->sheet()->getMergeCells());\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::mergeCells()\n     */\n    public function mergeCells(string $ceilRange): self\n    {\n        $this->sheet()->mergeCells($ceilRange);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::copyStyle()\n     */\n    public function copyStyle(string $cellFrom, string $rangeTo): self\n    {\n        $this->sheet()->duplicateStyle($this->sheet()->getStyle($cellFrom), $rangeTo);\n\n        if ($conditionalStyle = $this->sheet()->getConditionalStyles($cellFrom)) {\n            $this->sheet()->duplicateConditionalStyle($conditionalStyle, $rangeTo);\n        }\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::copyCellFormat()\n     */\n    public function copyCellFormat(string $cellFrom, string $rangeTo): self\n    {\n        $this->setCellFormat($rangeTo, $this->sheet()->getStyle($cellFrom)->getNumberFormat()->getFormatCode());\n\n        return $this;\n    }\n\n    /**\n     * Set cell (data) format\n     *\n     * @param string $range\n     * @param string $format\n     * @return self\n     */\n    public function setCellFormat(string $range, string $format): self\n    {\n        $this->sheet()->getStyle($range)->getNumberFormat()->setFormatCode($format);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::addRow()\n     */\n    public function addRow(int $rowBefore, int $qty = 1): self\n    {\n        $this->sheet()->insertNewRowBefore($rowBefore, $qty);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::deleteRow()\n     */\n    public function deleteRow(int $row, int $qty = 1): self\n    {\n        foreach ($this->getMergeCells() as $merge) {\n            preg_match('#(\\d+)#', $merge, $details);\n            if ($details[1] == $row) {\n                $this->sheet()->unmergeCells($merge);\n            }\n        }\n\n        $this->sheet()->removeRow($row, $qty);\n\n        return $this;\n    }\n\n    /**\n     * Add a column\n     *\n     * @param string $columnBefore\n     * @param int $qty\n     * @return self\n     */\n    public function addColumn(string $columnBefore, int $qty = 1): self\n    {\n        $this->sheet()->insertNewColumnBefore($columnBefore, $qty);\n\n        return $this;\n    }\n\n    /**\n     * Set auto-width for a column\n     *\n     * @param string $column\n     * @return self\n     */\n    public function autoWidth(string $column): self\n    {\n        $this->sheet()->getColumnDimension($column)->setAutoSize(true);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SheetsInterface::copyWidth()\n     */\n    public function copyWidth(string $columnFrom, string $columnTo): self\n    {\n        $width = $this->sheet()->getColumnDimension($columnFrom)->getWidth();\n        $this->setWidth($columnTo, $width);\n\n        return $this;\n    }\n\n    /**\n     * Set fixed width for a column\n     *\n     * @param string $column\n     * @param int|float $width\n     * @return self\n     */\n    public function setWidth(string $column, int|float $width): self\n    {\n        $this->sheet()->getColumnDimension($column)->setWidth($width);\n\n        return $this;\n    }\n\n    /**\n     * Copy row's height\n     *\n     * @param int $rowFrom\n     * @param int $rowTo\n     * @return self\n     */\n    public function copyHeight(int $rowFrom, int $rowTo): self\n    {\n        $height = $this->sheet()->getRowDimension($rowFrom)->getRowHeight();\n        $this->setHeight($rowTo, $height);\n\n        return $this;\n    }\n\n    /**\n     * Set fixed height for a row\n     *\n     * @param string $row\n     * @param int|float $height\n     * @return self\n     */\n    public function setHeight(string $row, int|float $height): self\n    {\n        $this->sheet()->getRowDimension($row)->setRowHeight($height);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\MixInterface::setSheetTitle()\n     */\n    public function setSheetTitle(string $title): self\n    {\n        $this->sheet()->setTitle($title);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\MixInterface::getSheetTitle()\n     */\n    public function getSheetTitle(): string\n    {\n        return $this->sheet()->getTitle();\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\MixInterface::mergeDriver()\n     */\n    public function mergeDriver(\\AnourValar\\Office\\Drivers\\MixInterface $driver): self\n    {\n        $index = $driver->spreadsheet->getActiveSheetIndex();\n        $this->spreadsheet->addExternalSheet($driver->sheet());\n        $driver->spreadsheet->createSheet($index);\n\n        return $this;\n    }\n\n    /**\n     * Apply cell`s style without format\n     *\n     * @param string $cellFrom\n     * @param string $rangeTo\n     * @param bool $copyAlignment\n     * @return self\n     */\n    public function copyStyleWithoutFormat(string $cellFrom, string $rangeTo, bool $copyAlignment = false): self\n    {\n        $style = $this->sheet()->getStyle($cellFrom)->exportArray();\n        if (! $copyAlignment) {\n            unset($style['alignment'], $style['numberFormat'], $style['protection']);\n        } else {\n            unset($style['numberFormat'], $style['protection']);\n        }\n\n        // @TODO: fixed ?\n        if (\n            ! isset($style['borders']['allBorders'])\n            && isset($style['borders']['bottom'], $style['borders']['top'])\n            && isset($style['borders']['left'], $style['borders']['right'])\n            && $style['borders']['bottom'] == $style['borders']['top']\n            && $style['borders']['bottom'] == $style['borders']['left']\n            && $style['borders']['bottom'] == $style['borders']['right']\n        ) {\n            $style['borders']['allBorders'] = $style['borders']['bottom'];\n\n            unset($style['borders']['bottom'], $style['borders']['top']);\n            unset($style['borders']['left'], $style['borders']['right']);\n        }\n\n        $this->sheet()->getStyle($rangeTo)->applyFromArray($style);\n\n        return $this;\n    }\n\n    /**\n     * Find a cell with the value\n     *\n     * @param mixed $value\n     * @param bool $strict\n     * @return array|null\n     */\n    public function findCell($value, bool $strict = false): ?array\n    {\n        foreach ($this->getValues(null) as $row => $rowData) {\n            foreach ($rowData as $column => $columnData) {\n                if (($strict && $columnData === $value) || (! $strict && $columnData == $value)) {\n                    return [$column, $row];\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Duplicate rows (with style, value) by range\n     *\n     * @param string $ceilRange\n     * @param callable $value\n     * @param int $indentRows\n     * @param bool $addRows\n     * @return self\n     */\n    public function duplicateRows(string $ceilRange, callable $value, int $indentRows = 0, bool $addRows = true): self\n    {\n        $range = explode(':', $ceilRange);\n        $range[0] = preg_split('#([A-Z]+)([\\d]+)#S', $range[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n        $range[1] = preg_split('#([A-Z]+)([\\d]+)#S', $range[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n\n        $shift = $range[1][1] - $range[0][1] + 1 + $indentRows;\n        $mergeCells = $this->getMergeCells();\n        $values = $this->getValues($ceilRange);\n\n        // Rows\n        if ($addRows) {\n            $this->addRow($range[1][1] + 1, $shift + $indentRows);\n        }\n\n        // Merge\n        foreach ($mergeCells as $item) {\n            $item = explode(':', $item);\n            $item[0] = preg_split('#([A-Z]+)([\\d]+)#S', $item[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n            $item[1] = preg_split('#([A-Z]+)([\\d]+)#S', $item[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n\n            if (\n                $item[0][1] >= $range[0][1] && $item[0][1] <= $range[1][1] // rows\n                && $item[1][1] >= $range[0][1] && $item[1][1] <= $range[1][1]\n                && $this->isColumnGE($item[0][0], $range[0][0]) && $this->isColumnLE($item[0][0], $range[1][0]) // columns\n                && $this->isColumnGE($item[1][0], $range[0][0]) && $this->isColumnLE($item[1][0], $range[1][0])\n            ) {\n                $this->mergeCells($item[0][0].($item[0][1] + $shift) . ':' . $item[1][0].($item[1][1] + $shift));\n            }\n        }\n\n        $curr = $range[0][1];\n        while ($curr <= $range[1][1]) { // rows\n            // Height\n            $this->copyHeight($curr, $curr + $shift);\n\n            // Style, CellFormat, Value\n            $column = $range[0][0];\n            while ($this->isColumnLE($column, $range[1][0])) {\n                $this->copyStyle($column . $curr, $column . ($curr + $shift));\n                $this->copyCellFormat($column . $curr, $column . ($curr + $shift));\n\n                if (isset($values[$curr][$column])) {\n                    $this->setValue($column . ($curr + $shift), $value($values[$curr][$column], $column, $curr), false);\n                }\n\n                $column = $this->strIncrement($column);\n            }\n\n            $curr++;\n        }\n\n        return $this;\n    }\n\n    /**\n     * Set custom style for the range of cells\n     *\n     * @param string $range\n     * @param array $style\n     * @return self\n     */\n    public function setStyle(string $range, array $style): self\n    {\n        if (isset($style['bold'])) {\n            $this->sheet()->getStyle($range)->getFont()->setBold($style['bold']);\n        }\n\n        if (isset($style['italic'])) {\n            $this->sheet()->getStyle($range)->getFont()->setItalic($style['italic']);\n        }\n\n        if (isset($style['size'])) {\n            $this->sheet()->getStyle($range)->getFont()->setSize($style['size']);\n        }\n\n        if (isset($style['underline'])) {\n            $this->sheet()->getStyle($range)->getFont()->setUnderline($style['underline']);\n        }\n\n        if (isset($style['color'])) {\n            $this->sheet()->getStyle($range)->getFont()->getColor()->setRGB($style['color']);\n        }\n\n        if (isset($style['background_color'])) {\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getFill()\n                ->setFillType(\\PhpOffice\\PhpSpreadsheet\\Style\\Fill::FILL_SOLID)\n                ->getStartColor()\n                ->setRGB($style['background_color']);\n        }\n\n        if (isset($style['borders'])) {\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getBorders()\n                ->getAllBorders()\n                ->setBorderStyle($style['borders'] ? Border::BORDER_THIN : Border::BORDER_NONE);\n        }\n\n        if (isset($style['borders_outline'])) {\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getBorders()\n                ->getOutline()\n                ->setBorderStyle($style['borders_outline'] ? Border::BORDER_THIN : Border::BORDER_NONE);\n        }\n\n        if (isset($style['align'])) {\n            $align = match ($style['align']) {\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::HORIZONTAL_LEFT => 'left',\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::HORIZONTAL_CENTER => 'center',\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::HORIZONTAL_RIGHT => 'right',\n            };\n\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getAlignment()->setHorizontal($align);\n        }\n\n        if (isset($style['valign'])) {\n            $valign = match ($style['valign']) {\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::VERTICAL_TOP => 'top',\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::VERTICAL_CENTER => 'center',\n                \\PhpOffice\\PhpSpreadsheet\\Style\\Alignment::VERTICAL_BOTTOM => 'bottom',\n            };\n\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getAlignment()->setVertical($valign);\n        }\n\n        if (isset($style['wrap'])) {\n            $this\n                ->sheet()\n                ->getStyle($range)\n                ->getAlignment()->setWrapText($style['wrap']);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Place an image\n     *\n     * @param string $filename\n     * @param string $cell\n     * @param array $options\n     * @return self\n     */\n    public function insertImage(string $filename, string $cell, array $options = []): self\n    {\n        $drawing = new \\PhpOffice\\PhpSpreadsheet\\Worksheet\\Drawing();\n\n        if (isset($options['base64'])) {\n            $filename = 'data:image/' . $options['base64'] . ';base64,' . base64_encode(file_get_contents($filename));\n        }\n\n        $drawing->setPath($filename);\n        $drawing->setCoordinates($cell);\n\n        if (isset($options['coordinates2'])) {\n            $drawing->setCoordinates2($options['coordinates2']);\n        }\n\n        if (isset($options['name'])) {\n            $drawing->setName($options['name']);\n        }\n\n        if (isset($options['offset_x'])) {\n            $drawing->setOffsetX($options['offset_x']);\n        }\n        if (isset($options['offset_x2'])) {\n            $drawing->setOffsetX2($options['offset_x2']);\n        }\n\n        if (isset($options['offset_y'])) {\n            $drawing->setOffsetY($options['offset_y']);\n        }\n        if (isset($options['offset_y2'])) {\n            $drawing->setOffsetY2($options['offset_y2']);\n        }\n\n        if (isset($options['rotation'])) {\n            $drawing->setRotation($options['rotation']);\n        }\n\n        if (isset($options['width']) && isset($options['height'])) {\n            $drawing\n                ->setResizeProportional(false)\n                ->setWidth($options['width'])\n                ->setHeight($options['height']);\n        } elseif (isset($options['width'])) {\n            $drawing->setWidth($options['width']);\n        } elseif (isset($options['height'])) {\n            $drawing->setHeight($options['height']);\n        }\n\n        $drawing->setWorksheet($this->sheet());\n\n        return $this;\n    }\n\n    /**\n     * \"Reader\" configuration\n     *\n     * @param \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver $instance\n     * @return void\n     */\n    protected function readConfiguration(PhpSpreadsheetDriver $instance): void\n    {\n        //\n    }\n\n    /**\n     * \"Writer\" configuration\n     *\n     * @param \\PhpOffice\\PhpSpreadsheet\\Writer\\IWriter $writer\n     * @return void\n     */\n    protected function writeConfiguration(\\PhpOffice\\PhpSpreadsheet\\Writer\\IWriter $writer): void\n    {\n        if ($writer instanceof \\PhpOffice\\PhpSpreadsheet\\Writer\\Csv) {\n            $writer->setDelimiter(';')->setUseBOM(true);\n        }\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Format $format\n     * @return string\n     */\n    protected function getFormat(\\AnourValar\\Office\\Format $format): string\n    {\n        return match ($format) {\n            \\AnourValar\\Office\\Format::Xlsx => 'Xlsx',\n            \\AnourValar\\Office\\Format::Pdf => 'Mpdf',\n            \\AnourValar\\Office\\Format::Html => 'Html',\n            \\AnourValar\\Office\\Format::Ods => 'Ods',\n            \\AnourValar\\Office\\Format::Csv => 'Csv',\n            default => throw new \\RuntimeException('Format is not supported.'),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Drivers/SaveInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface SaveInterface\n{\n    /**\n     * Save in specific format\n     *\n     * @param string $file\n     * @param \\AnourValar\\Office\\Format $format\n     * @return void\n     */\n    public function save(string $file, \\AnourValar\\Office\\Format $format): void;\n}\n"
  },
  {
    "path": "src/Drivers/SheetsInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface SheetsInterface extends SaveInterface, LoadInterface, MultiSheetInterface\n{\n    /**\n     * Set values\n     *\n     * @param array $data\n     * @param bool $autoCellFormat\n     * @return self\n     */\n    public function setValues(array $data, bool $autoCellFormat = true): self;\n\n    /**\n     * Get values (range)\n     *\n     * @param string|null $ceilRange\n     * @return array\n     */\n    public function getValues(?string $ceilRange): array;\n\n    /**\n     * Get merge cells (whole sheet)\n     *\n     * @return array\n     */\n    public function getMergeCells(): array;\n\n    /**\n     * Merge cells\n     *\n     * @param string $ceilRange\n     * @return self\n     */\n    public function mergeCells(string $ceilRange): self;\n\n    /**\n     * Apply cell`s style\n     *\n     * @param string $cellFrom\n     * @param string $rangeTo\n     * @return self\n     */\n    public function copyStyle(string $cellFrom, string $rangeTo): self;\n\n    /**\n     * Copy cell`s format\n     *\n     * @param string $cellFrom\n     * @param string $rangeTo\n     * @return self\n     */\n    public function copyCellFormat(string $cellFrom, string $rangeTo): self;\n\n    /**\n     * Add a row\n     *\n     * @param int $rowBefore\n     * @param int $qty\n     * @return self\n     */\n    public function addRow(int $rowBefore, int $qty = 1): self;\n\n    /**\n     * Delete a row\n     *\n     * @param int $row\n     * @param int $qty\n     * @return self\n     */\n    public function deleteRow(int $row, int $qty = 1): self;\n\n    /**\n     * Copy column's width\n     *\n     * @param string $columnFrom\n     * @param string $columnTo\n     * @return self\n     */\n    public function copyWidth(string $columnFrom, string $columnTo): self;\n}\n"
  },
  {
    "path": "src/Drivers/ZipDriver.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\nclass ZipDriver implements DocumentInterface, GridInterface\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n    use \\AnourValar\\Office\\Traits\\XFormat;\n\n    /**\n     * @var \\AnourValar\\Office\\Format\n     */\n    protected readonly \\AnourValar\\Office\\Format $format;\n\n    /**\n     * @var array\n     */\n    protected array $fileSystem;\n\n    /**\n     * @var array\n     */\n    protected array $gridOptions = [];\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\GridInterface::create()\n     */\n    public function create(): self\n    {\n        return $this->load(__DIR__ . '/../resources/grid.xlsx', \\AnourValar\\Office\\Format::Xlsx);\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\LoadInterface::load()\n     * @psalm-suppress InaccessibleProperty\n     */\n    public function load(string $file, \\AnourValar\\Office\\Format $format): self\n    {\n        if (! in_array($format, [\\AnourValar\\Office\\Format::Docx, \\AnourValar\\Office\\Format::Xlsx])) {\n            throw new \\LogicException('Driver only supports Docx, Xlsx formats.');\n        }\n\n        $instance = new static();\n        $fileSystem = [];\n\n        $zipArchive = new \\ZipArchive();\n        $zipArchive->open($file);\n        try {\n            $count = $zipArchive->numFiles;\n\n            for ($i = 0; $i < $count; $i++) {\n                $filename = $zipArchive->getNameIndex($i);\n                $content = $zipArchive->getFromName($filename);\n\n                $fileSystem[$filename] = $content;\n            }\n        } catch (\\Throwable $e) {\n            $zipArchive->close();\n            throw $e;\n        }\n        $zipArchive->close();\n\n        $instance->fileSystem = $fileSystem;\n        $instance->format = $format;\n        return $instance;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\SaveInterface::save()\n     */\n    public function save(string $file, \\AnourValar\\Office\\Format $format): void\n    {\n        if ($format != $this->format) {\n            throw new \\LogicException('Driver only supports saving in the same format.');\n        }\n\n        if (class_exists(\\ZipStream\\Option\\Archive::class)) {\n            $zipStream = new \\ZipStream\\ZipStream(); // 2.x\n        } else {\n            $zipStream = new \\ZipStream\\ZipStream(sendHttpHeaders: false, defaultEnableZeroHeader: false); // 3.x\n        }\n        ob_start();\n\n        try {\n            foreach ($this->fileSystem as $filename => $content) {\n                $zipStream->addFile($filename, $content);\n            }\n        } catch (\\Throwable $e) {\n            $zipStream->finish();\n            ob_get_clean();\n            throw $e;\n        }\n\n        $zipStream->finish();\n        file_put_contents($file, ob_get_clean());\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\DocumentInterface::replace()\n     */\n    public function replace(array $data): self\n    {\n        foreach ($data as &$value) {\n            $value = $this->escape($value);\n        }\n        unset($value);\n\n        foreach ($this->fileSystem as $filename => &$content) {\n            if ($content && mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'xml') {\n                $content = $this->handleReplace($content, $data);\n            }\n        }\n        unset($content);\n\n        return $this;\n    }\n\n    /**\n     * {@inheritDoc}\n     * @see \\AnourValar\\Office\\Drivers\\GridInterface::setGrid()\n     */\n    public function setGrid(iterable $data): self\n    {\n        $sheet = ''; // matrix\n        $cols = ''; // columns\n\n        $buckets = []; // text (sharedStrings)\n        $bucketsIndex = 0;\n\n        $row = 0; // rows count\n        $columnsCount = 0; // columns count\n        $firstColumn = 'A'; // columns shift\n\n        // Styles\n        $styles = $this->loadGridStyles();\n\n        // Head (titles)\n        while ($data->valid()) {\n            $row++;\n            $titles = $data->current();\n            $data->next();\n            $columnsCount = count($titles);\n\n            if (! $titles) {\n                continue;\n            }\n\n            $sheet .= '<row r=\"'.$row.'\" spans=\"1:'.$columnsCount.'\" ht=\"'.($this->gridOptions['height'][$row] ?? 25).'\" customHeight=\"1\" x14ac:dyDescent=\"0.3\">';\n            $column = 'A';\n            foreach ($titles as $value) {\n                $value = (string) $value;\n                if ($value === '') {\n                    $firstColumn = $this->strIncrement($firstColumn);\n                    $column = $this->strIncrement($column);\n                    continue;\n                }\n\n                $value = $this->escape($value);\n                $curr = $buckets[$value] ?? null;\n                if ($curr === null) {\n                    $curr = $bucketsIndex;\n                    $buckets[$value] = $curr;\n\n                    $bucketsIndex++;\n                }\n\n                $sheet .= '<c r=\"'.$column.$row.'\" t=\"s\" s=\"'.$styles['header'].'\"><v>'.$curr.'</v></c>';\n                $column = $this->strIncrement($column);\n            }\n            $sheet .= '</row>';\n\n            break;\n        }\n\n        // Custom styles\n        foreach (($this->gridOptions['style'] ?? []) as $column => $alias) {\n            if (isset($styles[$alias])) {\n                $styles[$column] = $styles[$alias];\n            }\n        }\n\n        // Body (data)\n        while ($data->valid()) {\n            $values = $data->current();\n            $data->next();\n            $row++;\n\n            $ht = '';\n            if (isset($this->gridOptions['height'][$row])) {\n                $ht = 'ht=\"'.$this->gridOptions['height'][$row].'\" customHeight=\"1\" ';\n            }\n\n            $sheet .= '<row r=\"'.$row.'\" '.$ht.'spans=\"1:'.$columnsCount.'\" x14ac:dyDescent=\"0.3\">';\n            $column = 'A';\n            foreach ($values as $value) {\n                if ($value instanceof \\Stringable && ! $value instanceof \\DateTimeInterface) {\n                    $value = (string) $value;\n                }\n\n                if ($value === null || $value === '') {\n\n                    if ($this->isColumnGE($column, $firstColumn)) {\n                        $sheet .= '<c r=\"'.$column.$row.'\" s=\"'.($styles[$column] ?? $styles['string']).'\"/>';\n                    }\n\n                } elseif (is_string($value)) {\n\n                    $value = $this->escape($value);\n                    $curr = $buckets[$value] ?? null;\n                    if ($curr === null) {\n                        $curr = $bucketsIndex;\n                        $buckets[$value] = $curr;\n\n                        $bucketsIndex++;\n                    }\n\n                    $style = ($styles[$column] ?? $styles['string']);\n                    $sheet .= '<c r=\"'.$column.$row.'\" t=\"s\" s=\"'.$style.'\"><v>'.$curr.'</v></c>';\n\n                } elseif (is_double($value)) {\n\n                    $style = ($styles[$column] ?? $styles['double']);\n                    $sheet .= '<c r=\"'.$column.$row.'\" s=\"'.$style.'\"><v>'.$value.'</v></c>';\n\n                } elseif (is_integer($value)) {\n\n                    $style = ($styles[$column] ?? $styles['integer']);\n                    $sheet .= '<c r=\"'.$column.$row.'\" s=\"'.$style.'\"><v>'.$value.'</v></c>';\n\n                } elseif ($value instanceof \\DateTimeInterface) {\n\n                    $style = ($styles[$column] ?? $styles['date']);\n                    $sheet .= '<c r=\"'.$column.$row.'\" s=\"'.$style.'\"><v>'.$this->excelDate($value).'</v></c>';\n\n                } else {\n\n                    throw new \\RuntimeException('Unsupported type of value.');\n\n                }\n\n                $column = $this->strIncrement($column);\n            }\n            $sheet .= '</row>';\n        }\n\n        // Columns\n        $column = 'A';\n        for ($index = 1; $index <= $columnsCount; $index++) {\n            if ($this->isColumnGE($column, $firstColumn)) {\n                $width = ($this->gridOptions['width'][$column] ?? 20);\n                $cols .= '<col min=\"'.$index.'\" max=\"'.$index.'\" width=\"'.$width.'\" customWidth=\"1\"/>';\n            }\n            $column = $this->strIncrement($column);\n        }\n\n        // Save buckets\n        $this->saveGridSharedStrings($buckets);\n\n        // Save columns & matrix\n        $this->saveGridWorksheet($cols, $sheet, $row, $column);\n\n        // Etc\n        $this->saveGridEtc();\n\n        return $this;\n    }\n\n    /**\n     * @param string $content\n     * @param array $data\n     * @return string\n     */\n    protected function handleReplace(string $content, array &$data): string\n    {\n        foreach ($data as $from => $to) {\n            $pattern = mb_str_split($from);\n            foreach ($pattern as &$patternItem) {\n                $patternItem = preg_quote($patternItem);\n                $patternItem .= '(\\<[^\\[]*)?';\n            }\n            unset($patternItem);\n            $pattern = implode('', $pattern);\n\n            $content = preg_replace_callback(\"#$pattern#Uu\", function ($patterns) use ($from, $to) {\n                if (strip_tags($patterns[0]) == $from) {\n                    return $to;\n                }\n\n                return $patterns[0];\n            }, $content);\n        }\n\n        return $content;\n    }\n\n\n    /**\n     * Set styles map for the grid template [header, integer, double, double_10, string, date, percentage, ...]\n     *\n     * @param string $column\n     * @param string $style\n     * @return self\n     */\n    public function setStyle(string $column, string $style): self\n    {\n        $this->gridOptions['style'][$column] = $style;\n\n        return $this;\n    }\n\n    /**\n     * Set column's width for the grid\n     *\n     * @param string $column\n     * @param int $width\n     * @return self\n     */\n    public function setWidth(string $column, int $width): self\n    {\n        $this->gridOptions['width'][$column] = $width;\n\n        return $this;\n    }\n\n    /**\n     * Set row's height for the grid\n     *\n     * @param string $row\n     * @param int|float $height\n     * @return self\n     */\n    public function setHeight(string $row, int|float $height): self\n    {\n        $this->gridOptions['height'][$row] = $height;\n\n        return $this;\n    }\n\n    /**\n     * Set sheet title for the grid\n     *\n     * @param string $title\n     * @return self\n     */\n    public function setSheetTitle(string $title): self\n    {\n        $this->fileSystem['xl/workbook.xml'] = preg_replace(\n            '#\\<sheet name=\"(.+)\" sheetId\\=\"(.+)\" r\\:id\\=\"(.+)\"\\/\\>#uU',\n            '<sheet name=\"'.$this->escape($title).'\" sheetId=\"$2\" r:id=\"$3\"/>',\n            $this->fileSystem['xl/workbook.xml']\n        );\n\n        return $this;\n    }\n\n    /**\n     * @return array\n     */\n    protected function loadGridStyles(): array\n    {\n        $styles = [];\n\n        // Bucket\n        preg_match_all('#<si><t>(.*)</t></si>#uU', $this->fileSystem['xl/sharedStrings.xml'], $buckets);\n        $buckets = $buckets[1];\n\n        // Matrix\n        preg_match_all(\n            '#<c r=\"[A-Z\\d]+\" s=\"(\\d+)\" t=\"s\"><v>(\\d+)</v></c>#uU',\n            $this->fileSystem['xl/worksheets/sheet1.xml'],\n            $matrix\n        );\n\n        // Parse the map\n        foreach ($matrix[2] as $key => $value) {\n            if (isset($buckets[$value])) {\n                $styles[mb_substr($buckets[$value], 1, -1)] = $matrix[1][$key];\n            }\n        }\n\n        // Presets\n        return array_merge(['header' => 1, 'string' => 1, 'double' => 1, 'integer' => 1, 'date' => 1], $styles);\n    }\n\n    /**\n     * @param array $buckets\n     * @return void\n     */\n    protected function saveGridSharedStrings(array &$buckets): void\n    {\n        $sst = '';\n        foreach (array_keys($buckets) as $word) {\n            $sst .= '<si><t>'.$word.'</t></si>';\n        }\n        $count = count($buckets);\n\n        $this->fileSystem['xl/sharedStrings.xml'] = <<<HERE\n        <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n        <sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"$count\" uniqueCount=\"$count\">\n        $sst\n        </sst>\n        HERE;\n    }\n\n    /**\n     * @param string $cols\n     * @param string $sheet\n     * @param int $lastRow\n     * @param string $lastColumn\n     * @return void\n     */\n    protected function saveGridWorksheet(string &$cols, string &$sheet, int $lastRow, string $lastColumn): void\n    {\n        $worksheet = 'xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" '\n            . 'xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" '\n            . 'xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" '\n            . 'mc:Ignorable=\"x14ac xr xr2 xr3\" '\n            . 'xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\" '\n            . 'xmlns:xr=\"http://schemas.microsoft.com/office/spreadsheetml/2014/revision\" '\n            . 'xmlns:xr2=\"http://schemas.microsoft.com/office/spreadsheetml/2015/revision2\" '\n            . 'xmlns:xr3=\"http://schemas.microsoft.com/office/spreadsheetml/2016/revision3\" '\n            . 'xr:uid=\"{9CD2C5FF-272A-44F0-88FE-0A0D7B1A3B22}\"';\n\n        if ($cols) {\n            $cols = \"<cols>$cols</cols>\";\n        }\n\n        if ($sheet) {\n            $sheet = \"<sheetData>$sheet</sheetData>\";\n        } else {\n            $sheet = '<sheetData/>';\n        }\n\n        if (! $lastRow) {\n            $lastRow = 1;\n        }\n\n        $this->fileSystem['xl/worksheets/sheet1.xml'] = <<<HERE\n        <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n        <worksheet $worksheet>\n        <dimension ref=\"A1:{$lastColumn}{$lastRow}\"/>\n        <sheetViews>\n        <sheetView tabSelected=\"1\" workbookViewId=\"0\"><selection activeCell=\"A1\" sqref=\"A1\"/></sheetView>\n        </sheetViews>\n        <sheetFormatPr defaultRowHeight=\"14.4\" x14ac:dyDescent=\"0.3\"/>\n        $cols\n        $sheet\n        <pageMargins left=\"0.7\" right=\"0.7\" top=\"0.75\" bottom=\"0.75\" header=\"0.3\" footer=\"0.3\"/>\n        <pageSetup paperSize=\"9\" orientation=\"portrait\" horizontalDpi=\"1200\" verticalDpi=\"1200\" r:id=\"rId1\"/>\n        </worksheet>\n        HERE;\n    }\n\n    /**\n     * @return void\n     */\n    protected function saveGridEtc(): void\n    {\n        // Created at timestamp\n        $this->fileSystem['docProps/core.xml'] = preg_replace(\n            '#(\\<dcterms\\:created xsi\\:type\\=\"[^\"]+\">)(.*?)(\\<\\/dcterms\\:created\\>)#',\n            '${1}' . date('Y-m-d\\TH:i:s\\Z') . '$3',\n            $this->fileSystem['docProps/core.xml']\n        );\n\n        // Modified at timestamp\n        $this->fileSystem['docProps/core.xml'] = preg_replace(\n            '#(\\<dcterms\\:modified xsi\\:type\\=\"[^\"]+\">)(.*?)(\\<\\/dcterms\\:modified\\>)#',\n            '${1}' . date('Y-m-d\\TH:i:s\\Z') . '$3',\n            $this->fileSystem['docProps/core.xml']\n        );\n    }\n}\n"
  },
  {
    "path": "src/Facades/ExportGridInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\n\ninterface ExportGridInterface\n{\n    /**\n     * Sheet title\n     *\n     * @param array $request\n     * @return string\n     */\n    public function sheetTitle(array $request): string;\n\n    /**\n     * Columns structure\n     *\n     * @param array $request\n     * @return array\n     */\n    public function columns(array $request): array;\n\n    /**\n     * Row iteration\n     *\n     * @param mixed $row\n     * @param \\AnourValar\\Office\\Drivers\\GridInterface $driver\n     * @param int $rowNumber\n     * @param array $request\n     * @return array\n     */\n    public function item($row, GridInterface $driver, int $rowNumber, array $request): array;\n\n    /**\n     * Filename\n     *\n     * @param string $ext\n     * @param array $request\n     * @return string\n     */\n    public function fileName(string $ext, array $request): string;\n}\n"
  },
  {
    "path": "src/Facades/ExportGridQueryInterface.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\n/**\n * Usage example:\n *\n * if (! in_array($format, [\\AnourValar\\Office\\Format::Xlsx, \\AnourValar\\Office\\Format::Csv])) {\n *     throw new \\App\\Exceptions\\ValidationException('Format is not supported.');\n * }\n *\n * $generatorData = $this->buildBy($myGrid->query()->acl(), array_replace($this->profile, $this->profileExport)); // out of context\n * $request = $this->getBuildRequest()->get();\n *\n * return response()->streamDownload(\n *     function () use ($generatorData, $myGrid, $exportService, $format, $request) {\n *         echo $exportService->grid($generatorData, $myGrid, $format, $request);\n *     },\n *     $myGrid->fileName($format->fileExtension(), $request),\n *     ['Access-Control-Expose-Headers' => 'Content-Disposition']\n * );\n */\n\ninterface ExportGridQueryInterface extends ExportGridInterface\n{\n    /**\n     * Laravel's Query builder (base query)\n     *\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function query(): \\Illuminate\\Database\\Eloquent\\Builder;\n}\n"
  },
  {
    "path": "src/Facades/ExportService.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\nuse AnourValar\\Office\\Format;\n\nclass ExportService\n{\n    /**\n     * Extra options\n     *\n     * @var string\n     */\n    public const PERCENTAGE = 'percentage';\n    public const DOUBLE_10 = 'double_10';\n    public const DATETIME = 'datetime';\n\n    /**\n     * Generate a grid\n     *\n     * @param \\Closure $dataGenerator\n     * @param \\AnourValar\\Office\\Facades\\ExportGridInterface $grid\n     * @param \\AnourValar\\Office\\Format $format\n     * @param array $request\n     * @return string\n     */\n    public function grid(\\Closure $dataGenerator, ExportGridInterface $grid, Format $format = Format::Xlsx, array $request = []): string\n    {\n        $extras = [];\n\n        return (new \\AnourValar\\Office\\GridService($this->getDriver($format)))\n            ->hookHeader(function (GridInterface $driver, mixed $header, string|int $key, string $column, int $rowNumber) use (&$extras) {\n                if (isset($header['width'])) {\n                    $driver->setWidth($column, $header['width']);\n                }\n\n                if (isset($header['height'])) {\n                    $driver->setHeight($rowNumber, $header['height']);\n                }\n\n                $extras = array_merge($extras, $this->handleExtras($driver, $column, $header));\n\n                return $header['title'];\n            })\n            ->hookRow(function (GridInterface $driver, mixed $row, string|int $key, int $rowNumber) use ($grid, $request) {\n                return $grid->item($row, $driver, $rowNumber, $request);\n            })\n            ->hookAfter(function (\n                GridInterface $driver,\n                ?string $headersRange,\n                ?string $dataRange,\n                ?string $totalRange,\n                array $columns\n            ) use ($grid, $request, &$extras) {\n                $driver->setSheetTitle($grid->sheetTitle($request));\n\n                foreach ($extras as $extra) {\n                    $extra();\n                }\n            })\n            ->generate($grid->columns($request), $dataGenerator)\n            ->save($format);\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\GridInterface $driver\n     * @param string $column\n     * @param array $header\n     * @return array\n     * @throws \\RuntimeException\n     */\n    protected function handleExtras(GridInterface $driver, string $column, array $header): array\n    {\n        $extras = [];\n\n        // percentage\n        if (! empty($header[self::PERCENTAGE])) {\n            if ($driver instanceof \\AnourValar\\Office\\Drivers\\ZipDriver) {\n                $driver->setStyle($column, 'percentage');\n            } elseif ($driver instanceof \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver) {\n                $extras[] = fn () => $driver->setCellFormat($column, \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver::FORMAT_PERCENTAGE);\n            } else {\n                throw new \\RuntimeException('The driver does not support the \"percentage\" feature.');\n            }\n        }\n\n        // double_10\n        if (! empty($header[self::DOUBLE_10])) {\n            if ($driver instanceof \\AnourValar\\Office\\Drivers\\ZipDriver) {\n                $driver->setStyle($column, 'double_10');\n            } elseif ($driver instanceof \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver) {\n                $extras[] = fn () => $driver->setCellFormat($column, \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver::FORMAT_DOUBLE_10);\n            } else {\n                throw new \\RuntimeException('The driver does not support the \"double_10\" feature.');\n            }\n        }\n\n        // datetime\n        if (! empty($header[self::DATETIME])) {\n            if ($driver instanceof \\AnourValar\\Office\\Drivers\\ZipDriver) {\n                $driver->setStyle($column, 'datetime');\n            } elseif ($driver instanceof \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver) {\n                $extras[] = fn () => $driver->setCellFormat($column, \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver::FORMAT_DATETIME);\n            } else {\n                throw new \\RuntimeException('The driver does not support the \"datetime\" feature.');\n            }\n        }\n\n        return $extras;\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Format $format\n     * @return \\AnourValar\\Office\\Drivers\\GridInterface\n     */\n    protected function getDriver(Format $format): GridInterface\n    {\n        if ($format == Format::Xlsx) {\n            return new \\AnourValar\\Office\\Drivers\\ZipDriver();\n        }\n\n        return new \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver();\n    }\n}\n"
  },
  {
    "path": "src/Format.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nenum Format: string\n{\n    case Xlsx = 'xlsx'; // sheets | grid => reader + writer\n    case Pdf = 'pdf'; // sheets | grid => writer\n    case Html = 'html'; // sheets | grid => reader + writer\n    case Ods = 'ods'; // sheets | grid => reader + writer\n    case Csv = 'csv'; // sheets | grid => reader + writer\n    case Docx = 'docx'; // document => reader + writer\n\n    /**\n     * @return string\n     */\n    public function fileExtension(): string\n    {\n        return match ($this) {\n            Format::Xlsx => 'xlsx',\n            Format::Pdf => 'pdf',\n            Format::Html => 'html',\n            Format::Ods => 'ods',\n            Format::Csv => 'csv',\n            Format::Docx => 'docx',\n        };\n    }\n\n    /**\n     * MIME\n     *\n     * @return string\n     */\n    public function contentType(): string\n    {\n        return match ($this) {\n            Format::Xlsx => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n            Format::Pdf => 'application/pdf',\n            Format::Html => 'text/html',\n            Format::Ods => 'application/vnd.oasis.opendocument.spreadsheet',\n            Format::Csv => 'text/csv',\n            Format::Docx => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n        };\n    }\n}\n"
  },
  {
    "path": "src/Generated.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Generated\n{\n    /**\n     * @var \\AnourValar\\Office\\Drivers\\SaveInterface\n     */\n    public readonly \\AnourValar\\Office\\Drivers\\SaveInterface $driver;\n\n    /**\n     * Handle template's saving\n     *\n     * @var \\Closure(\\AnourValar\\Office\\Drivers\\SaveInterface $driver, \\AnourValar\\Office\\Format $format): void\n     */\n    protected ?\\Closure $hookSave = null;\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\SaveInterface $driver\n     * @return void\n     */\n    public function __construct(\\AnourValar\\Office\\Drivers\\SaveInterface $driver)\n    {\n        $this->driver = $driver;\n    }\n\n    /**\n     * Save generated document to the buffer\n     *\n     * @param \\AnourValar\\Office\\Format $format\n     * @return string\n     */\n    public function save(Format $format): string\n    {\n        ob_start();\n\n        if ($this->hookSave) {\n            ($this->hookSave)($this->driver, $format);\n        } else {\n            $this->driver->save('php://output', $format);\n        }\n\n        return ob_get_clean();\n    }\n\n    /**\n     * Save generated document to the file\n     *\n     * @param string $filename\n     * @param \\AnourValar\\Office\\Format|null $format\n     * @return int|null\n     */\n    public function saveAs(string $filename, ?Format $format = null): ?int\n    {\n        if (! $format) {\n            $format = Format::from(mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)));\n        }\n\n        return file_put_contents($filename, $this->save($format));\n    }\n\n    /**\n     * Set hookSave\n     *\n     * @param \\Closure|null $closure\n     * @return self\n     */\n    public function hookSave(?\\Closure $closure): self\n    {\n        $this->hookSave = $closure;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/GridService.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\n\nclass GridService\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n\n    /**\n     * @var \\AnourValar\\Office\\Drivers\\GridInterface\n     */\n    protected \\AnourValar\\Office\\Drivers\\GridInterface $driver;\n\n    /**\n     * Handle template's creating\n     *\n     * @var \\Closure(GridInterface $driver): GridInterface\n     */\n    protected ?\\Closure $hookLoad = null;\n\n    /**\n     * Actions with template before data inserted\n     *\n     * @var \\Closure(GridInterface $driver, array &$headers, iterable &$data, string $leftTopCorner): void\n     */\n    protected ?\\Closure $hookBefore = null;\n\n    /**\n     * Header handler\n     *\n     * @var \\Closure(GridInterface $driver, mixed $header, string|int $key, string $column, int $rowNumber): string\n     */\n    protected ?\\Closure $hookHeader = null;\n\n    /**\n     * Row data handler\n     *\n     * @var \\Closure(GridInterface $driver, mixed $row, string|int $key, int $rowNumber): array\n     */\n    protected ?\\Closure $hookRow = null;\n\n    /**\n     * Actions with template after data inserted\n     *\n     * @var \\Closure(GridInterface $driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns): void\n     */\n    protected ?\\Closure $hookAfter = null;\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\GridInterface $driver\n     * @return void\n     */\n    public function __construct(GridInterface $driver = new \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver())\n    {\n        $this->driver = $driver;\n    }\n\n    /**\n     * Generate a document from the template (grid)\n     *\n     * @param array $headers\n     * @param iterable|\\Closure $data\n     * @param string $leftTopCorner\n     * @return \\AnourValar\\Office\\Generated\n     */\n    public function generate(array $headers, iterable|\\Closure $data, string $leftTopCorner = 'A1'): Generated\n    {\n        // Handle with data\n        if ($data instanceof \\Closure) {\n            $data = $data();\n        }\n\n        // Create new document\n        if ($this->hookLoad) {\n            $driver = ($this->hookLoad)($this->driver);\n            if ($driver instanceof Generated) {\n                $driver = $driver->driver;\n            }\n        } else {\n            $driver = $this->driver->create();\n        }\n\n        // Hook: before\n        if ($this->hookBefore) {\n            ($this->hookBefore)($driver, $headers, $data, $leftTopCorner);\n        }\n\n        // Set data\n        $driver->setGrid(\n            $this->getGenerator($driver, $headers, $data, $leftTopCorner, $headersRange, $dataRange, $totalRange, $columns)()\n        );\n\n        // Hook: after\n        if ($this->hookAfter) {\n            ($this->hookAfter)($driver, $headersRange, $dataRange, $totalRange, $columns);\n        }\n\n        return new Generated($driver);\n    }\n\n    /**\n     * Set hookLoad\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookLoad(?\\Closure $closure): self\n    {\n        $this->hookLoad = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookBefore\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookBefore(?\\Closure $closure): self\n    {\n        $this->hookBefore = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookHeader\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookHeader(?\\Closure $closure): self\n    {\n        $this->hookHeader = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookRow\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookRow(?\\Closure $closure): self\n    {\n        $this->hookRow = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookAfter\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookAfter(?\\Closure $closure): self\n    {\n        $this->hookAfter = $closure;\n\n        return $this;\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\GridInterface $driver\n     * @param array $headers\n     * @param iterable $data\n     * @param string $leftTopCorner\n     * @param mixed $headersRange\n     * @param mixed $dataRange\n     * @param mixed $totalRange\n     * @param mixed $columns\n     * @return \\Closure\n     * @psalm-suppress UnusedForeachValue\n     */\n    protected function getGenerator(\n        \\AnourValar\\Office\\Drivers\\GridInterface $driver,\n        array &$headers,\n        iterable &$data,\n        string $leftTopCorner,\n        &$headersRange = null,\n        &$dataRange = null,\n        &$totalRange = null,\n        &$columns = null\n    ): \\Closure {\n        return function () use ($driver, &$headers, &$data, $leftTopCorner, &$headersRange, &$dataRange, &$totalRange, &$columns) {\n            $ltc = preg_split('|([A-Z]+)|', $leftTopCorner, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);\n\n            // left top corner: row\n            $headerRow = 1;\n            while ($ltc[1] > 1) {\n                $ltc[1]--;\n                $headerRow++;\n                yield [];\n            }\n\n            // left top corner: column\n            $firstColumn = 'A';\n            $indent = [];\n            while ($this->isColumnLE($firstColumn, $ltc[0]) && $firstColumn != $ltc[0]) {\n                $firstColumn = $this->strIncrement($firstColumn);\n                $indent[] = '';\n            }\n\n            // Handle with header\n            $lastColumn = $firstColumn;\n            $isFirst = true;\n            $hasHeaders = false;\n            foreach ($headers as $key => &$header) {\n                if ($isFirst) {\n                    $isFirst = false;\n                } else {\n                    $lastColumn = $this->strIncrement($lastColumn);\n                }\n\n                if ($this->hookHeader) {\n                    // Hook: header\n                    $header = ($this->hookHeader)($driver, $header, $key, $lastColumn, $headerRow);\n                }\n\n                if ($header) {\n                    $hasHeaders = true;\n                }\n            }\n            unset($header);\n\n            // First iteration with headers\n            if ($hasHeaders) {\n                yield array_merge($indent, $headers);\n            } else {\n                $headerRow--;\n            }\n\n            // Iterations with data\n            $dataRow = $headerRow;\n            $isFirst = ! $hasHeaders;\n            foreach ($data as $key => $row) {\n                // Hook: row\n                if ($this->hookRow) {\n                    $row = ($this->hookRow)($driver, $row, $key, $dataRow + 1);\n                }\n\n                if (is_null($row)) {\n                    continue;\n                }\n\n                if ($isFirst) {\n                    foreach ($row as $item) {\n                        if ($isFirst) {\n                            $isFirst = false;\n                        } else {\n                            $lastColumn = $this->strIncrement($lastColumn);\n                        }\n                    }\n                }\n\n                yield array_merge($indent, $row);\n                $dataRow++;\n            }\n\n            // Statistic\n            $headersRange = null;\n            if ($hasHeaders) {\n                $headersRange = sprintf('%s%d:%s%d', $firstColumn, $headerRow, $lastColumn, $headerRow);\n            }\n\n            $dataRange = null;\n            if ($dataRow != $headerRow) {\n                $dataRange = sprintf('%s%d:%s%d', $firstColumn, ($headerRow + 1), $lastColumn, $dataRow);\n            }\n\n            $totalRange = null;\n            if ($hasHeaders || $dataRow != $headerRow) {\n                $totalRange = sprintf(\n                    '%s%d:%s%d',\n                    $firstColumn,\n                    ($hasHeaders ? $headerRow : ($headerRow + 1)),\n                    $lastColumn,\n                    $dataRow\n                );\n            }\n\n            $columns = [];\n            if ($totalRange) {\n                $keys = array_keys($headers);\n\n                while ($this->isColumnLE($firstColumn, $lastColumn)) {\n                    if (! $keys) {\n                        $columns[] = $firstColumn;\n                    } else {\n                        $columns[array_shift($keys)] = $firstColumn;\n                    }\n\n                    $firstColumn = $this->strIncrement($firstColumn);\n                }\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Mixer.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Mixer\n{\n    /**\n     * Mix generated documents\n     *\n     * @param \\AnourValar\\Office\\Generated[\\AnourValar\\Office\\Drivers\\MixInterface] $generated\n     * @throws \\LogicException\n     * @return \\AnourValar\\Office\\Generated\n     */\n    public function __invoke(...$generated): Generated\n    {\n        $referenceDriver = array_shift($generated);\n        if (! $referenceDriver instanceof \\AnourValar\\Office\\Generated) {\n            throw new \\LogicException('Input data must be instanceof Generated');\n        }\n\n        $referenceDriver = $referenceDriver->driver;\n        if (! $referenceDriver instanceof \\AnourValar\\Office\\Drivers\\MixInterface) {\n            throw new \\LogicException('Driver must implements MixInterface.');\n        }\n\n        $titles = [];\n        $count = $referenceDriver->getSheetCount();\n        for ($i = 0; $i < $count; $i++) {\n            $referenceDriver->setSheet($i);\n\n            $titles[] = $referenceDriver->getSheetTitle();\n        }\n\n        foreach ($generated as $driver) {\n            if (! $driver instanceof \\AnourValar\\Office\\Generated) {\n                throw new \\LogicException('Input data must be instanceof Generated');\n            }\n\n            $driver = $driver->driver;\n            if (! $driver instanceof $referenceDriver) {\n                throw new \\LogicException('All drivers should be instances of the same implementation.');\n            }\n\n            $count = $driver->getSheetCount();\n            for ($i = 0; $i < $count; $i++) {\n                $driver->setSheet($i);\n\n                $driver->setSheetTitle($titles[] = $this->getTitle($driver->getSheetTitle(), $titles));\n                $referenceDriver->mergeDriver($driver);\n            }\n        }\n\n        return new Generated($referenceDriver);\n    }\n\n    /**\n     * @param string $title\n     * @param array $titles\n     * @return string\n     */\n    protected function getTitle(string $title, array $titles): string\n    {\n        while (in_array($title, $titles, true)) {\n            $title = preg_replace_callback(\n                '#\\((\\d+)\\)$#',\n                fn ($patterns) => '(' . ++$patterns[1] . ')',\n                $title,\n                -1,\n                $count\n            );\n\n            if (! $count) {\n                $title .= ' (1)';\n            }\n\n        }\n\n        return  $title;\n    }\n}\n"
  },
  {
    "path": "src/Sheets/Parser.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Sheets;\n\nclass Parser\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n\n    /**\n     * Handle with special types of data\n     *\n     * @param mixed $data\n     * @return array\n     */\n    public function canonizeData(mixed $data): array\n    {\n        if (is_object($data) && method_exists($data, 'toArray')) {\n            $data = $data->toArray();\n        }\n\n        return $data;\n    }\n\n    /**\n     * Get schema for a document\n     *\n     * @param array $values\n     * @param array $data\n     * @param array $mergeCells\n     * @return \\AnourValar\\Office\\Sheets\\SchemaMapper\n     */\n    public function schema(array $values, array $data, array $mergeCells): SchemaMapper\n    {\n        $schema = new SchemaMapper();\n\n        // Step 0: Parse input arguments to a canon format\n        $values = $this->parseValues($values, $lastColumn);\n        $data = $this->parseData($data);\n        $mergeCells = $this->parseMergeCells($mergeCells);\n\n        // Step 1: Short path -> full path\n        $this->canonizeMarkers($values, $data);\n\n        // Step 2: Calculate additional rows & columns, redundant data\n        $dataSchema = $this->calculateDataSchema($values, $data, $mergeCells, $schema, $lastColumn);\n\n        // Step 3: Shift formulas\n        $this->shiftFormulas($dataSchema, $schema, $mergeCells);\n\n        // Step 4: Replace markers with data\n        $this->replaceMarkers($dataSchema, $data, $schema);\n\n        return $schema;\n    }\n\n    /**\n     * @param array $values\n     * @param mixed $lastColumn\n     * @return array\n     */\n    protected function parseValues(array $values, &$lastColumn): array\n    {\n        $lastColumn = 'A';\n        foreach ($values as &$columns) {\n            $currLastColumn = array_key_last($columns);\n            if ($this->isColumnLE($lastColumn, $currLastColumn)) {\n                $lastColumn = $currLastColumn;\n            }\n\n            $columns = array_filter($columns, fn ($item) => $item !== null && $item !== '');\n        }\n        unset($columns);\n\n        return $values;\n    }\n\n    /**\n     * @param array $data\n     * @return array\n     */\n    protected function parseData(array &$data): array\n    {\n        $result = [];\n\n        foreach ($this->dot($data) as $key => $value) {\n            if (is_array($value) && ! $value) {\n                continue;\n            }\n\n            $result[$key] = $value;\n        }\n\n        return $result;\n    }\n\n    /**\n     * @param array $mergeCells\n     * @return array\n     */\n    protected function parseMergeCells(array $mergeCells): array\n    {\n        foreach ($mergeCells as &$item) {\n            $item = explode(':', $item);\n\n            $item[0] = preg_split('#([A-Z]+)([\\d]+)#S', $item[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n            $item[1] = preg_split('#([A-Z]+)([\\d]+)#S', $item[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n        }\n        unset($item);\n\n        return $mergeCells;\n    }\n\n    /**\n     * @param array $values\n     * @param array $data\n     * @return void\n     */\n    protected function canonizeMarkers(array &$values, array &$data): void\n    {\n        foreach ($values as &$columns) {\n            foreach ($columns as &$value) {\n                if (! is_string($value)) {\n                    continue;\n                }\n\n                $value = preg_replace_callback(\n                    '#\\[(\\$?\\!\\s*|\\$?\\=\\s*)?([a-z\\d\\.\\_\\*]+)\\]#iS',\n                    function ($patterns) use ($data) {\n                        if (array_key_exists($patterns[2], $data)) {\n                            return $patterns[0];\n                        }\n\n                        $result = null;\n                        foreach (explode('.', $patterns[2]) as $pattern) {\n                            $changed = true;\n                            $prevResult = $result;\n                            while ($changed) {\n                                $changed = false;\n\n                                if ($this->isShortPath($result ? ($result . '.' . $pattern) : $pattern, $data)) {\n                                    if ($result) {\n                                        $result .= '.';\n                                    }\n\n                                    $result .= $pattern;\n                                    $changed = true;\n                                }\n\n                                if ($this->isShortPath($result . '.0', $data)) {\n                                    $result .= '.0';\n                                    $changed = true;\n                                }\n                            }\n\n                            if ($result === $prevResult && ($pattern != '*' || mb_substr($result, -2) != '.0')) {\n                                return $patterns[0];\n                            }\n                        }\n\n                        if ($result && array_key_exists($result, $data) && ! is_array($data[$result])) {\n                            $result = preg_replace('#\\.0(\\.|$)#S', '.*$1', $result);\n                            return sprintf('[%s%s]', $patterns[1], $result);\n                        }\n\n                        return $patterns[0];\n                    },\n                    $value\n                );\n            }\n            unset($value);\n        }\n        unset($columns);\n    }\n\n    /**\n     * @param array $values\n     * @param array $data\n     * @param array $mergeCells\n     * @param \\AnourValar\\Office\\Sheets\\SchemaMapper $schema\n     * @param string $lastColumn\n     * @return array\n     * @psalm-suppress UnusedForeachValue\n     */\n    protected function calculateDataSchema(\n        array &$values,\n        array &$data,\n        array &$mergeCells,\n        SchemaMapper &$schema,\n        string $lastColumn\n    ): array {\n        $dataSchema = [];\n        $shift = 0;\n        $step = 0;\n        $stepLeft = 0;\n        $stepOrigin = 0;\n        $stepRows = 0;\n\n        // fill in missing rows\n        $prevRow = 0;\n        foreach (array_keys($values) as $row) {\n            $diff = ($row - $prevRow);\n            while ($diff > 1) {\n                $values[$row - $diff + 1] = [];\n                $diff--;\n            }\n\n            $prevRow = $row;\n        }\n        ksort($values);\n\n        foreach ($values as $row => $columns) {\n            $maxMergeY = 0;\n\n            $additionRows = 0;\n            $additionColumns = 0;\n            $additionColumn = null;\n\n            foreach ($columns as $column => $value) {\n                foreach (array_keys($data) as $markerName) {\n                    if (! $this->hasMarker($markerName, $value)) {\n                        continue;\n                    }\n\n                    $qty = 0;\n                    $pattern = $markerName;\n                    while ($pattern = $this->increment($pattern, true)) {\n                        if (! array_key_exists($pattern, $data)) {\n                            break;\n                        }\n                        $qty++;\n                    }\n                    $additionRows = max($additionRows, $qty);\n\n                    $qty = 0;\n                    $pattern = $markerName;\n                    while ($pattern = $this->increment($pattern, false)) {\n                        if (! array_key_exists($pattern, $data)) {\n                            break;\n                        }\n                        $qty++;\n                    }\n                    if ($qty) {\n                        $additionColumns = max($additionColumns, $qty);\n                        $additionColumn = $column;\n                    }\n                }\n\n                if (is_string($value)) {\n                    $columns[$column] = preg_replace('#\\.\\*(\\.|\\])#S', '.0$1', $value);\n                }\n            }\n\n\n            if (! $stepRows && $this->shouldBeDeleted($columns, $data)) {\n                $this->deleteRow($schema, $mergeCells, $row + $shift);\n                $shift--;\n                continue;\n            }\n\n            if ($stepRows) {\n                $additionRows = $stepRows;\n            }\n            $currAdditionRows = $additionRows;\n\n            if (! $additionRows) {\n                foreach ($columns as $column => $value) {\n                    if (\n                        ! preg_match('#\\[(\\$?\\!\\s*|\\$?\\=\\s*)?[a-z][a-z\\d\\_\\.]+\\]#iS', (string) $value)\n                        && ! preg_match('#^\\=[A-Z][A-Z\\.\\d]#', (string) $value)\n                    ) {\n                        unset($columns[$column]);\n                    }\n                }\n\n                if (! $columns) {\n                    continue;\n                }\n            }\n\n            $curr = $additionColumn;\n            $additionColumnValue = isset($curr) ? ($columns[$curr] ?? null) : null;\n\n            $mergeMapX = [];\n            foreach ($mergeCells as $item) {\n                if ($additionColumn.($row + $shift) == $item[0][0].$item[0][1] && $item[0][1] == $item[1][1]) {\n                    while ($this->isColumnLE($item[0][0], $item[1][0]) && $item[0][0] != $item[1][0]) {\n                        $item[0][0] = $this->strIncrement($item[0][0]);\n                        $mergeMapX[] = $item[0][0];\n                    }\n                }\n            }\n\n            foreach ($mergeMapX as $mergeItemX) {\n                $curr = $this->strIncrement($curr);\n            }\n\n            while ($additionColumns) {\n                $curr = $this->strIncrement($curr);\n                $additionColumns--;\n\n                $additionColumnValue = $this->increments($additionColumnValue, false);\n                $columns[$curr] = $additionColumnValue;\n                $schema->copyStyle($additionColumn.($row + $shift), $curr.($row + $shift));\n                $schema->copyWidth($additionColumn, $curr);\n\n                if ($mergeMapX) {\n                    $originalCurr = $curr;\n                    foreach ($mergeMapX as $mergeItemX) {\n                        $curr = $this->strIncrement($curr);\n\n                        $schema->copyWidth($mergeItemX, $curr);\n                    }\n\n                    $schema->mergeCells(sprintf('%s%s:%s%s', $originalCurr, ($row + $shift), $curr, ($row + $shift)));\n                    $mergeCells[] = [ [$originalCurr, ($row + $shift)], [$curr, ($row + $shift)] ]; // fill in\n                }\n            }\n\n            $dataSchema[$row + $shift] = $columns;\n            $originalRow = ($row + $shift);\n\n            if ($additionRows) {\n                $firstColumn = 'A';\n                while ($this->isColumnLE($firstColumn, $lastColumn)) {\n                    if (! isset($columns[$firstColumn]) && ! $this->insideMerge($firstColumn, $originalRow, $mergeCells)) {\n                        $columns[$firstColumn] = null;\n                    }\n\n                    $firstColumn = $this->strIncrement($firstColumn);\n                }\n                uksort($columns, fn ($a, $b) => $this->isColumnLE($a, $b) ? -1 : 1);\n\n                foreach ($columns as $currKey => $currValue) {\n                    $hasMarker = preg_match('#\\[([a-z][a-z\\d\\.\\_]+)\\]#iS', (string) $currValue);\n\n                    foreach ($mergeCells as $item) {\n                        if ($currKey.$originalRow == $item[0][0].$item[0][1] && $item[0][1] != $item[1][1]) {\n                            if (! $hasMarker) {\n                                unset($columns[$currKey]);\n                                continue;\n                            }\n\n                            $maxMergeY = max($maxMergeY, ($item[1][1] - $item[0][1]));\n                        }\n                    }\n                }\n\n                $shift += $maxMergeY;\n            }\n\n            while ($additionRows) {\n                $shift += $step;\n                $shift++;\n                $additionRows--;\n\n                foreach ($columns as &$column) {\n                    if (is_string($column)) {\n                        $column = $this->increments($column, true);\n                    }\n                }\n                unset($column);\n\n                $dataSchema[$row + $shift] = array_filter($columns, fn ($item) => $item !== null && $item !== '');\n                if (! $step) {\n                    $this->addRow($schema, $mergeCells, $row + $shift);\n                }\n\n                foreach (array_keys($columns) as $curr) {\n                    $schema->copyStyle($curr.$originalRow, $curr.($row + $shift));\n\n                    foreach ($mergeCells as $item) {\n                        if ($curr.$originalRow == $item[0][0].$item[0][1]) {\n                            $diff = $item[1][1] - $item[0][1];\n                            $schema->mergeCells(\n                                sprintf('%s%s:%s%s', $item[0][0], ($row + $shift), $item[1][0], ($row + $shift + $diff))\n                            );\n                        }\n                    }\n                }\n\n                $iterate = $maxMergeY;\n                while ($iterate) {\n                    $shift++;\n                    $this->addRow($schema, $mergeCells, $row + $shift);\n                    $iterate--;\n                }\n            }\n\n            if ($stepLeft) {\n                $stepLeft--;\n            }\n            if (! $stepLeft) {\n                $step = 0;\n                $stepRows = 0;\n            } else {\n                $stepOrigin--;\n                $shift -= $stepOrigin - $stepLeft;\n            }\n\n            if ($maxMergeY) {\n                $stepRows = $currAdditionRows;\n                $step = $maxMergeY;\n                $stepLeft = $step;\n                $stepOrigin = (($maxMergeY + 1) * ($currAdditionRows)) + $step;\n                $shift -= $stepOrigin;\n            }\n        }\n        unset($values);\n\n        return $dataSchema;\n    }\n\n    /**\n     * @param array $values\n     * @param \\AnourValar\\Office\\Sheets\\SchemaMapper $schema\n     * @param array $mergeCells\n     * @return void\n     */\n    protected function shiftFormulas(array &$values, SchemaMapper &$schema, array &$mergeCells): void\n    {\n        // Prepares\n        $ranges = [];\n\n        $map = [];\n        foreach ($values as $row => $columns) {\n            foreach ($columns as $column => $value) {\n                if (preg_match('#^\\=[A-Z][A-Z\\.\\d]#', (string) $value)) {\n                    $map[$row][$column] = $value;\n                }\n            }\n        }\n\n        // \"Outside\" shifts\n        foreach ($schema->getOriginal()['rows'] as $action) {\n            foreach ($map as $row => $columns) {\n                foreach ($columns as $column => $value) {\n                    $map[$row][$column] = $values[$row][$column] = preg_replace_callback(\n                        '#([A-Z]+)([\\d]+)#S',\n                        function ($patterns) use ($action) {\n                            if ($action['action'] == 'add') {\n\n                                if ($patterns[2] >= $action['row']) {\n                                    return $patterns[1] . ++$patterns[2];\n                                }\n\n                            } else {\n\n                                if ($patterns[2] > $action['row']) {\n                                    return $patterns[1] . --$patterns[2];\n                                }\n\n                            }\n\n                            return $patterns[0];\n                        },\n                        $value\n                    );\n                }\n            }\n        }\n\n        // \"Inside\" shifts\n        $prev = 0;\n        $prevAction = null;\n        foreach ($schema->getOriginal()['rows'] as $action) {\n            if ($prevAction && $prevAction['row'] + 1 == $action['row'] && $action['action'] == 'add' && $prevAction['action'] == 'add') {\n                $prev++;\n            } else {\n                if ($prevAction && $prevAction['action'] == 'add') {\n                    $ranges[] = ['from' => ($prevAction['row'] - $prev - 1), 'to' => ($prevAction['row'])];\n                }\n\n                $prev = 0;\n            }\n\n            foreach ($map as $row => $columns) {\n                foreach ($columns as $column => $value) {\n                    $map[$row][$column] = $values[$row][$column] = preg_replace_callback(\n                        '#([A-Z]+)([\\d]+)#S',\n                        function ($patterns) use ($action, $row, $prev) {\n                            if ($action['action'] == 'add') {\n\n                                if ($action['row'] == $row && ($row - $prev) == ($patterns[2] + 1)) {\n                                    return $patterns[1] . (++$patterns[2] + $prev);\n                                }\n\n                            }\n\n                            return $patterns[0];\n                        },\n                        $value\n                    );\n                }\n            }\n\n            $prevAction = $action;\n        }\n        if ($prev || ($prevAction && $prevAction['action'] == 'add')) {\n            $ranges[] = ['from' => ($prevAction['row'] - $prev - 1), 'to' => ($prevAction['row'])];\n        }\n\n        // Dynamic table ranges\n        foreach ($ranges as $key => $value) {\n            foreach ($mergeCells as $merge) {\n                if (\n                    $value['from'] >= $merge[0][1]\n                    && $value['from'] <= $merge[1][1]\n                    && $merge[0][1] != $merge[1][1]\n                    && preg_match('#\\[([a-z][a-z\\d\\.\\_]+)\\]#iS', ($values[$merge[0][1]][$merge[0][0]] ?? ''))\n                ) {\n                    unset($ranges[$key]);\n                }\n            }\n        }\n\n        foreach ($map as $row => $columns) {\n            foreach ($columns as $column => $value) {\n                $map[$row][$column] = $values[$row][$column] = preg_replace_callback(\n                    '#([A-Z]+)([\\d]+)\\:([A-Z]+)([\\d]+)#S',\n                    function ($patterns) use ($ranges) {\n                        if ($patterns[2] == $patterns[4]) {\n                            foreach ($ranges as $range) {\n                                if ($patterns[2] == $range['from']) {\n                                    return sprintf('%s%d:%s%d', $patterns[1], $patterns[2], $patterns[3], $range['to']);\n                                }\n                            }\n                        }\n\n                        return $patterns[0];\n                    },\n                    $value\n                );\n            }\n        }\n    }\n\n    /**\n     * @param array $dataSchema\n     * @param array $data\n     * @param \\AnourValar\\Office\\Sheets\\SchemaMapper $schema\n     * @return void\n     */\n    protected function replaceMarkers(array &$dataSchema, array &$data, SchemaMapper &$schema): void\n    {\n        $canonizeKeys = ['scalar' => [], 'closure' => []];\n        $canonizeValues = ['scalar' => [], 'closure' => []];\n        foreach ($data as $from => $to) {\n            if (! preg_match('#^[a-z][a-z\\d\\.\\_]+$#iS', $from)) {\n                continue;\n            }\n\n            if (is_scalar($to) || is_null($to)) {\n                $canonizeKeys['scalar'][] = \"[$from]\";\n                $canonizeValues['scalar'][] = $to;\n            } else {\n                $canonizeKeys['closure'][] = \"[$from]\";\n                $canonizeValues['closure'][] = $to;\n            }\n        }\n\n        foreach ($dataSchema as $row => $columns) {\n            ksort($columns);\n\n            foreach ($columns as $column => $value) {\n                if (is_string($value) && $this->shouldBeDeleted([$value], $data, '$')) {\n                    $value = null;\n                }\n\n                if (is_string($value) && mb_strlen($value)) {\n                    $value = preg_replace('#\\[(\\$?\\!\\s*|\\$?\\=\\s*)[a-z][a-z\\d\\_\\.]+\\]#iS', '', $value);\n                    $value = trim($value);\n\n                    if (($key = array_search($value, $canonizeKeys['scalar'])) !== false) { // type (cast) support\n                        $value = $canonizeValues['scalar'][$key];\n                    } elseif (($key = array_search($value, $canonizeKeys['closure'])) !== false) {\n                        $value = $canonizeValues['closure'][$key];\n                    } else {\n                        $value = str_replace($canonizeKeys['scalar'], $canonizeValues['scalar'], $value);\n                    }\n                }\n\n                if (is_string($value) && mb_strlen($value)) {\n                    $value = preg_replace('#\\[[a-z][a-z\\d\\_\\.]+\\]#iS', '', $value);\n                    $value = trim($value);\n                }\n\n                if (is_string($value) && ! mb_strlen($value)) {\n                    $value = null;\n                }\n\n                $schema->addData($row, $column, $value);\n            }\n        }\n    }\n\n    /**\n     * @param string $path\n     * @param array $markers\n     * @return bool\n     */\n    private function isShortPath(string $path, array $markers): bool\n    {\n        if (array_key_exists($path, $markers)) {\n            return true;\n        }\n\n        if (! str_ends_with($path, '.')) {\n            $path .= '.';\n        }\n\n        foreach (array_keys($markers) as $marker) {\n            if (strpos($marker, $path) === 0) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @param string $marker\n     * @param string $value\n     * @return bool\n     */\n    private function hasMarker(string $marker, ?string $value): bool\n    {\n        $value = preg_replace('#\\.\\*(\\.|$|)#S', '.0$1', (string) $value, -1, $count);\n        if (! $count) {\n            return false;\n        }\n\n        if (strpos((string) $value, \"[$marker]\") !== false) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * @param array $columns\n     * @param array $data\n     * @param string $prefix\n     * @return bool\n     */\n    private function shouldBeDeleted(array $columns, array &$data, string $prefix = ''): bool\n    {\n        $prefix = preg_quote($prefix);\n\n        foreach ($columns as $column) {\n            if (is_null($column)) {\n                continue;\n            }\n\n            preg_match_all(\"#\\[{$prefix}\\=\\s*([a-z\\d\\.\\_]+)\\]#i\", $column, $patterns);\n            foreach (($patterns[1] ?? []) as $marker) {\n                if (! empty($data[$marker])) {\n                    continue;\n                }\n\n                foreach ($data as $key => $value) {\n                    if (strpos($key, $marker.'.') === 0 && ! empty($value)) {\n                        continue 2;\n                    }\n                }\n\n                return true;\n            }\n\n            preg_match_all(\"#\\[{$prefix}\\!\\s*([a-z\\d\\.\\_]+)\\]#i\", $column, $patterns);\n            foreach (($patterns[1] ?? []) as $marker) {\n                if (! empty($data[$marker])) {\n                    return true;\n                }\n\n                foreach ($data as $key => $value) {\n                    if (strpos($key, $marker.'.') === 0 && ! empty($value)) {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @param string $markerName\n     * @param bool $first\n     * @param int $shift\n     * @return string|null\n     */\n    private function increment(string $markerName, bool $first, int $shift = 1): ?string\n    {\n        $markerName = explode('.', $markerName);\n        if (! $first) {\n            $markerName = array_reverse($markerName);\n        }\n\n        if (! $first) {\n            $qty = 0;\n            foreach ($markerName as $item) {\n                if (is_numeric($item)) {\n                    $qty++;\n                }\n            }\n\n            if ($qty < 2) {\n                return null;\n            }\n        }\n\n        foreach ($markerName as &$item) {\n            if (is_numeric($item)) {\n                $item += $shift;\n\n                if (! $first) {\n                    $markerName = array_reverse($markerName);\n                }\n\n                return implode('.', $markerName);\n            }\n        }\n        unset($item);\n\n        return null;\n    }\n\n    /**\n     * @param string $value\n     * @param bool $first\n     * @param int $shift\n     * @return string|null\n     */\n    private function increments(string $value, bool $first, int $shift = 1): ?string\n    {\n        return preg_replace_callback(\n            '#\\[(\\$?\\!\\s*|\\$?\\=\\s*)?([a-z][a-z\\d\\.\\_]+)\\]#iS',\n            function ($patterns) use ($first, $shift) {\n                $patterns[2] = $this->increment($patterns[2], $first, $shift);\n                if ($patterns[2]) {\n                    return '[' . $patterns[1] . $patterns[2] . ']';\n                }\n\n                return $patterns[0];\n            },\n            $value\n        );\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Sheets\\SchemaMapper $schema\n     * @param array $mergeCells\n     * @param int $row\n     * @return void\n     */\n    private function addRow(SchemaMapper &$schema, array &$mergeCells, int $row): void\n    {\n        $schema->addRow($row);\n\n        foreach ($mergeCells as &$mergeCell) {\n            if ($mergeCell[0][1] >= $row) {\n                $mergeCell[0][1]++;\n            }\n\n            if ($mergeCell[1][1] >= $row) {\n                $mergeCell[1][1]++;\n            }\n        }\n        unset($mergeCell);\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Sheets\\SchemaMapper $schema\n     * @param array $mergeCells\n     * @param int $row\n     * @return void\n     */\n    private function deleteRow(SchemaMapper &$schema, array &$mergeCells, int $row): void\n    {\n        $schema->deleteRow($row);\n\n        foreach ($mergeCells as &$mergeCell) {\n            if ($mergeCell[0][1] >= $row) {\n                $mergeCell[0][1]--;\n            }\n\n            if ($mergeCell[1][1] >= $row) {\n                $mergeCell[1][1]--;\n            }\n        }\n        unset($mergeCell);\n    }\n\n    /**\n     * @param string $column\n     * @param int $row\n     * @param array $mergeCells\n     * @return bool\n     */\n    private function insideMerge(string $column, int $row, array &$mergeCells): bool\n    {\n        foreach ($mergeCells as $item) {\n            if (\n                $this->isColumnLE($item[0][0], $column)\n                && $this->isColumnGE($item[1][0], $column)\n                && $item[0][1] <= $row\n                && $item[1][1] >= $row\n                && ($item[0][0] != $column || $item[0][1] != $row)\n            ) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Sheets/SchemaMapper.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Sheets;\n\nclass SchemaMapper\n{\n    /**\n     * @var array\n     */\n    protected array $payload = [\n        'data' => [], //[ 1 => ['A' => 'foo'], '2' => ['B' => 'bar'] ]\n\n        'rows' => [], //[ ['action' => 'add', 'row' => 1, 'qty' => 1], ['action' => 'delete', 'row' => 2, 'qty' => 1] ]\n\n        'copy_style' => [], //[ ['from' => 'A1', 'to' => 'A2'] ]\n\n        'merge_cells' => [], //[ 'A1:B1', 'C1:D1']\n\n        'copy_width' => [], //[ ['from' => 'B', 'to' => 'C'] ]\n    ];\n\n    /**\n     * @return array\n     */\n    public function toArray(): array\n    {\n        ksort($this->payload['data']);\n\n        $this->normalizeRows($this->payload['rows']);\n\n        $this->normalizeCells($this->payload['copy_style']);\n\n        sort($this->payload['copy_width']);\n\n        return $this->payload;\n    }\n\n    /**\n     * @return array\n     */\n    public function getOriginal(): array\n    {\n        return $this->payload;\n    }\n\n    /**\n     * @param int $row\n     * @param string $column\n     * @param mixed $value\n     * @return self\n     */\n    public function addData(int $row, string $column, mixed $value): self\n    {\n        $this->payload['data'][$row][$column] = $value;\n\n        return $this;\n    }\n\n    /**\n     * @param int $rowBefore\n     * @return self\n     */\n    public function addRow(int $rowBefore): self\n    {\n        $this->payload['rows'][] = ['action' => 'add', 'row' => $rowBefore, 'qty' => 1];\n\n        return $this;\n    }\n\n    /**\n     * @param int $row\n     * @return self\n     */\n    public function deleteRow(int $row): self\n    {\n        $this->payload['rows'][] = ['action' => 'delete', 'row' => $row, 'qty' => 1];\n\n        return $this;\n    }\n\n    /**\n     * @param string $from\n     * @param string $to\n     * @return self\n     */\n    public function copyStyle(string $from, string $to): self\n    {\n        $this->payload['copy_style'][$from.$to] = ['from' => $from, 'to' => $to];\n\n        return $this;\n    }\n\n    /**\n     * @param string $ceilRange\n     * @return self\n     */\n    public function mergeCells(string $ceilRange): self\n    {\n        $this->payload['merge_cells'][] = $ceilRange;\n\n        return $this;\n    }\n\n    /**\n     * @param string $from\n     * @param string $to\n     * @return self\n     */\n    public function copyWidth(string $from, string $to): self\n    {\n        $this->payload['copy_width'][$from . $to] = ['from' => $from, 'to' => $to];\n\n        return $this;\n    }\n\n    /**\n     * @param array $rows\n     * @return void\n     */\n    protected function normalizeRows(array &$rows): void\n    {\n        $optimizedRows = [];\n\n        $curr = [];\n        foreach ($rows as $item) {\n            if ($curr) {\n                if ($item['action'] == 'add' && $curr['action'] == $item['action'] && $item['row'] == ($curr['row'] + $curr['qty'])) {\n                    $curr['qty']++;\n                } else {\n                    $optimizedRows[] = ['action' => $curr['action'], 'row' => $curr['row'], 'qty' => $curr['qty']];\n                    $curr = null;\n                }\n            }\n\n            if (! $curr) {\n                $curr = ['action' => $item['action'], 'row' => $item['row'], 'qty' => $item['qty']];\n            }\n        }\n\n        if ($curr) {\n            $optimizedRows[] = ['action' => $curr['action'], 'row' => $curr['row'], 'qty' => $curr['qty']];\n        }\n\n        $rows = $optimizedRows;\n    }\n\n    /**\n     * @param array $data\n     * @return void\n     */\n    protected function normalizeCells(array &$data): void\n    {\n        ksort($data, SORT_NATURAL);\n        $data = array_values($data);\n\n        $optimizedData = [];\n\n        $curr = [];\n        foreach ($data as $item) {\n            $expect = preg_split('#([A-Z]+)([\\d]+)#S', $item['to'], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);\n            $expect[1]++; // @phpstan-ignore-line\n            $expect = implode('', $expect);\n\n            if ($curr) {\n                if ($item['from'] == $curr['from'] && $item['to'] == $curr['expect']) {\n                    $curr['append'] = ':' . $curr['expect'];\n                    $curr['expect'] = $expect;\n                } else {\n                    $optimizedData[] = ['from' => $curr['from'], 'to' => $curr['to'] . $curr['append']];\n                    $curr = null;\n                }\n            }\n\n            if (! $curr) {\n                $curr = ['from' => $item['from'], 'to' => $item['to'], 'append' => '', 'expect' => $expect];\n            }\n        }\n\n        if ($curr) {\n            $optimizedData[] = ['from' => $curr['from'], 'to' => $curr['to'] . $curr['append']];\n        }\n\n        $data = $optimizedData;\n    }\n}\n"
  },
  {
    "path": "src/SheetsService.php",
    "content": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\SheetsInterface;\n\nclass SheetsService\n{\n    /**\n     * @var \\AnourValar\\Office\\Drivers\\SheetsInterface\n     */\n    protected \\AnourValar\\Office\\Drivers\\SheetsInterface $driver;\n\n    /**\n     * @var \\AnourValar\\Office\\Sheets\\Parser\n     */\n    protected \\AnourValar\\Office\\Sheets\\Parser $parser;\n\n    /**\n     * Handle template's loading\n     *\n     * @var \\Closure(SheetsInterface $driver, string $templateFile, Format $templateFormat): SheetsInterface\n     */\n    protected ?\\Closure $hookLoad = null;\n\n    /**\n     * Actions with template before data inserted\n     *\n     * @var \\Closure(SheetsInterface $driver, array &$data): void\n     */\n    protected ?\\Closure $hookBefore = null;\n\n    /**\n     * Cell's value handler (on set)\n     *\n     * @var \\Closure(SheetsInterface $driver, string $column, int $row, mixed $value, int $sheetIndex): mixed\n     */\n    protected ?\\Closure $hookValue = null;\n\n    /**\n     * Actions with template after data inserted\n     *\n     * @var \\Closure(SheetsInterface $driver): void\n     */\n    protected ?\\Closure $hookAfter = null;\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\SheetsInterface $driver\n     * @param \\AnourValar\\Office\\Sheets\\Parser $parser\n     * @return void\n     */\n    public function __construct(\n        SheetsInterface $driver = new \\AnourValar\\Office\\Drivers\\PhpSpreadsheetDriver(),\n        \\AnourValar\\Office\\Sheets\\Parser $parser = new \\AnourValar\\Office\\Sheets\\Parser()\n    ) {\n        $this->driver = $driver;\n        $this->parser = $parser;\n    }\n\n    /**\n     * Generate a document from the template (sheets)\n     *\n     * @param string|\\Stringable $templateFile\n     * @param mixed $data\n     * @param bool $autoCellFormat\n     * @return \\AnourValar\\Office\\Generated\n     */\n    public function generate(string|\\Stringable $templateFile, mixed $data, bool $autoCellFormat = false): Generated\n    {\n        // Handle with input data\n        $data = $this->parser->canonizeData($data);\n\n        // Open the template\n        $templateFormat = Format::tryFrom(mb_strtolower(pathinfo($templateFile, PATHINFO_EXTENSION))) ?? Format::Xlsx;\n\n        if ($this->hookLoad) {\n            $driver = ($this->hookLoad)($this->driver, $templateFile, $templateFormat);\n            if ($driver instanceof Generated) {\n                $driver = $driver->driver;\n            }\n        } else {\n            $driver = $this->driver->load($templateFile, $templateFormat);\n        }\n\n        // Hook: before\n        if ($this->hookBefore) {\n            ($this->hookBefore)($driver, $data);\n        }\n\n        // Handle sheets\n        $count = $driver->getSheetCount();\n        for ($sheetIndex = 0; $sheetIndex < $count; $sheetIndex++) {\n            $driver->setSheet($sheetIndex);\n\n            $this->handleSheet($driver, $data, $sheetIndex, $autoCellFormat);\n        }\n\n        // Hook: after\n        if ($this->hookAfter) {\n            ($this->hookAfter)($driver);\n        }\n\n        // Return\n        return new Generated($driver);\n    }\n\n    /**\n     * Set hookLoad\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookLoad(?\\Closure $closure): self\n    {\n        $this->hookLoad = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookBefore\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookBefore(?\\Closure $closure): self\n    {\n        $this->hookBefore = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookValue\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookValue(?\\Closure $closure): self\n    {\n        $this->hookValue = $closure;\n\n        return $this;\n    }\n\n    /**\n     * Set hookAfter\n     *\n     * @param ?\\Closure $closure\n     * @return self\n     */\n    public function hookAfter(?\\Closure $closure): self\n    {\n        $this->hookAfter = $closure;\n\n        return $this;\n    }\n\n    /**\n     * @param \\AnourValar\\Office\\Drivers\\SheetsInterface $driver\n     * @param array $data\n     * @param int $sheetIndex\n     * @throws \\LogicException\n     * @return void\n     */\n    protected function handleSheet(SheetsInterface &$driver, array &$data, int $sheetIndex, bool $autoCellFormat): void\n    {\n        // Get schema of the document\n        $schema = $this->parser->schema($driver->getValues(null), $data, $driver->getMergeCells())->toArray();\n\n        // Rows\n        foreach ($schema['rows'] as $row) {\n            if ($row['action'] == 'add') {\n                $driver->addRow($row['row'], $row['qty']);\n            } elseif ($row['action'] == 'delete') {\n                $driver->deleteRow($row['row'], $row['qty']);\n            } else {\n                throw new \\LogicException('Incorrect usage.');\n            }\n        }\n\n        // Copy style & cell format\n        foreach ($schema['copy_style'] as $item) {\n            $driver->copyStyle($item['from'], $item['to']);\n\n            if (! $autoCellFormat) {\n                $driver->copyCellFormat($item['from'], $item['to']);\n            }\n        }\n\n        // Merge cells\n        foreach ($schema['merge_cells'] as $item) {\n            $driver->mergeCells($item);\n        }\n\n        // Copy width\n        foreach ($schema['copy_width'] as $item) {\n            $driver->copyWidth($item['from'], $item['to']);\n        }\n\n        // Data\n        $driver->setValues($this->handleData($schema['data'], $driver, $sheetIndex), $autoCellFormat);\n    }\n\n    /**\n     * @param array $data\n     * @param \\AnourValar\\Office\\Drivers\\SheetsInterface $driver\n     * @param int $sheetIndex\n     * @return array\n     */\n    protected function handleData(array $data, SheetsInterface $driver, int $sheetIndex): array\n    {\n        foreach ($data as $row => &$columns) {\n            foreach ($columns as $column => &$value) {\n                if ($value instanceof \\Closure) {\n                    // Private Closure\n                    $value = $value($driver, $column, $row);\n\n                    if (is_null($value)) {\n                        unset($data[$row][$column]);\n                    }\n                } elseif ($this->hookValue) {\n                    // Hook: value\n                    $value = ($this->hookValue)($driver, $column, $row, $value, $sheetIndex);\n\n                    if (is_null($value)) {\n                        unset($data[$row][$column]);\n                    }\n                }\n            }\n            unset($value);\n        }\n        unset($columns);\n\n        return $data;\n    }\n}\n"
  },
  {
    "path": "src/Traits/Parser.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Traits;\n\ntrait Parser\n{\n    /**\n     * @param array $data\n     * @param string $prefix\n     * @return array\n     */\n    protected function dot(array $data, string $prefix = ''): array\n    {\n        $result = [];\n\n        foreach ($data as $key => $value) {\n            if (is_array($value)) {\n                $result = array_replace($result, $this->dot($value, $prefix.$key.'.'));\n            } else {\n                $result[$prefix.$key] = $value;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * @param string $compareColumn\n     * @param string $referenceColumn\n     * @return bool\n     */\n    protected function isColumnLE(string $compareColumn, string $referenceColumn): bool\n    {\n        $compareLength = strlen($compareColumn);\n        $referenceLength = strlen($referenceColumn);\n\n        if ($compareLength < $referenceLength) {\n            return true;\n        }\n\n        if ($compareLength > $referenceLength) {\n            return false;\n        }\n\n        return $compareColumn <= $referenceColumn;\n    }\n\n    /**\n     * @param string $compareColumn\n     * @param string $referenceColumn\n     * @return bool\n     */\n    protected function isColumnGE(string $compareColumn, string $referenceColumn): bool\n    {\n        $compareLength = strlen($compareColumn);\n        $referenceLength = strlen($referenceColumn);\n\n        if ($compareLength > $referenceLength) {\n            return true;\n        }\n\n        if ($compareLength < $referenceLength) {\n            return false;\n        }\n\n        return $compareColumn >= $referenceColumn;\n    }\n\n    /**\n     * Polyfill\n     *\n     * @param string $value\n     * @return string\n     */\n    protected function strIncrement(string $value): string\n    {\n        if (PHP_VERSION_ID >= 80300) {\n            return str_increment($value);\n        }\n\n        $value++; // @phpstan-ignore-line\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Traits/XFormat.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Traits;\n\ntrait XFormat\n{\n    /**\n     * @param \\DateTimeInterface $date\n     * @return float\n     */\n    protected function excelDate(\\DateTimeInterface $date): float\n    {\n        $year = (int) $date->format('Y');\n        $month = (int) $date->format('m');\n        $day = (int) $date->format('d');\n        $hours = (int) $date->format('H');\n        $minutes = (int) $date->format('i');\n        $seconds = (int) $date->format('s');\n\n        $leapYear = true;\n        if ($year == 1900 && $month <= 2) {\n            $leapYear = false;\n        }\n        $baseDate = 2415020;\n\n        if ($month > 2) {\n            $month -= 3;\n        } else {\n            $month += 9;\n            --$year;\n        }\n\n        $century = (int) substr($year, 0, 2);\n        $decade = (int) substr($year, 2, 2);\n        $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5);\n        $excelDate += $day + 1721119 - $baseDate + $leapYear;\n        $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;\n\n        return (float) $excelDate + $excelTime;\n    }\n\n    /**\n     * @param string|null $value\n     * @return string\n     */\n    protected function escape(?string $value): string\n    {\n        return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', true);\n    }\n}\n"
  },
  {
    "path": "tests/GridServiceTest.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass GridServiceTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @return void\n     */\n    public function test_generate_statistic_with_headers()\n    {\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:A1', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('A1:A1', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:A1', $headersRange);\n                $this->assertSame('A2:A2', $dataRange);\n                $this->assertSame('A1:A2', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['111'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:A1', $headersRange);\n                $this->assertSame('A2:A3', $dataRange);\n                $this->assertSame('A1:A3', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['foo-1'], ['foo-2'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:A1', $headersRange);\n                $this->assertSame('A2:A4', $dataRange);\n                $this->assertSame('A1:A4', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['foo-1'], ['foo-2'], ['foo-3'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:B1', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('A1:B1', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:B1', $headersRange);\n                $this->assertSame('A2:B2', $dataRange);\n                $this->assertSame('A1:B2', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:B1', $headersRange);\n                $this->assertSame('A2:B3', $dataRange);\n                $this->assertSame('A1:B3', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:B1', $headersRange);\n                $this->assertSame('A2:B4', $dataRange);\n                $this->assertSame('A1:B4', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:C1', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('A1:C1', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [  ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:C1', $headersRange);\n                $this->assertSame('A2:C2', $dataRange);\n                $this->assertSame('A1:C2', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1']  ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:C1', $headersRange);\n                $this->assertSame('A2:C3', $dataRange);\n                $this->assertSame('A1:C3', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2']  ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('A1:C1', $headersRange);\n                $this->assertSame('A2:C4', $dataRange);\n                $this->assertSame('A1:C4', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3']  ],\n            );\n    }\n\n    /**\n     * @return void\n     */\n    public function test_generate_statistic_with_headers_with_shift()\n    {\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:C5', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('C5:C5', $totalRange);\n                $this->assertSame(['one' => 'C'], $columns);\n            })\n            ->generate(\n                ['one' => 'foo'],\n                [ ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:C5', $headersRange);\n                $this->assertSame('C6:C6', $dataRange);\n                $this->assertSame('C5:C6', $totalRange);\n                $this->assertSame(['C'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['111'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:C5', $headersRange);\n                $this->assertSame('C6:C7', $dataRange);\n                $this->assertSame('C5:C7', $totalRange);\n                $this->assertSame(['C'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['foo-1'], ['foo-2'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:C5', $headersRange);\n                $this->assertSame('C6:C8', $dataRange);\n                $this->assertSame('C5:C8', $totalRange);\n                $this->assertSame(['C'], $columns);\n            })\n            ->generate(\n                ['foo'],\n                [ ['foo-1'], ['foo-2'], ['foo-3'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:D5', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('C5:D5', $totalRange);\n                $this->assertSame(['C', 'D'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:D5', $headersRange);\n                $this->assertSame('C6:D6', $dataRange);\n                $this->assertSame('C5:D6', $totalRange);\n                $this->assertSame(['C', 'D'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:D5', $headersRange);\n                $this->assertSame('C6:D7', $dataRange);\n                $this->assertSame('C5:D7', $totalRange);\n                $this->assertSame(['C', 'D'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:D5', $headersRange);\n                $this->assertSame('C6:D8', $dataRange);\n                $this->assertSame('C5:D8', $totalRange);\n                $this->assertSame(['C', 'D'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar'],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:E5', $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame('C5:E5', $totalRange);\n                $this->assertSame(['C', 'D', 'E'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [  ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:E5', $headersRange);\n                $this->assertSame('C6:E6', $dataRange);\n                $this->assertSame('C5:E6', $totalRange);\n                $this->assertSame(['C', 'D', 'E'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1']  ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:E5', $headersRange);\n                $this->assertSame('C6:E7', $dataRange);\n                $this->assertSame('C5:E7', $totalRange);\n                $this->assertSame(['C', 'D', 'E'], $columns);\n            })\n            ->generate(\n                ['foo', 'bar', 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2']  ],\n                'C5'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame('C5:E5', $headersRange);\n                $this->assertSame('C6:E8', $dataRange);\n                $this->assertSame('C5:E8', $totalRange);\n                $this->assertSame(['one' => 'C', 'two' => 'D', 'three' => 'E'], $columns);\n            })\n            ->generate(\n                ['one' => 'foo', 'two' => 'bar', 'three' => 'baz'],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3']  ],\n                'C5'\n            );\n    }\n\n    /**\n     * @return void\n     */\n    public function test_generate_statistic_without_headers()\n    {\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame(null, $totalRange);\n                $this->assertSame([], $columns);\n            })\n            ->generate(\n                [],\n                [ ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:A1', $dataRange);\n                $this->assertSame('A1:A1', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['111'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:A2', $dataRange);\n                $this->assertSame('A1:A2', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1'], ['foo-2'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:A3', $dataRange);\n                $this->assertSame('A1:A3', $totalRange);\n                $this->assertSame(['A'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1'], ['foo-2'], ['foo-3'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:B1', $dataRange);\n                $this->assertSame('A1:B1', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:B2', $dataRange);\n                $this->assertSame('A1:B2', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:B3', $dataRange);\n                $this->assertSame('A1:B3', $totalRange);\n                $this->assertSame(['A', 'B'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:C1', $dataRange);\n                $this->assertSame('A1:C1', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1']  ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:C2', $dataRange);\n                $this->assertSame('A1:C2', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2']  ],\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('A1:C3', $dataRange);\n                $this->assertSame('A1:C3', $totalRange);\n                $this->assertSame(['A', 'B', 'C'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3']  ],\n            );\n    }\n\n    /**\n     * @return void\n     */\n    public function test_generate_statistic_without_headers_with_shift()\n    {\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame(null, $dataRange);\n                $this->assertSame(null, $totalRange);\n                $this->assertSame([], $columns);\n            })\n            ->generate(\n                [],\n                [ ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:D4', $dataRange);\n                $this->assertSame('D4:D4', $totalRange);\n                $this->assertSame(['D'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['111'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:D5', $dataRange);\n                $this->assertSame('D4:D5', $totalRange);\n                $this->assertSame(['D'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1'], ['foo-2'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:D6', $dataRange);\n                $this->assertSame('D4:D6', $totalRange);\n                $this->assertSame(['D'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1'], ['foo-2'], ['foo-3'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:E4', $dataRange);\n                $this->assertSame('D4:E4', $totalRange);\n                $this->assertSame(['D', 'E'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:E5', $dataRange);\n                $this->assertSame('D4:E5', $totalRange);\n                $this->assertSame(['D', 'E'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:E6', $dataRange);\n                $this->assertSame('D4:E6', $totalRange);\n                $this->assertSame(['D', 'E'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:F4', $dataRange);\n                $this->assertSame('D4:F4', $totalRange);\n                $this->assertSame(['D', 'E', 'F'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1']  ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:F5', $dataRange);\n                $this->assertSame('D4:F5', $totalRange);\n                $this->assertSame(['D', 'E', 'F'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2']  ],\n                'D4'\n            );\n\n        (new \\AnourValar\\Office\\GridService($this->getDriver()))\n            ->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {\n                $this->assertSame(null, $headersRange);\n                $this->assertSame('D4:F6', $dataRange);\n                $this->assertSame('D4:F6', $totalRange);\n                $this->assertSame(['D', 'E', 'F'], $columns);\n            })\n            ->generate(\n                [],\n                [ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3']  ],\n                'D4'\n            );\n    }\n\n    /**\n     * @return \\AnourValar\\Office\\Drivers\\GridInterface\n     * @psalm-suppress UnusedForeachValue\n     */\n    protected function getDriver(): \\AnourValar\\Office\\Drivers\\GridInterface\n    {\n        return new class () implements \\AnourValar\\Office\\Drivers\\GridInterface {\n            public function create(): self\n            {\n                return $this;\n            }\n\n\n            public function setGrid(iterable $data): self\n            {\n                foreach ($data as $item) {\n\n                }\n\n                return $this;\n            }\n\n            public function save(string $file, \\AnourValar\\Office\\Format $format): void\n            {\n\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "tests/SheetsParserTest.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass SheetsParserTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var \\AnourValar\\Office\\Sheets\\Parser\n     */\n    protected \\AnourValar\\Office\\Sheets\\Parser $service;\n\n    /**\n     * @see \\PHPUnit\\Framework\\TestCase\n     *\n     * @return void\n     */\n    protected function setUp(): void\n    {\n        $this->service = new \\AnourValar\\Office\\Sheets\\Parser();\n    }\n\n    /**\n     * @return void\n     */\n    public function test_collision_names()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[title]',\n                    ],\n                    2 => [\n                        'A' => '[request.title]',\n                    ],\n                    3 => [\n                        'A' => '[response.body]',\n                    ],\n                    4 => [\n                        'A' => '[body]',\n                    ],\n                    5 => [\n                        'A' => '[products.title.title]',\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'request' => [],\n                    'response' => ['body' => 'bar'],\n                    'body' => [],\n                    'products' => [\n                        'title' => [111],\n                    ],\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[title]',\n                    ],\n                    2 => [\n                        'A' => '[request.title]',\n                    ],\n                    3 => [\n                        'A' => '[response.body]',\n                    ],\n                    4 => [\n                        'A' => '[body]',\n                    ],\n                    5 => [\n                        'A' => '[products.title.title]',\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'request' => null,\n                    'response' => ['body' => 'bar'],\n                    'body' => null,\n                    'products' => [\n                        'title' => [111],\n                    ],\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[title]',\n                    ],\n                    2 => [\n                        'A' => '[request.title]',\n                    ],\n                    3 => [\n                        'A' => '[response.body]',\n                    ],\n                    4 => [\n                        'A' => '[body]',\n                    ],\n                    5 => [\n                        'A' => '[products.title.title]',\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'response' => ['body' => 'bar'],\n                    'products' => [\n                        'title' => [111],\n                    ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'foo'],\n                        2 => ['A' => null],\n                        3 => ['A' => 'bar'],\n                        4 => ['A' => null],\n                        5 => ['A' => null],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_empty()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo] []',\n                        'B' => '[]',\n                        'C' => '[bar] []',\n                        'D' => '[foo] [baz.]',\n                        'E' => '[foo] [б]',\n                        'F' => '[foo] [$]',\n                        'G' => '[foo] [%]',\n                        'H' => '[foo] [5]',\n                        'K' => '[foo] [a]',\n                    ],\n                ],\n\n                'data' => [\n                    '' => 'NO',\n                    'bar' => 'BAR',\n                    'baz' => ['' => 'BAZ'],\n                    '5' => 'NO',\n                    'б' => 'NO',\n                    '$' => 'NO',\n                    '%' => 'NO',\n                    'a' => 'NO',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '[]',\n                            'C' => 'BAR []',\n                            'D' => 'BAZ',\n                            'E' => '[б]',\n                            'F' => '[$]',\n                            'G' => '[%]',\n                            'H' => '[5]',\n                            'K' => '[a]',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_scalar()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => 'test [foo] test',\n                        'E' => '[test10]',\n                        'F' => '[test]',\n                        'G' => '[test1]',\n                        'H' => 'hello [$=foo]',\n                        'I' => '[$= foo] world',\n                        'J' => '[$! foo] test',\n                        'K' => '[$! foo] test',\n                        'L' => '[$= foo1] test',\n                        'Q' => '=A1+B2+C3+D4+E5',\n                        'X' => '= A1',\n                        'W' => '=1',\n                        'Y' => '=AB100 + [test]',\n                    ],\n                    2 => [\n                        'A' => '[bar]',\n                        'B' => 'test [bar] test',\n                        'C' => 'bar',\n                        'D' => '[a.b] -> [a.c] -> [a.d] -> [a.e.f]',\n                        'H' => '[hello]',\n                        'J' => '[world]',\n                        'K' => '[k] [9] [9k] [k9]',\n                        'Y' => null,\n                    ],\n                    '3' => [\n                        'D' => '[bar] [!bar]',\n                        'Y' => null,\n                    ],\n                    '4' => [\n                        'D' => 'hello world',\n                        'Y' => null,\n                    ],\n                    '5' => [\n                        'A' => 1,\n                        'B' => 2,\n                        'Y' => null,\n                    ],\n                    '6' => [\n                        'Q' => '=A1+B2+C3+D4+E5',\n                        'Y' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'hello',\n                    'bar' => 'world',\n                    'a' => ['b' => '11', 'c' => '22', 'e' => ['f' => 'oops']],\n                    'test10' => 12.5,\n                    'test' => '3',\n                    'test1' => 5,\n                    'hello' => null,\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'hello',\n                            'C' => 'test hello test',\n                            'E' => 12.5,\n                            'F' => '3',\n                            'G' => 5,\n                            'H' => 'hello',\n                            'I' => 'world',\n                            'J' => null,\n                            'K' => null,\n                            'L' => null,\n                            'Q' => '=A1+B2+C3+D3+E4',\n                            'Y' => '=AB99 + 3',\n                        ],\n                        2 => [\n                            'A' => 'world',\n                            'B' => 'test world test',\n                            'D' => '11 -> 22 ->  -> oops',\n                            'H' => null,\n                            'J' => null,\n                            'K' => '[k] [9] [9k]',\n                        ],\n                        5 => [\n                            'Q' => '=A1+B2+C3+D3+E4',\n                        ],\n                    ],\n\n                    'rows' => [['action' => 'delete', 'row' => 3, 'qty' => 1]],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_not_scalar()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                        'B' => '[baz]',\n                    ],\n                    2 => [\n                        'A' => 'hello [bar] world',\n                        'B' => null,\n                    ],\n                    3 => [\n                        'A' => '[test] [=foo]',\n                        'B' => null,\n                    ],\n                    4 => [\n                        'A' => '[test2] [=bar]',\n                        'B' => null,\n                    ],\n                    5 => [\n                        'A' => '[test2] [!foo]',\n                        'B' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => function () {\n                    },\n                    'baz' => new \\DateTime('2022-11-16'),\n                    'test' => function () {\n                    },\n                    'test2' => function () {\n                        throw new \\LogicException('oops');\n                    },\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => $item['data']['foo'],\n                            'B' => $item['data']['baz'],\n                        ],\n                        2 => [\n                            'A' => 'hello  world',\n                        ],\n                        3 => [\n                            'A' => $item['data']['test'],\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 4, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 4, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n\n        $this->assertSame(\n            [\n                'data' => [\n                    1 => [\n                        'A' => 'hello',\n                    ],\n                ],\n\n                'rows' => [],\n\n                'copy_style' => [],\n\n                'merge_cells' => [],\n\n                'copy_width' => [],\n            ],\n            $this->service->schema([1 => ['A' => 'hello [world]']], ['world' => function () {\n            }], [])->toArray()\n        );\n    }\n\n    /**\n      * @return void\n      */\n    public function test_schema_conditions1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'AA [$= foo] [$= bar]',\n                        'B' => 'BB [$= baz] [$= foo] [$= bar]',\n                        'C' => 'CC [$= foo] [$= baz] [$= bar]',\n                        'D' => 'DD [$= foo] [$= bar] [$= baz]',\n                    ],\n\n                    2 => [\n                        'A' => 'AA [$! baz] [$! foobar]',\n                        'B' => 'BB [$! foo] [$! baz] [$! foobar]',\n                        'C' => 'CC [$! baz] [$! foo] [$! foobar]',\n                        'D' => 'DD [$! baz] [$! foobar] [$! foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'foo',\n                    'bar' => 'bar',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'AA',\n                            'B' => null,\n                            'C' => null,\n                            'D' => null,\n                        ],\n                        2 => [\n                            'A' => 'AA',\n                            'B' => null,\n                            'C' => null,\n                            'D' => null,\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n      * @return void\n      */\n    public function test_schema_conditions2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '11 [= foo] [= bar]',\n                    ],\n                    2 => [\n                        'A' => '22 [= baz] [= foo] [= bar]',\n                    ],\n                    3 => [\n                        'A' => '33 [= foo] [= baz] [= bar]',\n                    ],\n                    4 => [\n                        'A' => '44 [= foo] [= bar] [= baz]',\n                    ],\n\n                    5 => [\n                        'A' => '55 [! baz] [! foobar]',\n                    ],\n                    6 => [\n                        'A' => '66 [! foo] [! baz] [! foobar]',\n                    ],\n                    7 => [\n                        'A' => '77 [! baz] [! foo] [! foobar]',\n                    ],\n                    8 => [\n                        'A' => '88 [! baz] [! foobar] [! foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'foo',\n                    'bar' => 'bar',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '11',\n                        ],\n                        2 => [\n                            'A' => '55',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n\n                        ['action' => 'delete', 'row' => 3, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 3, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_zero_empty()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo] [=test]',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo] [= list]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar [= bar]',\n                        'B' => '[bar] 111',\n                        'C' => null,\n                    ],\n                    4 => [\n                        'A' => 'foo [= list.0]',\n                        'B' => '[foo]',\n                        'C' => null,\n                    ],\n                    5 => [\n                        'A' => 'bar',\n                        'B' => '[bar] 222 [=bar]',\n                        'C' => '=A3+B5+C7',\n                    ],\n                    6 => [\n                        'A' => 'foo [= list.0.c]',\n                        'B' => '[foo] [!list]',\n                        'C' => null,\n                    ],\n                    7 => [\n                        'A' => 'bar',\n                        'B' => '[bar] 333 [! list]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'bar',\n                            'B' => 'test2 111',\n                        ],\n                        2 => [\n                            'B' => 'test2 222',\n                            'C' => '=A1+B2+C3',\n                        ],\n                        3 => [\n                            'B' => 'test2 333',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 1, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 1, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"#$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_zero()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo [= list.0.c]',\n                        'B' => '[foo]',\n                        'K' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.0.c]',\n                        'D' => '[list.0.d]',\n                        'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'I' => '=A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo] [=list_c.0]',\n                        'K' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo] [!list_c]',\n\n                        'C' => '[list_c.0]',\n                        'D' => '[list_d.0]',\n                        'E' => '[foo] hello [list_c.0] -> [list_d.0] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar] [! list_c]',\n                        'I' => '=A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [],\n                    'list_d' => [],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n\n                        1 => [\n                            'B' => 'test1',\n                            'C' => null,\n                            'D' => null,\n                            'E' => 'test1 hello  ->  world test2',\n                            'G' => 'test2',\n                            'I' => '=A1*B1*C2',\n                            'K' => '=A1:A1',\n                        ],\n                        '2' => [\n                            'B' => 'test2',\n                            'I' => '=A1*B1*C2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 1, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"#$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_one()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.0.c]',\n                        'D' => '[list.0.d]',\n                        'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    4 => [\n                        'A' => '[bar] [! list]',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => '11', 'd' => '12'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.c]',\n                        'D' => '[list.d]',\n                        'E' => '[foo] hello [list.c] -> [list.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    4 => [\n                        'A' => '[bar] [!list]',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => '11', 'd' => '12'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c.0]',\n                        'D' => '[list_d.0]',\n                        'E' => '[foo] hello [list_c.0] -> [list_d.0] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    4 => [\n                        'A' => '[bar] [! list_c]',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => ['11'],\n                    'list_d' => ['12'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c]',\n                        'D' => '[list_d]',\n                        'E' => '[foo] hello [list_c] -> [list_d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    4 => [\n                        'A' => '[bar] [!list_c]',\n                        'K' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => ['11'],\n                    'list_d' => ['12'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'H' => '=A1*B2*C3',\n                            'K' => '=A2:A2',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => '11',\n                            'D' => '12',\n                            'E' => 'test1 hello 11 -> 12 world test2',\n                            'G' => 'test2',\n                            'H' => '=A1*B2*C3',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                            'H' => '=A1*B2*C3',\n                            'K' => '=A2:A2',\n                        ],\n                    ],\n\n                    'rows' => [['action' => 'delete', 'row' => 4, 'qty' => 1]],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_two()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.c]',\n                        'D' => '[list.d]',\n                        'E' => '[foo] hello [list.c] -> [list.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => '11', 'd' => '12'], ['c' => '21', 'd' => '22'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.c]',\n                        'D' => '[list.d]',\n                        'E' => '[foo] hello [list.*.c] -> [list.*.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => '11', 'd' => '12'], ['c' => '21', 'd' => '22'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c]',\n                        'D' => '[list_d]',\n                        'E' => '[foo] hello [list_c] -> [list_d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => ['11', '21'],\n                    'list_d' => ['12', '22'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c.*]',\n                        'D' => '[list_d.*]',\n                        'E' => '[foo] hello [list_c.*] -> [list_d.*] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'H' => '=A1*B2*C3',\n                        'J' => 'A1*B2*C3',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => ['11', '21'],\n                    'list_d' => ['12', '22'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'H' => '=A1*B2*C4',\n                            'K' => '=A2:A3',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '11',\n                            'D' => '12',\n                            'E' => 'test1 hello 11 -> 12 world test2',\n                            'F' => 'bar',\n                            'G' => 'test2',\n                            'H' => '=A1*B2*C4',\n                            'J' => 'A1*B2*C3',\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '21',\n                            'D' => '22',\n                            'E' => 'test1 hello 21 -> 22 world test2',\n                            'F' => 'bar',\n                            'G' => 'test2',\n                            'H' => '=A1*B3*C4',\n                            'J' => 'A1*B2*C3',\n                        ],\n                        4 => [\n                            'B' => 'test2',\n                            'H' => '=A1*B2*C4',\n                            'K' => '=A2:A3',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3'],\n                        ['from' => 'B2', 'to' => 'B3'],\n                        ['from' => 'C2', 'to' => 'C3'],\n                        ['from' => 'D2', 'to' => 'D3'],\n                        ['from' => 'E2', 'to' => 'E3'],\n                        ['from' => 'F2', 'to' => 'F3'],\n                        ['from' => 'G2', 'to' => 'G3'],\n                        ['from' => 'H2', 'to' => 'H3'],\n                        ['from' => 'I2', 'to' => 'I3'],\n                        ['from' => 'J2', 'to' => 'J3'],\n                        ['from' => 'K2', 'to' => 'K3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_three()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.c]',\n                        'D' => '[list.d]',\n                        'E' => '[foo] hello [list.c] -> [list.d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.*.c]',\n                        'D' => '[list.*.d]',\n                        'E' => '[foo] hello [list.*.c] -> [list.*.d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c]',\n                        'D' => '[list_d]',\n                        'E' => '[foo] hello [list_c] -> [list_d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [11, '21', '31'],\n                    'list_d' => [12.5, '22', '32'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c.*]',\n                        'D' => '[list_d.*]',\n                        'E' => '[foo] hello [list_c.*] -> [list_d.*] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [11, '21', '31'],\n                    'list_d' => [12.5, '22', '32'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'I' => '=A2*C2+B1+D5+E$2',\n                            'K' => '=A2:A4',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => 11,\n                            'D' => 12.5,\n                            'E' => 'test1 hello 11 -> 12.5 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A2*C2+B1+D5+E$2',\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '21',\n                            'D' => '22',\n                            'E' => 'test1 hello 21 -> 22 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A3*C3+B1+D5+E$2',\n                        ],\n                        4 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '31',\n                            'D' => '32',\n                            'E' => 'test1 hello 31 -> 32 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A4*C4+B1+D5+E$2',\n                        ],\n                        5 => [\n                            'B' => 'test2',\n                            'I' => '=A2*C2+B1+D5+E$2',\n                            'K' => '=A2:A4',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                        ['from' => 'E2', 'to' => 'E3:E4'],\n                        ['from' => 'G2', 'to' => 'G3:G4'],\n                        ['from' => 'H2', 'to' => 'H3:H4'],\n                        ['from' => 'I2', 'to' => 'I3:I4'],\n                        ['from' => 'J2', 'to' => 'J3:J4'],\n                        ['from' => 'K2', 'to' => 'K3:K4'],\n                    ],\n\n                    'merge_cells' => ['E3:F3', 'E4:F4'],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_three_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'G' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.0.c]',\n                        'D' => '[list.0.d]',\n                        'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'G' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'G' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c.0]',\n                        'D' => '[list_d.0]',\n                        'E' => '[foo] hello [list_c.0] -> [list_d.0] world [bar]',\n\n                        'F' => 'bar',\n                        'G' => '[bar]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'G' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [11, '21', '31'],\n                    'list_d' => [12.5, '22', '32'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => 11,\n                            'D' => 12.5,\n                            'E' => 'test1 hello 11 -> 12.5 world test2',\n                            'G' => 'test2',\n                        ],\n\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_four()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.c]',\n                        'D' => '[list.d]',\n                        'E' => '[foo] hello [list.c] -> [list.d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'], ['c' => '41', 'd' => '42'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list.*.c]',\n                        'D' => '[list.*.d]',\n                        'E' => '[foo] hello [list.*.c] -> [list.*.d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'], ['c' => '41', 'd' => '42'] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c]',\n                        'D' => '[list_d]',\n                        'E' => '[foo] hello [list_c] -> [list_d] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [11, '21', '31', '41'],\n                    'list_d' => [12.5, '22', '32', '42'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[list_c.*]',\n                        'D' => '[list_d.*]',\n                        'E' => '[foo] hello [list_c.*] -> [list_d.*] world [bar]',\n\n                        'G' => 'bar',\n                        'H' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'I' => '=A2*C2+B1+D3+E$2',\n                        'K' => '=A2:A2',\n                    ],\n                ],\n\n                'data' => [\n                    'list_c' => [11, '21', '31', '41'],\n                    'list_d' => [12.5, '22', '32', '42'],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['E2:F2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'I' => '=A2*C2+B1+D6+E$2',\n                            'K' => '=A2:A5',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => 11,\n                            'D' => 12.5,\n                            'E' => 'test1 hello 11 -> 12.5 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A2*C2+B1+D6+E$2',\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '21',\n                            'D' => '22',\n                            'E' => 'test1 hello 21 -> 22 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A3*C3+B1+D6+E$2',\n                        ],\n                        4 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '31',\n                            'D' => '32',\n                            'E' => 'test1 hello 31 -> 32 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A4*C4+B1+D6+E$2',\n                        ],\n                        5 => [\n                            'A' => 'foo',\n                            'B' => 'test1',\n                            'C' => '41',\n                            'D' => '42',\n                            'E' => 'test1 hello 41 -> 42 world test2',\n                            'G' => 'bar',\n                            'H' => 'test2',\n                            'I' => '=A5*C5+B1+D6+E$2',\n                        ],\n                        6 => [\n                            'B' => 'test2',\n                            'I' => '=A2*C2+B1+D6+E$2',\n                            'K' => '=A2:A5',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 3],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A5'],\n                        ['from' => 'B2', 'to' => 'B3:B5'],\n                        ['from' => 'C2', 'to' => 'C3:C5'],\n                        ['from' => 'D2', 'to' => 'D3:D5'],\n                        ['from' => 'E2', 'to' => 'E3:E5'],\n                        ['from' => 'G2', 'to' => 'G3:G5'],\n                        ['from' => 'H2', 'to' => 'H3:H5'],\n                        ['from' => 'I2', 'to' => 'I3:I5'],\n                        ['from' => 'J2', 'to' => 'J3:J5'],\n                        ['from' => 'K2', 'to' => 'K3:K5'],\n                    ],\n\n                    'merge_cells' => ['E3:F3', 'E4:F4', 'E5:F5'],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_zero_empty()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.c] [= test]',\n                    ],\n                    3 => [\n                        'A' => 'bar [= test]',\n                        'B' => '[foo]',\n                        'C' => null,\n                    ],\n                    4 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => []] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo [= test]',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[foo] [= test]',\n                        'C' => null,\n                    ],\n                    4 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [[]] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A2)',\n                        ],\n                        2 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"#$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_zero()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => []] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => []] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [[]] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [[]] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A2)',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => null,\n                        ],\n                        '3' => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"#$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_one()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.c.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c.*]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.0.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.*.*]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A2)',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => '11',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_two()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c.*]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A2)',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => '11',\n                            'D' => '12',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'C2', 'to' => 'D2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [['from' => 'C', 'to' => 'D']],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_two_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.c.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.0.0.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11', '12']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => '11',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_three()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12', '13']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12', '13']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix.*.c.*]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ ['c' => ['11', '12', '13']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n\n                        'C' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [ [['11', '12', '13']] ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A2)',\n                        ],\n                        2 => [\n                            'B' => 'test1',\n                            'C' => '11',\n                            'D' => '12',\n                            'E' => '13',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'C2', 'to' => 'D2'],\n                        ['from' => 'C2', 'to' => 'E2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'C', 'to' => 'D'],\n                        ['from' => 'C', 'to' => 'E'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_equal()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.b]',\n                        'C' => '[matrix.c]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2', 'e2']],\n                        ['b' => 'b3', 'c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.*.b]',\n                        'C' => '[matrix.*.c]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2', 'e2']],\n                        ['b' => 'b3', 'c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'C' => '=SUM(A2:A2)',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.*.b]',\n                        'C' => '[matrix.*.c]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2', 'e2']],\n                        ['b' => 'b3', 'c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'C' => '=SUM(A2:A4)',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'b1',\n                            'C' => 'c1',\n                            'D' => 'd1',\n                            'E' => 'e1',\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'b2',\n                            'C' => 'c2',\n                            'D' => 'd2',\n                            'E' => 'e2',\n                        ],\n                        4 => [\n                            'A' => 'foo',\n                            'B' => 'b3',\n                            'C' => 'c3',\n                            'D' => 'd3',\n                            'E' => 'e3',\n                        ],\n                        5 => [\n                            'B' => 'test2',\n                            'C' => 'foo: test1 bar: test2',\n                            'D' => 'bar: test2 foo: test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'C2', 'to' => 'D2'],\n                        ['from' => 'C2', 'to' => 'E2'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                        ['from' => 'E2', 'to' => 'E3:E4'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'C', 'to' => 'D'],\n                        ['from' => 'C', 'to' => 'E'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_equal_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.0.b]',\n                        'C' => '[matrix.0.c.0]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2', 'e2']],\n                        ['b' => 'b3', 'c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'B' => 'b1',\n                            'C' => 'c1',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                            'C' => 'foo: test1 bar: test2',\n                            'D' => 'bar: test2 foo: test1',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_irr()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.0.b] [= matrix.0.b]',\n                        'C' => '[matrix.0.c.0] [=matrix.c]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2']],\n                        ['c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'D' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[matrix.b]',\n                        'C' => '[matrix.c]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'C' => 'foo: [foo] bar: [bar]',\n                        'D' => 'bar: [bar] foo: [foo]',\n                    ],\n                ],\n\n                'data' => [\n                    'matrix' => [\n                        ['b' => 'b1', 'c' => ['c1', 'd1', 'e1']],\n                        ['b' => 'b2', 'c' => ['c2', 'd2']],\n                        ['c' => ['c3', 'd3', 'e3']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'b1',\n                            'C' => 'c1',\n                            'D' => 'd1',\n                            'E' => 'e1',\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'b2',\n                            'C' => 'c2',\n                            'D' => 'd2',\n                            'E' => null,\n                        ],\n                        4 => [\n                            'A' => 'foo',\n                            'B' => null,\n                            'C' => 'c3',\n                            'D' => 'd3',\n                            'E' => 'e3',\n                        ],\n                        5 => [\n                            'B' => 'test2',\n                            'C' => 'foo: test1 bar: test2',\n                            'D' => 'bar: test2 foo: test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'C2', 'to' => 'D2'],\n                        ['from' => 'C2', 'to' => 'E2'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                        ['from' => 'E2', 'to' => 'E3:E4'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'C', 'to' => 'D'],\n                        ['from' => 'C', 'to' => 'E'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.b]',\n                        'C' => '[rows.c]',\n                        'E' => '[columns.e]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        ['e' => ['E10', 'E11', '']],\n                        ['e' => ['E20', 'E21', 'E22']],\n                        ['e' => ['E30', 'E31', 'E32']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.*.b]',\n                        'C' => '[rows.*.c]',\n                        'E' => '[columns.*.e.*]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        ['e' => ['E10', 'E11', '']],\n                        ['e' => ['E20', 'E21', 'E22']],\n                        ['e' => ['E30', 'E31', 'E32']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'b1',\n                            'C' => 'c1',\n                            'E' => 'E10',\n                            'F' => 'E11',\n                            'G' => null,\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'b2',\n                            'C' => 'c2',\n                            'E' => 'E20',\n                            'F' => 'E21',\n                            'G' => 'E22',\n                        ],\n                        4 => [\n                            'A' => 'foo',\n                            'B' => null,\n                            'C' => null,\n                            'E' => 'E30',\n                            'F' => 'E31',\n                            'G' => 'E32',\n                        ],\n                        5 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                        ['from' => 'E2', 'to' => 'E3:E4'],\n                        ['from' => 'E2', 'to' => 'F2'],\n                        ['from' => 'E2', 'to' => 'G2'],\n                        ['from' => 'F2', 'to' => 'F3:F4'],\n                        ['from' => 'G2', 'to' => 'G3:G4'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'E', 'to' => 'F'],\n                        ['from' => 'E', 'to' => 'G'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination1_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => null,\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.0.b]',\n                        'C' => '[rows.0.c]',\n                        'E' => '[columns.0.e.0]',\n                    ],\n                    3 => [\n                        'A' => 'bar',\n                        'B' => '[bar]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        ['e' => ['E10', 'E11', '']],\n                        ['e' => ['E20', 'E21', 'E22']],\n                        ['e' => ['E30', 'E31', 'E32']],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                        ],\n                        2 => [\n                            'B' => 'b1',\n                            'C' => 'c1',\n                            'E' => 'E10',\n                        ],\n                        3 => [\n                            'B' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => '[columns.one]',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.b]',\n                        'C' => '[rows.c]',\n                        'E' => '[columns.two]',\n                    ],\n                    3 => [\n                        'A' => '[foo]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        [\n                            'one' => ['01', '02', '03'],\n                            'two' => [15000, 20000, 30000],\n                        ],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['C2:D2'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => '[columns.*.one]',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.b]',\n                        'C' => '[rows.c]',\n                        'E' => '[columns.*.two.*]',\n                    ],\n                    3 => [\n                        'A' => '[foo]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        [\n                            'one' => ['01', '02', '03'],\n                            'two' => [15000, 20000, 30000],\n                        ],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n\n                'merge_cells' => ['C2:D2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'E' => '01',\n                            'F' => '02',\n                            'G' => '03',\n                        ],\n                        2 => [\n                            'A' => 'foo',\n                            'B' => 'b1',\n                            'C' => 'c1',\n                            'E' => 15000,\n                            'F' => 20000,\n                            'G' => 30000,\n                        ],\n                        3 => [\n                            'A' => 'foo',\n                            'B' => 'b2',\n                            'C' => 'c2',\n                            'E' => null,\n                            'F' => null,\n                            'G' => null,\n                        ],\n                        4 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3'],\n                        ['from' => 'B2', 'to' => 'B3'],\n                        ['from' => 'C2', 'to' => 'C3'],\n                        ['from' => 'E1', 'to' => 'F1'],\n                        ['from' => 'E1', 'to' => 'G1'],\n                        ['from' => 'E2', 'to' => 'E3'],\n                        ['from' => 'E2', 'to' => 'F2'],\n                        ['from' => 'E2', 'to' => 'G2'],\n                        ['from' => 'F2', 'to' => 'F3'],\n                        ['from' => 'G2', 'to' => 'G3'],\n                    ],\n\n                    'merge_cells' => ['C3:D3'],\n\n                    'copy_width' => [\n                        ['from' => 'E', 'to' => 'F'],\n                        ['from' => 'E', 'to' => 'G'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination2_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'foo',\n                        'B' => '[foo]',\n                        'E' => '[columns.*.one.*]',\n                    ],\n                    2 => [\n                        'A' => 'foo',\n                        'B' => '[rows.1.b]',\n                        'C' => '[rows.1.c]',\n                        'E' => '[columns.*.two.*]',\n                    ],\n                    3 => [\n                        'A' => '[foo]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'rows' => [\n                        ['b' => 'b1', 'c' => 'c1'],\n                        ['b' => 'b2', 'c' => 'c2'],\n                    ],\n                    'columns' => [\n                        [\n                            'one' => ['01', '02', '03'],\n                            'two' => [15000, 20000, 30000],\n                        ],\n                    ],\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'test1',\n                            'E' => '01',\n                            'F' => '02',\n                            'G' => '03',\n                        ],\n                        2 => [\n                            'B' => 'b2',\n                            'C' => 'c2',\n                            'E' => 15000,\n                            'F' => 20000,\n                            'G' => 30000,\n                        ],\n                        3 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'E1', 'to' => 'F1'],\n                        ['from' => 'E1', 'to' => 'G1'],\n                        ['from' => 'E2', 'to' => 'F2'],\n                        ['from' => 'E2', 'to' => 'G2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'E', 'to' => 'F'],\n                        ['from' => 'E', 'to' => 'G'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[title] [=column.month]',\n                        'C' => '=D6 [oops]',\n                        'D' => 'D6',\n                        'Q' => null,\n                    ],\n                    3 => [\n                        'G' => '[column.month] [= column.month]',\n                        'Q' => null,\n                    ],\n                    4 => [\n                        'A' => '[list.name]',\n                        'B' => '[list.count]',\n                        'C' => 'kg',\n                        'D' => '[list.price]',\n                        'E' => '[comment]',\n                        'G' => '[column.amount]',\n                        'Q' => '=A1+B3+C4+D6',\n                    ],\n                    6 => [\n                        'B' => '[total.count]',\n                        'Q' => null,\n                    ],\n                    7 => [\n                        'C' => '=D6 [oops]',\n                        'D' => 'D6',\n                        'Q' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'total' => ['count' => 3],\n                    'comment' => 'bar',\n                    'list' => [\n                        ['name' => 'Product 1', 'count' => 2, 'price' => 753.14],\n                        ['name' => 'Product 2', 'count' => 1, 'price' => 123],\n                    ],\n                    'column' => [\n                        [\n                            'month' => ['01', '02', '03'],\n                            'amount' => [15000, 20000, 30000],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['G4:H4'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[title] [=column.*.month]',\n                        'C' => '=D6',\n                        'D' => 'D6',\n                        'Q' => null,\n                    ],\n                    3 => [\n                        'G' => '[column.*.month] [= column.*.month.*]',\n                        'Q' => null,\n                    ],\n                    4 => [\n                        'A' => '[list.*.name]',\n                        'B' => '[list.*.count]',\n                        'C' => 'kg',\n                        'D' => '[list.price]',\n                        'E' => '[comment]',\n                        'G' => '[column.*.amount.*]',\n                        'Q' => '=A1+B3+C4+D6',\n                    ],\n                    6 => [\n                        'B' => '[total.count]',\n                        'Q' => null,\n                    ],\n                    7 => [\n                        'C' => '=D6 [oops]',\n                        'D' => 'D6',\n                        'Q' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'total' => ['count' => 3],\n                    'comment' => 'bar',\n                    'list' => [\n                        ['name' => 'Product 1', 'count' => 2, 'price' => 753.14],\n                        ['name' => 'Product 2', 'count' => 1, 'price' => 123],\n                    ],\n                    'column' => [\n                        [\n                            'month' => ['01', '02', '03'],\n                            'amount' => [15000, 20000, 30000],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['G4:H4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'foo',\n                            'C' => '=D7',\n                        ],\n                        3 => [\n                            'G' => '01',\n                            'H' => '02',\n                            'I' => '03',\n                        ],\n                        4 => [\n                            'A' => 'Product 1',\n                            'B' => 2,\n                            'C' => 'kg',\n                            'D' => 753.14,\n                            'E' => 'bar',\n                            'G' => 15000,\n                            'I' => 20000,\n                            'K' => 30000,\n                            'Q' => '=A1+B3+C4+D7',\n                        ],\n                        5 => [\n                            'A' => 'Product 2',\n                            'B' => 1,\n                            'C' => 'kg',\n                            'D' => 123,\n                            'E' => 'bar',\n                            'G' => null,\n                            'I' => null,\n                            'K' => null,\n                            'Q' => '=A1+B3+C5+D7',\n                        ],\n                        7 => [\n                            'B' => 3,\n                        ],\n                        8 => [\n                            'C' => '=D7',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 5, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A4', 'to' => 'A5'],\n                        ['from' => 'B4', 'to' => 'B5'],\n                        ['from' => 'C4', 'to' => 'C5'],\n                        ['from' => 'D4', 'to' => 'D5'],\n                        ['from' => 'E4', 'to' => 'E5'],\n                        ['from' => 'F4', 'to' => 'F5'],\n                        ['from' => 'G3', 'to' => 'H3'],\n                        ['from' => 'G3', 'to' => 'I3'],\n                        ['from' => 'G4', 'to' => 'G5'],\n                        ['from' => 'G4', 'to' => 'I4'],\n                        ['from' => 'G4', 'to' => 'K4'],\n                        ['from' => 'I4', 'to' => 'I5'],\n                        ['from' => 'K4', 'to' => 'K5'],\n                        ['from' => 'M4', 'to' => 'M5'],\n                        ['from' => 'N4', 'to' => 'N5'],\n                        ['from' => 'O4', 'to' => 'O5'],\n                        ['from' => 'P4', 'to' => 'P5'],\n                        ['from' => 'Q4', 'to' => 'Q5'],\n                    ],\n\n                    'merge_cells' => [\n                        'I4:J4', 'K4:L4',\n                        'G5:H5', 'I5:J5', 'K5:L5',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'G', 'to' => 'H'],\n                        ['from' => 'G', 'to' => 'I'],\n                        ['from' => 'G', 'to' => 'K'],\n                        ['from' => 'H', 'to' => 'J'],\n                        ['from' => 'H', 'to' => 'L'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_combination3_limit()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[title]',\n                        'G' => null,\n                    ],\n                    3 => [\n                        'G' => '[column.*.month.*] [= column.0.month]',\n                    ],\n                    4 => [\n                        'A' => '[list.0.name] [=column.0.month.0]',\n                        'B' => '[list.0.count]',\n                        'C' => 'kg',\n                        'D' => '[list.0.price]',\n                        'E' => '[comment]',\n                        'G' => '[column.0.amount.0]',\n                    ],\n                    6 => [\n                        'B' => '[total.count]',\n                        'G' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'title' => 'foo',\n                    'total' => ['count' => 3],\n                    'comment' => 'bar',\n                    'list' => [\n                        ['name' => 'Product 1', 'count' => 2, 'price' => 753.14],\n                        ['name' => 'Product 2', 'count' => 1, 'price' => 123],\n                    ],\n                    'column' => [\n                        [\n                            'month' => ['01', '02', '03'],\n                            'amount' => [15000, 20000, 30000],\n                        ],\n                    ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'foo',\n                        ],\n                        3 => [\n                            'G' => '01',\n                            'H' => '02',\n                            'I' => '03',\n                        ],\n                        4 => [\n                            'A' => 'Product 1',\n                            'B' => 2,\n                            'D' => 753.14,\n                            'E' => 'bar',\n                            'G' => 15000,\n                        ],\n                        6 => [\n                            'B' => 3,\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'G3', 'to' => 'H3'],\n                        ['from' => 'G3', 'to' => 'I3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'G', 'to' => 'H'],\n                        ['from' => 'G', 'to' => 'I'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_table_with_formula1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=A4',\n                        'B' => '=A3',\n                        'C' => '=A2',\n                        'D' => '=A1',\n                        'E' => '=A2:A2',\n                        'F' => '=B2:G2',\n                    ],\n                    2 => [\n                        'A' => '[table.price]',\n                        'B' => '[table.count]',\n                        'C' => '=A2*B2',\n                        'D' => '=A1+A3+A4',\n                        'E' => 'A1+A3+A4',\n                        'F' => null,\n                    ],\n                    3 => [\n                        'A' => '=A4',\n                        'B' => '=A3',\n                        'C' => '=A2',\n                        'D' => '=A1',\n                        'E' => '=A2:A2',\n                        'F' => '=B2:G2',\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        ['price' => 11, 'count' => 1],\n                        ['price' => 12, 'count' => 2],\n                        ['price' => 13, 'count' => 3],\n                        ['price' => 14, 'count' => 4],\n                    ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=A7',\n                            'B' => '=A6',\n                            'C' => '=A2',\n                            'D' => '=A1',\n                            'E' => '=A2:A5',\n                            'F' => '=B2:G5',\n                        ],\n                        2 => [\n                            'A' => 11,\n                            'B' => 1,\n                            'C' => '=A2*B2',\n                            'D' => '=A1+A6+A7',\n                            'E' => 'A1+A3+A4',\n                        ],\n                        3 => [\n                            'A' => 12,\n                            'B' => 2,\n                            'C' => '=A3*B3',\n                            'D' => '=A1+A6+A7',\n                            'E' => 'A1+A3+A4',\n                        ],\n                        4 => [\n                            'A' => 13,\n                            'B' => 3,\n                            'C' => '=A4*B4',\n                            'D' => '=A1+A6+A7',\n                            'E' => 'A1+A3+A4',\n                        ],\n                        5 => [\n                            'A' => 14,\n                            'B' => 4,\n                            'C' => '=A5*B5',\n                            'D' => '=A1+A6+A7',\n                            'E' => 'A1+A3+A4',\n                        ],\n                        6 => [\n                            'A' => '=A7',\n                            'B' => '=A6',\n                            'C' => '=A2',\n                            'D' => '=A1',\n                            'E' => '=A2:A5',\n                            'F' => '=B2:G5',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 3],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A5'],\n                        ['from' => 'B2', 'to' => 'B3:B5'],\n                        ['from' => 'C2', 'to' => 'C3:C5'],\n                        ['from' => 'D2', 'to' => 'D3:D5'],\n                        ['from' => 'E2', 'to' => 'E3:E5'],\n                        ['from' => 'F2', 'to' => 'F3:F5'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_table_with_formula2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=A1',\n                        'B' => '=A2',\n                        'C' => '=A3',\n                        'D' => '=A4',\n                        'E' => '=A5',\n                        'F' => '=A6',\n                        'G' => '=A7',\n                        'H' => '=A8',\n                        'J' => '=A3:A3',\n                        'K' => '=A3:C3',\n                    ],\n                    2 => [\n                        'A' => '[= hello]',\n                        'K' => null,\n                    ],\n                    3 => [\n                        'A' => '[table.price]',\n                        'B' => '[table.count]',\n                        'C' => '=A3*B3',\n                        'K' => null,\n                    ],\n                    4 => [\n                        'A' => '[=hello]',\n                        'K' => null,\n                    ],\n                    5 => [\n                        'A' => '[!table]',\n                        'K' => null,\n                    ],\n                    6 => [\n                        'A' => '[!table.count]',\n                        'K' => null,\n                    ],\n                    7 => [\n                        'A' => '=A1',\n                        'B' => '=A2',\n                        'C' => '=A3',\n                        'D' => '=A4',\n                        'E' => '=A5',\n                        'F' => '=A6',\n                        'G' => '=A7',\n                        'H' => '=A8',\n                        'J' => '=A3:A3',\n                        'K' => '=A3:C3',\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        ['price' => 11, 'count' => 1],\n                        ['price' => 12, 'count' => 2],\n                        ['price' => 13, 'count' => 3],\n                        ['price' => 14, 'count' => 4],\n                        ['price' => 15, 'count' => 5],\n                    ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=A1',\n                            'B' => '=A2',\n                            'C' => '=A2',\n                            'D' => '=A7',\n                            'E' => '=A7',\n                            'F' => '=A7',\n                            'G' => '=A7',\n                            'H' => '=A8',\n                            'J' => '=A2:A6',\n                            'K' => '=A2:C6',\n                        ],\n                        2 => [\n                            'A' => 11,\n                            'B' => 1,\n                            'C' => '=A2*B2',\n                        ],\n                        3 => [\n                            'A' => 12,\n                            'B' => 2,\n                            'C' => '=A3*B3',\n                        ],\n                        4 => [\n                            'A' => 13,\n                            'B' => 3,\n                            'C' => '=A4*B4',\n                        ],\n                        5 => [\n                            'A' => 14,\n                            'B' => 4,\n                            'C' => '=A5*B5',\n                        ],\n                        6 => [\n                            'A' => 15,\n                            'B' => 5,\n                            'C' => '=A6*B6',\n                        ],\n                        7 => [\n                            'A' => '=A1',\n                            'B' => '=A2',\n                            'C' => '=A2',\n                            'D' => '=A7',\n                            'E' => '=A7',\n                            'F' => '=A7',\n                            'G' => '=A7',\n                            'H' => '=A8',\n                            'J' => '=A2:A6',\n                            'K' => '=A2:C6',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'add', 'row' => 3, 'qty' => 4],\n                        ['action' => 'delete', 'row' => 7, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 7, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 7, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A6'],\n                        ['from' => 'B2', 'to' => 'B3:B6'],\n                        ['from' => 'C2', 'to' => 'C3:C6'],\n                        ['from' => 'D2', 'to' => 'D3:D6'],\n                        ['from' => 'E2', 'to' => 'E3:E6'],\n                        ['from' => 'F2', 'to' => 'F3:F6'],\n                        ['from' => 'G2', 'to' => 'G3:G6'],\n                        ['from' => 'H2', 'to' => 'H3:H6'],\n                        ['from' => 'I2', 'to' => 'I3:I6'],\n                        ['from' => 'J2', 'to' => 'J3:J6'],\n                        ['from' => 'K2', 'to' => 'K3:K6'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_table_with_formula3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=AA1',\n                        'B' => '=BB2',\n                        'C' => '=CC3',\n                        'D' => '=DD4',\n                        'E' => '=EE5',\n                        'F' => '=FF6',\n                        'G' => '=A3:A3',\n                    ],\n                    2 => [\n                        'A' => '[= hello]',\n                        'G' => null,\n                    ],\n                    3 => [\n                        'A' => '[table.price]',\n                        'B' => '[table.count]',\n                        'C' => '=A3*B3+C1+D2+E4+F5',\n                        'G' => null,\n                    ],\n                    4 => [\n                        'A' => '=AA1',\n                        'B' => '=BB2',\n                        'C' => '=CC3',\n                        'D' => '=DD4',\n                        'E' => '=EE5',\n                        'F' => '=FF6',\n                        'G' => '=A3:A3',\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        ['price' => 11, 'count' => 1],\n                        ['price' => 12, 'count' => 2],\n                        ['price' => 13, 'count' => 3],\n                        ['price' => 14, 'count' => 4],\n                        ['price' => 15, 'count' => 5],\n                    ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=AA1',\n                            'B' => '=BB2',\n                            'C' => '=CC2',\n                            'D' => '=DD7',\n                            'E' => '=EE8',\n                            'F' => '=FF9',\n                            'G' => '=A2:A6',\n                        ],\n                        2 => [\n                            'A' => 11,\n                            'B' => 1,\n                            'C' => '=A2*B2+C1+D2+E7+F8',\n                        ],\n                        3 => [\n                            'A' => 12,\n                            'B' => 2,\n                            'C' => '=A3*B3+C1+D3+E7+F8',\n                        ],\n                        4 => [\n                            'A' => 13,\n                            'B' => 3,\n                            'C' => '=A4*B4+C1+D4+E7+F8',\n                        ],\n                        5 => [\n                            'A' => 14,\n                            'B' => 4,\n                            'C' => '=A5*B5+C1+D5+E7+F8',\n                        ],\n                        6 => [\n                            'A' => 15,\n                            'B' => 5,\n                            'C' => '=A6*B6+C1+D6+E7+F8',\n                        ],\n                        7 => [\n                            'A' => '=AA1',\n                            'B' => '=BB2',\n                            'C' => '=CC2',\n                            'D' => '=DD7',\n                            'E' => '=EE8',\n                            'F' => '=FF9',\n                            'G' => '=A2:A6',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 2, 'qty' => 1],\n                        ['action' => 'add', 'row' => 3, 'qty' => 4],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A6'],\n                        ['from' => 'B2', 'to' => 'B3:B6'],\n                        ['from' => 'C2', 'to' => 'C3:C6'],\n                        ['from' => 'D2', 'to' => 'D3:D6'],\n                        ['from' => 'E2', 'to' => 'E3:E6'],\n                        ['from' => 'F2', 'to' => 'F3:F6'],\n                        ['from' => 'G2', 'to' => 'G3:G6'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_1x1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'B' => 'two',\n                            'C' => 'three',\n                            'D' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'B2'],\n                        ['from' => 'A2', 'to' => 'C2'],\n                        ['from' => 'A2', 'to' => 'D2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'B'],\n                        ['from' => 'A', 'to' => 'C'],\n                        ['from' => 'A', 'to' => 'D'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_1x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:B2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'C' => 'two',\n                            'E' => 'three',\n                            'G' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'C2'],\n                        ['from' => 'A2', 'to' => 'E2'],\n                        ['from' => 'A2', 'to' => 'G2'],\n                    ],\n\n                    'merge_cells' => ['C2:D2', 'E2:F2', 'G2:H2'],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'C'],\n                        ['from' => 'A', 'to' => 'E'],\n                        ['from' => 'A', 'to' => 'G'],\n                        ['from' => 'B', 'to' => 'D'],\n                        ['from' => 'B', 'to' => 'F'],\n                        ['from' => 'B', 'to' => 'H'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_1x3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:C2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'D' => 'two',\n                            'G' => 'three',\n                            'J' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'D2'],\n                        ['from' => 'A2', 'to' => 'G2'],\n                        ['from' => 'A2', 'to' => 'J2'],\n                    ],\n\n                    'merge_cells' => ['D2:F2', 'G2:I2', 'J2:L2'],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'D'],\n                        ['from' => 'A', 'to' => 'G'],\n                        ['from' => 'A', 'to' => 'J'],\n                        ['from' => 'B', 'to' => 'E'],\n                        ['from' => 'B', 'to' => 'H'],\n                        ['from' => 'B', 'to' => 'K'],\n                        ['from' => 'C', 'to' => 'F'],\n                        ['from' => 'C', 'to' => 'I'],\n                        ['from' => 'C', 'to' => 'L'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_3x1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                        [['one-b', 'two-b', 'three-b', 'four-b']],\n                        [['one-c', 'two-c', 'three-c', 'four-c']],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'B' => 'two',\n                            'C' => 'three',\n                            'D' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'one-b',\n                            'B' => 'two-b',\n                            'C' => 'three-b',\n                            'D' => 'four-b',\n                        ],\n                        4 => [\n                            'A' => 'one-c',\n                            'B' => 'two-c',\n                            'C' => 'three-c',\n                            'D' => 'four-c',\n                        ],\n                        5 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'A2', 'to' => 'B2'],\n                        ['from' => 'A2', 'to' => 'C2'],\n                        ['from' => 'A2', 'to' => 'D2'],\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'B'],\n                        ['from' => 'A', 'to' => 'C'],\n                        ['from' => 'A', 'to' => 'D'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_3x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                        [['one-b', 'two-b', 'three-b', 'four-b']],\n                        [['one-c', 'two-c', 'three-c', 'four-c']],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:B2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'C' => 'two',\n                            'E' => 'three',\n                            'G' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'one-b',\n                            'C' => 'two-b',\n                            'E' => 'three-b',\n                            'G' => 'four-b',\n                        ],\n                        4 => [\n                            'A' => 'one-c',\n                            'C' => 'two-c',\n                            'E' => 'three-c',\n                            'G' => 'four-c',\n                        ],\n                        5 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'A2', 'to' => 'C2'],\n                        ['from' => 'A2', 'to' => 'E2'],\n                        ['from' => 'A2', 'to' => 'G2'],\n                        ['from' => 'C2', 'to' => 'C3:C4'],\n                        ['from' => 'E2', 'to' => 'E3:E4'],\n                        ['from' => 'G2', 'to' => 'G3:G4'],\n                    ],\n\n                    'merge_cells' => [\n                        'C2:D2', 'E2:F2', 'G2:H2',\n                        'A3:B3', 'C3:D3', 'E3:F3', 'G3:H3',\n                        'A4:B4', 'C4:D4', 'E4:F4', 'G4:H4',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'C'],\n                        ['from' => 'A', 'to' => 'E'],\n                        ['from' => 'A', 'to' => 'G'],\n                        ['from' => 'B', 'to' => 'D'],\n                        ['from' => 'B', 'to' => 'F'],\n                        ['from' => 'B', 'to' => 'H'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_3x3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo]',\n                    ],\n                    2 => [\n                        'A' => '[matrix]',\n                    ],\n                    3 => [\n                        'A' => '[bar]',\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => 'test1',\n                    'bar' => 'test2',\n                    'matrix' => [\n                        [['one', 'two', 'three', 'four']],\n                        [['one-b', 'two-b', 'three-b', 'four-b']],\n                        [['one-c', 'two-c', 'three-c', 'four-c']],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:C2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'test1',\n                        ],\n                        2 => [\n                            'A' => 'one',\n                            'D' => 'two',\n                            'G' => 'three',\n                            'J' => 'four',\n                        ],\n                        3 => [\n                            'A' => 'one-b',\n                            'D' => 'two-b',\n                            'G' => 'three-b',\n                            'J' => 'four-b',\n                        ],\n                        4 => [\n                            'A' => 'one-c',\n                            'D' => 'two-c',\n                            'G' => 'three-c',\n                            'J' => 'four-c',\n                        ],\n                        5 => [\n                            'A' => 'test2',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3:A4'],\n                        ['from' => 'A2', 'to' => 'D2'],\n                        ['from' => 'A2', 'to' => 'G2'],\n                        ['from' => 'A2', 'to' => 'J2'],\n                        ['from' => 'D2', 'to' => 'D3:D4'],\n                        ['from' => 'G2', 'to' => 'G3:G4'],\n                        ['from' => 'J2', 'to' => 'J3:J4'],\n                    ],\n\n                    'merge_cells' => [\n                        'D2:F2', 'G2:I2', 'J2:L2',\n                        'A3:C3', 'D3:F3', 'G3:I3', 'J3:L3',\n                        'A4:C4', 'D4:F4', 'G4:I4', 'J4:L4',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'A', 'to' => 'D'],\n                        ['from' => 'A', 'to' => 'G'],\n                        ['from' => 'A', 'to' => 'J'],\n                        ['from' => 'B', 'to' => 'E'],\n                        ['from' => 'B', 'to' => 'H'],\n                        ['from' => 'B', 'to' => 'K'],\n                        ['from' => 'C', 'to' => 'F'],\n                        ['from' => 'C', 'to' => 'I'],\n                        ['from' => 'C', 'to' => 'L'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_multi1()\n    {\n        $data = [\n            [\n                'values' => [\n                    2 => [\n                        'C' => '[project.months]',\n                    ],\n                    3 => [\n                        'A' => '[project.name]',\n                        'C' => '[project.amount]',\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        'name' => ['N1', 'N2', 'N3'],\n                        'months' => [['01', '02', '03']],\n                        'amount' => [\n                            [101, 201, 301],\n                            [102, 202, 302],\n                            [103, 203, 303],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['C2:D2', 'A3:B3', 'C3:D3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        2 => [\n                            'C' => '01',\n                            'E' => '02',\n                            'G' => '03',\n                        ],\n                        3 => [\n                            'A' => 'N1',\n                            'C' => 101,\n                            'E' => 201,\n                            'G' => 301,\n                        ],\n                        4 => [\n                            'A' => 'N2',\n                            'C' => 102,\n                            'E' => 202,\n                            'G' => 302,\n                        ],\n                        5 => [\n                            'A' => 'N3',\n                            'C' => 103,\n                            'E' => 203,\n                            'G' => 303,\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A3', 'to' => 'A4:A5'],\n                        ['from' => 'C2', 'to' => 'E2'],\n                        ['from' => 'C2', 'to' => 'G2'],\n                        ['from' => 'C3', 'to' => 'C4:C5'],\n                        ['from' => 'C3', 'to' => 'E3'],\n                        ['from' => 'C3', 'to' => 'G3'],\n                        ['from' => 'E3', 'to' => 'E4:E5'],\n                        ['from' => 'G3', 'to' => 'G4:G5'],\n                    ],\n\n                    'merge_cells' => [\n                        'E2:F2', 'G2:H2',\n                        'E3:F3', 'G3:H3',\n                        'A4:B4', 'C4:D4', 'E4:F4', 'G4:H4',\n                        'A5:B5', 'C5:D5', 'E5:F5', 'G5:H5',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'C', 'to' => 'E'],\n                        ['from' => 'C', 'to' => 'G'],\n                        ['from' => 'D', 'to' => 'F'],\n                        ['from' => 'D', 'to' => 'H'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_matrix_merge_multi2()\n    {\n        $data = [\n            [\n                'values' => [\n                    2 => [\n                        'D' => '[project.months]',\n                    ],\n                    3 => [\n                        'A' => '[project.name]',\n                        'D' => '[project.amount]',\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        'name' => ['N1', 'N2', 'N3'],\n                        'months' => [['01', '02', '03']],\n                        'amount' => [\n                            [101, 201, 301],\n                            [102, 202, 302],\n                            [103, 203, 303],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['D2:F2', 'A3:C3', 'D3:F3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        2 => [\n                            'D' => '01',\n                            'G' => '02',\n                            'J' => '03',\n                        ],\n                        3 => [\n                            'A' => 'N1',\n                            'D' => 101,\n                            'G' => 201,\n                            'J' => 301,\n                        ],\n                        4 => [\n                            'A' => 'N2',\n                            'D' => 102,\n                            'G' => 202,\n                            'J' => 302,\n                        ],\n                        5 => [\n                            'A' => 'N3',\n                            'D' => 103,\n                            'G' => 203,\n                            'J' => 303,\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A3', 'to' => 'A4:A5'],\n                        ['from' => 'D2', 'to' => 'G2'],\n                        ['from' => 'D2', 'to' => 'J2'],\n                        ['from' => 'D3', 'to' => 'D4:D5'],\n                        ['from' => 'D3', 'to' => 'G3'],\n                        ['from' => 'D3', 'to' => 'J3'],\n                        ['from' => 'G3', 'to' => 'G4:G5'],\n                        ['from' => 'J3', 'to' => 'J4:J5'],\n                    ],\n\n                    'merge_cells' => [\n                        'G2:I2', 'J2:L2',\n                        'G3:I3', 'J3:L3',\n                        'A4:C4', 'D4:F4', 'G4:I4', 'J4:L4',\n                        'A5:C5', 'D5:F5', 'G5:I5', 'J5:L5',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'D', 'to' => 'G'],\n                        ['from' => 'D', 'to' => 'J'],\n                        ['from' => 'E', 'to' => 'H'],\n                        ['from' => 'E', 'to' => 'K'],\n                        ['from' => 'F', 'to' => 'I'],\n                        ['from' => 'F', 'to' => 'L'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_0x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => '[project.amount_2]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                        ],\n                        2 => [\n                            'A' => null,\n                            'B' => null,\n                        ],\n                        3 => [\n                            'B' => null,\n                            'C' => null,\n                        ],\n                        4 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_1x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => '[project.amount_2]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                            'C' => 102,\n                        ],\n                        4 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_2x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => '[project.amount_2]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                            'C' => 102,\n                        ],\n                        4 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        5 => [\n                            'B' => 201,\n                            'C' => 202,\n                        ],\n                        6 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A4'],\n                        ['from' => 'B2', 'to' => 'B4'],\n                        ['from' => 'B3', 'to' => 'B5'],\n                        ['from' => 'C2', 'to' => 'C4'],\n                        ['from' => 'C3', 'to' => 'C5'],\n                    ],\n\n                    'merge_cells' => [\n                        'A4:A5',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_2x2_skip()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B2:B2)',\n                    ],\n                    2 => [\n                        'A' => 'Hello',\n                        'B' => '[project.name]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'B' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['name' => 'project 1'],\n                        ['name' => 'project 2'],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A3)',\n                            'B' => '=SUM(B2:B3)',\n                        ],\n                        2 => [\n                            'A' => 'Hello',\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 'project 2',\n                        ],\n                        5 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'B2', 'to' => 'B3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_3x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => '[project.amount_2]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                        ['id' => 3, 'name' => 'project 3', 'amount_1' => 301, 'amount_2' => 302],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                            'C' => 102,\n                        ],\n                        4 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        5 => [\n                            'B' => 201,\n                            'C' => 202,\n                        ],\n                        6 => [\n                            'A' => 3,\n                            'B' => 'project 3',\n                        ],\n                        7 => [\n                            'B' => 301,\n                            'C' => 302,\n                        ],\n                        8 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 4],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A4'],\n                        ['from' => 'A2', 'to' => 'A6'],\n                        ['from' => 'B2', 'to' => 'B4'],\n                        ['from' => 'B2', 'to' => 'B6'],\n                        ['from' => 'B3', 'to' => 'B5'],\n                        ['from' => 'B3', 'to' => 'B7'],\n                        ['from' => 'C2', 'to' => 'C4'],\n                        ['from' => 'C2', 'to' => 'C6'],\n                        ['from' => 'C3', 'to' => 'C5'],\n                        ['from' => 'C3', 'to' => 'C7'],\n                    ],\n\n                    'merge_cells' => [\n                        'A4:A5', 'A6:A7',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_4x2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => null,\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => '[project.amount_2]',\n                    ],\n                    4 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                        ['id' => 3, 'name' => 'project 3', 'amount_1' => 301, 'amount_2' => 302],\n                        ['id' => 4, 'name' => 'project 4', 'amount_1' => 401, 'amount_2' => 402],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                            'C' => 102,\n                        ],\n                        4 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        5 => [\n                            'B' => 201,\n                            'C' => 202,\n                        ],\n                        6 => [\n                            'A' => 3,\n                            'B' => 'project 3',\n                        ],\n                        7 => [\n                            'B' => 301,\n                            'C' => 302,\n                        ],\n                        8 => [\n                            'A' => 4,\n                            'B' => 'project 4',\n                        ],\n                        9 => [\n                            'B' => 401,\n                            'C' => 402,\n                        ],\n                        10 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 6],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A4'],\n                        ['from' => 'A2', 'to' => 'A6'],\n                        ['from' => 'A2', 'to' => 'A8'],\n                        ['from' => 'B2', 'to' => 'B4'],\n                        ['from' => 'B2', 'to' => 'B6'],\n                        ['from' => 'B2', 'to' => 'B8'],\n                        ['from' => 'B3', 'to' => 'B5'],\n                        ['from' => 'B3', 'to' => 'B7'],\n                        ['from' => 'B3', 'to' => 'B9'],\n                        ['from' => 'C2', 'to' => 'C4'],\n                        ['from' => 'C2', 'to' => 'C6'],\n                        ['from' => 'C2', 'to' => 'C8'],\n                        ['from' => 'C3', 'to' => 'C5'],\n                        ['from' => 'C3', 'to' => 'C7'],\n                        ['from' => 'C3', 'to' => 'C9'],\n                    ],\n\n                    'merge_cells' => [\n                        'A4:A5', 'A6:A7', 'A8:A9',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_4x3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => '=SUM(C4:C4)',\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'C' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'C' => null,\n                    ],\n                    4 => [\n                        'C' => '[project.amount_2]',\n                    ],\n                    6 => [\n                        'A' => '[foo]',\n                        'C' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                        ['id' => 3, 'name' => 'project 3', 'amount_1' => 301, 'amount_2' => 302],\n                        ['id' => 4, 'name' => 'project 4', 'amount_1' => 401, 'amount_2' => 402],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                            'C' => '=SUM(C4:C4)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                        ],\n                        4 => [\n                            'C' => 102,\n                        ],\n                        5 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        6 => [\n                            'B' => 201,\n                        ],\n                        7 => [\n                            'C' => 202,\n                        ],\n                        8 => [\n                            'A' => 3,\n                            'B' => 'project 3',\n                        ],\n                        9 => [\n                            'B' => 301,\n                        ],\n                        10 => [\n                            'C' => 302,\n                        ],\n                        11 => [\n                            'A' => 4,\n                            'B' => 'project 4',\n                        ],\n                        12 => [\n                            'B' => 401,\n                        ],\n                        13 => [\n                            'C' => 402,\n                        ],\n                        15 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 5, 'qty' => 9],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A5'],\n                        ['from' => 'A2', 'to' => 'A8'],\n                        ['from' => 'A2', 'to' => 'A11'],\n                        ['from' => 'B2', 'to' => 'B5'],\n                        ['from' => 'B2', 'to' => 'B8'],\n                        ['from' => 'B2', 'to' => 'B11'],\n                        ['from' => 'B3', 'to' => 'B6'],\n                        ['from' => 'B3', 'to' => 'B9'],\n                        ['from' => 'B3', 'to' => 'B12'],\n                        ['from' => 'B4', 'to' => 'B7'],\n                        ['from' => 'B4', 'to' => 'B10'],\n                        ['from' => 'B4', 'to' => 'B13'],\n                        ['from' => 'C2', 'to' => 'C5'],\n                        ['from' => 'C2', 'to' => 'C8'],\n                        ['from' => 'C2', 'to' => 'C11'],\n                        ['from' => 'C3', 'to' => 'C6'],\n                        ['from' => 'C3', 'to' => 'C9'],\n                        ['from' => 'C3', 'to' => 'C12'],\n                        ['from' => 'C4', 'to' => 'C7'],\n                        ['from' => 'C4', 'to' => 'C10'],\n                        ['from' => 'C4', 'to' => 'C13'],\n                    ],\n\n                    'merge_cells' => [\n                        'A5:A7', 'A8:A10', 'A11:A13',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_4x4()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => '=SUM(C4:C4)',\n                        'D' => '=SUM(D5:D5)',\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'D' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'D' => null,\n                    ],\n                    4 => [\n                        'C' => '[project.amount_2]',\n                        'D' => null,\n                    ],\n                    6 => [\n                        'A' => '[foo]',\n                        'D' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                        ['id' => 3, 'name' => 'project 3', 'amount_1' => 301, 'amount_2' => 302],\n                        ['id' => 4, 'name' => 'project 4', 'amount_1' => 401, 'amount_2' => 402],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A5'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                            'C' => '=SUM(C4:C4)',\n                            'D' => '=SUM(D5:D5)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                        ],\n                        4 => [\n                            'C' => 102,\n                        ],\n                        6 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        7 => [\n                            'B' => 201,\n                        ],\n                        8 => [\n                            'C' => 202,\n                        ],\n                        10 => [\n                            'A' => 3,\n                            'B' => 'project 3',\n                        ],\n                        11 => [\n                            'B' => 301,\n                        ],\n                        12 => [\n                            'C' => 302,\n                        ],\n                        14 => [\n                            'A' => 4,\n                            'B' => 'project 4',\n                        ],\n                        15 => [\n                            'B' => 401,\n                        ],\n                        16 => [\n                            'C' => 402,\n                        ],\n                        18 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 6, 'qty' => 12],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A6'],\n                        ['from' => 'A2', 'to' => 'A10'],\n                        ['from' => 'A2', 'to' => 'A14'],\n                        ['from' => 'B2', 'to' => 'B6'],\n                        ['from' => 'B2', 'to' => 'B10'],\n                        ['from' => 'B2', 'to' => 'B14'],\n                        ['from' => 'B3', 'to' => 'B7'],\n                        ['from' => 'B3', 'to' => 'B11'],\n                        ['from' => 'B3', 'to' => 'B15'],\n                        ['from' => 'B4', 'to' => 'B8'],\n                        ['from' => 'B4', 'to' => 'B12'],\n                        ['from' => 'B4', 'to' => 'B16'],\n                        ['from' => 'B5', 'to' => 'B9'],\n                        ['from' => 'B5', 'to' => 'B13'],\n                        ['from' => 'B5', 'to' => 'B17'],\n                        ['from' => 'C2', 'to' => 'C6'],\n                        ['from' => 'C2', 'to' => 'C10'],\n                        ['from' => 'C2', 'to' => 'C14'],\n                        ['from' => 'C3', 'to' => 'C7'],\n                        ['from' => 'C3', 'to' => 'C11'],\n                        ['from' => 'C3', 'to' => 'C15'],\n                        ['from' => 'C4', 'to' => 'C8'],\n                        ['from' => 'C4', 'to' => 'C12'],\n                        ['from' => 'C4', 'to' => 'C16'],\n                        ['from' => 'C5', 'to' => 'C9'],\n                        ['from' => 'C5', 'to' => 'C13'],\n                        ['from' => 'C5', 'to' => 'C17'],\n                        ['from' => 'D2', 'to' => 'D6'],\n                        ['from' => 'D2', 'to' => 'D10'],\n                        ['from' => 'D2', 'to' => 'D14'],\n                        ['from' => 'D3', 'to' => 'D7'],\n                        ['from' => 'D3', 'to' => 'D11'],\n                        ['from' => 'D3', 'to' => 'D15'],\n                        ['from' => 'D4', 'to' => 'D8'],\n                        ['from' => 'D4', 'to' => 'D12'],\n                        ['from' => 'D4', 'to' => 'D16'],\n                        ['from' => 'D5', 'to' => 'D9'],\n                        ['from' => 'D5', 'to' => 'D13'],\n                        ['from' => 'D5', 'to' => 'D17'],\n                    ],\n\n                    'merge_cells' => [\n                        'A6:A9', 'A10:A13', 'A14:A17',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_4x5()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(B3:B3)',\n                        'C' => '=SUM(C4:C4)',\n                        'D' => '=SUM(D5:D5)',\n                        'E' => '=SUM(E6:E6)',\n                    ],\n                    2 => [\n                        'A' => '[project.id]',\n                        'B' => '[project.name]',\n                        'E' => null,\n                    ],\n                    3 => [\n                        'B' => '[project.amount_1]',\n                        'E' => null,\n                    ],\n                    5 => [\n                        'C' => '[project.amount_2]',\n                        'E' => null,\n                    ],\n                    7 => [\n                        'A' => '[foo]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'project' => [\n                        ['id' => 1, 'name' => 'project 1', 'amount_1' => 101, 'amount_2' => 102],\n                        ['id' => 2, 'name' => 'project 2', 'amount_1' => 201, 'amount_2' => 202],\n                        ['id' => 3, 'name' => 'project 3', 'amount_1' => 301, 'amount_2' => 302],\n                        ['id' => 4, 'name' => 'project 4', 'amount_1' => 401, 'amount_2' => 402],\n                    ],\n                    'foo' => 'test1',\n                ],\n\n                'merge_cells' => ['A2:A6'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A2)',\n                            'B' => '=SUM(B3:B3)',\n                            'C' => '=SUM(C4:C4)',\n                            'D' => '=SUM(D5:D5)',\n                            'E' => '=SUM(E6:E6)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                            'B' => 'project 1',\n                        ],\n                        3 => [\n                            'B' => 101,\n                        ],\n                        5 => [\n                            'C' => 102,\n                        ],\n                        7 => [\n                            'A' => 2,\n                            'B' => 'project 2',\n                        ],\n                        8 => [\n                            'B' => 201,\n                        ],\n                        10 => [\n                            'C' => 202,\n                        ],\n                        12 => [\n                            'A' => 3,\n                            'B' => 'project 3',\n                        ],\n                        13 => [\n                            'B' => 301,\n                        ],\n                        15 => [\n                            'C' => 302,\n                        ],\n                        17 => [\n                            'A' => 4,\n                            'B' => 'project 4',\n                        ],\n                        18 => [\n                            'B' => 401,\n                        ],\n                        20 => [\n                            'C' => 402,\n                        ],\n                        22 => [\n                            'A' => 'test1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 7, 'qty' => 15],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A7'],\n                        ['from' => 'A2', 'to' => 'A12'],\n                        ['from' => 'A2', 'to' => 'A17'],\n                        ['from' => 'B2', 'to' => 'B7'],\n                        ['from' => 'B2', 'to' => 'B12'],\n                        ['from' => 'B2', 'to' => 'B17'],\n                        ['from' => 'B3', 'to' => 'B8'],\n                        ['from' => 'B3', 'to' => 'B13'],\n                        ['from' => 'B3', 'to' => 'B18'],\n                        ['from' => 'B4', 'to' => 'B9'],\n                        ['from' => 'B4', 'to' => 'B14'],\n                        ['from' => 'B4', 'to' => 'B19'],\n                        ['from' => 'B5', 'to' => 'B10'],\n                        ['from' => 'B5', 'to' => 'B15'],\n                        ['from' => 'B5', 'to' => 'B20'],\n                        ['from' => 'B6', 'to' => 'B11'],\n                        ['from' => 'B6', 'to' => 'B16'],\n                        ['from' => 'B6', 'to' => 'B21'],\n                        ['from' => 'C2', 'to' => 'C7'],\n                        ['from' => 'C2', 'to' => 'C12'],\n                        ['from' => 'C2', 'to' => 'C17'],\n                        ['from' => 'C3', 'to' => 'C8'],\n                        ['from' => 'C3', 'to' => 'C13'],\n                        ['from' => 'C3', 'to' => 'C18'],\n                        ['from' => 'C4', 'to' => 'C9'],\n                        ['from' => 'C4', 'to' => 'C14'],\n                        ['from' => 'C4', 'to' => 'C19'],\n                        ['from' => 'C5', 'to' => 'C10'],\n                        ['from' => 'C5', 'to' => 'C15'],\n                        ['from' => 'C5', 'to' => 'C20'],\n                        ['from' => 'C6', 'to' => 'C11'],\n                        ['from' => 'C6', 'to' => 'C16'],\n                        ['from' => 'C6', 'to' => 'C21'],\n                        ['from' => 'D2', 'to' => 'D7'],\n                        ['from' => 'D2', 'to' => 'D12'],\n                        ['from' => 'D2', 'to' => 'D17'],\n                        ['from' => 'D3', 'to' => 'D8'],\n                        ['from' => 'D3', 'to' => 'D13'],\n                        ['from' => 'D3', 'to' => 'D18'],\n                        ['from' => 'D4', 'to' => 'D9'],\n                        ['from' => 'D4', 'to' => 'D14'],\n                        ['from' => 'D4', 'to' => 'D19'],\n                        ['from' => 'D5', 'to' => 'D10'],\n                        ['from' => 'D5', 'to' => 'D15'],\n                        ['from' => 'D5', 'to' => 'D20'],\n                        ['from' => 'D6', 'to' => 'D11'],\n                        ['from' => 'D6', 'to' => 'D16'],\n                        ['from' => 'D6', 'to' => 'D21'],\n                        ['from' => 'E2', 'to' => 'E7'],\n                        ['from' => 'E2', 'to' => 'E12'],\n                        ['from' => 'E2', 'to' => 'E17'],\n                        ['from' => 'E3', 'to' => 'E8'],\n                        ['from' => 'E3', 'to' => 'E13'],\n                        ['from' => 'E3', 'to' => 'E18'],\n                        ['from' => 'E4', 'to' => 'E9'],\n                        ['from' => 'E4', 'to' => 'E14'],\n                        ['from' => 'E4', 'to' => 'E19'],\n                        ['from' => 'E5', 'to' => 'E10'],\n                        ['from' => 'E5', 'to' => 'E15'],\n                        ['from' => 'E5', 'to' => 'E20'],\n                        ['from' => 'E6', 'to' => 'E11'],\n                        ['from' => 'E6', 'to' => 'E16'],\n                        ['from' => 'E6', 'to' => 'E21'],\n                    ],\n\n                    'merge_cells' => [\n                        'A7:A11', 'A12:A16', 'A17:A21',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_merge_multi()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[names]',\n                    ],\n                    2 => [\n                        'A' => '[months]',\n                        'B' => '[amounts.p1]',\n                    ],\n                    4 => [\n                        'B' => '[amounts.p2]',\n                    ],\n                    '5' => [\n                        'A' => '=A1',\n                        'B' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'names' => [['One', 'Two', 'Three']],\n                    'months' => ['01', '02', '03'],\n                    'amounts' => [\n                        'p1' => [\n                            ['One R1 P1', 'Two R1 P1', 'Three R1 P1'],\n                            ['One R2 P1', 'Two R2 P1', 'Three R2 P1'],\n                            ['One R3 P1', 'Two R3 P1', 'Three R3 P1'],\n                        ],\n                        'p2' => [\n                            ['One R1 P2', 'Two R1 P2', 'Three R1 P2'],\n                            ['One R2 P2', 'Two R2 P2', 'Three R2 P2'],\n                            ['One R3 P2', 'Two R3 P2', 'Three R3 P2'],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:A4', 'B1:C1', 'B2:C2', 'B4:C4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'One',\n                            'D' => 'Two',\n                            'F' => 'Three',\n                        ],\n                        2 => [\n                            'A' => '01',\n                            'B' => 'One R1 P1',\n                            'D' => 'Two R1 P1',\n                            'F' => 'Three R1 P1',\n                        ],\n                        4 => [\n                            'B' => 'One R1 P2',\n                            'D' => 'Two R1 P2',\n                            'F' => 'Three R1 P2',\n                        ],\n                        5 => [\n                            'A' => '02',\n                            'B' => 'One R2 P1',\n                            'D' => 'Two R2 P1',\n                            'F' => 'Three R2 P1',\n                        ],\n                        7 => [\n                            'B' => 'One R2 P2',\n                            'D' => 'Two R2 P2',\n                            'F' => 'Three R2 P2',\n                        ],\n                        8 => [\n                            'A' => '03',\n                            'B' => 'One R3 P1',\n                            'D' => 'Two R3 P1',\n                            'F' => 'Three R3 P1',\n                        ],\n                        10 => [\n                            'B' => 'One R3 P2',\n                            'D' => 'Two R3 P2',\n                            'F' => 'Three R3 P2',\n                        ],\n                        11 => [\n                            'A' => '=A1',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 5, 'qty' => 6],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A5'],\n                        ['from' => 'A2', 'to' => 'A8'],\n                        ['from' => 'B1', 'to' => 'D1'],\n                        ['from' => 'B1', 'to' => 'F1'],\n                        ['from' => 'B2', 'to' => 'B5'],\n                        ['from' => 'B2', 'to' => 'B8'],\n                        ['from' => 'B2', 'to' => 'D2'],\n                        ['from' => 'B2', 'to' => 'F2'],\n                        ['from' => 'B3', 'to' => 'B6'],\n                        ['from' => 'B3', 'to' => 'B9'],\n                        ['from' => 'B4', 'to' => 'B7'],\n                        ['from' => 'B4', 'to' => 'B10'],\n                        ['from' => 'B4', 'to' => 'D4'],\n                        ['from' => 'B4', 'to' => 'F4'],\n                        ['from' => 'D2', 'to' => 'D5'],\n                        ['from' => 'D2', 'to' => 'D8'],\n                        ['from' => 'D4', 'to' => 'D7'],\n                        ['from' => 'D4', 'to' => 'D10'],\n                        ['from' => 'F2', 'to' => 'F5'],\n                        ['from' => 'F2', 'to' => 'F8'],\n                        ['from' => 'F4', 'to' => 'F7'],\n                        ['from' => 'F4', 'to' => 'F10'],\n                    ],\n\n                    'merge_cells' => [\n                        'D1:E1', 'F1:G1',\n                        'D2:E2', 'F2:G2',\n                        'A5:A7', 'B5:C5', 'D5:E5', 'F5:G5',\n                        'A8:A10', 'B8:C8', 'D8:E8', 'F8:G8',\n                        'D4:E4', 'F4:G4',\n                        'B7:C7', 'D7:E7', 'F7:G7',\n                        'B10:C10', 'D10:E10', 'F10:G10',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'B', 'to' => 'D'],\n                        ['from' => 'B', 'to' => 'F'],\n                        ['from' => 'C', 'to' => 'E'],\n                        ['from' => 'C', 'to' => 'G'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_shift1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[list.qty]',\n                        'B' => null,\n                    ],\n                    2 => [\n                        'A' => '=SUM(A1:A1)',\n                        'B' => '=SUM(A1:A10)',\n                    ],\n                    3 => [\n                        'A' => '-',\n                        'B' => '[! list]',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['qty' => 1], ['qty' => 2] ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 1,\n                        ],\n                        2 => [\n                            'A' => 2,\n                        ],\n                        3 => [\n                            'A' => '=SUM(A1:A2)',\n                            'B' => '=SUM(A1:A10)',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 4, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2'],\n                        ['from' => 'B1', 'to' => 'B2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_shift2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(A2:A20)',\n                    ],\n                    2 => [\n                        'A' => '[list.qty]',\n                        'B' => null,\n                    ],\n                    3 => [\n                        'A' => '=SUM(A2:A2)',\n                        'B' => '=SUM(A2:A20)',\n                    ],\n                    4 => [\n                        'A' => '-',\n                        'B' => '[! list]',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['qty' => 1], ['qty' => 2] ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=SUM(A2:A3)',\n                            'B' => '=SUM(A2:A20)',\n                        ],\n                        2 => [\n                            'A' => 1,\n                        ],\n                        3 => [\n                            'A' => 2,\n                        ],\n                        4 => [\n                            'A' => '=SUM(A2:A3)',\n                            'B' => '=SUM(A2:A20)',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 1],\n                        ['action' => 'delete', 'row' => 5, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3'],\n                        ['from' => 'B2', 'to' => 'B3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_shift3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[list.qty]',\n                    ],\n                    2 => [\n                        'A' => '=SUM(A1:A1)',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['qty' => 1], ['qty' => 2] ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 1,\n                        ],\n                        2 => [\n                            'A' => 2,\n                        ],\n                        3 => [\n                            'A' => '=SUM(A1:A2)',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_shift4()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=A2:A2',\n                    ],\n                    2 => [\n                        'A' => '[list.qty]',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [ ['qty' => 1], ['qty' => 2] ],\n                ],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => '=A2:A3',\n                        ],\n                        2 => [\n                            'A' => 1,\n                        ],\n                        3 => [\n                            'A' => 2,\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 1],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], [])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_merge1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[list.a]',\n                        'B' => '[list.b]',\n                        'C' => '[list.c]',\n                    ],\n                    2 => [\n                        'C' => 'Foo',\n                    ],\n                ],\n\n                'data' => [\n                    'list' => [\n                        ['a' => 'a1', 'b' => 'b1', 'c' => 'c1'],\n                        ['a' => 'a2', 'b' => 'b2', 'c' => 'c2'],\n                        ['a' => 'a3', 'b' => 'b3', 'c' => 'c3'],\n                        ['a' => 'a4', 'b' => 'b4', 'c' => 'c4'],\n                    ],\n                ],\n\n                'merge_cells' => ['A1:A3', 'B1:B2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'A' => 'a1',\n                            'B' => 'b1',\n                            'C' => 'c1',\n                        ],\n                        2 => [\n                            'C' => 'Foo',\n                        ],\n                        4 => [\n                            'A' => 'a2',\n                            'B' => 'b2',\n                            'C' => 'c2',\n                        ],\n                        5 => [\n                            'C' => 'Foo',\n                        ],\n                        7 => [\n                            'A' => 'a3',\n                            'B' => 'b3',\n                            'C' => 'c3',\n                        ],\n                        8 => [\n                            'C' => 'Foo',\n                        ],\n                        10 => [\n                            'A' => 'a4',\n                            'B' => 'b4',\n                            'C' => 'c4',\n                        ],\n                        11 => [\n                            'C' => 'Foo',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 9],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A4'],\n                        ['from' => 'A1', 'to' => 'A7'],\n                        ['from' => 'A1', 'to' => 'A10'],\n                        ['from' => 'B1', 'to' => 'B4'],\n                        ['from' => 'B1', 'to' => 'B7'],\n                        ['from' => 'B1', 'to' => 'B10'],\n                        ['from' => 'C1', 'to' => 'C4'],\n                        ['from' => 'C1', 'to' => 'C7'],\n                        ['from' => 'C1', 'to' => 'C10'],\n                        ['from' => 'C2', 'to' => 'C5'],\n                        ['from' => 'C2', 'to' => 'C8'],\n                        ['from' => 'C2', 'to' => 'C11'],\n                    ],\n\n                    'merge_cells' => ['A4:A6', 'B4:B5', 'A7:A9', 'B7:B8', 'A10:A12', 'B10:B11'],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_multi_merge2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[managers.name]',\n                    ],\n                    2 => [\n                        'A' => '[managers.sales.month]',\n                        'B' => '[managers.sales.amount]',\n                    ],\n                    3 => [\n                        'B' => '[managers.sales.qty]',\n                    ],\n                    4 => [\n                        'B' => 'Hello',\n                    ],\n                ],\n\n                'data' => [\n                    'managers' => [\n                        'name' => [['Liam', 'Noah', 'Emma']],\n                        'sales' => [\n                            ['month' => '01', 'qty' => [[1, 2, 3]], 'amount' => [100, 101, 102]],\n                            ['month' => '02', 'qty' => [[1, 2, 3]], 'amount' => [200, 201, 202]],\n                            ['month' => '03', 'qty' => [[1, 2, 3]], 'amount' => [300, 301, 302]],\n                        ],\n                    ],\n                ],\n\n                'merge_cells' => ['B1:C1', 'A2:A4', 'B2:C2', 'B3:C3', 'B4:C4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => [\n                            'B' => 'Liam',\n                            'D' => 'Noah',\n                            'F' => 'Emma',\n                        ],\n                        2 => [\n                            'A' => '01',\n                            'B' => 100,\n                            'D' => 101,\n                            'F' => 102,\n                        ],\n                        3 => [\n                            'B' => 1,\n                            'D' => 2,\n                            'F' => 3,\n                        ],\n                        4 => [\n                            'B' => 'Hello',\n                        ],\n                        5 => [\n                            'A' => '02',\n                            'B' => 200,\n                            'D' => 201,\n                            'F' => 202,\n                        ],\n                        6 => [\n                            'B' => 1,\n                            'D' => 2,\n                            'F' => 3,\n                        ],\n                        7 => [\n                            'B' => 'Hello',\n                        ],\n                        8 => [\n                            'A' => '03',\n                            'B' => 300,\n                            'D' => 301,\n                            'F' => 302,\n                        ],\n                        9 => [\n                            'B' => 1,\n                            'D' => 2,\n                            'F' => 3,\n                        ],\n                        10 => [\n                            'B' => 'Hello',\n                        ],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 5, 'qty' => 6],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A5'],\n                        ['from' => 'A2', 'to' => 'A8'],\n                        ['from' => 'B1', 'to' => 'D1'],\n                        ['from' => 'B1', 'to' => 'F1'],\n                        ['from' => 'B2', 'to' => 'B5'],\n                        ['from' => 'B2', 'to' => 'B8'],\n                        ['from' => 'B2', 'to' => 'D2'],\n                        ['from' => 'B2', 'to' => 'F2'],\n                        ['from' => 'B3', 'to' => 'B6'],\n                        ['from' => 'B3', 'to' => 'B9'],\n                        ['from' => 'B3', 'to' => 'D3'],\n                        ['from' => 'B3', 'to' => 'F3'],\n                        ['from' => 'B4', 'to' => 'B7'],\n                        ['from' => 'B4', 'to' => 'B10'],\n                        ['from' => 'D2', 'to' => 'D5'],\n                        ['from' => 'D2', 'to' => 'D8'],\n                        ['from' => 'D3', 'to' => 'D6'],\n                        ['from' => 'D3', 'to' => 'D9'],\n                        ['from' => 'F2', 'to' => 'F5'],\n                        ['from' => 'F2', 'to' => 'F8'],\n                        ['from' => 'F3', 'to' => 'F6'],\n                        ['from' => 'F3', 'to' => 'F9'],\n                    ],\n\n                    'merge_cells' => [\n                        'D1:E1', 'F1:G1',\n                        'D2:E2', 'F2:G2',\n                        'A5:A7', 'B5:C5', 'D5:E5', 'F5:G5',\n                        'A8:A10', 'B8:C8', 'D8:E8', 'F8:G8',\n                        'D3:E3', 'F3:G3',\n                        'B6:C6', 'D6:E6', 'F6:G6',\n                        'B9:C9', 'D9:E9', 'F9:G9',\n                        'B7:C7', 'B10:C10',\n                    ],\n\n                    'copy_width' => [\n                        ['from' => 'B', 'to' => 'D'],\n                        ['from' => 'B', 'to' => 'F'],\n                        ['from' => 'C', 'to' => 'E'],\n                        ['from' => 'C', 'to' => 'G'],\n                    ],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_list_long()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[foo1]',\n                        'B' => '[bar1]',\n                        'C' => 'foo1',\n                        'D' => 'bar1',\n                    ],\n                    10 => [\n                        'A' => '[foo2]',\n                        'B' => '[bar2]',\n                        'C' => 'foo2',\n                        'D' => 'bar2',\n                    ],\n                ],\n\n                'data' => [\n                    'foo1' => [\n                        'foo1-1', 'foo1-2', 'foo1-3', 'foo1-4', 'foo1-5', 'foo1-6', 'foo1-7', 'foo1-8',\n                        'foo1-9', 'foo1-10', 'foo1-11', 'foo1-12', 'foo1-13', 'foo1-14', 'foo1-15', 'foo1-16',\n                    ],\n                    'bar1' => [\n                        'bar1-1', 'bar1-2', 'bar1-3', 'bar1-4', 'bar1-5', 'bar1-6', 'bar1-7', 'bar1-8',\n                        'bar1-9', 'bar1-10', 'bar1-11', 'bar1-12', 'bar1-13', 'bar1-14', 'bar1-15', 'bar1-16',\n                    ],\n\n                    'foo2' => [\n                        'foo2-1', 'foo2-2', 'foo2-3', 'foo2-4', 'foo2-5', 'foo2-6', 'foo2-7', 'foo2-8',\n                        'foo2-9', 'foo2-10', 'foo2-11', 'foo2-12', 'foo2-13', 'foo2-14', 'foo2-15', 'foo2-16',\n                    ],\n                    'bar2' => [\n                        'bar2-1', 'bar2-2', 'bar2-3', 'bar2-4', 'bar2-5', 'bar2-6', 'bar2-7', 'bar2-8',\n                        'bar2-9', 'bar2-10', 'bar2-11', 'bar2-12', 'bar2-13', 'bar2-14', 'bar2-15', 'bar2-16',\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'foo1-1', 'B' => 'bar1-1', 'C' => 'foo1', 'D' => 'bar1'],\n                        2 => ['A' => 'foo1-2', 'B' => 'bar1-2', 'C' => 'foo1', 'D' => 'bar1'],\n                        3 => ['A' => 'foo1-3', 'B' => 'bar1-3', 'C' => 'foo1', 'D' => 'bar1'],\n                        4 => ['A' => 'foo1-4', 'B' => 'bar1-4', 'C' => 'foo1', 'D' => 'bar1'],\n                        5 => ['A' => 'foo1-5', 'B' => 'bar1-5', 'C' => 'foo1', 'D' => 'bar1'],\n                        6 => ['A' => 'foo1-6', 'B' => 'bar1-6', 'C' => 'foo1', 'D' => 'bar1'],\n                        7 => ['A' => 'foo1-7', 'B' => 'bar1-7', 'C' => 'foo1', 'D' => 'bar1'],\n                        8 => ['A' => 'foo1-8', 'B' => 'bar1-8', 'C' => 'foo1', 'D' => 'bar1'],\n                        9 => ['A' => 'foo1-9', 'B' => 'bar1-9', 'C' => 'foo1', 'D' => 'bar1'],\n                        10 => ['A' => 'foo1-10', 'B' => 'bar1-10', 'C' => 'foo1', 'D' => 'bar1'],\n                        11 => ['A' => 'foo1-11', 'B' => 'bar1-11', 'C' => 'foo1', 'D' => 'bar1'],\n                        12 => ['A' => 'foo1-12', 'B' => 'bar1-12', 'C' => 'foo1', 'D' => 'bar1'],\n                        13 => ['A' => 'foo1-13', 'B' => 'bar1-13', 'C' => 'foo1', 'D' => 'bar1'],\n                        14 => ['A' => 'foo1-14', 'B' => 'bar1-14', 'C' => 'foo1', 'D' => 'bar1'],\n                        15 => ['A' => 'foo1-15', 'B' => 'bar1-15', 'C' => 'foo1', 'D' => 'bar1'],\n                        16 => ['A' => 'foo1-16', 'B' => 'bar1-16', 'C' => 'foo1', 'D' => 'bar1'],\n\n                        25 => ['A' => 'foo2-1', 'B' => 'bar2-1', 'C' => 'foo2', 'D' => 'bar2'],\n                        26 => ['A' => 'foo2-2', 'B' => 'bar2-2', 'C' => 'foo2', 'D' => 'bar2'],\n                        27 => ['A' => 'foo2-3', 'B' => 'bar2-3', 'C' => 'foo2', 'D' => 'bar2'],\n                        28 => ['A' => 'foo2-4', 'B' => 'bar2-4', 'C' => 'foo2', 'D' => 'bar2'],\n                        29 => ['A' => 'foo2-5', 'B' => 'bar2-5', 'C' => 'foo2', 'D' => 'bar2'],\n                        30 => ['A' => 'foo2-6', 'B' => 'bar2-6', 'C' => 'foo2', 'D' => 'bar2'],\n                        31 => ['A' => 'foo2-7', 'B' => 'bar2-7', 'C' => 'foo2', 'D' => 'bar2'],\n                        32 => ['A' => 'foo2-8', 'B' => 'bar2-8', 'C' => 'foo2', 'D' => 'bar2'],\n                        33 => ['A' => 'foo2-9', 'B' => 'bar2-9', 'C' => 'foo2', 'D' => 'bar2'],\n                        34 => ['A' => 'foo2-10', 'B' => 'bar2-10', 'C' => 'foo2', 'D' => 'bar2'],\n                        35 => ['A' => 'foo2-11', 'B' => 'bar2-11', 'C' => 'foo2', 'D' => 'bar2'],\n                        36 => ['A' => 'foo2-12', 'B' => 'bar2-12', 'C' => 'foo2', 'D' => 'bar2'],\n                        37 => ['A' => 'foo2-13', 'B' => 'bar2-13', 'C' => 'foo2', 'D' => 'bar2'],\n                        38 => ['A' => 'foo2-14', 'B' => 'bar2-14', 'C' => 'foo2', 'D' => 'bar2'],\n                        39 => ['A' => 'foo2-15', 'B' => 'bar2-15', 'C' => 'foo2', 'D' => 'bar2'],\n                        40 => ['A' => 'foo2-16', 'B' => 'bar2-16', 'C' => 'foo2', 'D' => 'bar2'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 15],\n                        ['action' => 'add', 'row' => 26, 'qty' => 15],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A16'],\n                        ['from' => 'A25', 'to' => 'A26:A40'],\n                        ['from' => 'B1', 'to' => 'B2:B16'],\n                        ['from' => 'B25', 'to' => 'B26:B40'],\n                        ['from' => 'C1', 'to' => 'C2:C16'],\n                        ['from' => 'C25', 'to' => 'C26:C40'],\n                        ['from' => 'D1', 'to' => 'D2:D16'],\n                        ['from' => 'D25', 'to' => 'D26:D40'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_several_tables1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[tableOne.a]',\n                        'C' => '',\n                        'D' => '[tableOne.d]',\n                    ],\n                    3 => [\n                        'A' => '[tableTwo.a]',\n                        'C' => '',\n                        'D' => '[tableTwo.d]',\n                    ],\n                    5 => [\n                        'A' => '[tableThree.a]',\n                        'C' => '',\n                        'D' => '[tableThree.d]',\n                    ],\n                ],\n\n                'data' => [\n                    'tableOne' => [\n                        'a' => ['one-a-1', 'one-a-2', 'one-a-3'],\n                        'd' => ['one-d-1', 'one-d-2', 'one-d-3'],\n                    ],\n                    'tableTwo' => [\n                        'a' => ['two-a-1', 'two-a-2', 'two-a-3'],\n                        'd' => ['two-d-1', 'two-d-2', 'two-d-3'],\n                    ],\n                    'tableThree' => [\n                        'a' => ['three-a-1', 'three-a-2', 'three-a-3'],\n                        'd' => ['three-d-1', 'three-d-2', 'three-d-3'],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'one-a-1', 'D' => 'one-d-1'],\n                        2 => ['A' => 'one-a-2', 'D' => 'one-d-2'],\n                        3 => ['A' => 'one-a-3', 'D' => 'one-d-3'],\n                        5 => ['A' => 'two-a-1', 'D' => 'two-d-1'],\n                        6 => ['A' => 'two-a-2', 'D' => 'two-d-2'],\n                        7 => ['A' => 'two-a-3', 'D' => 'two-d-3'],\n                        9 => ['A' => 'three-a-1', 'D' => 'three-d-1'],\n                        10 => ['A' => 'three-a-2', 'D' => 'three-d-2'],\n                        11 => ['A' => 'three-a-3', 'D' => 'three-d-3'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                        ['action' => 'add', 'row' => 6, 'qty' => 2],\n                        ['action' => 'add', 'row' => 10, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'A5', 'to' => 'A6:A7'],\n                        ['from' => 'A9', 'to' => 'A10:A11'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'B5', 'to' => 'B6:B7'],\n                        ['from' => 'B9', 'to' => 'B10:B11'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'C5', 'to' => 'C6:C7'],\n                        ['from' => 'C9', 'to' => 'C10:C11'],\n                        ['from' => 'D1', 'to' => 'D2:D3'],\n                        ['from' => 'D5', 'to' => 'D6:D7'],\n                        ['from' => 'D9', 'to' => 'D10:D11'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_several_tables2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[tableOne.a]',\n                        'C' => null,\n                        'D' => '[tableOne.d]',\n                    ],\n                    3 => [\n                        'A' => '[tableTwo.a]',\n                        'C' => null,\n                        'D' => '[tableTwo.d]',\n                    ],\n                    5 => [\n                        'A' => '[tableThree.a]',\n                        'C' => null,\n                        'D' => '[tableThree.d]',\n                    ],\n                ],\n\n                'data' => [\n                    'tableOne' => [\n                        'a' => ['one-a-1'],\n                        'd' => ['one-d-1'],\n                    ],\n                    'tableTwo' => [],\n                    'tableThree' => [\n                        'a' => ['three-a-1', 'three-a-2', 'three-a-3'],\n                        'd' => ['three-d-1', 'three-d-2', 'three-d-3'],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'one-a-1', 'D' => 'one-d-1'],\n                        3 => ['A' => null, 'D' => null],\n                        5 => ['A' => 'three-a-1', 'D' => 'three-d-1'],\n                        6 => ['A' => 'three-a-2', 'D' => 'three-d-2'],\n                        7 => ['A' => 'three-a-3', 'D' => 'three-d-3'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 6, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A5', 'to' => 'A6:A7'],\n                        ['from' => 'B5', 'to' => 'B6:B7'],\n                        ['from' => 'C5', 'to' => 'C6:C7'],\n                        ['from' => 'D5', 'to' => 'D6:D7'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_several_tables3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[tableOne.a]',\n                        'B' => '[tableOne.b]',\n                    ],\n                    3 => [\n                        'A' => '[tableTwo.a]',\n                        'B' => '[tableTwo.b]',\n                    ],\n                    5 => [\n                        'A' => '[tableThree.a]',\n                        'B' => '[tableThree.b]',\n                    ],\n                ],\n\n                'data' => [\n                    'tableOne' => [\n                        'a' => ['one-a-1', 'one-a-2', 'one-a-3'],\n                        'b' => ['one-b-1', 'one-b-2', 'one-b-3'],\n                    ],\n                    'tableTwo' => [\n                        'a' => ['two-a-1', 'two-a-2', 'two-a-3'],\n                        'b' => ['two-b-1', 'two-b-2', 'two-b-3'],\n                    ],\n                    'tableThree' => [\n                        'a' => ['three-a-1', 'three-a-2', 'three-a-3'],\n                        'b' => ['three-b-1', 'three-b-2', 'three-b-3'],\n                    ],\n                ],\n\n                'merge_cells' => ['A1:A2', 'B1:B2', 'A3:A4', 'B3:B4', 'A5:A6', 'B5:B6'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'one-a-1', 'B' => 'one-b-1'],\n                        3 => ['A' => 'one-a-2', 'B' => 'one-b-2'],\n                        5 => ['A' => 'one-a-3', 'B' => 'one-b-3'],\n                        7 => ['A' => 'two-a-1', 'B' => 'two-b-1'],\n                        9 => ['A' => 'two-a-2', 'B' => 'two-b-2'],\n                        11 => ['A' => 'two-a-3', 'B' => 'two-b-3'],\n                        13 => ['A' => 'three-a-1', 'B' => 'three-b-1'],\n                        15 => ['A' => 'three-a-2', 'B' => 'three-b-2'],\n                        17 => ['A' => 'three-a-3', 'B' => 'three-b-3'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 4],\n                        ['action' => 'add', 'row' => 9, 'qty' => 4],\n                        ['action' => 'add', 'row' => 15, 'qty' => 4],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A3'],\n                        ['from' => 'A1', 'to' => 'A5'],\n                        ['from' => 'A7', 'to' => 'A9'],\n                        ['from' => 'A7', 'to' => 'A11'],\n                        ['from' => 'A13', 'to' => 'A15'],\n                        ['from' => 'A13', 'to' => 'A17'],\n                        ['from' => 'B1', 'to' => 'B3'],\n                        ['from' => 'B1', 'to' => 'B5'],\n                        ['from' => 'B7', 'to' => 'B9'],\n                        ['from' => 'B7', 'to' => 'B11'],\n                        ['from' => 'B13', 'to' => 'B15'],\n                        ['from' => 'B13', 'to' => 'B17'],\n                    ],\n\n                    'merge_cells' => [\n                        'A3:A4', 'B3:B4',\n                        'A5:A6', 'B5:B6',\n                        'A9:A10', 'B9:B10',\n                        'A11:A12', 'B11:B12',\n                        'A15:A16', 'B15:B16',\n                        'A17:A18', 'B17:B18',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_several_tables4()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[tableOne.a] [= tableOne]',\n                        'B' => '[tableOne.b]',\n                    ],\n                    3 => [\n                        'A' => '[tableTwo.a] [= tableTwo]',\n                        'B' => '[tableTwo.b]',\n                    ],\n                    4 => [\n                        'A' => '[tableThree.a] [= tableThree]',\n                        'B' => '[tableThree.b]',\n                    ],\n                ],\n\n                'data' => [\n                    'tableOne' => [\n                        'a' => ['one-a-1', 'one-a-2', 'one-a-3'],\n                        'b' => ['one-b-1', 'one-b-2', 'one-b-3'],\n                    ],\n                    'tableTwo' => [],\n                    'tableThree' => [\n                        'a' => ['three-a-1', 'three-a-2', 'three-a-3'],\n                        'b' => ['three-b-1', 'three-b-2', 'three-b-3'],\n                    ],\n                ],\n\n                'merge_cells' => ['A1:A2', 'B1:B2', 'A4:A5', 'B4:B5'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'one-a-1', 'B' => 'one-b-1'],\n                        3 => ['A' => 'one-a-2', 'B' => 'one-b-2'],\n                        5 => ['A' => 'one-a-3', 'B' => 'one-b-3'],\n                        7 => ['A' => 'three-a-1', 'B' => 'three-b-1'],\n                        9 => ['A' => 'three-a-2', 'B' => 'three-b-2'],\n                        11 => ['A' => 'three-a-3', 'B' => 'three-b-3'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 4],\n                        ['action' => 'delete', 'row' => 7, 'qty' => 1],\n                        ['action' => 'add', 'row' => 9, 'qty' => 4],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A3'],\n                        ['from' => 'A1', 'to' => 'A5'],\n                        ['from' => 'A7', 'to' => 'A9'],\n                        ['from' => 'A7', 'to' => 'A11'],\n                        ['from' => 'B1', 'to' => 'B3'],\n                        ['from' => 'B1', 'to' => 'B5'],\n                        ['from' => 'B7', 'to' => 'B9'],\n                        ['from' => 'B7', 'to' => 'B11'],\n                    ],\n\n                    'merge_cells' => [\n                        'A3:A4', 'B3:B4',\n                        'A5:A6', 'B5:B6',\n                        'A9:A10', 'B9:B10',\n                        'A11:A12', 'B11:B12',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[table.a] / [table.b] [$= table.a]',\n                        'B' => 'foo',\n                        'D' => 'bar',\n                    ],\n                    2 => [\n                        'A' => 'baz',\n                        'D' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        'a' => ['one-a-1', null, 'one-a-3'],\n                        'b' => ['one-b-1', 'one-b-2', 'one-b-3'],\n                    ],\n                ],\n\n                'merge_cells' => ['B1:C1', 'D1:F1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'one-a-1 / one-b-1', 'B' => 'foo', 'D' => 'bar'],\n                        2 => ['A' => null, 'B' => 'foo', 'D' => 'bar'],\n                        3 => ['A' => 'one-a-3 / one-b-3', 'B' => 'foo', 'D' => 'bar'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'D1', 'to' => 'D2:D3'],\n                    ],\n\n                    'merge_cells' => [\n                        'B2:C2', 'D2:F2',\n                        'B3:C3', 'D3:F3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'Hello',\n                        'B' => '[table.a]',\n                        'C' => 'bar 1',\n                        'D' => 'bar 2',\n                        'F' => 'bar 3',\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        'a' => ['one', 'two', 'three'],\n                    ],\n                ],\n\n                'merge_cells' => ['A1:A3', 'D1:E1', 'F1:H1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'Hello', 'B' => 'one', 'C' => 'bar 1', 'D' => 'bar 2', 'F' => 'bar 3'],\n                        2 => ['B' => 'two', 'C' => 'bar 1', 'D' => 'bar 2', 'F' => 'bar 3'],\n                        3 => ['B' => 'three', 'C' => 'bar 1', 'D' => 'bar 2', 'F' => 'bar 3'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'D1', 'to' => 'D2:D3'],\n                        ['from' => 'F1', 'to' => 'F2:F3'],\n                    ],\n\n                    'merge_cells' => [\n                        'D2:E2', 'F2:H2',\n                        'D3:E3', 'F3:H3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'Hello',\n                        'C' => '[table.a]',\n                        'D' => 0,\n                        'E' => '0',\n                        'F' => null,\n                        'G' => '',\n                        'H' => '=ROW()-5',\n                    ],\n                    2 => [\n                        'A' => '[foo]',\n                        'B' => '[bar]',\n                        'C' => 1,\n                        'D' => '1',\n                        'H' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        'a' => ['one', 'two', 'three'],\n                    ],\n                    'bar' => 'hello',\n                ],\n\n                'merge_cells' => ['A1:B3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 'Hello', 'C' => 'one', 'D' => 0, 'E' => '0', 'H' => '=ROW()-5'],\n                        2 => ['C' => 'two', 'D' => 0, 'E' => '0', 'H' => '=ROW()-5'],\n                        3 => ['C' => 'three', 'D' => 0, 'E' => '0', 'H' => '=ROW()-5'],\n                        4 => ['A' => null, 'B' => 'hello'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'D1', 'to' => 'D2:D3'],\n                        ['from' => 'E1', 'to' => 'E2:E3'],\n                        ['from' => 'F1', 'to' => 'F2:F3'],\n                        ['from' => 'G1', 'to' => 'G2:G3'],\n                        ['from' => 'H1', 'to' => 'H2:H3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge4()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => 'Hello',\n                        'B' => null,\n                    ],\n                    2 => [\n                        'B' => '[table.a]',\n                    ],\n                    4 => [\n                        'A' => '[bar]',\n                        'B' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        'a' => ['one', 'two', 'three'],\n                    ],\n                    'bar' => 'hello',\n                ],\n\n                'merge_cells' => ['A1:B3'],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => null,\n                    ],\n                    2 => [\n                        'B' => '[table.a]',\n                    ],\n                    4 => [\n                        'A' => '[bar]'\n                    ],\n                ],\n\n                'data' => [\n                    'table' => [\n                        'a' => ['one', 'two', 'three'],\n                    ],\n                    'bar' => 'hello',\n                ],\n\n                'merge_cells' => ['A1:B3'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        2 => ['B' => 'one'],\n                        3 => ['B' => 'two'],\n                        4 => ['B' => 'three'],\n                        6 => ['A' => 'hello'],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 3, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'B2', 'to' => 'B3:B4'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge5()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(B3:B3)',\n                        'B' => null,\n                    ],\n                    2 => [\n                        'A' => 'Foo',\n                        'B' => null,\n                    ],\n                    3 => [\n                        'B' => '[product.amount]'\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'amount' => [111, 222, 333],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:A4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => '=SUM(B3:B5)'],\n                        3 => ['B' => 111],\n                        4 => ['B' => 222],\n                        5 => ['B' => 333],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 4, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'B3', 'to' => 'B4:B5'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_merge6()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '=SUM(B3:B3)',\n                        'B' => null,\n                    ],\n                    2 => [\n                        'A' => '[product.id]',\n                        'B' => null,\n                    ],\n                    3 => [\n                        'B' => '[product.amount]'\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                        'amount' => [111, 222, 333],\n                    ],\n                ],\n\n                'merge_cells' => ['A2:A4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => '=SUM(B3:B3)'],\n                        2 => ['A' => 1],\n                        3 => ['B' => 111],\n                        5 => ['A' => 2],\n                        6 => ['B' => 222],\n                        8 => ['A' => 3],\n                        9 => ['B' => 333],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 5, 'qty' => 6],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A2', 'to' => 'A5'],\n                        ['from' => 'A2', 'to' => 'A8'],\n                        ['from' => 'B2', 'to' => 'B5'],\n                        ['from' => 'B2', 'to' => 'B8'],\n                        ['from' => 'B3', 'to' => 'B6'],\n                        ['from' => 'B3', 'to' => 'B9'],\n                    ],\n\n                    'merge_cells' => ['A5:A7', 'A8:A10'],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_alias1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id.*]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        ['id' => 1],\n                        ['id' => 2],\n                        ['id' => 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.*.id]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        ['id' => 1],\n                        ['id' => 2],\n                        ['id' => 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 1],\n                        2 => ['A' => 2],\n                        3 => ['A' => 3],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                    ],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_alias2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.*.id]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id.*]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        ['id' => 1],\n                        ['id' => 2],\n                        ['id' => 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => null],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_alias3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id.2]',\n                    ],\n                    2 => [\n                        'A' => '[product.id.1]',\n                    ],\n                    3 => [\n                        'A' => '[product.id.0]',\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => [],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 3],\n                        2 => ['A' => 2],\n                        3 => ['A' => 1],\n                    ],\n\n                    'rows' => [],\n\n                    'copy_style' => [],\n\n                    'merge_cells' => [],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles1()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => ['C1:D1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 1],\n                        2 => ['A' => 2],\n                        3 => ['A' => 3],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'E1', 'to' => 'E2:E3'],\n                    ],\n\n                    'merge_cells' => [\n                        'C2:D2', 'C3:D3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles2()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id]',\n                        'D' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => ['C1:D1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 1],\n                        2 => ['A' => 2],\n                        3 => ['A' => 3],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                    ],\n\n                    'merge_cells' => [\n                        'C2:D2', 'C3:D3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles3()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'B' => '[product.id]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => ['C1:D1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['B' => 1],\n                        2 => ['B' => 2],\n                        3 => ['B' => 3],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'E1', 'to' => 'E2:E3'],\n                    ],\n\n                    'merge_cells' => [\n                        'C2:D2', 'C3:D3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles4()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[product.id]',\n                        'BA' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'product' => [\n                        'id' => [1, 2, 3],\n                    ],\n                ],\n\n                'merge_cells' => ['AB1:BA1'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['A' => 1],\n                        2 => ['A' => 2],\n                        3 => ['A' => 3],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'AA1', 'to' => 'AA2:AA3'],\n                        ['from' => 'AB1', 'to' => 'AB2:AB3'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'D1', 'to' => 'D2:D3'],\n                        ['from' => 'E1', 'to' => 'E2:E3'],\n                        ['from' => 'F1', 'to' => 'F2:F3'],\n                        ['from' => 'G1', 'to' => 'G2:G3'],\n                        ['from' => 'H1', 'to' => 'H2:H3'],\n                        ['from' => 'I1', 'to' => 'I2:I3'],\n                        ['from' => 'J1', 'to' => 'J2:J3'],\n                        ['from' => 'K1', 'to' => 'K2:K3'],\n                        ['from' => 'L1', 'to' => 'L2:L3'],\n                        ['from' => 'M1', 'to' => 'M2:M3'],\n                        ['from' => 'N1', 'to' => 'N2:N3'],\n                        ['from' => 'O1', 'to' => 'O2:O3'],\n                        ['from' => 'P1', 'to' => 'P2:P3'],\n                        ['from' => 'Q1', 'to' => 'Q2:Q3'],\n                        ['from' => 'R1', 'to' => 'R2:R3'],\n                        ['from' => 'S1', 'to' => 'S2:S3'],\n                        ['from' => 'T1', 'to' => 'T2:T3'],\n                        ['from' => 'U1', 'to' => 'U2:U3'],\n                        ['from' => 'V1', 'to' => 'V2:V3'],\n                        ['from' => 'W1', 'to' => 'W2:W3'],\n                        ['from' => 'X1', 'to' => 'X2:X3'],\n                        ['from' => 'Y1', 'to' => 'Y2:Y3'],\n                        ['from' => 'Z1', 'to' => 'Z2:Z3'],\n                    ],\n\n                    'merge_cells' => [\n                        'AB2:BA2', 'AB3:BA3',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles5()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'C' => '[foo.id]',\n                        'E' => null,\n                    ],\n                    2 => [\n                        'A' => '[bar.id]',\n                        'E' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => [\n                        'id' => [1, 2, 3],\n                    ],\n                    'bar' => [\n                        'id' => [4, 5, 6],\n                    ],\n                ],\n\n                'merge_cells' => ['C1:E1', 'A2:D2'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['C' => 1],\n                        2 => ['C' => 2],\n                        3 => ['C' => 3],\n                        4 => ['A' => 4],\n                        5 => ['A' => 5],\n                        6 => ['A' => 6],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                        ['action' => 'add', 'row' => 5, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'A4', 'to' => 'A5:A6'],\n                        ['from' => 'B1', 'to' => 'B2:B3'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'E4', 'to' => 'E5:E6'],\n                    ],\n\n                    'merge_cells' => [\n                        'C2:E2', 'C3:E3',\n                        'A5:D5', 'A6:D6',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n\n    /**\n     * @return void\n     */\n    public function test_schema_styles6()\n    {\n        $data = [\n            [\n                'values' => [\n                    1 => [\n                        'A' => '[= baz]',\n                        'H' => null,\n                    ],\n                    2 => [\n                        'C' => '[foo.id]',\n                        'H' => null,\n                    ],\n                    4 => [\n                        'A' => '[bar.id]',\n                        'H' => null,\n                    ],\n                ],\n\n                'data' => [\n                    'foo' => [\n                        'id' => [1, 2, 3],\n                    ],\n                    'bar' => [\n                        'id' => [4, 5, 6],\n                    ],\n                    '' => 'baz',\n                ],\n\n                'merge_cells' => ['A2:B2', 'C2:G2', 'A4:H4'],\n            ],\n        ];\n\n        foreach ($data as $id => $item) {\n            $this->assertSame(\n                [\n                    'data' => [\n                        1 => ['C' => 1],\n                        2 => ['C' => 2],\n                        3 => ['C' => 3],\n                        5 => ['A' => 4],\n                        6 => ['A' => 5],\n                        7 => ['A' => 6],\n                    ],\n\n                    'rows' => [\n                        ['action' => 'delete', 'row' => 1, 'qty' => 1],\n                        ['action' => 'add', 'row' => 2, 'qty' => 2],\n                        ['action' => 'add', 'row' => 6, 'qty' => 2],\n                    ],\n\n                    'copy_style' => [\n                        ['from' => 'A1', 'to' => 'A2:A3'],\n                        ['from' => 'A5', 'to' => 'A6:A7'],\n                        ['from' => 'C1', 'to' => 'C2:C3'],\n                        ['from' => 'H1', 'to' => 'H2:H3'],\n                    ],\n\n                    'merge_cells' => [\n                        'A2:B2', 'C2:G2', 'A3:B3', 'C3:G3',\n                        'A6:H6', 'A7:H7',\n                    ],\n\n                    'copy_width' => [],\n                ],\n                $this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),\n                \"$id\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "tests/TraitsTest.php",
    "content": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass TraitsTest extends \\PHPUnit\\Framework\\TestCase\n{\n    use \\AnourValar\\Office\\Traits\\Parser;\n\n    /**\n     * @return void\n     */\n    public function test_isColumnLE()\n    {\n        $this->assertTrue($this->isColumnLE('A', 'B'));\n        $this->assertTrue($this->isColumnLE('B', 'B'));\n        $this->assertFalse($this->isColumnLE('C', 'B'));\n\n        $this->assertTrue($this->isColumnLE('Z', 'AA'));\n        $this->assertTrue($this->isColumnLE('AA', 'AA'));\n        $this->assertFalse($this->isColumnLE('AB', 'AA'));\n\n        $this->assertFalse($this->isColumnLE('AAA', 'ZZ'));\n        $this->assertTrue($this->isColumnLE('AAA', 'AAA'));\n        $this->assertTrue($this->isColumnLE('AAA', 'AAB'));\n    }\n\n    /**\n     * @return void\n     */\n    public function test_isColumnGE()\n    {\n        $this->assertFalse($this->isColumnGE('A', 'B'));\n        $this->assertTrue($this->isColumnGE('B', 'B'));\n        $this->assertTrue($this->isColumnGE('C', 'B'));\n\n        $this->assertFalse($this->isColumnGE('Z', 'AA'));\n        $this->assertTrue($this->isColumnGE('AA', 'AA'));\n        $this->assertTrue($this->isColumnGE('AB', 'AA'));\n\n        $this->assertTrue($this->isColumnGE('AAA', 'ZZ'));\n        $this->assertTrue($this->isColumnGE('AAA', 'AAA'));\n        $this->assertFalse($this->isColumnGE('AAA', 'AAB'));\n    }\n\n    /**\n     * @return void\n     */\n    public function test_sort()\n    {\n        $columns = ['BA' => 7,  'AZ' => 6, 'A' => 1, 'B' => 2, 'Z' => 3, 'AA' => 4, 'AB' => 5];\n\n        uksort($columns, fn ($a, $b) => $this->isColumnLE($a, $b) ? -1 : 1);\n\n        $this->assertSame(\n            ['A' => 1, 'B' => 2, 'Z' => 3, 'AA' => 4, 'AB' => 5, 'AZ' => 6, 'BA' => 7],\n            $columns\n        );\n    }\n}\n"
  }
]