diff --git a/.travis.yml b/.travis.yml
index 52b729e..b23c7fd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,8 @@ install:
script:
- mkdir -p build/logs
- - php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
+ - php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover
after_script:
- - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi
+ - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi
+ - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi
diff --git a/README.md b/README.md
index 6bb73aa..a34c365 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ $reader->setEncoding('UTF-16LE');
The writer always generate CSV files encoded in UTF-8, with a BOM.
-### Configuring the XLSX and ODS writers
+### Configuring the XLSX and ODS readers and writers
#### Row styling
@@ -163,7 +163,6 @@ Font | Bold | `StyleBuilder::setFontBold()`
| Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
-
#### New sheet creation
It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet:
@@ -208,6 +207,20 @@ $writer->setShouldUseInlineStrings(false); // will use shared strings
> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings!
+#### Date/Time formatting
+
+When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects.
+It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet.
+
+```php
+use Box\Spout\Reader\ReaderFactory;
+use Box\Spout\Common\Type;
+
+$reader = ReaderFactory::create(Type::XLSX);
+$reader->setShouldFormatDates(false); // default value
+$reader->setShouldFormatDates(true); // will return formatted dates
+```
+
### Playing with sheets
When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet:
diff --git a/composer.json b/composer.json
index 46c412e..e88e2c8 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,7 @@
"ext-simplexml": "*"
},
"require-dev": {
- "phpunit/phpunit": ">=3.7",
- "scrutinizer/ocular": "~1.1"
+ "phpunit/phpunit": "^4.8.0"
},
"suggest": {
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
diff --git a/composer.lock b/composer.lock
index ae8bb33..df7690a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,77 +4,10 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "0866be323931eaa6e9431b3bbf0a817a",
+ "hash": "8957b9da742e28d7250c02fca8f9a5a7",
+ "content-hash": "973b8a4a1d8c520dd99fcd32cb5e022f",
"packages": [],
"packages-dev": [
- {
- "name": "doctrine/annotations",
- "version": "v1.2.6",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/annotations.git",
- "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/annotations/zipball/f4a91702ca3cd2e568c3736aa031ed00c3752af4",
- "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4",
- "shasum": ""
- },
- "require": {
- "doctrine/lexer": "1.*",
- "php": ">=5.3.2"
- },
- "require-dev": {
- "doctrine/cache": "1.*",
- "phpunit/phpunit": "4.*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.3.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Doctrine\\Common\\Annotations\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Benjamin Eberlei",
- "email": "kontakt@beberlei.de"
- },
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
- },
- {
- "name": "Jonathan Wage",
- "email": "jonwage@gmail.com"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
- }
- ],
- "description": "Docblock Annotations Parser",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "annotations",
- "docblock",
- "parser"
- ],
- "time": "2015-06-17 12:21:22"
- },
{
"name": "doctrine/instantiator",
"version": "1.0.5",
@@ -129,362 +62,6 @@
],
"time": "2015-06-14 21:17:01"
},
- {
- "name": "doctrine/lexer",
- "version": "v1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/lexer.git",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.2"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Doctrine\\Common\\Lexer\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
- }
- ],
- "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "lexer",
- "parser"
- ],
- "time": "2014-09-09 13:34:57"
- },
- {
- "name": "guzzle/guzzle",
- "version": "v3.9.3",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/guzzle3.git",
- "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9",
- "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9",
- "shasum": ""
- },
- "require": {
- "ext-curl": "*",
- "php": ">=5.3.3",
- "symfony/event-dispatcher": "~2.1"
- },
- "replace": {
- "guzzle/batch": "self.version",
- "guzzle/cache": "self.version",
- "guzzle/common": "self.version",
- "guzzle/http": "self.version",
- "guzzle/inflection": "self.version",
- "guzzle/iterator": "self.version",
- "guzzle/log": "self.version",
- "guzzle/parser": "self.version",
- "guzzle/plugin": "self.version",
- "guzzle/plugin-async": "self.version",
- "guzzle/plugin-backoff": "self.version",
- "guzzle/plugin-cache": "self.version",
- "guzzle/plugin-cookie": "self.version",
- "guzzle/plugin-curlauth": "self.version",
- "guzzle/plugin-error-response": "self.version",
- "guzzle/plugin-history": "self.version",
- "guzzle/plugin-log": "self.version",
- "guzzle/plugin-md5": "self.version",
- "guzzle/plugin-mock": "self.version",
- "guzzle/plugin-oauth": "self.version",
- "guzzle/service": "self.version",
- "guzzle/stream": "self.version"
- },
- "require-dev": {
- "doctrine/cache": "~1.3",
- "monolog/monolog": "~1.0",
- "phpunit/phpunit": "3.7.*",
- "psr/log": "~1.0",
- "symfony/class-loader": "~2.1",
- "zendframework/zend-cache": "2.*,<2.3",
- "zendframework/zend-log": "2.*,<2.3"
- },
- "suggest": {
- "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated."
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.9-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Guzzle": "src/",
- "Guzzle\\Tests": "tests/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "Guzzle Community",
- "homepage": "https://github.com/guzzle/guzzle/contributors"
- }
- ],
- "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle",
- "homepage": "http://guzzlephp.org/",
- "keywords": [
- "client",
- "curl",
- "framework",
- "http",
- "http client",
- "rest",
- "web service"
- ],
- "time": "2015-03-18 18:23:50"
- },
- {
- "name": "jms/metadata",
- "version": "1.5.1",
- "source": {
- "type": "git",
- "url": "https://github.com/schmittjoh/metadata.git",
- "reference": "22b72455559a25777cfd28c4ffda81ff7639f353"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/22b72455559a25777cfd28c4ffda81ff7639f353",
- "reference": "22b72455559a25777cfd28c4ffda81ff7639f353",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "require-dev": {
- "doctrine/cache": "~1.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.5.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Metadata\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache"
- ],
- "authors": [
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com",
- "homepage": "https://github.com/schmittjoh",
- "role": "Developer of wrapped JMSSerializerBundle"
- }
- ],
- "description": "Class/method/property metadata management in PHP",
- "keywords": [
- "annotations",
- "metadata",
- "xml",
- "yaml"
- ],
- "time": "2014-07-12 07:13:19"
- },
- {
- "name": "jms/parser-lib",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/schmittjoh/parser-lib.git",
- "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d",
- "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d",
- "shasum": ""
- },
- "require": {
- "phpoption/phpoption": ">=0.9,<2.0-dev"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "JMS\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache2"
- ],
- "description": "A library for easily creating recursive-descent parsers.",
- "time": "2012-11-18 18:08:43"
- },
- {
- "name": "jms/serializer",
- "version": "0.16.0",
- "source": {
- "type": "git",
- "url": "https://github.com/schmittjoh/serializer.git",
- "reference": "c8a171357ca92b6706e395c757f334902d430ea9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/c8a171357ca92b6706e395c757f334902d430ea9",
- "reference": "c8a171357ca92b6706e395c757f334902d430ea9",
- "shasum": ""
- },
- "require": {
- "doctrine/annotations": "1.*",
- "jms/metadata": "~1.1",
- "jms/parser-lib": "1.*",
- "php": ">=5.3.2",
- "phpcollection/phpcollection": "~0.1"
- },
- "require-dev": {
- "doctrine/orm": "~2.1",
- "doctrine/phpcr-odm": "~1.0.1",
- "jackalope/jackalope-doctrine-dbal": "1.0.*",
- "propel/propel1": "~1.7",
- "symfony/filesystem": "2.*",
- "symfony/form": "~2.1",
- "symfony/translation": "~2.0",
- "symfony/validator": "~2.0",
- "symfony/yaml": "2.*",
- "twig/twig": ">=1.8,<2.0-dev"
- },
- "suggest": {
- "symfony/yaml": "Required if you'd like to serialize data to YAML format."
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "0.15-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "JMS\\Serializer": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache2"
- ],
- "authors": [
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com",
- "homepage": "https://github.com/schmittjoh",
- "role": "Developer of wrapped JMSSerializerBundle"
- }
- ],
- "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.",
- "homepage": "http://jmsyst.com/libs/serializer",
- "keywords": [
- "deserialization",
- "jaxb",
- "json",
- "serialization",
- "xml"
- ],
- "time": "2014-03-18 08:39:00"
- },
- {
- "name": "phpcollection/phpcollection",
- "version": "0.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/schmittjoh/php-collection.git",
- "reference": "b8bf55a0a929ca43b01232b36719f176f86c7e83"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/b8bf55a0a929ca43b01232b36719f176f86c7e83",
- "reference": "b8bf55a0a929ca43b01232b36719f176f86c7e83",
- "shasum": ""
- },
- "require": {
- "phpoption/phpoption": "1.*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "0.3-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "PhpCollection": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache2"
- ],
- "authors": [
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com",
- "homepage": "https://github.com/schmittjoh",
- "role": "Developer of wrapped JMSSerializerBundle"
- }
- ],
- "description": "General-Purpose Collection Library for PHP",
- "keywords": [
- "collection",
- "list",
- "map",
- "sequence",
- "set"
- ],
- "time": "2014-03-11 13:46:42"
- },
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.4",
@@ -534,73 +111,26 @@
],
"time": "2015-02-03 12:10:50"
},
- {
- "name": "phpoption/phpoption",
- "version": "1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/schmittjoh/php-option.git",
- "reference": "5d099bcf0393908bf4ad69cc47dafb785d51f7f5"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/5d099bcf0393908bf4ad69cc47dafb785d51f7f5",
- "reference": "5d099bcf0393908bf4ad69cc47dafb785d51f7f5",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.3-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "PhpOption\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache2"
- ],
- "authors": [
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com",
- "homepage": "https://github.com/schmittjoh",
- "role": "Developer of wrapped JMSSerializerBundle"
- }
- ],
- "description": "Option Type for PHP",
- "keywords": [
- "language",
- "option",
- "php",
- "type"
- ],
- "time": "2014-01-09 22:37:17"
- },
{
"name": "phpspec/prophecy",
- "version": "v1.5.0",
+ "version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7"
+ "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7",
- "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
+ "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
+ "php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "~2.0",
- "sebastian/comparator": "~1.1"
+ "sebastian/comparator": "~1.1",
+ "sebastian/recursion-context": "~1.0"
},
"require-dev": {
"phpspec/phpspec": "~2.0"
@@ -608,7 +138,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4.x-dev"
+ "dev-master": "1.5.x-dev"
}
},
"autoload": {
@@ -641,20 +171,20 @@
"spy",
"stub"
],
- "time": "2015-08-13 10:07:40"
+ "time": "2016-02-15 07:46:21"
},
{
"name": "phpunit/php-code-coverage",
- "version": "2.2.2",
+ "version": "2.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c"
+ "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c",
- "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
+ "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"shasum": ""
},
"require": {
@@ -703,7 +233,7 @@
"testing",
"xunit"
],
- "time": "2015-08-04 03:42:39"
+ "time": "2015-10-06 15:47:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -795,21 +325,24 @@
},
{
"name": "phpunit/php-timer",
- "version": "1.0.7",
+ "version": "1.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b"
+ "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
- "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260",
+ "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
+ "require-dev": {
+ "phpunit/phpunit": "~4|~5"
+ },
"type": "library",
"autoload": {
"classmap": [
@@ -832,20 +365,20 @@
"keywords": [
"timer"
],
- "time": "2015-06-21 08:01:12"
+ "time": "2016-05-12 18:03:57"
},
{
"name": "phpunit/php-token-stream",
- "version": "1.4.6",
+ "version": "1.4.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b"
+ "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3ab72c62e550370a6cd5dc873e1a04ab57562f5b",
- "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
+ "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
"shasum": ""
},
"require": {
@@ -881,20 +414,20 @@
"keywords": [
"tokenizer"
],
- "time": "2015-08-16 08:51:00"
+ "time": "2015-09-15 10:49:45"
},
{
"name": "phpunit/phpunit",
- "version": "4.8.5",
+ "version": "4.8.26",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc"
+ "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b7417edaf28059ea63d86be941e6004dbfcc0cc",
- "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74",
+ "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74",
"shasum": ""
},
"require": {
@@ -908,7 +441,7 @@
"phpunit/php-code-coverage": "~2.1",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
- "phpunit/php-timer": ">=1.0.6",
+ "phpunit/php-timer": "^1.0.6",
"phpunit/phpunit-mock-objects": "~2.3",
"sebastian/comparator": "~1.1",
"sebastian/diff": "~1.2",
@@ -953,20 +486,20 @@
"testing",
"xunit"
],
- "time": "2015-08-19 09:20:57"
+ "time": "2016-05-17 03:09:28"
},
{
"name": "phpunit/phpunit-mock-objects",
- "version": "2.3.7",
+ "version": "2.3.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "5e2645ad49d196e020b85598d7c97e482725786a"
+ "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5e2645ad49d196e020b85598d7c97e482725786a",
- "reference": "5e2645ad49d196e020b85598d7c97e482725786a",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
+ "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"shasum": ""
},
"require": {
@@ -1009,43 +542,7 @@
"mock",
"xunit"
],
- "time": "2015-08-19 09:14:08"
- },
- {
- "name": "scrutinizer/ocular",
- "version": "1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/scrutinizer-ci/ocular.git",
- "reference": "8e0a8c7f085bc4857bd52132833679dcfd504fc1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/scrutinizer-ci/ocular/zipball/8e0a8c7f085bc4857bd52132833679dcfd504fc1",
- "reference": "8e0a8c7f085bc4857bd52132833679dcfd504fc1",
- "shasum": ""
- },
- "require": {
- "guzzle/guzzle": "~3.0",
- "jms/serializer": "~0.13",
- "phpoption/phpoption": "~1.0",
- "symfony/console": "~2.0",
- "symfony/process": "~2.3"
- },
- "require-dev": {
- "symfony/filesystem": "~2.0"
- },
- "bin": [
- "bin/ocular"
- ],
- "type": "library",
- "autoload": {
- "psr-0": {
- "Scrutinizer\\Ocular\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "time": "2014-04-12 20:46:35"
+ "time": "2015-10-02 06:51:40"
},
{
"name": "sebastian/comparator",
@@ -1113,28 +610,28 @@
},
{
"name": "sebastian/diff",
- "version": "1.3.0",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3"
+ "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3",
- "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
+ "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
- "phpunit/phpunit": "~4.2"
+ "phpunit/phpunit": "~4.8"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.3-dev"
+ "dev-master": "1.4-dev"
}
},
"autoload": {
@@ -1157,24 +654,24 @@
}
],
"description": "Diff implementation",
- "homepage": "http://www.github.com/sebastianbergmann/diff",
+ "homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff"
],
- "time": "2015-02-22 15:13:53"
+ "time": "2015-12-08 07:14:41"
},
{
"name": "sebastian/environment",
- "version": "1.3.2",
+ "version": "1.3.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44"
+ "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44",
- "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716",
+ "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716",
"shasum": ""
},
"require": {
@@ -1211,7 +708,7 @@
"environment",
"hhvm"
],
- "time": "2015-08-03 06:14:51"
+ "time": "2016-05-17 03:18:57"
},
{
"name": "sebastian/exporter",
@@ -1281,16 +778,16 @@
},
{
"name": "sebastian/global-state",
- "version": "1.0.0",
+ "version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01"
+ "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
- "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
+ "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
"shasum": ""
},
"require": {
@@ -1328,20 +825,20 @@
"keywords": [
"global state"
],
- "time": "2014-10-06 09:23:50"
+ "time": "2015-10-12 03:26:01"
},
{
"name": "sebastian/recursion-context",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "994d4a811bafe801fb06dccbee797863ba2792ba"
+ "reference": "913401df809e99e4f47b27cdd781f4a258d58791"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba",
- "reference": "994d4a811bafe801fb06dccbee797863ba2792ba",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
+ "reference": "913401df809e99e4f47b27cdd781f4a258d58791",
"shasum": ""
},
"require": {
@@ -1381,7 +878,7 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2015-06-21 08:04:50"
+ "time": "2015-11-11 19:50:13"
},
{
"name": "sebastian/version",
@@ -1418,200 +915,36 @@
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2015-06-21 13:59:46"
},
- {
- "name": "symfony/console",
- "version": "v2.7.3",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/Console.git",
- "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e",
- "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.9"
- },
- "require-dev": {
- "psr/log": "~1.0",
- "symfony/event-dispatcher": "~2.1",
- "symfony/phpunit-bridge": "~2.7",
- "symfony/process": "~2.1"
- },
- "suggest": {
- "psr/log": "For using the console logger",
- "symfony/event-dispatcher": "",
- "symfony/process": ""
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.7-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Console\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Console Component",
- "homepage": "https://symfony.com",
- "time": "2015-07-28 15:18:12"
- },
- {
- "name": "symfony/event-dispatcher",
- "version": "v2.7.3",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/EventDispatcher.git",
- "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3",
- "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.9"
- },
- "require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "~2.0,>=2.0.5",
- "symfony/dependency-injection": "~2.6",
- "symfony/expression-language": "~2.6",
- "symfony/phpunit-bridge": "~2.7",
- "symfony/stopwatch": "~2.3"
- },
- "suggest": {
- "symfony/dependency-injection": "",
- "symfony/http-kernel": ""
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.7-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\EventDispatcher\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony EventDispatcher Component",
- "homepage": "https://symfony.com",
- "time": "2015-06-18 19:21:56"
- },
- {
- "name": "symfony/process",
- "version": "v2.7.3",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/Process.git",
- "reference": "48aeb0e48600321c272955132d7606ab0a49adb3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/Process/zipball/48aeb0e48600321c272955132d7606ab0a49adb3",
- "reference": "48aeb0e48600321c272955132d7606ab0a49adb3",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.9"
- },
- "require-dev": {
- "symfony/phpunit-bridge": "~2.7"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.7-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Process\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Process Component",
- "homepage": "https://symfony.com",
- "time": "2015-07-01 11:25:50"
- },
{
"name": "symfony/yaml",
- "version": "v2.7.3",
+ "version": "v2.8.6",
"source": {
"type": "git",
- "url": "https://github.com/symfony/Yaml.git",
- "reference": "71340e996171474a53f3d29111d046be4ad8a0ff"
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff",
- "reference": "71340e996171474a53f3d29111d046be4ad8a0ff",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
+ "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
- "require-dev": {
- "symfony/phpunit-bridge": "~2.7"
- },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7-dev"
+ "dev-master": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
- }
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -1629,7 +962,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
- "time": "2015-07-28 14:07:07"
+ "time": "2016-03-29 19:00:15"
}
],
"aliases": [],
diff --git a/src/Spout/Reader/AbstractReader.php b/src/Spout/Reader/AbstractReader.php
index d6d38e2..cb476ab 100644
--- a/src/Spout/Reader/AbstractReader.php
+++ b/src/Spout/Reader/AbstractReader.php
@@ -19,6 +19,9 @@ abstract class AbstractReader implements ReaderInterface
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
+ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
+ protected $shouldFormatDates = false;
+
/**
* Returns whether stream wrappers are supported
*
@@ -49,7 +52,7 @@ abstract class AbstractReader implements ReaderInterface
abstract protected function closeReader();
/**
- * @param $globalFunctionsHelper
+ * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
* @return AbstractReader
*/
public function setGlobalFunctionsHelper($globalFunctionsHelper)
@@ -58,6 +61,18 @@ abstract class AbstractReader implements ReaderInterface
return $this;
}
+ /**
+ * Sets whether date/time values should be returned as PHP objects or be formatted as strings.
+ *
+ * @param bool $shouldFormatDates
+ * @return AbstractReader
+ */
+ public function setShouldFormatDates($shouldFormatDates)
+ {
+ $this->shouldFormatDates = $shouldFormatDates;
+ return $this;
+ }
+
/**
* Prepares the reader to read the given file. It also makes sure
* that the file exists and is readable.
diff --git a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
index bd21576..b39af21 100644
--- a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
+++ b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
@@ -23,6 +23,7 @@ class CellValueFormatter
/** Definition of XML nodes names used to parse data */
const XML_NODE_P = 'p';
const XML_NODE_S = 'text:s';
+ const XML_NODE_A = 'text:a';
/** Definition of XML attribute used to parse data */
const XML_ATTRIBUTE_TYPE = 'office:value-type';
@@ -33,14 +34,19 @@ class CellValueFormatter
const XML_ATTRIBUTE_CURRENCY = 'office:currency';
const XML_ATTRIBUTE_C = 'text:c';
+ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
+ protected $shouldFormatDates;
+
/** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */
protected $escaper;
/**
- *
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*/
- public function __construct()
+ public function __construct($shouldFormatDates)
{
+ $this->shouldFormatDates = $shouldFormatDates;
+
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
$this->escaper = new \Box\Spout\Common\Escaper\ODS();
}
@@ -98,6 +104,8 @@ class CellValueFormatter
$spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
$numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1;
$currentPValue .= str_repeat(' ', $numSpaces);
+ } else if ($childNode->nodeName === self::XML_NODE_A) {
+ $currentPValue .= $childNode->nodeValue;
}
}
@@ -119,6 +127,7 @@ class CellValueFormatter
{
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
$nodeIntValue = intval($nodeValue);
+ // The "==" is intentionally not a "===" because only the value matters, not the type
$cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
return $cellValue;
}
@@ -141,15 +150,27 @@ class CellValueFormatter
* Returns the cell Date value from the given node.
*
* @param \DOMNode $node
- * @return \DateTime|null The value associated with the cell or NULL if invalid date value
+ * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
*/
protected function formatDateCellValue($node)
{
- try {
- $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
- return new \DateTime($nodeValue);
- } catch (\Exception $e) {
- return null;
+ // The XML node looks like this:
+ //
+ // 05/19/16 04:39 PM
+ //
+
+ if ($this->shouldFormatDates) {
+ // The date is already formatted in the "p" tag
+ $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
+ return $nodeWithValueAlreadyFormatted->nodeValue;
+ } else {
+ // otherwise, get it from the "date-value" attribute
+ try {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
+ return new \DateTime($nodeValue);
+ } catch (\Exception $e) {
+ return null;
+ }
}
}
@@ -157,15 +178,27 @@ class CellValueFormatter
* Returns the cell Time value from the given node.
*
* @param \DOMNode $node
- * @return \DateInterval|null The value associated with the cell or NULL if invalid time value
+ * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value
*/
protected function formatTimeCellValue($node)
{
- try {
- $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
- return new \DateInterval($nodeValue);
- } catch (\Exception $e) {
- return null;
+ // The XML node looks like this:
+ //
+ // 01:24:00 PM
+ //
+
+ if ($this->shouldFormatDates) {
+ // The date is already formatted in the "p" tag
+ $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
+ return $nodeWithValueAlreadyFormatted->nodeValue;
+ } else {
+ // otherwise, get it from the "time-value" attribute
+ try {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
+ return new \DateInterval($nodeValue);
+ } catch (\Exception $e) {
+ return null;
+ }
}
}
diff --git a/src/Spout/Reader/ODS/Reader.php b/src/Spout/Reader/ODS/Reader.php
index b4093ae..a52bafa 100644
--- a/src/Spout/Reader/ODS/Reader.php
+++ b/src/Spout/Reader/ODS/Reader.php
@@ -42,7 +42,7 @@ class Reader extends AbstractReader
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
- $this->sheetIterator = new SheetIterator($filePath);
+ $this->sheetIterator = new SheetIterator($filePath, $this->shouldFormatDates);
} else {
throw new IOException("Could not open $filePath for reading.");
}
diff --git a/src/Spout/Reader/ODS/RowIterator.php b/src/Spout/Reader/ODS/RowIterator.php
index aa7a496..e91ad90 100644
--- a/src/Spout/Reader/ODS/RowIterator.php
+++ b/src/Spout/Reader/ODS/RowIterator.php
@@ -45,11 +45,12 @@ class RowIterator implements IteratorInterface
/**
* @param XMLReader $xmlReader XML Reader, positioned on the "" element
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*/
- public function __construct($xmlReader)
+ public function __construct($xmlReader, $shouldFormatDates)
{
$this->xmlReader = $xmlReader;
- $this->cellValueFormatter = new CellValueFormatter();
+ $this->cellValueFormatter = new CellValueFormatter($shouldFormatDates);
}
/**
@@ -186,7 +187,7 @@ class RowIterator implements IteratorInterface
/**
* empty() replacement that honours 0 as a valid value
*
- * @param $value The cell value
+ * @param string|int|float|bool|\DateTime|\DateInterval|null $value The cell value
* @return bool
*/
protected function isEmptyCellValue($value)
diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php
index c78e4aa..98d00b1 100644
--- a/src/Spout/Reader/ODS/Sheet.php
+++ b/src/Spout/Reader/ODS/Sheet.php
@@ -27,12 +27,13 @@ class Sheet implements SheetInterface
/**
* @param XMLReader $xmlReader XML Reader, positioned on the "" element
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
*/
- public function __construct($xmlReader, $sheetIndex, $sheetName)
+ public function __construct($xmlReader, $shouldFormatDates, $sheetIndex, $sheetName)
{
- $this->rowIterator = new RowIterator($xmlReader);
+ $this->rowIterator = new RowIterator($xmlReader, $shouldFormatDates);
$this->index = $sheetIndex;
$this->name = $sheetName;
}
diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php
index f8683f0..d0010bd 100644
--- a/src/Spout/Reader/ODS/SheetIterator.php
+++ b/src/Spout/Reader/ODS/SheetIterator.php
@@ -22,6 +22,9 @@ class SheetIterator implements IteratorInterface
/** @var string $filePath Path of the file to be read */
protected $filePath;
+ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
+ protected $shouldFormatDates;
+
/** @var XMLReader The XMLReader object that will help read sheet's XML data */
protected $xmlReader;
@@ -36,11 +39,13 @@ class SheetIterator implements IteratorInterface
/**
* @param string $filePath Path of the file to be read
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
- public function __construct($filePath)
+ public function __construct($filePath, $shouldFormatDates)
{
$this->filePath = $filePath;
+ $this->shouldFormatDates = $shouldFormatDates;
$this->xmlReader = new XMLReader();
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
@@ -109,7 +114,7 @@ class SheetIterator implements IteratorInterface
$escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
$sheetName = $this->escaper->unescape($escapedSheetName);
- return new Sheet($this->xmlReader, $sheetName, $this->currentSheetIndex);
+ return new Sheet($this->xmlReader, $this->shouldFormatDates, $sheetName, $this->currentSheetIndex);
}
/**
diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
index e63384d..286d348 100644
--- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
+++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
@@ -29,6 +29,8 @@ class CellValueFormatter
/** Constants used for date formatting */
const NUM_SECONDS_IN_ONE_DAY = 86400;
+ const NUM_SECONDS_IN_ONE_HOUR = 3600;
+ const NUM_SECONDS_IN_ONE_MINUTE = 60;
/**
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
@@ -42,17 +44,22 @@ class CellValueFormatter
/** @var StyleHelper Helper to work with styles */
protected $styleHelper;
+ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
+ protected $shouldFormatDates;
+
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
protected $escaper;
/**
* @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
* @param StyleHelper $styleHelper Helper to work with styles
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*/
- public function __construct($sharedStringsHelper, $styleHelper)
+ public function __construct($sharedStringsHelper, $styleHelper, $shouldFormatDates)
{
$this->sharedStringsHelper = $sharedStringsHelper;
$this->styleHelper = $styleHelper;
+ $this->shouldFormatDates = $shouldFormatDates;
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
$this->escaper = new \Box\Spout\Common\Escaper\XLSX();
@@ -166,7 +173,7 @@ class CellValueFormatter
$shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
if ($shouldFormatAsDate) {
- return $this->formatExcelTimestampValue(floatval($nodeValue));
+ return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId);
} else {
$nodeIntValue = intval($nodeValue);
return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
@@ -176,33 +183,86 @@ class CellValueFormatter
/**
* Returns a cell's PHP Date value, associated to the given timestamp.
* NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
+ * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1.
*
* @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
* @return \DateTime|null The value associated with the cell or NULL if invalid date value
*/
- protected function formatExcelTimestampValue($nodeValue)
+ protected function formatExcelTimestampValue($nodeValue, $cellStyleId)
{
// Fix for the erroneous leap year in Excel
if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
--$nodeValue;
}
- // The value 1.0 represents 1900-01-01. Numbers below 1.0 are not valid Excel dates.
- if ($nodeValue < 1.0) {
+ if ($nodeValue >= 1) {
+ // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01.
+ return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId);
+ } else if ($nodeValue >= 0) {
+ // Values between 0 and 1 represent "times".
+ return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId);
+ } else {
+ // invalid date
return null;
}
+ }
+ /**
+ * Returns a cell's PHP DateTime value, associated to the given timestamp.
+ * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date).
+ *
+ * @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return \DateTime|string The value associated with the cell
+ */
+ protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId)
+ {
+ $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY);
+ $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR);
+ $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE);
+ $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE);
+
+ // using the base Excel date (Jan 1st, 1900) - not relevant here
+ $dateObj = new \DateTime('1900-01-01');
+ $dateObj->setTime($hours, $minutes, $seconds);
+
+ if ($this->shouldFormatDates) {
+ $styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId);
+ $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat);
+ return $dateObj->format($phpDateFormat);
+ } else {
+ return $dateObj;
+ }
+ }
+
+ /**
+ * Returns a cell's PHP Date value, associated to the given timestamp.
+ * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
+ *
+ * @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+ */
+ protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId)
+ {
// Do not use any unix timestamps for calculation to prevent
// issues with numbers exceeding 2^31.
$secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
$secondsRemainder = round($secondsRemainder, 0);
try {
- $cellValue = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
- $cellValue->modify('+' . intval($nodeValue) . 'days');
- $cellValue->modify('+' . $secondsRemainder . 'seconds');
+ $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
+ $dateObj->modify('+' . intval($nodeValue) . 'days');
+ $dateObj->modify('+' . $secondsRemainder . 'seconds');
- return $cellValue;
+ if ($this->shouldFormatDates) {
+ $styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId);
+ $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat);
+ return $dateObj->format($phpDateFormat);
+ } else {
+ return $dateObj;
+ }
} catch (\Exception $e) {
return null;
}
diff --git a/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php b/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
new file mode 100644
index 0000000..4acbef7
--- /dev/null
+++ b/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
@@ -0,0 +1,122 @@
+ [
+ // Time
+ 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem
+ ':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month)
+ 'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month)
+ 'ss' => 's', // Seconds, with leading zeros
+ '.s' => '', // Ignore (fractional seconds format does not exist in PHP)
+
+ // Date
+ 'e' => 'Y', // Full numeric representation of a year, 4 digits
+ 'yyyy' => 'Y', // Full numeric representation of a year, 4 digits
+ 'yy' => 'y', // Two digit representation of a year
+ 'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...)
+ 'mmmm' => 'F', // Full textual representation of a month
+ 'mmm' => 'M', // Short textual representation of a month, three letters
+ 'mm' => 'm', // Numeric representation of a month, with leading zeros
+ 'm' => 'n', // Numeric representation of a month, without leading zeros
+ 'dddd' => 'l', // Full textual representation of the day of the week
+ 'ddd' => 'D', // Textual representation of a day, three letters
+ 'dd' => 'd', // Day of the month, 2 digits with leading zeros
+ 'd' => 'j', // Day of the month without leading zeros
+ ],
+ self::KEY_HOUR_12 => [
+ 'hh' => 'h', // 12-hour format of an hour without leading zeros
+ 'h' => 'g', // 12-hour format of an hour without leading zeros
+ ],
+ self::KEY_HOUR_24 => [
+ 'hh' => 'H', // 24-hour hours with leading zero
+ 'h' => 'G', // 24-hour format of an hour without leading zeros
+ ],
+ ];
+
+ /**
+ * Converts the given Excel date format to a format understandable by the PHP date function.
+ *
+ * @param string $excelDateFormat Excel date format
+ * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php)
+ */
+ public static function toPHPDateFormat($excelDateFormat)
+ {
+ // Remove brackets potentially present at the beginning of the format string
+ $dateFormat = preg_replace('/^(\[\$[^\]]+?\])/i', '', $excelDateFormat);
+
+ // Double quotes are used to escape characters that must not be interpreted.
+ // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
+ // By exploding the format string using double quote as a delimiter, we can get all parts
+ // that must be transformed (even indexes) and all parts that must not be (odd indexes).
+ $dateFormatParts = explode('"', $dateFormat);
+
+ foreach ($dateFormatParts as $partIndex => $dateFormatPart) {
+ // do not look at odd indexes
+ if ($partIndex % 2 === 1) {
+ continue;
+ }
+
+ // Make sure all characters are lowercase, as the mapping table is using lowercase characters
+ $transformedPart = strtolower($dateFormatPart);
+
+ // Remove escapes related to non-format characters
+ $transformedPart = str_replace('\\', '', $transformedPart);
+
+ // Apply general transformation first...
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]);
+
+ // ... then apply hour transformation, for 12-hour or 24-hour format
+ if (self::has12HourFormatMarker($dateFormatPart)) {
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]);
+ } else {
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]);
+ }
+
+ // overwrite the parts array with the new transformed part
+ $dateFormatParts[$partIndex] = $transformedPart;
+ }
+
+ // Merge all transformed parts back together
+ $phpDateFormat = implode('"', $dateFormatParts);
+
+ // Finally, to have the date format compatible with the DateTime::format() function, we need to escape
+ // all characters that are inside double quotes (and double quotes must be removed).
+ // For instance, ["Day " dd] should become [\D\a\y\ dd]
+ $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) {
+ $stringToEscape = $matches[1];
+ $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
+ return '\\' . implode('\\', $letters);
+ }, $phpDateFormat);
+
+ return $phpDateFormat;
+ }
+
+ /**
+ * @param string $excelDateFormat Date format as defined by Excel
+ * @return bool Whether the given date format has the 12-hour format marker
+ */
+ private static function has12HourFormatMarker($excelDateFormat)
+ {
+ return (stripos($excelDateFormat, 'am/pm') !== false);
+ }
+}
diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php
index 3400509..5f74f44 100644
--- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php
+++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php
@@ -14,18 +14,13 @@ use Box\Spout\Reader\XLSX\Sheet;
class SheetHelper
{
/** Paths of XML files relative to the XLSX file root */
- const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
/** Namespaces for the XML files */
- const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships';
const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
- /** Value of the Override attribute used in [Content_Types].xml to define sheets */
- const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
-
/** @var string Path of the XLSX file being read */
protected $filePath;
@@ -35,6 +30,9 @@ class SheetHelper
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
+ /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
+ protected $shouldFormatDates;
+
/** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml.rels file */
protected $workbookXMLRelsAsXMLElement;
@@ -45,12 +43,14 @@ class SheetHelper
* @param string $filePath Path of the XLSX file being read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*/
- public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
+ public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates)
{
$this->filePath = $filePath;
$this->sharedStringsHelper = $sharedStringsHelper;
$this->globalFunctionsHelper = $globalFunctionsHelper;
+ $this->shouldFormatDates = $shouldFormatDates;
}
/**
@@ -63,66 +63,52 @@ class SheetHelper
{
$sheets = [];
- $contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
- self::CONTENT_TYPES_XML_FILE_PATH,
- self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
- );
+ // Starting from "workbook.xml" as this file is the source of truth for the sheets order
+ $workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
+ $sheetNodes = $workbookXMLElement->xpath('//ns:sheet');
- // find all nodes defining a sheet
- $sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
- $numSheetNodes = count($sheetNodes);
-
- for ($i = 0; $i < $numSheetNodes; $i++) {
- $sheetNode = $sheetNodes[$i];
- $sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
-
- $sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath);
+ foreach ($sheetNodes as $sheetIndex => $sheetNode) {
+ $sheets[] = $this->getSheetFromSheetXMLNode($sheetNode, $sheetIndex);
}
- // make sure the sheets are sorted by index
- // (as the sheets are not necessarily in this order in the XML file)
- usort($sheets, function ($sheet1, $sheet2) {
- return ($sheet1->getIndex() - $sheet2->getIndex());
- });
-
return $sheets;
}
/**
- * Returns an instance of a sheet, given the path of its data XML file.
- * We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet.
- * Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
- * The entry contains the ID and name of the sheet.
+ * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
+ * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
+ * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
*
- * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
+ * @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $sheetNode XML Node describing the sheet, as defined in "workbook.xml"
+ * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
*/
- protected function getSheetFromXML($sheetDataXMLFilePath)
+ protected function getSheetFromSheetXMLNode($sheetNode, $sheetIndexZeroBased)
{
- // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
- // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
- $sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
-
- // find the node associated to the given file path
- $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
- $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
- $relationshipNode = $relationshipNodes[0];
-
- $relationshipSheetId = $relationshipNode->getAttribute('Id');
-
- $workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
- $sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $relationshipSheetId . '"]');
- $sheetNode = $sheetNodes[0];
+ // To retrieve namespaced attributes, some versions of LibXML will accept prefixing the attribute
+ // with the namespace directly (tested on LibXML 2.9.3). For older versions (tested on LibXML 2.7.8),
+ // attributes need to be retrieved without the namespace hint.
+ $sheetId = $sheetNode->getAttribute('r:id');
+ if ($sheetId === null) {
+ $sheetId = $sheetNode->getAttribute('id');
+ }
$escapedSheetName = $sheetNode->getAttribute('name');
- $sheetIdOneBased = $sheetNode->getAttribute('sheetId');
- $sheetIndexZeroBased = $sheetIdOneBased - 1;
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
$escaper = new \Box\Spout\Common\Escaper\XLSX();
$sheetName = $escaper->unescape($escapedSheetName);
- return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName);
+ // find the file path of the sheet, by looking at the "workbook.xml.res" file
+ $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
+ $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Id="' . $sheetId . '"]');
+ $relationshipNode = $relationshipNodes[0];
+
+ // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
+ // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
+ $sheetDataXMLFilePath = '/xl/' . $relationshipNode->getAttribute('Target');
+
+ return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $this->shouldFormatDates, $sheetIndexZeroBased, $sheetName);
}
/**
diff --git a/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/src/Spout/Reader/XLSX/Helper/StyleHelper.php
index 403d647..462433c 100644
--- a/src/Spout/Reader/XLSX/Helper/StyleHelper.php
+++ b/src/Spout/Reader/XLSX/Helper/StyleHelper.php
@@ -30,6 +30,25 @@ class StyleHelper
/** By convention, default style ID is 0 */
const DEFAULT_STYLE_ID = 0;
+ /**
+ * @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
+ * @var array Mapping between built-in numFmtId and the associated format - for dates only
+ */
+ protected static $builtinNumFmtIdToNumFormatMapping = [
+ 14 => 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy'
+ 15 => 'd-mmm-yy',
+ 16 => 'd-mmm',
+ 17 => 'mmm-yy',
+ 18 => 'h:mm AM/PM',
+ 19 => 'h:mm:ss AM/PM',
+ 20 => 'h:mm',
+ 21 => 'h:mm:ss',
+ 22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm',
+ 45 => 'mm:ss',
+ 46 => '[h]:mm:ss',
+ 47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0',
+ ];
+
/** @var string Path of the XLSX file being read */
protected $filePath;
@@ -171,18 +190,30 @@ class StyleHelper
protected function doesNumFmtIdIndicateDate($numFmtId)
{
return (
- $this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
- $this->isNumFmtIdCustomDateFormat($numFmtId)
+ !$this->doesNumFmtIdIndicateGeneralFormat($numFmtId) &&
+ (
+ $this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
+ $this->isNumFmtIdCustomDateFormat($numFmtId)
+ )
);
}
+ /**
+ * @param int $numFmtId
+ * @return bool Whether the number format ID indicates the "General" format (0 by convention)
+ */
+ protected function doesNumFmtIdIndicateGeneralFormat($numFmtId)
+ {
+ return ($numFmtId === 0);
+ }
+
/**
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a timestamp
*/
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
{
- $builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
+ $builtInDateFormatIds = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
return in_array($numFmtId, $builtInDateFormatIds);
}
@@ -223,4 +254,27 @@ class StyleHelper
return $hasFoundDateFormatCharacter;
}
+
+ /**
+ * Returns the format as defined in "styles.xml" of the given style.
+ * NOTE: It is assumed that the style DOES have a number format associated to it.
+ *
+ * @param int $styleId Zero-based style ID
+ * @return string The number format associated with the given style
+ */
+ public function getNumberFormat($styleId)
+ {
+ $stylesAttributes = $this->getStylesAttributes();
+ $styleAttributes = $stylesAttributes[$styleId];
+ $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
+
+ if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
+ $numberFormat = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
+ } else {
+ $customNumberFormats = $this->getCustomNumberFormats();
+ $numberFormat = $customNumberFormats[$numFmtId];
+ }
+
+ return $numberFormat;
+ }
}
diff --git a/src/Spout/Reader/XLSX/Reader.php b/src/Spout/Reader/XLSX/Reader.php
index 42c6f02..bcf02cc 100644
--- a/src/Spout/Reader/XLSX/Reader.php
+++ b/src/Spout/Reader/XLSX/Reader.php
@@ -69,7 +69,7 @@ class Reader extends AbstractReader
$this->sharedStringsHelper->extractSharedStrings();
}
- $this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper);
+ $this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper, $this->shouldFormatDates);
} else {
throw new IOException("Could not open $filePath for reading.");
}
diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php
index d1913bd..c7491ac 100644
--- a/src/Spout/Reader/XLSX/RowIterator.php
+++ b/src/Spout/Reader/XLSX/RowIterator.php
@@ -59,8 +59,9 @@ class RowIterator implements IteratorInterface
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
*/
- public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper)
+ public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates)
{
$this->filePath = $filePath;
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
@@ -68,7 +69,7 @@ class RowIterator implements IteratorInterface
$this->xmlReader = new XMLReader();
$this->styleHelper = new StyleHelper($filePath);
- $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper);
+ $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $shouldFormatDates);
}
/**
diff --git a/src/Spout/Reader/XLSX/Sheet.php b/src/Spout/Reader/XLSX/Sheet.php
index 85a4dc9..a1c7d95 100644
--- a/src/Spout/Reader/XLSX/Sheet.php
+++ b/src/Spout/Reader/XLSX/Sheet.php
@@ -25,12 +25,13 @@ class Sheet implements SheetInterface
* @param string $filePath Path of the XLSX file being read
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
* @param Helper\SharedStringsHelper Helper to work with shared strings
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
* @param string $sheetName Name of the sheet
*/
- public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName)
+ public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates, $sheetIndex, $sheetName)
{
- $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper);
+ $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates);
$this->index = $sheetIndex;
$this->name = $sheetName;
}
diff --git a/src/Spout/Reader/XLSX/SheetIterator.php b/src/Spout/Reader/XLSX/SheetIterator.php
index 7b3d3dd..f7a3f59 100644
--- a/src/Spout/Reader/XLSX/SheetIterator.php
+++ b/src/Spout/Reader/XLSX/SheetIterator.php
@@ -24,12 +24,13 @@ class SheetIterator implements IteratorInterface
* @param string $filePath Path of the file to be read
* @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+ * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
* @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
*/
- public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper)
+ public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates)
{
// Fetch all available sheets
- $sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper);
+ $sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates);
$this->sheets = $sheetHelper->getSheets();
if (count($this->sheets) === 0) {
diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php
index 8683459..759d842 100644
--- a/tests/Spout/Reader/ODS/ReaderTest.php
+++ b/tests/Spout/Reader/ODS/ReaderTest.php
@@ -164,6 +164,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows);
}
+ /**
+ * @return void
+ */
+ public function testReadShouldSupportFormatDatesAndTimesIfSpecified()
+ {
+ $shouldFormatDates = true;
+ $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.ods', $shouldFormatDates);
+
+ $expectedRows = [
+ ['05/19/2016', '5/19/16', '05/19/2016 16:39:00', '05/19/16 04:39 PM', '5/19/2016'],
+ ['11:29', '13:23:45', '01:23:45', '01:23:45 AM', '01:23:45 PM'],
+ ];
+ $this->assertEquals($expectedRows, $allRows);
+ }
+
/**
* @return void
*/
@@ -436,16 +451,35 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
}
+ /**
+ * https://github.com/box/spout/issues/218
+ * @return void
+ */
+ public function testReaderShouldReadTextInHyperlinks()
+ {
+ $allRows = $this->getAllRowsForFile('sheet_with_hyperlinks.ods');
+
+ $expectedRows = [
+ ['email', 'text'],
+ ['1@example.com', 'text'],
+ ['2@example.com', 'text and https://github.com/box/spout/issues/218 and text'],
+ ];
+
+ $this->assertEquals($expectedRows, $allRows, 'Text in hyperlinks should be read');
+ }
+
/**
* @param string $fileName
+ * @param bool|void $shouldFormatDates
* @return array All the read rows the given file
*/
- private function getAllRowsForFile($fileName)
+ private function getAllRowsForFile($fileName, $shouldFormatDates = false)
{
$allRows = [];
$resourcePath = $this->getResourcePath($fileName);
$reader = ReaderFactory::create(Type::ODS);
+ $reader->setShouldFormatDates($shouldFormatDates);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
index 73863ae..92831ab 100644
--- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
+++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
@@ -18,8 +18,11 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
[CellValueFormatter::CELL_TYPE_NUMERIC, 42429, '2016-02-29 00:00:00'],
[CellValueFormatter::CELL_TYPE_NUMERIC, '146098', '2299-12-31 00:00:00'],
[CellValueFormatter::CELL_TYPE_NUMERIC, -700, null],
- [CellValueFormatter::CELL_TYPE_NUMERIC, 0, null],
- [CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, null],
+ [CellValueFormatter::CELL_TYPE_NUMERIC, 0, '1900-01-01 00:00:00'],
+ [CellValueFormatter::CELL_TYPE_NUMERIC, 0.25, '1900-01-01 06:00:00'],
+ [CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, '1900-01-01 12:00:00'],
+ [CellValueFormatter::CELL_TYPE_NUMERIC, 0.75, '1900-01-01 18:00:00'],
+ [CellValueFormatter::CELL_TYPE_NUMERIC, 0.99999, '1900-01-01 23:59:59'],
[CellValueFormatter::CELL_TYPE_NUMERIC, 1, '1900-01-01 00:00:00'],
[CellValueFormatter::CELL_TYPE_NUMERIC, 59.999988425926, '1900-02-28 23:59:59'],
[CellValueFormatter::CELL_TYPE_NUMERIC, 60.458333333333, '1900-02-28 11:00:00'],
@@ -68,7 +71,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
->with(123)
->will($this->returnValue(true));
- $formatter = new CellValueFormatter(null, $styleHelperMock);
+ $formatter = new CellValueFormatter(null, $styleHelperMock, false);
$result = $formatter->extractAndFormatNodeValue($nodeMock);
if ($expectedDateAsString === null) {
@@ -117,7 +120,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
->method('shouldFormatNumericValueAsDate')
->will($this->returnValue(false));
- $formatter = new CellValueFormatter(null, $styleHelperMock);
+ $formatter = new CellValueFormatter(null, $styleHelperMock, false);
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0);
$this->assertEquals($expectedFormattedValue, $formattedValue);
@@ -160,7 +163,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE)
->will($this->returnValue($nodeListMock));
- $formatter = new CellValueFormatter(null, null);
+ $formatter = new CellValueFormatter(null, null, false);
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock);
$this->assertEquals($expectedFormattedValue, $formattedValue);
diff --git a/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php b/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php
new file mode 100644
index 0000000..b6d852c
--- /dev/null
+++ b/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php
@@ -0,0 +1,47 @@
+assertEquals($expectedPHPDateFormat, $phpDateFormat);
+ }
+}
diff --git a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php b/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php
index 3b8edff..57e8acb 100644
--- a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php
+++ b/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php
@@ -59,6 +59,16 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($shouldFormatAsDate);
}
+ /**
+ * @return void
+ */
+ public function testShouldFormatNumericValueAsDateWithGeneralFormat()
+ {
+ $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 0]]);
+ $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
+ $this->assertFalse($shouldFormatAsDate);
+ }
+
/**
* @return void
*/
diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php
index b1e6fdd..8620ed5 100644
--- a/tests/Spout/Reader/XLSX/ReaderTest.php
+++ b/tests/Spout/Reader/XLSX/ReaderTest.php
@@ -23,7 +23,7 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
{
return [
['/path/to/fake/file.xlsx'],
- ['file_with_no_sheets_in_content_types.xlsx'],
+ ['file_with_no_sheets_in_workbook_xml.xlsx'],
['file_with_sheet_xml_not_matching_content_types.xlsx'],
['file_corrupted.xlsx'],
];
@@ -181,6 +181,43 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows);
}
+ /**
+ * @return void
+ */
+ public function testReadShouldSupportDifferentTimesAsNumericTimestamp()
+ {
+ // make sure dates are always created with the same timezone
+ date_default_timezone_set('UTC');
+
+ $allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_times.xlsx');
+
+ $expectedRows = [
+ [
+ \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 00:00:00'),
+ \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 11:29:00'),
+ \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 23:29:00'),
+ \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 01:42:25'),
+ \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 13:42:25'),
+ ]
+ ];
+ $this->assertEquals($expectedRows, $allRows);
+ }
+
+ /**
+ * @return void
+ */
+ public function testReadShouldSupportFormatDatesAndTimesIfSpecified()
+ {
+ $shouldFormatDates = true;
+ $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.xlsx', $shouldFormatDates);
+
+ $expectedRows = [
+ ['1/13/2016', '01/13/2016', '13-Jan-16', 'Wednesday January 13, 16', 'Today is 1/13/2016'],
+ ['4:43:25', '04:43', '4:43', '4:43:25 AM', '4:43:25 PM'],
+ ];
+ $this->assertEquals($expectedRows, $allRows);
+ }
+
/**
* @return void
*/
@@ -481,14 +518,16 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
/**
* @param string $fileName
+ * @param bool|void $shouldFormatDates
* @return array All the read rows the given file
*/
- private function getAllRowsForFile($fileName)
+ private function getAllRowsForFile($fileName, $shouldFormatDates = false)
{
$allRows = [];
$resourcePath = $this->getResourcePath($fileName);
$reader = ReaderFactory::create(Type::XLSX);
+ $reader->setShouldFormatDates($shouldFormatDates);
$reader->open($resourcePath);
foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) {
diff --git a/tests/resources/ods/sheet_with_dates_and_times.ods b/tests/resources/ods/sheet_with_dates_and_times.ods
new file mode 100644
index 0000000..0e0fb5f
Binary files /dev/null and b/tests/resources/ods/sheet_with_dates_and_times.ods differ
diff --git a/tests/resources/ods/sheet_with_hyperlinks.ods b/tests/resources/ods/sheet_with_hyperlinks.ods
new file mode 100644
index 0000000..7246db5
Binary files /dev/null and b/tests/resources/ods/sheet_with_hyperlinks.ods differ
diff --git a/tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx b/tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx
deleted file mode 100644
index 597b230..0000000
Binary files a/tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx and /dev/null differ
diff --git a/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx b/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx
new file mode 100644
index 0000000..74de527
Binary files /dev/null and b/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx differ
diff --git a/tests/resources/xlsx/sheet_with_dates_and_times.xlsx b/tests/resources/xlsx/sheet_with_dates_and_times.xlsx
new file mode 100644
index 0000000..769e03b
Binary files /dev/null and b/tests/resources/xlsx/sheet_with_dates_and_times.xlsx differ
diff --git a/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx b/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx
new file mode 100644
index 0000000..e3ab29e
Binary files /dev/null and b/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx differ