diff --git a/.gitignore b/.gitignore index 5cd1641..85e5a9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ phpunit.xml -1 *~ phpunit +composer.lock + diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b8339..6bd199d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGE LOG +## [v0.3 Release]() + + * Changed CSVelte\Flavor to CSVelte\Dialect to be more consistent with CSVW + * Changed CSVelte\Taster to CSVelte\Sniffer because Taster no longer makes sense (also now more consistent with python csv package) + * Moved all Collection-related functionality into its own library (nozavroni/collect) + * Removed all CSVelte functions with the exception of `CSVelte\streamize`, which has been renamed to `CSVelte\to_stream`. + ## [v0.2.2 Release]() * Refactored CSVelte\Collection class, introducing a variety of specialized collection classes ([#141](https://github.com/nozavroni/csvelte/issues/141)). diff --git a/composer.json b/composer.json index 5601a25..a8c8a42 100755 --- a/composer.json +++ b/composer.json @@ -9,28 +9,33 @@ { "name": "Luke Visinoni", "email": "luke.visinoni@gmail.com", - "homepage": "https://plus.google.com/+LukeVisinoni", - "role": "Creator and Sole Developer" + "homepage": "http://luke.visinoni.net/" } ], + "support": { + "issues": "https://github.com/nozavroni/csvelte/issues" + }, "require": { - "php": ">=5.6" + "php": "^5.6 || ^7.0", + "danielstjules/stringy": "^3.1", + "nozavroni/collect": "^0.2.1" }, "require-dev": { "phpunit/phpunit": "^5.4", "mikey179/vfsStream": "^1.6", - "fzaninotto/Faker": "^1.6" + "fzaninotto/Faker": "^1.6", + "symfony/var-dumper": "^3.4" }, "autoload": { "psr-4": { - "CSVelte\\": ["src/CSVelte"] + "CSVelte\\": "src/CSVelte" }, "files": ["src/CSVelte/functions.php"] }, "autoload-dev": { "psr-4": { - "org\\": ["vendor/mikey179/vfsStream/src/main/php"], - "CSVelteTest\\": ["tests/CSVelte"] + "org\\": "vendor/mikey179/vfsStream/src/main/php", + "CSVelteTest\\": "tests/CSVelte" } } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 6561c90..0000000 --- a/composer.lock +++ /dev/null @@ -1,1423 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "899bfda7b502d3373c3c218998905878", - "content-hash": "b851a3fb62fec6c9e9ffa22d606be9ed", - "packages": [], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14 21:17:01" - }, - { - "name": "fzaninotto/faker", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/fzaninotto/Faker.git", - "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/44f9a286a04b80c76a4e5fb7aad8bb539b920123", - "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123", - "shasum": "" - }, - "require": { - "php": "^5.3.3|^7.0" - }, - "require-dev": { - "ext-intl": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" - }, - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "time": "2016-04-29 12:21:54" - }, - { - "name": "mikey179/vfsStream", - "version": "v1.6.4", - "source": { - "type": "git", - "url": "https://github.com/mikey179/vfsStream.git", - "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/0247f57b2245e8ad2e689d7cee754b45fbabd592", - "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "org\\bovigo\\vfs\\": "src/main/php" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Frank Kleine", - "homepage": "http://frankkleine.de/", - "role": "Developer" - } - ], - "description": "Virtual file system to mock the real file system in unit tests.", - "homepage": "http://vfs.bovigo.org/", - "time": "2016-07-18 14:02:57" - }, - { - "name": "myclabs/deep-copy", - "version": "1.5.5", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2016-10-31 17:19:45" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2015-12-27 11:43:31" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30 07:12:33" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.2.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2016-11-25 06:54:22" - }, - { - "name": "phpspec/prophecy", - "version": "v1.6.2", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", - "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1", - "sebastian/recursion-context": "^1.0|^2.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.0", - "phpunit/phpunit": "^4.8 || ^5.6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2016-11-21 14:58:47" - }, - { - "name": "phpunit/php-code-coverage", - "version": "4.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/903fd6318d0a90b4770a009ff73e4a4e9c437929", - "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "^1.4.2", - "sebastian/code-unit-reverse-lookup": "~1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "~1.0|~2.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.4.0", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2016-11-28 16:00:31" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2016-10-03 07:40:28" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21 13:50:34" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" - }, - "dist": { - "type": "zip", - "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": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2016-05-12 18:03:57" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", - "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2016-11-15 14:06:22" - }, - { - "name": "phpunit/phpunit", - "version": "5.7.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de164acc2f2bb0b79beb892a36260264b2a03233" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de164acc2f2bb0b79beb892a36260264b2a03233", - "reference": "de164acc2f2bb0b79beb892a36260264b2a03233", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.3", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", - "symfony/yaml": "~2.1|~3.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.7.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2016-12-09 02:48:53" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2016-12-08 20:27:08" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13 06:45:14" - }, - { - "name": "sebastian/comparator", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a1ed12e8b2409076ab22e3897126211ff8b1f7f", - "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2016-11-19 09:18:40" - }, - { - "name": "sebastian/diff", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2015-12-08 07:14:41" - }, - { - "name": "sebastian/environment", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-11-26 07:53:53" - }, - { - "name": "sebastian/exporter", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-11-19 08:54:04" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12 03:26:01" - }, - { - "name": "sebastian/object-enumerator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", - "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-11-19 07:35:10" - }, - { - "name": "sebastian/recursion-context", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19 07:33:16" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28 20:34:47" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03 07:35:21" - }, - { - "name": "symfony/yaml", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "f2300ba8fbb002c028710b92e1906e7457410693" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f2300ba8fbb002c028710b92e1906e7457410693", - "reference": "f2300ba8fbb002c028710b92e1906e7457410693", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "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 Yaml Component", - "homepage": "https://symfony.com", - "time": "2016-11-18 21:17:59" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23 20:04:58" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.6" - }, - "platform-dev": [] -} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 39af704..20fe3a2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,21 +11,12 @@ bootstrap="tests/bootstrap.php"> - tests/CSVelte/IO/StreamTest.php - tests/CSVelte/IO/ResourceTest.php - tests/CSVelte/Table/RowTest.php - tests/CSVelte/AutoloaderTest.php - tests/CSVelte/Collection/CollectionTest.php - tests/CSVelte/Collection/CharCollectionTest.php - tests/CSVelte/Collection/NumericCollectionTest.php - tests/CSVelte/Collection/MultiCollectionTest.php - tests/CSVelte/Collection/TabularCollectionTest.php - tests/CSVelte/CSVelteTest.php - tests/CSVelte/FlavorTest.php - tests/CSVelte/FunctionsTest.php + tests/CSVelte/DialectTest.php + tests/CSVelte/SnifferTest.php tests/CSVelte/ReaderTest.php - tests/CSVelte/TasterTest.php tests/CSVelte/WriterTest.php + tests/CSVelte/IO/StreamResourceTest.php + tests/CSVelte/IO/StreamTest.php diff --git a/src/CSVelte/Autoloader.php b/src/CSVelte/Autoloader.php deleted file mode 100644 index aea540f..0000000 --- a/src/CSVelte/Autoloader.php +++ /dev/null @@ -1,143 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte; - -/** - * CSVelte Autoloader. - * - * For those crazy silly people who aren't using Composer, simply include this - * file to have CSVelte's files auto-loaded by PHP. - * - * @package CSVelte - * @subpackage Autoloader - * - * @since v0.1 - */ -class Autoloader -{ - /** - * @var string Constant for namespace separator - */ - const NAMESPACE_SEPARATOR = '\\'; - - /** - * An array of paths that will be searched when attempting to load a class. - * - * @var array - */ - protected $paths; - - /** - * Autoloader Constructor. - * - * @param array $paths Paths to search for classes - */ - public function __construct($paths = []) - { - $this->paths = explode(PATH_SEPARATOR, get_include_path()); - foreach ($paths as $path) { - $this->addPath($path); - } - } - - /** - * Add path to list of search paths. - * - * Attempts to deduce the absolute (real) path from the path specified by the - * $path argument. If successful, the absolute path is added to the search - * path list and the method returns true. If one can't be found, it adds $path - * to the search path list, as-is and returns false - * - * @param string $path A path to add to the list of search paths - * - * @return bool - */ - public function addPath($path) - { - $paths = $this->getPaths(); - if ($rp = realpath($path)) { - if (in_array($rp, $paths)) { - return true; - } - $this->paths []= $rp; - - return true; - } - $this->paths []= $path; - - return false; - } - - /** - * Retrieve search path list (array). - * - * Simply returns the array containing all the paths that will be searched - * when attempting to load a class. - * - * @return array An array of paths to search for classes - */ - public function getPaths() - { - return $this->paths; - } - - /** - * Register the autoloader. - * - * Registers this library's autoload function with the SPL-provided autoload - * queue. This allows for CSVelte's autoloader to work its magic without - * having to worry about interfering with any other autoloader. - * - * Also adds all of this class's search paths to PHP's include path. - * - * @return bool Whatever the return value of spl_autoload_register is - * - * @see spl_autoload_register - */ - public function register() - { - set_include_path(implode(PATH_SEPARATOR, $this->getPaths())); - spl_autoload_register([$this, 'load']); - } - - /** - * Load a class. - * - * This is the function (or method in this case) used to autoload all of - * CSVelte's classes. It need not be called directly, but rather regestered - * with the SPL's autoload queue using this class's register method. - * - * @param string $className The fully qualified class name to load - * - * @return bool - * - * @see Autoloader::register() - */ - public function load($className) - { - if (class_exists($className)) { - return; - } - $fqcp = str_replace(self::NAMESPACE_SEPARATOR, DIRECTORY_SEPARATOR, $className); - $paths = $this->getPaths(); - foreach ($paths as $path) { - $classPath = $path . DIRECTORY_SEPARATOR . $fqcp . '.php'; - if (file_exists($classPath) && is_readable($classPath)) { - require_once($classPath); - - return; - } - } - } -} diff --git a/src/CSVelte/CSVelte.php b/src/CSVelte/CSVelte.php deleted file mode 100755 index e42c708..0000000 --- a/src/CSVelte/CSVelte.php +++ /dev/null @@ -1,154 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte; - -use CSVelte\Exception\IOException; -use CSVelte\IO\Stream; - -use Iterator; - -/** - * CSVelte Facade. - * - * This class consists of static factory methods for easily generating commonly - * used objects such as readers and writers, as well as convenience methods for - * commonly used functionality such as exporting CSV data to a file. - * - * @package CSVelte - * @subpackage Factory/Adapter - * - * @since v0.1 - */ -class CSVelte -{ - /** - * CSVelte\Reader Factory. - * - * Factory method for creating a new CSVelte\Reader object - * Used to create a local file CSV reader object. - * - * @param string $filename The filename to read - * @param Flavor|array|null $flavor An explicit flavor object that will be - * passed to the reader or an array of flavor attributes to override the - * default flavor attributes - * - * @throws IOException - * - * @return \CSVelte\Reader An iterator for specified CSV file - */ - public static function reader($filename, $flavor = null) - { - self::assertFileIsReadable($filename); - $file = Stream::open($filename); - - return new Reader($file, $flavor); - } - - /** - * String Reader Factory. - * - * Factory method for creating a new CSVelte\Reader object for reading - * from a PHP string - * - * @param string $str The CSV data to read - * @param Flavor|array|null $flavor An explicit flavor object that will be - * passed to the reader or an array of flavor attributes to override the - * default flavor attributes - * - * @return Reader An iterator for provided CSV data - */ - public static function stringReader($str, $flavor = null) - { - return new Reader(streamize($str), $flavor); - } - - /** - * CSVelte\Writer Factory. - * - * Factory method for creating a new CSVelte\Writer object for writing - * CSV data to a file. If file doesn't exist, it will be created. If it - * already contains data, it will be overwritten. - * - * @param string $filename The filename to write to. - * @param Flavor|array|null $flavor An explicit flavor object that will be - * passed to the reader or an array of flavor attributes to override the - * default flavor attributes - * - * @return Writer A writer object for writing to given filename - */ - public static function writer($filename, $flavor = null) - { - $file = Stream::open($filename, 'w+'); - - return new Writer($file, $flavor); - } - - /** - * Export CSV data to local file. - * - * Facade method for exporting data to given filename. IF file doesn't exist - * it will be created. If it does exist it will be overwritten. - * - * @param string $filename The filename to export data to - * @param Iterator|array $data Data to write to CSV file - * @param Flavor|array|null $flavor An explicit flavor object that will be - * passed to the reader or an array of flavor attributes to override the - * default flavor attributes - * - * @return int Number of rows written - */ - public static function export($filename, $data, $flavor = null) - { - $file = Stream::open($filename, 'w+'); - $writer = new Writer($file, $flavor); - - return $writer->writeRows($data); - } - - /** - * Assert that file is readable. - * - * Assert that a particular file exists and is readable (user has permission - * to read/access it) - * - * @param string $filename The name of the file you wish to check - * - * @throws IOException - * - * @internal - */ - protected static function assertFileIsReadable($filename) - { - self::assertFileExists($filename); - if (!is_readable($filename)) { - throw new IOException('Permission denied for: ' . $filename, IOException::ERR_FILE_PERMISSION_DENIED); - } - } - - /** - * Assert that a particular file exists. - * - * @param string $filename The name of the file you wish to check - * - * @throws IOException - * - * @internal - */ - protected static function assertFileExists($filename) - { - if (!file_exists($filename)) { - throw new IOException('File does not exist: ' . $filename, IOException::ERR_FILE_NOT_FOUND); - } - } -} diff --git a/src/CSVelte/Collection/AbstractCollection.php b/src/CSVelte/Collection/AbstractCollection.php deleted file mode 100644 index 63db4aa..0000000 --- a/src/CSVelte/Collection/AbstractCollection.php +++ /dev/null @@ -1,1025 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -use ArrayAccess; -use ArrayIterator; -use Closure; -use Countable; -use CSVelte\Contract\Collectable; -use InvalidArgumentException; -use Iterator; -use OutOfBoundsException; - -use function CSVelte\is_traversable; - -/** - * Class AbstractCollection. - * - * This is the abstract class that all other collection classes are based on. - * Although it's possible to use a completely custom Collection class by simply - * implementing the "Collectable" interface, extending this class gives you a - * whole slew of convenient methods for free. - * - * @package CSVelte\Collection - * - * @since v0.2.2 - * - * @author Luke Visinoni - * @copyright Copyright (c) 2016 Luke Visinoni - * - * @todo Implement Serializable, other Interfaces - * @todo Implement __toString() in such a way that by deault it - * will return a CSV-formatted string but you can configure - * it to return other formats if you want - */ -abstract class AbstractCollection implements - ArrayAccess, - Countable, - Iterator - /*Collectable*/ -{ - /** - * @var array The collection of data this object represents - */ - protected $data = []; - - /** - * @var bool True unless we have advanced past the end of the data array - */ - protected $isValid = true; - - /** - * AbstractCollection constructor. - * - * @param mixed $data The data to wrap - */ - public function __construct($data = []) - { - $this->setData($data); - } - - /** - * Invoke object. - * - * Magic "invoke" method. Called when object is invoked as if it were a function. - * - * @param mixed $val The value (depends on other param value) - * @param mixed $index The index (depends on other param value) - * - * @return mixed (Depends on parameter values) - */ - public function __invoke($val = null, $index = null) - { - if (is_null($val)) { - if (is_null($index)) { - return $this->toArray(); - } - - return $this->delete($index); - } - if (is_null($index)) { - // @todo cast $val to array? - return $this->merge($val); - } - - return $this->set($val, $index); - } - - /** - * Convert collection to string. - * - * @return string A string representation of this collection - * - * @todo Eventually I would like to add a $delim property so that - * I can easily join collection items together with a particular - * character (or set of characters). I would then add a few methods - * to change the delim property. It would default to a comma. - */ - public function __toString() - { - return $this->join(); - } - - // BEGIN ArrayAccess methods - - /** - * Whether a offset exists. - * - * @param mixed $offset An offset to check for. - * - * @return bool true on success or false on failure. - * - * @see http://php.net/manual/en/arrayaccess.offsetexists.php - */ - public function offsetExists($offset) - { - return $this->has($offset); - } - - /** - * Offset to retrieve. - * - * @param mixed $offset The offset to retrieve. - * - * @return mixed Can return all value types. - * - * @see http://php.net/manual/en/arrayaccess.offsetget.php - */ - public function offsetGet($offset) - { - return $this->get($offset, null, true); - } - - /** - * Offset to set. - * - * @param mixed $offset The offset to assign the value to. - * @param mixed $value The value to set. - * - * @see http://php.net/manual/en/arrayaccess.offsetset.php - */ - public function offsetSet($offset, $value) - { - $this->set($offset, $value); - } - - /** - * Offset to unset. - * - * @param mixed $offset The offset to unset. - * - * @see http://php.net/manual/en/arrayaccess.offsetunset.php - */ - public function offsetUnset($offset) - { - $this->delete($offset); - } - - // END ArrayAccess methods - - // BEGIN Countable methods - - public function count() - { - return count($this->data); - } - - // END Countable methods - - // BEGIN Iterator methods - - /** - * Return the current element. - * - * Returns the current element in the collection. The internal array pointer - * of the data array wrapped by the collection should not be advanced by this - * method. No side effects. Return current element only. - * - * @return mixed - */ - public function current() - { - return current($this->data); - } - - /** - * Return the current key. - * - * Returns the current key in the collection. No side effects. - * - * @return mixed - */ - public function key() - { - return key($this->data); - } - - /** - * Advance the internal pointer forward. - * - * Although this method will return the current value after advancing the - * pointer, you should not expect it to. The interface does not require it - * to return any value at all. - * - * @return mixed - */ - public function next() - { - $next = next($this->data); - $key = key($this->data); - if (isset($key)) { - return $next; - } - $this->isValid = false; - } - - /** - * Rewind the internal pointer. - * - * Return the internal pointer to the first element in the collection. Again, - * this method is not required to return anything by its interface, so you - * should not count on a return value. - * - * @return mixed - */ - public function rewind() - { - $this->isValid = !empty($this->data); - - return reset($this->data); - } - - /** - * Is internal pointer in a valid position? - * - * If the internal pointer is advanced beyond the end of the collection, this method will return false. - * - * @return bool True if internal pointer isn't past the end - */ - public function valid() - { - return $this->isValid; - } - - public function sort($alg = null) - { - if (is_null($alg)) { - $alg = 'natcasesort'; - } - $alg($this->data); - - return static::factory($this->data); - } - - /** - * Does this collection have a value at given index? - * - * @param mixed $index The index to check - * - * @return bool - */ - public function has($index) - { - return array_key_exists($index, $this->data); - } - - /** - * Get value at a given index. - * - * Accessor for this collection of data. You can optionally provide a default - * value for when the collection doesn't find a value at the given index. It can - * also optionally throw an OutOfBoundsException if no value is found. - * - * @param mixed $index The index of the data you want to get - * @param mixed $default The default value to return if none available - * @param bool $throw True if you want an exception to be thrown if no data found at $index - * - * @throws OutOfBoundsException If $throw is true and $index isn't found - * - * @return mixed The data found at $index or failing that, the $default - * - * @todo Use OffsetGet, OffsetSet, etc. internally here and on set, has, delete, etc. - */ - public function get($index, $default = null, $throw = false) - { - if (isset($this->data[$index])) { - return $this->data[$index]; - } - if ($throw) { - throw new OutOfBoundsException(__CLASS__ . ' could not find value at index ' . $index); - } - - return $default; - } - - /** - * Set a value at a given index. - * - * Setter for this collection. Allows setting a value at a given index. - * - * @param mixed $index The index to set a value at - * @param mixed $val The value to set $index to - * - * @return $this - */ - public function set($index, $val) - { - $this->data[$index] = $val; - - return $this; - } - - /** - * Unset a value at a given index. - * - * Unset (delete) value at the given index. - * - * @param mixed $index The index to unset - * @param bool $throw True if you want an exception to be thrown if no data found at $index - * - * @throws OutOfBoundsException If $throw is true and $index isn't found - * - * @return $this - */ - public function delete($index, $throw = false) - { - if (isset($this->data[$index])) { - unset($this->data[$index]); - } else { - if ($throw) { - throw new OutOfBoundsException('No value found at given index: ' . $index); - } - } - - return $this; - } - - /** - * Does this collection have a value at specified numerical position? - * - * Returns true if collection contains a value (any value including null) - * at specified numerical position. - * - * @param int $pos The position - * - * @return bool - * - * @todo I feel like it would make more sense to have this start at position 1 rather than 0 - */ - public function hasPosition($pos) - { - try { - $this->getKeyAtPosition($pos); - - return true; - } catch (OutOfBoundsException $e) { - return false; - } - } - - /** - * Return value at specified numerical position. - * - * @param int $pos The numerical position - * - * @throws OutOfBoundsException if no pair at position - * - * @return mixed - */ - public function getValueAtPosition($pos) - { - return $this->data[$this->getKeyAtPosition($pos)]; - } - - /** - * Return key at specified numerical position. - * - * @param int $pos The numerical position - * - * @throws OutOfBoundsException if no pair at position - * - * @return mixed - */ - public function getKeyAtPosition($pos) - { - $i = 0; - foreach ($this as $key => $val) { - if ($i === $pos) { - return $key; - } - $i++; - } - throw new OutOfBoundsException("No element at expected position: $pos"); - } - - /** - * @param int $pos The numerical position - * - * @throws OutOfBoundsException if no pair at position - * - * @return array - */ - public function getPairAtPosition($pos) - { - $pairs = $this->pairs(); - - return $pairs[$this->getKeyAtPosition($pos)]; - } - - /** - * Get collection as array. - * - * @return array This collection as an array - */ - public function toArray() - { - $arr = []; - foreach ($this as $index => $value) { - if (is_object($value) && method_exists($value, 'toArray')) { - $value = $value->toArray(); - } - $arr[$index] = $value; - } - - return $arr; - } - - /** - * Get this collection's keys as a collection. - * - * @return AbstractCollection Containing this collection's keys - */ - public function keys() - { - return static::factory(array_keys($this->data)); - } - - /** - * Get this collection's values as a collection. - * - * This method returns this collection's values but completely re-indexed (numerically). - * - * @return AbstractCollection Containing this collection's values - */ - public function values() - { - return static::factory(array_values($this->data)); - } - - /** - * Merge data into collection. - * - * Merges input data into this collection. Input can be an array or another collection. Returns a NEW collection object. - * - * @param Traversable|array $data The data to merge with this collection - * - * @return AbstractCollection A new collection with $data merged in - */ - public function merge($data) - { - $this->assertCorrectInputDataType($data); - $coll = static::factory($this->data); - foreach ($data as $index => $value) { - $coll->set($index, $value); - } - - return $coll; - } - - /** - * Determine if this collection contains a value. - * - * Allows you to pass in a value or a callback function and optionally an index, - * and tells you whether or not this collection contains that value. If the $index param is specified, only that index will be looked under. - * - * @param mixed|callable $value The value to check for - * @param mixed $index The (optional) index to look under - * - * @return bool True if this collection contains $value - * - * @todo Maybe add $identical param for identical comparison (===) - * @todo Allow negative offset for second param - */ - public function contains($value, $index = null) - { - return (bool) $this->first(function ($val, $key) use ($value, $index) { - if (is_callable($value)) { - $found = $value($val, $key); - } else { - $found = ($value == $val); - } - if ($found) { - if (is_null($index)) { - return true; - } - if (is_array($index)) { - return in_array($key, $index); - } - - return $key == $index; - } - - return false; - }); - } - - /** - * Get duplicate values. - * - * Returns a collection of arrays where the key is the duplicate value - * and the value is an array of keys from the original collection. - * - * @return AbstractCollection A new collection with duplicate values. - */ - public function duplicates() - { - $dups = []; - $this->walk(function ($val, $key) use (&$dups) { - $dups[$val][] = $key; - }); - - return static::factory($dups)->filter(function ($val) { - return count($val) > 1; - }); - } - - /** - * Pop an element off the end of this collection. - * - * @return mixed The last item in this collectio n - */ - public function pop() - { - return array_pop($this->data); - } - - /** - * Shift an element off the beginning of this collection. - * - * @return mixed The first item in this collection - */ - public function shift() - { - return array_shift($this->data); - } - - /** - * Push a item(s) onto the end of this collection. - * - * Returns a new collection with $items added. - * - * @param array $items Any number of arguments will be pushed onto the - * - * @return mixed The first item in this collection - */ - public function push(...$items) - { - array_push($this->data, ...$items); - - return static::factory($this->data); - } - - /** - * Unshift item(s) onto the beginning of this collection. - * - * Returns a new collection with $items added. - * - * @return mixed The first item in this collection - */ - public function unshift(...$items) - { - array_unshift($this->data, ...$items); - - return static::factory($this->data); - } - - /** - * Pad this collection to a certain size. - * - * Returns a new collection, padded to the given size, with the given value. - * - * @param int $size The number of items that should be in the collection - * @param null $with The value to pad the collection with - * - * @return AbstractCollection A new collection padded to specified length - */ - public function pad($size, $with = null) - { - return static::factory(array_pad($this->data, $size, $with)); - } - - /** - * Apply a callback to each item in collection. - * - * Applies a callback to each item in collection and returns a new collection - * containing each iteration's return value. - * - * @param callable $callback The callback to apply - * - * @return AbstractCollection A new collection with callback return values - */ - public function map(callable $callback) - { - return static::factory(array_map($callback, $this->data)); - } - - /** - * Apply a callback to each item in collection. - * - * Applies a callback to each item in collection. The callback should return - * false to filter any item from the collection. - * - * @param callable $callback The callback function - * @param null $extraContext Extra context to pass as third param in callback - * - * @return $this - * - * @see php.net array_walk - */ - public function walk(callable $callback, $extraContext = null) - { - array_walk($this->data, $callback, $extraContext); - - return $this; - } - - /** - * Iterate over each item that matches criteria in callback. - * - * @param Closure|callable $callback A callback to use - * @param object $bindTo The object to bind to - * - * @return AbstractCollection - */ - public function each(Closure $callback, $bindTo = null) - { - if (is_null($bindTo)) { - $bindTo = $this; - } - if (!is_object($bindTo)) { - throw new InvalidArgumentException('Second argument must be an object.'); - } - $cb = $callback->bindTo($bindTo); - $return = []; - foreach ($this as $key => $val) { - if ($cb($val, $key)) { - $return[$key] = $val; - } - } - - return static::factory($return); - } - - /** - * Get each key/value as an array pair. - * - * Returns a collection of arrays where each item in the collection is [key,value] - * - * @return AbstractCollection - */ - public function pairs() - { - return static::factory(array_map( - function ($key, $val) { - return [$key, $val]; - }, - array_keys($this->data), - array_values($this->data) - )); - } - - /** - * Reduce the collection to a single value. - * - * Using a callback function, this method will reduce this collection to a - * single value. - * - * @param callable $callback The callback function used to reduce - * @param null $initial The initial carry value - * - * @return mixed The single value produced by reduction algorithm - */ - public function reduce(callable $callback, $initial = null) - { - return array_reduce($this->data, $callback, $initial); - } - - /** - * Filter the collection. - * - * Using a callback function, this method will filter out unwanted values, returning - * a new collection containing only the values that weren't filtered. - * - * @param callable $callback The callback function used to filter - * @param int $flag array_filter flag(s) (ARRAY_FILTER_USE_KEY or ARRAY_FILTER_USE_BOTH) - * - * @return AbstractCollection A new collection with only values that weren't filtered - * - * @see php.net array_filter - */ - public function filter(callable $callback, $flag = ARRAY_FILTER_USE_BOTH) - { - return static::factory(array_filter($this->data, $callback, $flag)); - } - - /** - * Return the first item that meets given criteria. - * - * Using a callback function, this method will return the first item in the collection - * that causes the callback function to return true. - * - * @param callable $callback The callback function - * - * @return null|mixed The first item in the collection that causes callback to return true - */ - public function first(callable $callback) - { - foreach ($this->data as $index => $value) { - if ($callback($value, $index)) { - return $value; - } - } - - return null; - } - - /** - * Return the last item that meets given criteria. - * - * Using a callback function, this method will return the last item in the collection - * that causes the callback function to return true. - * - * @param callable $callback The callback function - * - * @return null|mixed The last item in the collection that causes callback to return true - */ - public function last(callable $callback) - { - $reverse = $this->reverse(true); - - return $reverse->first($callback); - } - - /** - * Returns collection in reverse order. - * - * @param null $preserveKeys True if you want to preserve collection's keys - * - * @return AbstractCollection This collection in reverse order. - */ - public function reverse($preserveKeys = null) - { - return static::factory(array_reverse($this->data, $preserveKeys)); - } - - /** - * Get unique items. - * - * Returns a collection of all the unique items in this collection. - * - * @return AbstractCollection This collection with duplicate items removed - */ - public function unique() - { - return static::factory(array_unique($this->data)); - } - - /** - * Join collection together using a delimiter. - * - * @param string $delimiter The delimiter string/char - * - * @return string - */ - public function join($delimiter = '') - { - return implode($delimiter, $this->data); - } - - /** - * Counts how many times each value occurs in a collection. - * - * Returns a new collection with values as keys and how many times that - * value appears in the collection. Works best with scalar values but will - * attempt to work on collections of objects as well. - * - * @return AbstractCollection - * - * @todo Right now, collections of arrays or objects are supported via the - * __toString() or spl_object_hash() - * @todo NumericCollection::counts() does the same thing... - */ - public function frequency() - { - $frequency = []; - foreach ($this as $key => $val) { - if (!is_scalar($val)) { - if (!is_object($val)) { - $val = new ArrayIterator($val); - } - - if (method_exists($val, '__toString')) { - $val = (string) $val; - } else { - $val = spl_object_hash($val); - } - } - if (!isset($frequency[$val])) { - $frequency[$val] = 0; - } - $frequency[$val]++; - } - - return static::factory($frequency); - } - - /** - * Collection factory method. - * - * This method will analyze input data and determine the most appropriate Collection - * class to use. It will then instantiate said Collection class with the given - * data and return it. - * - * @param mixed $data The data to wrap - * - * @return AbstractCollection A collection containing $data - */ - public static function factory($data = null) - { - if (static::isTabular($data)) { - $class = TabularCollection::class; - } elseif (static::isMultiDimensional($data)) { - $class = MultiCollection::class; - } elseif (static::isAllNumeric($data)) { - $class = NumericCollection::class; - } elseif (static::isCharacterSet($data)) { - $class = CharCollection::class; - } else { - $class = Collection::class; - } - - return new $class($data); - } - - /** - * Is input data tabular? - * - * Returns true if input data is tabular in nature. This means that it is a - * two-dimensional array with the same keys (columns) for each element (row). - * - * @param mixed $data The data structure to check - * - * @return bool True if data structure is tabular - */ - public static function isTabular($data) - { - if (!is_traversable($data)) { - return false; - } - foreach ($data as $row) { - if (!is_traversable($row)) { - return false; - } - $columns = array_keys($row); - if (!isset($cmp_columns)) { - $cmp_columns = $columns; - } else { - if ($cmp_columns != $columns) { - return false; - } - } - // if row contains an array it isn't tabular - if (array_reduce($row, function ($carry, $item) { - return is_array($item) && $carry; - }, true)) { - return false; - } - } - - return true; - } - - /** - * Check data for multiple dimensions. - * - * This method is to determine whether a data structure is multi-dimensional. - * That is to say, it is a traversable structure that contains at least one - * traversable structure. - * - * @param mixed $data The input data - * - * @return bool - */ - public static function isMultiDimensional($data) - { - if (!is_traversable($data)) { - return false; - } - foreach ($data as $elem) { - if (is_traversable($elem)) { - return true; - } - } - - return false; - } - - /** - * Determine if structure contains all numeric values. - * - * @param mixed $data The input data - * - * @return bool - */ - public static function isAllNumeric($data) - { - if (!is_traversable($data)) { - return false; - } - foreach ($data as $val) { - if (!is_numeric($val)) { - return false; - } - } - - return true; - } - - /** - * Is data a string of characters? - * - * Just checks to see if input is a string of characters or a string - * of digits. - * - * @param mixed $data Data to check - * - * @return bool - */ - public static function isCharacterSet($data) - { - return - is_string($data) || - is_numeric($data); - } - - // END Iterator methods - - /** - * Set collection data. - * - * Sets the collection data. - * - * @param array $data The data to wrap - * - * @return $this - */ - protected function setData($data) - { - if (is_null($data)) { - $data = []; - } - $this->assertCorrectInputDataType($data); - $data = $this->prepareData($data); - foreach ($data as $index => $value) { - $this->set($index, $value); - } - reset($this->data); - - return $this; - } - - /** - * Assert input data is of the correct structure. - * - * @param mixed $data Data to check - * - * @throws InvalidArgumentException If invalid data structure - */ - protected function assertCorrectInputDataType($data) - { - if (!$this->isConsistentDataStructure($data)) { - throw new InvalidArgumentException(__CLASS__ . ' expected traversable data, got: ' . gettype($data)); - } - } - - /** - * Convert input data to an array. - * - * Convert the input data to an array that can be worked with by a collection. - * - * @param mixed $data The input data - * - * @return array - */ - protected function prepareData($data) - { - return $data; - } - - /** - * Determine whether data is consistent with a given collection type. - * - * This method is used to determine whether input data is consistent with a - * given collection type. For instance, CharCollection requires a string. - * NumericCollection requires an array or traversable set of numeric data. - * TabularCollection requires a two-dimensional data structure where all the - * keys are the same in every row. - * - * @param mixed $data Data structure to check for consistency - * - * @return bool - */ - abstract protected function isConsistentDataStructure($data); -} diff --git a/src/CSVelte/Collection/CharCollection.php b/src/CSVelte/Collection/CharCollection.php deleted file mode 100644 index 4f2a85d..0000000 --- a/src/CSVelte/Collection/CharCollection.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -class CharCollection extends AbstractCollection -{ - /** - * Apply a callback to each item in collection. - * - * Applies a callback to each item in collection and returns a new collection - * containing each iteration's return value. - * - * @param callable $callback The callback to apply - * - * @return AbstractCollection A new collection with callback return values - */ - public function map(callable $callback) - { - return new self(implode('', array_map($callback, $this->data))); - } - - /** - * {@inheritdoc} - */ - public function push(...$items) - { - $result = parent::push(...$items); - - return new self(implode('', $result->toArray())); - } - - /** - * {@inheritdoc} - */ - public function unshift(...$items) - { - $result = parent::unshift(...$items); - - return new self(implode('', $result->toArray())); - } - - /** - * Convert input data to an array. - * - * Convert the input data to an array that can be worked with by a collection. - * - * @param mixed $data The input data - * - * @return array - */ - protected function prepareData($data) - { - if (!is_string($data)) { - $data = (string) $data; - } - - return str_split($data); - } - - /** - * Is data consistent with this collection type? - * - * @param mixed $data The data to check - * - * @return bool - */ - protected function isConsistentDataStructure($data) - { - return static::isCharacterSet($data); - } -} diff --git a/src/CSVelte/Collection/Collection.php b/src/CSVelte/Collection/Collection.php deleted file mode 100644 index b7ce0e3..0000000 --- a/src/CSVelte/Collection/Collection.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -use function CSVelte\is_traversable; - -class Collection extends AbstractCollection -{ - /** - * Is correct input data type? - * - * @param mixed $data The data to assert correct type of - * - * @return bool - */ - protected function isConsistentDataStructure($data) - { - // this collection may only contain scalar or null values - if (!is_traversable($data)) { - return false; - } - foreach ($data as $key => $val) { - if (is_traversable($val)) { - return false; - } - } - - return true; - } -} diff --git a/src/CSVelte/Collection/MultiCollection.php b/src/CSVelte/Collection/MultiCollection.php deleted file mode 100644 index a87e482..0000000 --- a/src/CSVelte/Collection/MultiCollection.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -use function CSVelte\is_traversable; - -class MultiCollection extends AbstractCollection -{ - /** - * {@inheritdoc} - */ - public function contains($value, $index = null) - { - if (parent::contains($value, $index)) { - return true; - } - foreach ($this->data as $key => $arr) { - if (is_traversable($arr)) { - $coll = static::factory($arr); - if ($coll->contains($value, $index)) { - return true; - } - } - } - - return false; - } - - protected function isConsistentDataStructure($data) - { - return static::isMultiDimensional($data); - } -} diff --git a/src/CSVelte/Collection/NumericCollection.php b/src/CSVelte/Collection/NumericCollection.php deleted file mode 100644 index 818d987..0000000 --- a/src/CSVelte/Collection/NumericCollection.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -use function CSVelte\is_traversable; - -/** - * Class NumericCollection. - * - * @package CSVelte\Collection - * - * @todo $this->set('foo', 'bar'); should throw an exception because only - * numeric values are allowed. Either that or converted to int. - */ -class NumericCollection extends AbstractCollection -{ - /** - * Increment an item. - * - * Increment the item specified by $key by one value. Intended for integers - * but also works (using this term loosely) for letters. Any other data type - * it may modify is unintended behavior at best. - * - * This method modifies its internal data array rather than returning a new - * collection. - * - * @param mixed $key The key of the item you want to increment. - * @param int $interval The interval that $key should be incremented by - * - * @return $this - */ - public function increment($key, $interval = 1) - { - $val = $this->get($key, null, true); - for ($i = 0; $i < $interval; $i++) { - $val++; - } - $this->set($key, $val); - - return $this; - } - - /** - * Decrement an item. - * - * Frcrement the item specified by $key by one value. Intended for integers. - * Does not work for letters and if it does anything to anything else, it's - * unintended at best. - * - * This method modifies its internal data array rather than returning a new - * collection. - * - * @param mixed $key The key of the item you want to decrement. - * @param int $interval The interval that $key should be decremented by - * - * @return $this - */ - public function decrement($key, $interval = 1) - { - $val = $this->get($key, null, true); - for ($i = 0; $i < $interval; $i++) { - $val--; - } - $this->set($key, $val); - - return $this; - } - - /** - * Get the sum. - * - * @return mixed The sum of all values in collection - */ - public function sum() - { - return array_sum($this->toArray()); - } - - /** - * Get the average. - * - * @return float|int The average value from the collection - */ - public function average() - { - return $this->sum() / $this->count(); - } - - /** - * Get the mode. - * - * @return float|int The mode - */ - public function mode() - { - $counts = $this->counts()->toArray(); - arsort($counts); - $mode = key($counts); - - return (strpos($mode, '.')) ? floatval($mode) : intval($mode); - } - - /** - * Get the median value. - * - * @return float|int The median value - */ - public function median() - { - $count = $this->count(); - $data = $this->toArray(); - natcasesort($data); - $middle = $count / 2; - $values = array_values($data); - if ($count % 2 == 0) { - // even number, use middle - $low = $values[$middle - 1]; - $high = $values[$middle]; - - return ($low + $high) / 2; - } - // odd number return median - return $values[$middle]; - } - - /** - * Get the maximum value. - * - * @return mixed The maximum - */ - public function max() - { - return max($this->data); - } - - /** - * Get the minimum value. - * - * @return mixed The minimum - */ - public function min() - { - return min($this->data); - } - - /** - * Get the number of times each item occurs in the collection. - * - * This method will return a NumericCollection where keys are the - * values and values are the number of times that value occurs in - * the original collection. - * - * @return NumericCollection - */ - public function counts() - { - return new self(array_count_values($this->toArray())); - } - - protected function isConsistentDataStructure($data) - { - if (!is_traversable($data)) { - return false; - } - foreach ($data as $val) { - if (!is_numeric($val)) { - return false; - } - } - - return true; - } -} diff --git a/src/CSVelte/Collection/TabularCollection.php b/src/CSVelte/Collection/TabularCollection.php deleted file mode 100644 index 357b01f..0000000 --- a/src/CSVelte/Collection/TabularCollection.php +++ /dev/null @@ -1,154 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Collection; - -use BadMethodCallException; -use OutOfBoundsException; - -class TabularCollection extends MultiCollection -{ - /** - * Magic method call. - * - * @param string $method The name of the method - * @param array $args The argument list - * - * @throws BadMethodCallException If no method exists - * - * @return mixed - * - * @todo Add phpdoc comments for dynamic methods - * @todo throw BadMethodCallException - */ - public function __call($method, $args) - { - $argc = count($args); - if ($argc == 1 && $this->hasColumn($index = array_pop($args))) { - $column = $this->getColumn($index); - if (method_exists($column, $method)) { - return call_user_func_array([$column, $method], $args); - } - } - throw new BadMethodCallException('Method does not exist: ' . __CLASS__ . "::{$method}()"); - } - - /** - * Does this collection have specified column? - * - * @param mixed $column The column index - * - * @return bool - */ - public function hasColumn($column) - { - try { - $this->getColumn($column); - - return true; - } catch (OutOfBoundsException $e) { - return false; - } - } - - /** - * Get column as collection. - * - * @param mixed $column The column index - * @param bool $throw Throw an exception on failure - * - * @return AbstractCollection|false - */ - public function getColumn($column, $throw = true) - { - $values = array_column($this->data, $column); - if (count($values)) { - return static::factory($values); - } - if ($throw) { - throw new OutOfBoundsException(__CLASS__ . ' could not find column: ' . $column); - } - - return false; - } - - /** - * Does this collection have a row at specified index? - * - * @param int $offset The column index - * - * @return bool - */ - public function hasRow($offset) - { - try { - $this->getRow($offset); - - return true; - } catch (OutOfBoundsException $e) { - return false; - } - } - - /** - * Get row at specified index. - * - * @param int $offset The row offset (starts from 0) - * - * @return AbstractCollection|false - */ - public function getRow($offset) - { - return $this->getValueAtPosition($offset); - } - - /** - * {@inheritdoc} - */ - public function map(callable $callback) - { - $ret = []; - foreach ($this->data as $key => $row) { - $ret[$key] = $callback(static::factory($row)); - } - - return static::factory($ret); - } - - /** - * {@inheritdoc} - */ - public function walk(callable $callback, $extraContext = null) - { - foreach ($this as $offset => $row) { - $callback(static::factory($row), $offset, $extraContext); - } - - return $this; - } - - /** - * Is input data structure valid? - * - * In order to determine whether a given data structure is valid for a - * particular collection type (tabular, numeric, etc.), we have this method. - * - * @param mixed $data The data structure to check - * - * @return bool True if data structure is tabular - */ - protected function isConsistentDataStructure($data) - { - return static::isTabular($data); - } -} diff --git a/src/CSVelte/Contract/Collectable.php b/src/CSVelte/Contract/Collectable.php deleted file mode 100644 index e0cc3d9..0000000 --- a/src/CSVelte/Contract/Collectable.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Contract; - -interface Collectable -{ - public function __construct(); - - public function __invoke($val = null, $key = null); - - public function toArray(); - - public function keys(); - - public function values(); - - public function merge($data); - - public function contains($val, $index = null); - - public function pop(); - - public function shift(); - - public function push(...$items); - - public function unshift(...$items); - - public function pad($size, $with = null); - - public function has($index); - - public function get($index, $default = null); - - public function set($index, $value = null); - - public function map(callable $callback); - - public function walk(callable $callback, $extraContext = null); - - public function reduce(callable $callback, $initial = null); - - public function filter(callable $callback); - - public function first(callable $callback); - - public function last(callable $callback); - - public function frequency(); - - public function unique(); - - public function duplicates(); - - public function flip(); - - public function reverse(); - - public function pairs(); - - public function join($glue = ''); - - public function isEmpty(); - - public function value(callable $callback); - - public function sort(callable $callback, $preserveKeys = true); - - public function reverse($preserveKeys = true); -} diff --git a/src/CSVelte/Contract/Streamable.php b/src/CSVelte/Contract/Streamable.php index 92711a3..19a47c8 100644 --- a/src/CSVelte/Contract/Streamable.php +++ b/src/CSVelte/Contract/Streamable.php @@ -1,30 +1,17 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Contract; -/** - * Streamable Interface. - * - * Implementors of this class will be acceptable by the reader, writer, taster, - * and various other classes that expect stream-like data. This interface replaces - * the old Readable, Writable, and Seekable interfaces which were useless. - * - * @package CSVelte - * @subpackage Contract (Interfaces) - * - * @since v0.2.1 - */ interface Streamable { /** diff --git a/src/CSVelte/Dialect.php b/src/CSVelte/Dialect.php new file mode 100644 index 0000000..f4f0836 --- /dev/null +++ b/src/CSVelte/Dialect.php @@ -0,0 +1,447 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte; + +use Traversable; +use function Noz\collect, + Noz\to_array; + +/** + * CSV Dialect - Default dialect + * + * Due to CSV being without any definitive format definition for so long, many dialects of it exist. This class allows + * the creation of reusable "dialects" for commonly used flavors of CSV. You can make one for Excel CSV, another for + * tab delimited CSV, another for pipe-delimited, etc. + * + * @todo Where is escapeChar? Do we just use a backslash if doubleQuote is false? + */ +class Dialect +{ + /** Quote none - never quote any columns */ + const QUOTE_NONE = 0; + /** Quote all - always quote all columns */ + const QUOTE_ALL = 1; + /** Quote minimal - quote only those columns that contain the delimiter or quote character */ + const QUOTE_MINIMAL = 2; + /** Quote non-numeric - quote only those columns that contain non-numeric values */ + const QUOTE_NONNUMERIC = 3; + + /** Trim all - trim empty space from start and end */ + const TRIM_ALL = true; + /** Trim none - do not trim at all */ + const TRIM_NONE = false; + /** Trim start - trim empty space from the start (left) */ + const TRIM_START = 'start'; + /** Trim end - trim empty space from the end (right) */ + const TRIM_END = 'end'; + + /** Standard attributes (from W3 CSVW working group) */ + + /** @var string The character to use to begin a comment line */ + protected $commentPrefix = "#"; + + /** @var string The character to delimit columns with */ + protected $delimiter = ","; + + /** @var bool Whether to escape quotes within a column by preceding them with another quote */ + protected $doubleQuote = true; + + /** @var string The character encoding for this dialect */ + protected $encoding = "utf-8"; + + /** @var bool Whether the dialect expects a header row (or rows) within the data */ + protected $header = true; + + /** @var int How many header rows are expected within the data */ + protected $headerRowCount = 1; + + /** @var string The line ending character or character sequence */ + protected $lineTerminator = "\n"; + + /** @var string The quoting character (used to quote columns depending on quoteStyle) */ + protected $quoteChar = '"'; + + /** @var bool Whether blank rows within the data should be skipped/ignored */ + protected $skipBlankRows = false; + + /** @var int How many columns to skip/ignore */ + protected $skipColumns = 0; + + /** @var bool Whether to skip/ignore initial space within a column */ + protected $skipInitialSpace = false; + + /** @var int How many rows to skip/ignore */ + protected $skipRows = 0; + + /** @var bool|string Whether to trim empty space and where (see TRIM_* constants above) */ + protected $trim = self::TRIM_ALL; + + /** Non-standard attributes (my own additions) */ + + /** @var int The quoting style (see QUOTE_* constants above) */ + protected $quoteStyle = self::QUOTE_MINIMAL; + + /** + * Dialect constructor. + * + * Any of the above properties may be set within the $attribs array to initialize the dialect with different + * attributes than are defined above. + * + * @param array|Traversable $attribs An array of dialect attributes + */ + public function __construct($attribs = null) + { + $attribs = to_array($attribs, true); + foreach ($attribs as $attr => $val) { + if (property_exists($this, $attr)) { + // find the appropriate setter... + foreach (['set', 'setIs', 'setHas'] as $prefix) { + $setter = $prefix . ucfirst(strtolower($attr)); + if (method_exists($this, $setter)) { + $this->{$setter}($val); + } + } + } + } + } + + /** + * Set comment prefix character(s) + * + * @param string $commentPrefix The character(s) used to begin a comment + * + * @return self + */ + public function setCommentPrefix($commentPrefix) + { + $this->commentPrefix = (string) $commentPrefix; + return $this; + } + + /** + * Get comment prefix character(s) + * + * @return string + */ + public function getCommentPrefix() + { + return $this->commentPrefix; + } + + /** + * Set delimiter character(s) + * + * @param string $delimiter The character(s) used to delimit data + * + * @return self + */ + public function setDelimiter($delimiter) + { + $this->delimiter = (string) $delimiter; + return $this; + } + + /** + * Set delimiter character(s) + * + * @return string + */ + public function getDelimiter() + { + return $this->delimiter; + } + + /** + * Set double quote + * + * @param bool $doubleQuote Whether to escape quote character with a preceding quote character + * + * @return self + */ + public function setIsDoubleQuote($doubleQuote) + { + $this->doubleQuote = (bool) $doubleQuote; + return $this; + } + + /** + * Get double quote + * + * @return bool + */ + public function isDoubleQuote() + { + return $this->doubleQuote; + } + + /** + * Set character encoding + * + * @param string $encoding The character encoding + * + * @return self + */ + public function setEncoding($encoding) + { + $this->encoding = (string) $encoding; + return $this; + } + + /** + * Get character encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set header row flag + * + * @param bool $header Whether the data has header row(s) + * + * @return self + */ + public function setHasHeader($header) + { + $this->header = (bool) $header; + return $this; + } + + /** + * Get whether dialect expects header row(s) + * + * @return bool + */ + public function hasHeader() + { + return $this->header; + } + + /** + * Set header row count + * + * @param int $headerRowCount The amount of expected header rows + * + * @return self + */ + public function setHeaderRowCount($headerRowCount) + { + $this->headerRowCount = (int) $headerRowCount; + return $this; + } + + /** + * Get header row count + * + * @return int + */ + public function getHeaderRowCount() + { + return $this->headerRowCount; + } + + /** + * Set line terminator character or character sequence + * + * @param string $lineTerminator The line ending character(s) + * + * @return self + */ + public function setLineTerminator($lineTerminator) + { + $this->lineTerminator = (string) $lineTerminator; + return $this; + } + + /** + * Get line terminator + * + * @return string + */ + public function getLineTerminator() + { + return $this->lineTerminator; + } + + /** + * Set quote character + * + * @param string $quoteChar The quote character + * + * @return self + */ + public function setQuoteChar($quoteChar) + { + $this->quoteChar = (string) $quoteChar; + return $this; + } + + /** + * Get quote character + * + * @return string + */ + public function getQuoteChar() + { + return $this->quoteChar; + } + + /** + * Set whether to skip blank rows + * + * @param bool $skipBlankRows Whether to skip blank rows + * + * @return self + */ + public function setIsSkipBlankRows($skipBlankRows) + { + $this->skipBlankRows = (bool) $skipBlankRows; + return $this; + } + + /** + * Get skip blank rows flag + * + * @return bool + */ + public function isSkipBlankRows() + { + return $this->skipBlankRows; + } + + /** + * Set number of columns to skip/ignore + * + * @param int $skipColumns The number of columns to skip/ignore + * + * @return self + */ + public function setSkipColumns($skipColumns) + { + $this->skipColumns = (int) $skipColumns; + return $this; + } + + /** + * Get number of columns to skip/ignore + * + * @return int + */ + public function getSkipColumns() + { + return $this->skipColumns; + } + + /** + * Set skip initial space flag + * + * @param bool $skipInitialSpace Skip initial space flag + * + * @return self + */ + public function setIsSkipInitialSpace($skipInitialSpace) + { + $this->skipInitialSpace = (bool) $skipInitialSpace; + return $this; + } + + /** + * Get skip initial space flag + * + * @return bool + */ + public function isSkipInitialSpace() + { + return $this->skipInitialSpace; + } + + /** + * Set number of rows to skip/ignore + * + * @param int $skipRows Number of rows to skip/ignore + * + * @return self + */ + public function setSkipRows($skipRows) + { + $this->skipRows = (int) $skipRows; + return $this; + } + + /** + * Get number of rows to skip/ignore + * + * @return int + */ + public function getSkipRows() + { + return $this->skipRows; + } + + /** + * Set trim type + * + * Allows you to set whether you want data to be trimmed on one, both, or neither sides. + * + * @param bool|string $trim The type trimming you want to do (see TRIM_* constants above) + * + * @return self + */ + public function setTrim($trim) + { + $this->trim = $trim; + return $this; + } + + /** + * Get trim type + * + * The type will coincide with one of the TRIM_* constants defined above. + * + * @return bool|string + */ + public function getTrim() + { + return $this->trim; + } + + /** + * Set the quoting style + * + * Allows you to set how data is quoted + * + * @param int $quoteStyle The desired quoting style (see QUOTE_* constants above) + * + * @return self + */ + public function setQuoteStyle($quoteStyle) + { + $this->quoteStyle = (int) $quoteStyle; + return $this; + } + + /** + * Get quoting style + * + * The quoteStyle value will coincide with one of the QUOTE_* constants defined above. + * + * @return int + */ + public function getQuoteStyle() + { + return $this->quoteStyle; + } +} \ No newline at end of file diff --git a/src/CSVelte/Exception/CSVelteException.php b/src/CSVelte/Exception/CSVelteException.php old mode 100755 new mode 100644 index afa9c91..471eec3 --- a/src/CSVelte/Exception/CSVelteException.php +++ b/src/CSVelte/Exception/CSVelteException.php @@ -1,31 +1,17 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Exception; use Exception; -/** - * CSVelte\Exception - * A generic catch-all exception thrown whenever CSVelte doesn't have a more - * specific or more appropriate exception to throw. Also the exception all other - * exceptions in the library inherit from. - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class CSVelteException extends Exception -{ -} +class CSVelteException extends Exception {} \ No newline at end of file diff --git a/src/CSVelte/Exception/DeprecatedException.php b/src/CSVelte/Exception/DeprecatedException.php deleted file mode 100755 index f158519..0000000 --- a/src/CSVelte/Exception/DeprecatedException.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\DeprecatedException - * This exception is thrown when users attempt to use features of the library - * that have been deprecated. This allows code to not necessarily braek even when - * the feature they're trying to use no longer exists or will be removed in the - * next version. - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class DeprecatedException extends CSVelteException -{ -} diff --git a/src/CSVelte/Exception/EndOfFileException.php b/src/CSVelte/Exception/EndOfFileException.php deleted file mode 100755 index 257e011..0000000 --- a/src/CSVelte/Exception/EndOfFileException.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\EndOfFileException - * Thrown when user attempts to access/read from a file/stream/resource when its - * internal pointer has already reached the end of the file. - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class EndOfFileException extends CSVelteException -{ -} diff --git a/src/CSVelte/Exception/FlavorException.php b/src/CSVelte/Exception/FlavorException.php deleted file mode 100755 index ba67061..0000000 --- a/src/CSVelte/Exception/FlavorException.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\FlavorException - * Thrown when a non-existant flavor is requested from Flavor::create(). - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 - */ -class FlavorException extends CSVelteException -{ -} diff --git a/src/CSVelte/Exception/HeaderException.php b/src/CSVelte/Exception/HeaderException.php deleted file mode 100755 index 64c9887..0000000 --- a/src/CSVelte/Exception/HeaderException.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\HeaderException - * There are various methods throughout the library that expect a CSV source to - * have a header row. Rather than doing something like:. - - if (if $file->hasHeader()) { - $header = $file->getHeader() - } - - * you can instead simply call $header->getHeader() and handle this exception if - * said file has no header - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 - */ -class HeaderException extends CSVelteException -{ - /** - * Error code for invalid/inconsistent header/column count. - */ - const ERR_HEADER_COUNT = 1; -} diff --git a/src/CSVelte/Exception/IOException.php b/src/CSVelte/Exception/IOException.php index fcdd2a5..1025e1f 100644 --- a/src/CSVelte/Exception/IOException.php +++ b/src/CSVelte/Exception/IOException.php @@ -1,29 +1,17 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Exception; -/** - * CSVelte\Exception\IOException - * Thrown when user attempts to access/read a file in a way that it doesn't allow. - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 - */ class IOException extends CSVelteException { /** diff --git a/src/CSVelte/Exception/ImmutableException.php b/src/CSVelte/Exception/ImmutableException.php deleted file mode 100755 index 9d941de..0000000 --- a/src/CSVelte/Exception/ImmutableException.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\ImmutableException - * Thrown when user tries to change an attribute of an immutable (read-only) - * object (such as a CSVelte\Flavor object). - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class ImmutableException extends CSVelteException -{ -} diff --git a/src/CSVelte/Exception/NotYetImplementedException.php b/src/CSVelte/Exception/NotYetImplementedException.php old mode 100755 new mode 100644 index 2300112..68bb548 --- a/src/CSVelte/Exception/NotYetImplementedException.php +++ b/src/CSVelte/Exception/NotYetImplementedException.php @@ -1,28 +1,21 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Exception; /** - * CSVelte\Exception\NotYetImplementedException - * This is just an exception I use internally for methods that I must include due - * to inheritance or what-have-you but that I have yet to implement. + * NotYetImplementedException * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni + * @todo This is thrown when user attempts to use a feature that is not yet implemented. Once I release v1.0, I intend + * to remove this exception entirely, but until then it helps me keep certain interfaces consistent. */ -class NotYetImplementedException extends CSVelteException -{ -} +class NotYetImplementedException extends CSVelteException {} diff --git a/src/CSVelte/Exception/TasterException.php b/src/CSVelte/Exception/SnifferException.php old mode 100755 new mode 100644 similarity index 54% rename from src/CSVelte/Exception/TasterException.php rename to src/CSVelte/Exception/SnifferException.php index ddf22e5..dfa8040 --- a/src/CSVelte/Exception/TasterException.php +++ b/src/CSVelte/Exception/SnifferException.php @@ -1,28 +1,18 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Exception; -/** - * CSVelte\Exception\TasterException - * Used by CSVelte\Taster to report errors in "flavor tasting" (format inference). - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class TasterException extends CSVelteException +class SnifferException extends CSVelteException { /** * Could not determine delimiter. diff --git a/src/CSVelte/Exception/WriterException.php b/src/CSVelte/Exception/WriterException.php deleted file mode 100644 index 4dec2a0..0000000 --- a/src/CSVelte/Exception/WriterException.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Exception; - -/** - * CSVelte\Exception\WriterException - * The CSVelte\Writer class throws this exception for various issues. - * - * @package CSVelte\Exception - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ -class WriterException extends CSVelteException -{ -} diff --git a/src/CSVelte/Flavor.php b/src/CSVelte/Flavor.php deleted file mode 100755 index 89273e9..0000000 --- a/src/CSVelte/Flavor.php +++ /dev/null @@ -1,276 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte; - -use CSVelte\Exception\ImmutableException; -use InvalidArgumentException; - -/** - * CSV Flavor. - * - * Represents a particular "flavor" of CSV. Inspired by python's csv "dialects". - * Also inspired by Frictionless Data's "dialect description" format and the W3C's - * CSV on the Web Working Group and their work on CSV dialects. - * - * @package CSVelte - * @subpackage Flavor - * - * @since v0.1 - * - * @property string $delimiter The delimiter character - * @property string $quoteChar The quoting character - * @property string $lineTerminator The character sequence used to terminate rows of data - * @property string $escapeChar The character used to escape quotes within a quoted string - * Mutually exclusive to $doubleQuote - * @property bool $doubleQuote If true, quote characters will be escaped by preceding them - * with another quote character. Mutually exclusive to $escapeChar - * @property string $quoteStyle One of four class constants that determine which cells are quoted - * @property bool $header If true, first row should be treated as a header row - */ -class Flavor -{ - /** - * Quote all cells. - * Set Flavor::$quoteStyle to this to quote all cells, regardless of data type. - * - * @var string - */ - const QUOTE_ALL = 'quote_all'; - - /** - * Quote no cells. - * Set Flavor::$quoteStyle to this to quote no columns, regardless of data type. - * - * @var string - */ - const QUOTE_NONE = 'quote_none'; - - /** - * Quote minimal columns. - * Set Flavor::$quoteStyle to this to quote only cells that contain special - * characters such as newlines or the delimiter character. - * - * @var string - */ - const QUOTE_MINIMAL = 'quote_minimal'; - - /** - * Quote non-numeric cells. - * Set Flavor::$quoteStyle to this to quote only cells that contain - * non-numeric data. - * - * @var string - */ - const QUOTE_NONNUMERIC = 'quote_nonnumeric'; - - /** - * Delimiter character. - * This is the character that will be used to separate data cells within a - * row of CSV data. Usually a comma. - * - * @var string - */ - protected $delimiter = ','; - - /** - * Quote character. - * This is the character that will be used to enclose (quote) data cells. It - * is usually a double quote character but single quote is allowed. - * - * @var string - */ - protected $quoteChar = '"'; - - /** - * Escape character. - * This character will be used to escape quotes within quoted text. It is - * mutually exclusive to the doubleQuote attribute. Usually a backspace. - * - * @var string - */ - protected $escapeChar = '\\'; - - /** - * Double quote escape mode. - * If set to true, quote characters within quoted text will be escaped by - * preceding them with the same quote character. - * - * @var bool - */ - protected $doubleQuote = true; - - /** - * Not yet implemented. - * - * @ignore - */ - // protected $skipInitialSpace = false; - - /** - * Quoting style. - * This may be set to one of four values: - * * *Flavor::QUOTE_NONE* - To never quote data cells - * * *Flavor::QUOTE_ALL* - To always quote data cells - * * *Flavor::QUOTE_MINIMAL* - To only quote data cells that contain special characters such as quote character or delimiter character - * * *Flavor::QUOTE_NONNUMERIC* - To quote data cells that contain non-numeric data. - * - * @var string - */ - protected $quoteStyle = self::QUOTE_MINIMAL; - - /** - * Line terminator string sequence. - * This is a character or sequence of characters that will be used to denote - * the end of a row within the data. - * - * @var string - */ - protected $lineTerminator = "\r\n"; - - /** - * Header. - * If set to true, this means the first line of the CSV data is to be treated - * as the column headers. - * - * @var bool - */ - protected $header; - - /** - * Class constructor. - * - * The attributes that make up a flavor object can only be specified by - * passing them in an array as key => value pairs to the constructor. Once - * the flavor object is created, its attributes cannot be changed. - * - * @param array $attributes The attributes that define this particular flavor. These - * attributes are immutable. They can only be set here. - */ - public function __construct($attributes = null) - { - if (!is_null($attributes)) { - if (!is_array($attributes)) { - // @todo throw exception? - return; - } - foreach ($attributes as $attr => $val) { - $this->assertValidAttribute($attr); - $this->$attr = $val; - } - } - } - - /** - * Attribute accessor magic method. - * - * @param string $attr The attribute to "get" - * - * @throws InvalidArgumentException - * - * @return string The attribute value - * - * @internal - */ - public function __get($attr) - { - $this->assertValidAttribute($attr); - - return $this->$attr; - } - - /** - * Attribute accessor (setter) magic method. - * Disabled because attributes are immutable (read-only). - * - * @param string $attr The attribute name you're attempting to set - * @param mixed $val The attribute value - * - * @throws ImmutableException - * - * @internal param The $string attribute to "set" - * @internal param The $string attribute value - * @internal - */ - public function __set($attr, $val) - { - throw new ImmutableException('Cannot change attributes on an immutable object: ' . self::class . '::$' . $attr); - } - - /** - * Does this flavor of CSV have a header row? - * - * The difference between $flavor->header and $flavor->hasHeader() is that - * hasHeader() is always going to give you a boolean value, whereas - * $flavor->header may be null. A null value for header could mean that the - * taster class could not reliably determine whether or not there was a - * header row or it could simply mean that the flavor was instantiated with - * no value for the header property. - * - * @return bool - */ - public function hasHeader() - { - return (bool) $this->header; - } - - /** - * Copy this flavor object. - * - * Because flavor attributes are immutable, it is implossible to change their - * attributes. If you need to change a flavor's attributes, call this method - * instead, specifying which attributes are to be changed. - * - * @param array $attribs An array of attribute name/values to change in the copied flavor - * - * @return Flavor A flavor object with your new attributes - * - * @todo I may want to remove the array type-hint so that this can accept - * array-like objects and iterables as well. Not sure... - */ - public function copy(array $attribs = []) - { - return new self(array_merge($this->toArray(), $attribs)); - } - - /** - * Get this object as an array. - * - * @return array This object as an array - */ - public function toArray() - { - return get_object_vars($this); - } - - /** - * Assert valid attribute name. - * Assert that a particular attribute is valid (basically just that it exists) - * and throw an exception otherwise. - * - * @param string $attr The attribute to check validity of - * - * @throws InvalidArgumentException - * - * @internal - * - * @todo This should accept a second parameter for value that asserts the value - * is a valid value - */ - protected function assertValidAttribute($attr) - { - if (!property_exists(self::class, $attr)) { - throw new InvalidArgumentException('Unknown attribute: ' . $attr); - } - } -} diff --git a/src/CSVelte/Flavor/Excel.php b/src/CSVelte/Flavor/Excel.php deleted file mode 100755 index d9f3952..0000000 --- a/src/CSVelte/Flavor/Excel.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Flavor; - -use CSVelte\Flavor as FlavorBase; - -/** - * Excel CSV "flavor" - * This is the most common flavor of CSV as it is what is produced by Excel, the - * 900 pound Gorilla of CSV importing/exporting. It is also technically the - * "standard" CSV format according to RFC 4180. - * - * @package CSVelte\Reader - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @see https://tools.ietf.org/html/rfc4180 - */ -class Excel extends FlavorBase -{ - protected $escapeChar = null; -} diff --git a/src/CSVelte/Flavor/ExcelTab.php b/src/CSVelte/Flavor/ExcelTab.php deleted file mode 100755 index c6e80d8..0000000 --- a/src/CSVelte/Flavor/ExcelTab.php +++ /dev/null @@ -1,32 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Flavor; - -/** - * Excel CSV "flavor" - * This is the most common flavor of CSV as it is what is produced by Excel, the - * 900 pound Gorilla of CSV importing/exporting. It is also technically the - * "standard" CSV format according to RFC 4180. - * - * @package CSVelte\Reader - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @see https://tools.ietf.org/html/rfc4180 - */ -class ExcelTab extends Excel -{ - protected $delimiter = "\t"; -} diff --git a/src/CSVelte/Flavor/Unix.php b/src/CSVelte/Flavor/Unix.php deleted file mode 100755 index 6a975a9..0000000 --- a/src/CSVelte/Flavor/Unix.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Flavor; - -use CSVelte\Flavor as FlavorBase; - -/** - * Excel CSV "flavor" - * This is the most common flavor of CSV as it is what is produced by Excel, the - * 900 pound Gorilla of CSV importing/exporting. It is also technically the - * "standard" CSV format according to RFC 4180. - * - * @package CSVelte\Reader - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @see https://tools.ietf.org/html/rfc4180 - */ -class Unix extends FlavorBase -{ - protected $quoteChar = '"'; - protected $escapeChar = '\\'; - protected $doubleQuote = false; - protected $lineTerminator = "\n"; - protected $quoteStyle = FlavorBase::QUOTE_NONNUMERIC; -} diff --git a/src/CSVelte/Flavor/UnixTab.php b/src/CSVelte/Flavor/UnixTab.php deleted file mode 100755 index f84c583..0000000 --- a/src/CSVelte/Flavor/UnixTab.php +++ /dev/null @@ -1,32 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Flavor; - -/** - * Excel CSV "flavor" - * This is the most common flavor of CSV as it is what is produced by Excel, the - * 900 pound Gorilla of CSV importing/exporting. It is also technically the - * "standard" CSV format according to RFC 4180. - * - * @package CSVelte\Reader - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @see https://tools.ietf.org/html/rfc4180 - */ -class UnixTab extends Unix -{ - protected $delimiter = "\t"; -} diff --git a/src/CSVelte/IO/BufferStream.php b/src/CSVelte/IO/BufferStream.php index 63877e5..ca9a615 100644 --- a/src/CSVelte/IO/BufferStream.php +++ b/src/CSVelte/IO/BufferStream.php @@ -1,15 +1,14 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\IO; @@ -23,14 +22,6 @@ * Read operations pull from the buffer, write operations fill up the buffer. * When the buffer reaches a * - * @package CSVelte - * @subpackage CSVelte\IO - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2.1 - * * @todo Add methods to convert KB and MB to bytes so that you don't have * to actually know how many bytes are in 16KB. You would just do * $buffer = new BufferStream('16KB'); diff --git a/src/CSVelte/IO/IteratorStream.php b/src/CSVelte/IO/IteratorStream.php index 623caa1..fa557a1 100644 --- a/src/CSVelte/IO/IteratorStream.php +++ b/src/CSVelte/IO/IteratorStream.php @@ -1,15 +1,14 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\IO; @@ -24,14 +23,6 @@ * * A read-only stream that uses an iterable to continuously fill up a buffer as * read operations deplete it. - * - * @package CSVelte - * @subpackage CSVelte\IO - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2.1 */ class IteratorStream implements Streamable { @@ -173,8 +164,8 @@ public function getContents() $contents = ''; while (!$this->eof()) { $contents .= $this->read( - // kind of arbitrary... we have to specify something for the - // chunk length, so I just used the buffer's "high water mark" + // kind of arbitrary... we have to specify something for the + // chunk length, so I just used the buffer's "high water mark" $this->buffer->getMetadata('hwm') ); } diff --git a/src/CSVelte/IO/Stream.php b/src/CSVelte/IO/Stream.php index 023104c..94772d7 100644 --- a/src/CSVelte/IO/Stream.php +++ b/src/CSVelte/IO/Stream.php @@ -1,45 +1,25 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\IO; use CSVelte\Contract\Streamable; use CSVelte\Exception\IOException; -use CSVelte\Traits\IsReadable; -use CSVelte\Traits\IsSeekable; - -use CSVelte\Traits\IsWritable; - +use CSVelte\Traits; use Exception; -/** - * CSVelte Stream. - * - * Represents a stream for input/output. Implements both readable and writable - * interfaces so that it can be passed to either ``CSVelte\Reader`` or - * ``CSVelte\Writer``. - * - * @package CSVelte - * @subpackage CSVelte\IO - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 - */ class Stream implements Streamable { - use IsReadable, IsWritable, IsSeekable; + use Traits\IsReadable, Traits\IsWritable, Traits\IsSeekable; /** * @var StreamResource A stream resource object @@ -59,6 +39,8 @@ class Stream implements Streamable * * @todo Not sure if this belongs in this class or in Resource. I'm leaving * it here for now, simply because I'm worried Stream will become superfluous + * + * @todo THERE IS NO NEED FOR STREAM AND STREAMRESOURCE -- it was a cute idea, but just combine them and stop being a big chode */ protected $meta; diff --git a/src/CSVelte/IO/StreamResource.php b/src/CSVelte/IO/StreamResource.php index 63eaf27..659bc8f 100644 --- a/src/CSVelte/IO/StreamResource.php +++ b/src/CSVelte/IO/StreamResource.php @@ -1,19 +1,17 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\IO; -use CSVelte\Contract\Streamable; use CSVelte\Exception\IOException; use InvalidArgumentException; @@ -24,13 +22,10 @@ * me to provide a nice, clean, easy-to-use interface for opening stream * resources in a particular mode as well as to lazy-open a stream. * - * @package CSVelte - * @subpackage CSVelte\IO - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2.1 + * @since v0.2.1 + * @todo Annoyingly, this class is currently receiving an "F" from my code analysis tool because of its supposed + * complexity. I believe the high number of methods is the issue. Despite its high number of methods, I don't + * feel like this class is overly complex. I would like to find a way to configure the tool to ignore this. */ class StreamResource { @@ -189,7 +184,7 @@ public function __construct( $mode = null, $lazy = null, $use_include_path = null, - $context_options = null, + $context_options = null, // this should just be an array -> $context $context_params = null ) { // first, check if we're wrapping an existing stream resource @@ -198,14 +193,13 @@ public function __construct( return; } -// throw new InvalidArgumentException("Argument one for " . __METHOD__ . " must be a URI or a stream resource."); // ok we're opening a new stream resource handle $this->setUri($uri) - ->setMode($mode) - ->setLazy($lazy) - ->setUseIncludePath($use_include_path) - ->setContext($context_options, $context_params); + ->setMode($mode) + ->setLazy($lazy) + ->setUseIncludePath($use_include_path) + ->setContext($context_options, $context_params); if (!$this->isLazy()) { $this->connect(); } @@ -224,7 +218,9 @@ public function __destruct() * * Creates and returns a Stream object for this resource * - * @return Streamable A stream for this resource + * @todo Should add a getStream() method and return $this->>getStream() from this instead + * + * @return Stream A stream for this resource */ public function __invoke() { @@ -237,13 +233,14 @@ public function __invoke() * File open is (by default) delayed until the user explicitly calls connect() * or they request the resource handle with getHandle(). * - * @throws \CSVelte\Exception\IOException if connection fails + * @throws IOException if connection fails * * @return bool True if connection was successful */ public function connect() { if (!$this->isConnected()) { + /** @var IOException $e */ $e = null; $errhandler = function () use (&$e) { $e = new IOException(sprintf( @@ -290,7 +287,7 @@ public function disconnect() * Set the stream URI. Can only be set if the connection isn't open yet. * If you try to set the URI on an open resource, an IOException will be thrown * - * @param string $uri The URI for this stream resource to open + * @param string|object $uri The URI for this stream resource to open * * @throws \InvalidArgumentException if not a valid stream uri * @throws \CSVelte\Exception\IOException if stream has already been opened @@ -360,9 +357,9 @@ public function setMode($mode = null) $this->flag = ''; $this->setBaseMode($base) - ->setIsPlus($plus == '+') - ->setIsText($flag == 't') - ->setIsBinary($flag == 'b'); + ->setIsPlus($plus == '+') + ->setIsText($flag == 't') + ->setIsBinary($flag == 'b'); return $this; } @@ -901,7 +898,7 @@ protected function setLazy($lazy) if (is_null($lazy)) { $lazy = true; } - $this->lazy = (bool) $lazy; + $this->lazy = (bool) $lazy; return $this; } @@ -963,4 +960,4 @@ protected function assertValidWrapper($name) throw new InvalidArgumentException("{$name} is not a known stream wrapper."); } } -} +} \ No newline at end of file diff --git a/src/CSVelte/Reader.php b/src/CSVelte/Reader.php old mode 100755 new mode 100644 index 9084ef3..68efd1c --- a/src/CSVelte/Reader.php +++ b/src/CSVelte/Reader.php @@ -1,525 +1,252 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte; +use Iterator; +use Countable; use CSVelte\Contract\Streamable; +use Noz\Collection\Collection; +use Stringy\Stringy; -use CSVelte\Exception\EndOfFileException; -use CSVelte\Reader\FilteredIterator as FilteredReader; -use CSVelte\Table\HeaderRow; - -use CSVelte\Table\Row; - -use function - CSVelte\streamize; +use function Noz\collect; +use function Stringy\create as s; -/** - * CSV Reader. - * - * Reads CSV data from any object that implements CSVelte\Contract\Readable. - * - * @package CSVelte - * @subpackage Reader - * - * @since v0.1 - * - * @todo Also, is there any way to do some kind of caching or something? Probably - * not but if you could that would be a cool feature... - */ -class Reader implements \Iterator +class Reader implements Iterator, Countable { - const PLACEHOLDER_DELIM = '[=[__DLIM__]=]'; - const PLACEHOLDER_NEWLINE = '[=[__NWLN__]=]'; + /** @var Streamable The input stream to read from */ + protected $input; - /** - * This class supports any sources of input that implements this interface. - * This way I can read from local files, streams, FTP, any class that implements - * the "Readable" interface. - * - * @var Contract\Streamable - */ - protected $source; + /** @var Dialect The *dialect* of CSV to read */ + protected $dialect; - /** - * @var Flavor The "flavor" or format of the CSV being read - */ - protected $flavor; - - /** - * @var Table\Row|null Row currently loaded into memory - */ + /** @var Collection The line currently sitting in memory */ protected $current; - /** - * @var int The current line being read (from input source) - */ - protected $line = 0; - - /** - * @var Table\HeaderRow The header row (if any) - */ + /** @var Collection The header row */ protected $header; - /** - * @var array An array of callback functions - */ - protected $filters = []; - - /** - * @var bool True if current line ended while inside a quoted string - */ - protected $open = false; - - /** - * @var bool True if last character read was the escape character - */ - protected $escape = false; + /** @var int The current line number */ + protected $lineNo; /** - * Reader Constructor. - * Initializes a reader object using an input source and optionally a flavor. + * Reader constructor. * - * @param mixed $input The source of our CSV data - * @param Flavor|array|null $flavor The "flavor" or format specification object - */ - public function __construct($input, $flavor = null) - { - $this->setSource($input) - ->setFlavor($flavor) - ->rewind(); - } - - /** - * Flavor Getter. + * Although this is the constructor, I don't envision it being used much in userland. I think much more common + * methods of creating readers will be available within CSVelte base class such as CSVelte::fromPath(), + * CSVelte::fromString(), CSVelte::fromSplFileObject, CSVelte::toSplFileObject, CSVelte::toPath(), etc. * - * Retreive the "flavor" object being used by the reader - * - * @return Flavor + * @param Streamable $input The source being read from + * @param Dialect $dialect The dialect being read */ - public function getFlavor() + public function __construct(Streamable $input, Dialect $dialect = null) { - return $this->flavor; + if (is_null($dialect)) { + $dialect = new Dialect; + } + $this->setInputStream($input) + ->setDialect($dialect); } /** - * Check if flavor object defines header. - * - * Determine whether or not the input source's CSV data contains a header - * row or not. Unless you explicitly specify so within your Flavor object, - * this method is a logical best guess. The CSV format does not - * provide metadata of any kind and therefor does not provide this info. + * Get csv data as a two-dimensional array * - * @return bool True if the input source has a header row (or, to be more ) - * accurate, if the flavor SAYS it has a header row) - * - * @todo Rather than always reading in Taster::SAMPLE_SIZE, read in ten lines at a time until - * whatever method it is has enough data to make a reliable decision/guess + * @return array */ - public function hasHeader() + public function toArray() { - return $this->getFlavor()->header; + return iterator_to_array($this); } /** - * Retrieve current row. + * Set the CSV dialect * - * @return Table\Row The current row - */ - public function current() - { - return $this->current; - } - - /** - * Advance to the next row. + * @param Dialect $dialect The *dialect* of CSV to read * - * @return Table\Row|null The current row (if there is one) + * @return self */ - public function next() + public function setDialect(Dialect $dialect) { - $this->current = null; - $this->load(); - - return $this->current; + $this->dialect = $dialect; + // call rewind because new dialect needs to be used to re-read + return $this->rewind(); } /** - * Determine if current position has valid row. + * Get dialect * - * @return bool True if current row is valid + * @return Dialect */ - public function valid() + public function getDialect() { - return (bool) $this->current; + return $this->dialect; } /** - * Retrieve current row key (line number). + * Fetch a single row * - * @return int The current line number - */ - public function key() - { - return $this->line; - } - - /** - * Rewind to the beginning of the dataset. + * Fetch the next row from the CSV data. If no more data available, returns false. * - * @return Table\Row|null The current row + * @return array|false */ - public function rewind() + public function fetchRow() { - $this->line = 0; - $this->source->rewind(); - $this->current = null; - $this->load(); - if ($this->hasHeader()) { - $this->next(); + if (!$this->valid()) { + return false; } - - return $this->current(); - } - - /** - * Retrieve header row. - * - * @return Table\HeaderRow The header row if there is one - */ - public function header() - { - return $this->header; - } - - /** - * Add anonumous function as filter. - * - * Add an anonymous function that accepts the current row as its only argument. - * Return true from the function to keep that row, false otherwise. - * - * @param callable $filter An anonymous function to filter out row by certain criteria - * - * @return $this - */ - public function addFilter(callable $filter) - { - array_push($this->filters, $filter); - - return $this; + $line = $this->current(); + $this->next(); + return $line; } /** - * Add multiple filters at once. - * - * Add an array of anonymous functions to filter out certain rows. + * Set input stream * - * @param array $filters An array of anonymous functions + * @param Streamable $stream The input stream to read from * - * @return $this + * @return self */ - public function addFilters(array $filters) + protected function setInputStream(Streamable $stream) { - foreach ($filters as $filter) { - $this->addFilter($filter); - } - + $this->input = $stream; return $this; } /** - * Returns an iterator with rows from user-supplied filter functions removed. - * - * @return FilteredReader An iterator with filtered rows - */ - public function filter() - { - return new FilteredReader($this, $this->filters); - } - - /** - * Retrieve the contents of the dataset as an array of arrays. - * - * @return array An array of arrays of CSV content - */ - public function toArray() - { - return array_map(function ($row) { - return $row->toArray(); - }, iterator_to_array($this)); - } - - /** - * Set the flavor. - * - * Set the ``CSVelte\Flavor`` object, used to determine CSV format. + * Loads next line into memory * - * @param Flavor|array|null $flavor Either an array or a flavor object + * Reads from input one character at a time until a newline is reached that isn't within quotes. Once a completed + * line has been loaded, it is assigned to the `$this->current` property. Subsequent calls will continue to load + * successive lines until the end of the input source is reached. * - * @return $this + * @return self */ - protected function setFlavor($flavor = null) + protected function loadLine() { - if (is_array($flavor)) { - $flavor = new Flavor($flavor); - } - // @todo put this inside a try/catch - if (is_null($flavor)) { - $flavor = taste($this->source); - } - if (is_null($flavor->header)) { - // Flavor is immutable, give me a new one with header set to lickHeader return val - $flavor = $flavor->copy(['header' => taste_has_header($this->source)]); + $d = $this->getDialect(); + $line = ''; + while ($str = $this->input->readLine($d->getLineTerminator())) { + $line .= $str; + if (count(s($line)->split($d->getQuoteChar())) % 2) { + break; + } } - $this->flavor = $flavor; - + $this->current = $this->parseLine($line); return $this; } /** - * Set the reader source. + * Parse a line of CSV into individual fields * - * The reader can accept anything that implements Readable and is actually - * readable (can be read). This will make sure that whatever is passed to - * the reader meets these expectations and set $this->source. It can also - * accept any string (or any object with a __toString() method), or an - * SplFileObject, so long as it represents a file rather than a directory. + * Accepts a line (string) of CSV data that it then splits at the delimiter character. The method is smart, in that + * it knows not to split at delimiters within quotes. Ultimately, fields are placed into a collection and returned. * - * @param mixed $input See description + * @param string $line A single line of CSV data to parse into individual fields * - * @return $this - */ - protected function setSource($input) - { - if (!($input instanceof Streamable)) { - $input = streamize($input); - } - $this->source = $input; - - return $this; - } - - /** - * Load a line into memory. + * @return Collection */ - protected function load() + protected function parseLine($line) { - if (is_null($this->current)) { - try { - $line = $this->readLine(); - $this->line++; - $parsed = $this->parse($line); - if ($this->hasHeader() && $this->line === 1) { - $this->header = new HeaderRow($parsed); - } else { - $this->current = new Row($parsed); - if ($this->header) { - $this->current->setHeaderRow($this->header); - } - } - } catch (EndOfFileException $e) { - $this->current = null; + $d = $this->getDialect(); + $fields = collect(s($line) + ->trimRight($d->getLineTerminator()) + ->split($d->getDelimiter() . "(?=([^\"]*\"[^\"]*\")*[^\"]*$)")); + if (!is_null($this->header)) { + // @todo there may be cases where this gives a false positive... + if (count($fields) == count($this->header)) { + $fields = $fields->rekey($this->header); } } - } - - /** - * Read single line from CSV data source (stream, file, etc.), taking into - * account CSV's de-facto quoting rules with respect to designated line - * terminator character when they fall within quoted strings. - * - * @throws Exception\EndOfFileException when eof has been reached - * and the read buffer has all been returned - * - * @return string A CSV row (could possibly span multiple lines depending on - * quoting and escaping) - */ - protected function readLine() - { - $f = $this->getFlavor(); - $eol = $f->lineTerminator; - try { - do { - if (!isset($lines)) { - $lines = []; - } - if (false === ($line = $this->source->readLine($eol))) { - throw new EndOfFileException('End of file reached'); - } - array_push($lines, rtrim($line, $eol)); - } while ($this->inQuotedString(end($lines), $f->quoteChar, $f->escapeChar)); - } catch (EndOfFileException $e) { - // only throw the exception if we don't already have lines in the buffer - if (!count($lines)) { - throw $e; + return $fields->map(function(Stringy $field, $pos) use ($d) { + if ($d->isDoubleQuote()) { + $field = $field->replace('""', '"'); } - } - - return rtrim(implode($eol, $lines), $eol); + return (string) $field->trim($d->getQuoteChar()); + }); } + /** == BEGIN: SPL implementation methods == */ + /** - * Determine whether last line ended while a quoted string was still "open". - * - * This method is used in a loop to determine if each line being read ends - * while a quoted string is still "open". + * Get current row * - * @param string $line Line of csv to analyze - * @param string $quoteChar The quote/enclosure character to use - * @param string $escapeChar The escape char/sequence to use - * - * @return bool True if currently within a quoted string + * @return array */ - protected function inQuotedString($line, $quoteChar, $escapeChar) + public function current() { - if (!empty($line)) { - do { - if (!isset($i)) { - $i = 0; - } - $c = $line[$i++]; - if ($this->escape) { - $this->escape = false; - continue; - } - $this->escape = ($c == $escapeChar); - if ($c == $quoteChar) { - $this->open = !$this->open; - } - } while ($i < strlen($line)); - } - - return $this->open; + return $this->current->toArray(); } /** - * Temporarily replace special characters within a quoted string. + * Move pointer to beginning of the next line internally and then load the line * - * Replace all instances of newlines and whatever character you specify (as - * the delimiter) that are contained within quoted text. The replacements are - * simply a special placeholder string. This is done so that I can use the - * very unsmart "explode" function and not have to worry about it exploding - * on delimiters or newlines within quotes. Once I have exploded, I typically - * sub back in the real characters before doing anything else. - * - * @param string $data The string to do the replacements on - * @param string $delim The delimiter character to replace - * @param string $quo The quote character - * @param string $eol Line terminator character/sequence - * - * @return string The data with replacements performed - * - * @internal - * - * @todo I could probably pass in (maybe optionally) the newline character I - * want to replace as well. I'll do that if I need to. - * @todo Create a regex class so you can do $regex->escape() rather than - * preg_quote + * @return self */ - protected function replaceQuotedSpecialChars($data, $delim, $quo, $eol) + public function next() { - return preg_replace_callback('/([' . preg_quote($quo, '/') . '])(.*)\1/imsU', function ($matches) use ($delim, $eol) { - $ret = str_replace($eol, self::PLACEHOLDER_NEWLINE, $matches[0]); - $ret = str_replace($delim, self::PLACEHOLDER_DELIM, $ret); - - return $ret; - }, $data); + $this->loadLine(); + $this->lineNo++; + return $this; } /** - * Undo temporary special char replacements. - * - * Replace the special character placeholders with the characters they - * originally substituted. - * - * @param string $data The data to undo replacements in - * @param string $delim The delimiter character - * @param string $eol The character or string of characters used to terminate lines + * Get current line number * - * @return string The data with placeholders replaced with original characters - * - * @internal + * @return int */ - protected function undoReplaceQuotedSpecialChars($data, $delim, $eol) + public function key() { - $replacements = [self::PLACEHOLDER_DELIM => $delim, self::PLACEHOLDER_NEWLINE => $eol]; - if (array_walk($replacements, function ($replacement, $placeholder) use (&$data) { - $data = str_replace($placeholder, $replacement, $data); - })) { - return $data; - } + return $this->lineNo; } /** - * Remove quotes wrapping text. - * - * @param string $data The data to unquote - * - * @return string The data with quotes stripped from the outside of it + * Have we reached the end of the CSV data? * - * @internal + * @return bool */ - protected function unQuote($data) + public function valid() { - $escapeChar = $this->getFlavor()->doubleQuote ? $this->getFlavor()->quoteChar : $this->getFlavor()->escapeChar; - $quoteChar = $this->getFlavor()->quoteChar; - $data = $this->unEscape($data, $escapeChar, $quoteChar); - - return preg_replace('/^(["\'])(.*)\1$/ms', '\2', $data); + return !$this->input->eof(); } /** - * "Unescape" a string. - * - * Replaces escaped characters with their unescaped versions. - * - * @internal + * Rewind to the beginning * - * @param string $str The string to unescape - * @param string $esc The escape character used - * @param string $quo The quote character used + * Rewinds the internal pointer to the beginning of the CSV data, load first line, and reset line number to 1. Also + * loads the header (if one exists) and uses its values as indexes within rows. * - * @return mixed The string with characters unescaped - * - * @todo This actually shouldn't even be necessary. Characters should be read - * in one at a time and a quote that follows another should just be ignored - * deeming this unnecessary. + * @return self */ - protected function unEscape($str, $esc, $quo) + public function rewind() { - return str_replace($esc . $quo, $quo, $str); + $this->lineNo = 1; + $this->input->rewind(); + if ($this->getDialect()->hasHeader()) { + $this->loadLine(); + $this->header = $this->current(); + } + $this->loadLine(); + return $this; } /** - * Parse a line of CSV data into an array of columns. - * - * @param string $line A line of CSV data to parse + * Get number of lines in the CSV data (not including header) * - * @return array An array of columns - * - * @internal + * @return int */ - protected function parse($line) + public function count() { - $f = $this->getFlavor(); - $replaced = $this->replaceQuotedSpecialChars($line, $f->delimiter, $f->quoteChar, $f->lineTerminator); - $columns = explode($f->delimiter, $replaced); - $that = $this; - - return array_map(function ($val) use ($that, $f) { - $undone = $that->undoReplaceQuotedSpecialChars($val, $f->delimiter, $f->lineTerminator); - - return $this->unQuote($undone); - }, $columns); + return count($this->toArray()); } -} + + /** == END: SPL implementation methods == */ +} \ No newline at end of file diff --git a/src/CSVelte/Reader/FilteredIterator.php b/src/CSVelte/Reader/FilteredIterator.php deleted file mode 100644 index 01f5357..0000000 --- a/src/CSVelte/Reader/FilteredIterator.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Reader; - -use CSVelte\Reader as CsvReader; -use FilterIterator; - -/** - * Filtered Reader Iterator. - * - * This class is not intended to be instantiated manually. It is returned by the - * CSVelte\Reader class when filter() is called to iterate over the CSV file, - * skipping all rows that don't pass the filter(s) tests. - * - * @package CSVelte - * @subpackage Reader - * - * @since v0.1 - * - * @internal - */ -class FilteredIterator extends FilterIterator -{ - /** - * A list of callback functions. - * - * @var array of Callable objects/functions - */ - protected $filters = []; - - /** - * FilteredIterator Constructor. - * - * Initializes the iterator using the CSV reader and its array of callback - * filter functions/callables. - * - * @param \CSVelte\Reader $reader The CSV reader being iterated - * @param array $filters The list of callbacks - */ - public function __construct(CsvReader $reader, array $filters) - { - $this->filters = $filters; - parent::__construct($reader); - } - - /** - * Run filters against each row. - * Loop through all of the callback functions, and if any of them fail, do - * not include this row in the iteration. - * - * @return bool - * - * @todo filter functions should accept current row, index, AND ref to reader - */ - public function accept() - { - $reader = $this->getInnerIterator(); - foreach ($this->filters as $filter) { - if (!$filter($reader->current())) { - return false; - } - } - - return true; - } - - /** - * Return this object as an array. - * - * @return array This object as an array - */ - public function toArray() - { - return array_map(function ($row) { - return $row->toArray(); - }, iterator_to_array($this)); - } -} diff --git a/src/CSVelte/Sniffer.php b/src/CSVelte/Sniffer.php new file mode 100644 index 0000000..c10828a --- /dev/null +++ b/src/CSVelte/Sniffer.php @@ -0,0 +1,190 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte; + +use CSVelte\Contract\Streamable; + +use CSVelte\Exception\SnifferException; +use CSVelte\Sniffer\SniffDelimiterByConsistency; +use CSVelte\Sniffer\SniffDelimiterByDistribution; +use CSVelte\Sniffer\SniffHeaderByDataType; +use CSVelte\Sniffer\SniffLineTerminatorByCount; +use CSVelte\Sniffer\SniffQuoteAndDelimByAdjacency; +use CSVelte\Sniffer\SniffQuoteStyle; +use Noz\Collection\Collection; +use function Noz\to_array; +use RuntimeException; + +use function Noz\collect; +use function Stringy\create as s; + +class Sniffer +{ + /** CSV data sample size - sniffer will use this many bytes to make its determinations */ + const SAMPLE_SIZE = 2500; + + /** + * ASCII character codes for "invisibles". + */ + const HORIZONTAL_TAB = 9; + const LINE_FEED = 10; + const CARRIAGE_RETURN = 13; + const SPACE = 32; + + /** + * @var array A list of possible delimiters to check for (in order of preference) + */ + protected $delims = [',', "\t", ';', '|', ':', '-', '_', '#', '/', '\\', '$', '+', '=', '&', '@']; + + /** + * @var Streamable A stream of the sample data + */ + protected $stream; + + /** + * Sniffer constructor. + * + * @param Streamable $stream The data to sniff + * @param array $delims A list of possible delimiter characters in order of preference + */ + public function __construct(Streamable $stream, $delims = null) + { + $this->stream = $stream; + if (!is_null($delims)) { + $this->setPossibleDelimiters($delims); + } + } + + /** + * Set possible delimiter characters + * + * @param array $delims A list of possible delimiter characters + * + * @return self + */ + public function setPossibleDelimiters(array $delims) + { + $this->delims = collect($delims) + ->filter(function($val) { + return s($val)->length() == 1; + }) + ->values() + ->toArray(); + + return $this; + } + + /** + * Get list of possible delimiter characters + * + * @return array + */ + public function getPossibleDelimiters() + { + return $this->delims; + } + + /** + * Sniff CSV data (determine its dialect) + * + * Since CSV is less a format than a collection of similar formats, you can never be certain how a particular CSV + * file is formatted. This method inspects CSV data and returns its "dialect", an object that can be passed to + * either a `CSVelte\Reader` or `CSVelte\Writer` object to tell it what "dialect" of CSV to use. + * + * @todo look into which other Dialect attributes you can sniff for + * + * @return Dialect + */ + public function sniff() + { + $sample = $this->stream->read(static::SAMPLE_SIZE); + $lineTerminator = $this->sniffLineTerminator($sample); + try { + list($quoteChar, $delimiter) = $this->sniffQuoteAndDelim($sample, $lineTerminator); + } catch (SnifferException $e) { + if ($e->getCode() !== SnifferException::ERR_QUOTE_AND_DELIM) { + throw $e; + } + $quoteChar = '"'; + $delimiter = $this->sniffDelimiter($sample, $lineTerminator); + } + /** + * @todo Should this be null? Because doubleQuote = true means this = null + */ + $escapeChar = '\\'; + $quoteStyle = $this->sniffQuotingStyle($sample, $delimiter, $lineTerminator); + $header = $this->sniffHasHeader($sample, $delimiter, $lineTerminator); + $encoding = s($sample)->getEncoding(); + + return new Dialect(compact('quoteChar', 'escapeChar', 'delimiter', 'lineTerminator', 'quoteStyle', 'header', 'encoding')); + } + + /** + * Sniff sample data for line terminator character + * + * @param string $data The sample data + * + * @return string + */ + protected function sniffLineTerminator($data) + { + $sniffer = new SniffLineTerminatorByCount(); + return $sniffer->sniff($data); + } + + /** + * Sniff quote and delimiter chars + * + * The best way to determine quote and delimiter characters is when columns + * are quoted, often you can seek out a pattern of delim, quote, stuff, quote, delim + * but this only works if you have quoted columns. If you don't you have to + * determine these characters some other way... (see lickDelimiter). + * + * @throws SnifferException + * + * @param string $data The data to analyze + * @param string $lineTerminator The line terminator char/sequence + * + * @return array A two-row array containing quotechar, delimchar + */ + protected function sniffQuoteAndDelim($data, $lineTerminator) + { + $sniffer = new SniffQuoteAndDelimByAdjacency(compact('lineTerminator')); + return $sniffer->sniff($data); + } + + protected function sniffDelimiter($data, $lineTerminator) + { + $delimiters = $this->getPossibleDelimiters(); + $consistency = new SniffDelimiterByConsistency(compact('lineTerminator', 'delimiters')); + $winners = $consistency->sniff($data); + if (count($winners) > 1) { + $delimiters = $winners; + return (new SniffDelimiterByDistribution(compact('lineTerminator', 'delimiters'))) + ->sniff($data); + } + return current($winners); + } + + protected function sniffQuotingStyle($data, $delimiter, $lineTerminator) + { + $sniffer = new SniffQuoteStyle(compact( 'lineTerminator', 'delimiter')); + return $sniffer->sniff($data); + } + + protected function sniffHasHeader($data, $delimiter, $lineTerminator) + { + $sniffer = new SniffHeaderByDataType(compact( 'lineTerminator', 'delimiter')); + return $sniffer->sniff($data); + } +} diff --git a/src/CSVelte/Sniffer/AbstractSniffer.php b/src/CSVelte/Sniffer/AbstractSniffer.php new file mode 100644 index 0000000..8f89c04 --- /dev/null +++ b/src/CSVelte/Sniffer/AbstractSniffer.php @@ -0,0 +1,104 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +abstract class AbstractSniffer +{ + /** + * Placeholder strings -- hold the place of newlines and delimiters contained + * within quoted text so that the explode method doesn't split incorrectly. + */ + const PLACEHOLDER_NEWLINE = '[__NEWLINE__]'; + const PLACEHOLDER_DELIM = '[__DELIMIT__]'; + + protected $options = []; + + public function __construct(array $options = []) + { + $this->setOptions($options); + } + + protected function setOptions(array $options) + { + $this->options = array_merge($this->options, $options); + return $this; + } + + protected function setOption($option, $value) + { + if (array_key_exists($option, $this->options)) { + $this->options[$option] = $value; + } + return $this; + } + + protected function getOption($option) + { + if (array_key_exists($option, $this->options)) { + return $this->options[$option]; + }; + } + + /** + * Replace all instances of newlines and whatever character you specify (as + * the delimiter) that are contained within quoted text. The replacements are + * simply a special placeholder string. + * + * @param string $data The string to do the replacements on + * @param string $delim The delimiter character to replace + * + * @return string The data with replacements performed + */ + protected function replaceQuotedSpecialChars($data, $delim = null, $eol = null) + { + if (is_null($eol)) { + $eol = "\r\n|\r|\n"; + } + return preg_replace_callback('/([\'"])(.*)\1/imsU', function ($matches) use ($delim, $eol) { + $ret = preg_replace("/({$eol})/", static::PLACEHOLDER_NEWLINE, $matches[0]); + if (!is_null($delim)) { + $ret = str_replace($delim, static::PLACEHOLDER_DELIM, $ret); + } + return $ret; + }, $data); + } + + /** + * Replaces all quoted columns with a blank string. I was using this method + * to prevent explode() from incorrectly splitting at delimiters and newlines + * within quotes when parsing a file. But this was before I wrote the + * replaceQuotedSpecialChars method which (at least to me) makes more sense. + * + * @param string $data The string to replace quoted strings within + * + * @return string The input string with quoted strings removed + */ + protected function removeQuotedStrings($data) + { + return preg_replace($pattern = '/(["\'])(?:(?=(\\\\?))\2.)*?\1/sm', $replace = '', $data); + } + + protected function unQuote($string) + { + return preg_replace('/^(["\'])(.*)\1$/', '\2', (string) $string); + } + + /** + * Analyze data (sniff) + * + * @param string $data The data to analyze (sniff) + * + * @return string|string[] + */ + abstract public function sniff($data); +} \ No newline at end of file diff --git a/src/CSVelte/Sniffer/SniffDelimiterByConsistency.php b/src/CSVelte/Sniffer/SniffDelimiterByConsistency.php new file mode 100644 index 0000000..ba538e2 --- /dev/null +++ b/src/CSVelte/Sniffer/SniffDelimiterByConsistency.php @@ -0,0 +1,87 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +use function Noz\collect; +use Noz\Collection\Collection; +use function Stringy\create as s; + +class SniffDelimiterByConsistency extends AbstractSniffer +{ + /** + * Guess delimiter in a string of data + * + * Guesses the delimiter character by analyzing the count consistency of possible delimiters across several lines. + * Basically, the character that occurs roughly the same number of times on each line will be returned. It is + * possible for this sniffer to return multiple characters if there is a tie. + * + * @param string $data The data to analyze + * + * @return string[] + */ + public function sniff($data) + { + // build a table of characters and their frequencies for each line. We + // will use this frequency table to then build a table of frequencies of + // each frequency (in 10 lines, "tab" occurred 5 times on 7 of those + // lines, 6 times on 2 lines, and 7 times on 1 line) + + $delimiters = $this->getOption('delimiters'); + $lineTerminator = $this->getOption('lineTerminator') ?: "\n"; + // @todo it would probably make for more consistent results if you popped the last line since it will most likely be truncated due to the arbitrary nature of the sample size + $lines = collect(explode($lineTerminator, $this->removeQuotedStrings($data))); + $frequencies = $lines->map(function($line) use ($delimiters) { + $preferred = array_flip($delimiters); + return collect($preferred) + ->map(function() { return 0; }) + ->merge(collect(s($line)->chars())->frequency()->kintersect($preferred)) + ->toArray(); + }); + + // now determine the mode for each char to decide the "expected" amount + // of times a char (possible delim) will occur on each line... + $modes = collect($delimiters) + ->flip() + ->map(function($freq, $delim) use ($frequencies) { + return $frequencies->getColumn($delim)->mode(); + }) + ->filter(); + + /** @var Collection $consistencies */ + $consistencies = $frequencies->recollect(function(Collection $accum, $freq, $line_no) use ($modes) { + + $modes->each(function($expected, $char) use ($accum, $freq) { + /** @var Collection $freq */ + if (collect($freq)->get($char) == $expected) { + $matches = $accum->get($char, 0); + $accum->set($char, ++$matches); + } + }); + return $accum; + + }) + ->sort() + ->reverse(); + + $winners = $consistencies->filter(function($freq) use ($consistencies) { + return $freq === $consistencies->max(); + }) + ->keys(); + + // return winners in order of preference + return collect($delimiters) + ->intersect($winners) + ->values() + ->toArray(); + } +} \ No newline at end of file diff --git a/src/CSVelte/Sniffer/SniffDelimiterByDistribution.php b/src/CSVelte/Sniffer/SniffDelimiterByDistribution.php new file mode 100644 index 0000000..e22de05 --- /dev/null +++ b/src/CSVelte/Sniffer/SniffDelimiterByDistribution.php @@ -0,0 +1,57 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +use function Noz\collect; +use Noz\Collection\Collection; +use function Stringy\create as s; + +class SniffDelimiterByDistribution extends AbstractSniffer +{ + /** + * Guess delimiter in a string of data + * + * Guesses the delimiter in a data set by analyzing which of the provided possible delimiter characters is most + * evenly distributed (horizontally) across the dataset. + * + * @param string $data The data to analyze + * + * @return string[] + */ + public function sniff($data) + { + $lineTerminator = $this->getOption('lineTerminator') ?: "\n"; + $delimiters = $this->getOption('delimiters'); + $lines = collect(explode($lineTerminator, $this->removeQuotedStrings($data))); + return collect($delimiters)->flip()->map(function($x, $char) use ($lines) { + + // standard deviation + $sd = $lines->map(function($line, $line_no) use ($char) { + $delimited = collect(s($line)->split($char)) + ->map(function($str) { + return $str->length(); + }); + // standard deviation + $avg = $delimited->average(); + return sqrt($delimited->recollect(function(Collection $d, $len) use ($avg) { + return $d->add(pow($len - $avg, 2)); + }) + ->sum() / $delimited->count()); + }); + return $sd->average(); + + }) + ->sort() + ->getKeyAt(1); + } +} \ No newline at end of file diff --git a/src/CSVelte/Sniffer/SniffHeaderByDataType.php b/src/CSVelte/Sniffer/SniffHeaderByDataType.php new file mode 100644 index 0000000..9e8d7be --- /dev/null +++ b/src/CSVelte/Sniffer/SniffHeaderByDataType.php @@ -0,0 +1,136 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +use CSVelte\Dialect; +use CSVelte\Reader; +use Noz\Collection\Collection; + +use function CSVelte\to_stream; +use function Noz\collect; +use function Stringy\create as s; + +class SniffHeaderByDataType extends AbstractSniffer +{ + /** + * Guess whether there is a header row + * + * Guesses whether the data has a header row by comparing the data types of the first row with the types of + * corresponding columns in other rows. + * + * @note Unlike the original version of this method, this one will be used to ALSO determine HOW MANY header rows + * there likely are. So, compare the header to rows at the END of the sample. + * + * @param string $data The data to analyze + * + * @return bool + */ + public function sniff($data) + { + $delimiter = $this->getOption('delimiter'); + $getFieldInfo = function($val) { + return [ + 'value' => $val, + 'type' => $this->getType($val), + 'length' => s($val)->length() + ]; + }; + $reader = new Reader(to_stream($data), new Dialect(['delimiter' => $delimiter, 'header' => false])); + $lines = collect($reader->toArray()); + $header = collect($lines->shift()) ->map($getFieldInfo); + $lines->pop(); // get rid of the last line because it may be incomplete + $comparison = $lines->slice(0, 10)->map(function($fields) use ($getFieldInfo) { + return array_map($getFieldInfo, $fields); + }); + + /** + * @var Collection $header + * @var Collection $noHeader + */ + list($header, $noHeader) = $header->map(function($hval, $hind) use ($comparison) { + + $isHeader = 0; + $type = $comparison->getColumn($hind)->getColumn('type'); + $length = $comparison->getColumn($hind)->getColumn('length'); + if ($distinct = $type->distinct()) { + if ($distinct->count() == 1) { + if ($distinct->getValueAt(1) != $hval['type']) { + $isHeader = 1; + } + } + } + + if (!$isHeader) { + // use standard deviation to determine if header is wildly different length than others + $mean = $length->average(); + $sd = sqrt($length->map(function ($len) use ($mean) { + return pow($len - $mean, 2); + })->average()); + + $diff_head_avg = abs($hval['length'] - $mean); + if ($diff_head_avg > $sd) { + $isHeader = 1; + } + } + return $isHeader; + + }) + ->partition(function($val) { + return (bool) $val; + }); + + return $header->count() > $noHeader->count(); + } + + protected function getType($value) + { + $str = s($value); + switch (true) { + case is_numeric($value): + return 'numeric'; + // note - the order of these is important, do not change unless you know what you're doing + case is_string($value): + if (preg_match('/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/i', $value)) { + return 'email'; + } + if (preg_match('/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i', $value)) { + return 'url'; + } + if (strtotime($value) !== false) { + return 'datetime'; + } + if (preg_match('/^[+-]?[¥£€$]\d+(\.\d+)$/', $value)) { + return 'currency'; + } + if (preg_match('/^[a-z0-9_-]{1,35}$/i', $value)) { + return 'identifier'; + } + if (preg_match('/^[a-z0-9 _\/&\(\),\.?\'!-]{1,50}$/i', $value)) { + return 'text_short'; + } + if (preg_match('/^[a-z0-9 _\/&\(\),\.?\'!-]{100,}$/i', $value)) { + return 'text_long'; + } + if ($str->isAlphanumeric()) { + return 'alnum'; + } + if ($str->isBlank()) { + return 'blank'; + } + if ($str->isJson()) { + return 'json'; + } + } + return 'other'; + } +} diff --git a/src/CSVelte/Sniffer/SniffLineTerminatorByCount.php b/src/CSVelte/Sniffer/SniffLineTerminatorByCount.php new file mode 100644 index 0000000..ced00ff --- /dev/null +++ b/src/CSVelte/Sniffer/SniffLineTerminatorByCount.php @@ -0,0 +1,53 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +class SniffLineTerminatorByCount extends AbstractSniffer +{ + /** + * End-of-line constants + */ + const EOL_WINDOWS = 0; + const EOL_UNIX = 1; + const EOL_OTHER = 2; + + /** + * Guess line terminator in a string of data + * + * Using the number of times it occurs, guess which line terminator is most likely. + * + * @param string $data The data to analyze + * + * @return string + */ + public function sniff($data) + { + // in this case we really only care about newlines so we pass in a comma as the delim + $str = $this->replaceQuotedSpecialChars($data, ','); + $eols = [ + static::EOL_WINDOWS => "\r\n", // 0x0D - 0x0A - Windows, DOS OS/2 + static::EOL_UNIX => "\n", // 0x0A - - Unix, OSX + static::EOL_OTHER => "\r", // 0x0D - - Other + ]; + + $curCount = 0; + $curEol = PHP_EOL; + foreach ($eols as $k => $eol) { + if (($count = substr_count($str, $eol)) > $curCount) { + $curCount = $count; + $curEol = $eol; + } + } + return $curEol; + } +} \ No newline at end of file diff --git a/src/CSVelte/Sniffer/SniffQuoteAndDelimByAdjacency.php b/src/CSVelte/Sniffer/SniffQuoteAndDelimByAdjacency.php new file mode 100644 index 0000000..c8b06e0 --- /dev/null +++ b/src/CSVelte/Sniffer/SniffQuoteAndDelimByAdjacency.php @@ -0,0 +1,79 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +use CSVelte\Sniffer; +use CSVelte\Exception\SnifferException; +use RuntimeException; + +use function Noz\collect; + +class SniffQuoteAndDelimByAdjacency extends AbstractSniffer +{ + /** + * Guess quote and delimiter character(s) + * + * If there are quoted values within the data, it is often easiest to guess the quote and delimiter characters at + * the same time by analyzing their adjacency to one-another. That is to say, in cases where certain values are + * wrapped in quotes, it can often be determined what not only that quote character is, but also the delimiter + * because it is often on either side of the quote character. + * + * @param string $data The data to analyze + * + * @return string[] + */ + public function sniff($data) + { + /** + * @var array An array of pattern matches + */ + $matches = null; + /** + * @var array An array of patterns (regex) + */ + $patterns = []; + $lineTerminator = $this->getOption('lineTerminator') ?: PHP_EOL; + // delim can be anything but line breaks, quotes, alphanumeric, underscore, backslash, or any type of spaces + $antidelims = implode(["\r", "\n", "\w", preg_quote('"', '/'), preg_quote("'", '/'), preg_quote(chr(Sniffer::SPACE), '/')]); + $delim = "(?P[^{$antidelims}])"; + $quote = "(?P\"|'|`)"; // @todo I think MS Excel uses some strange encoding for fancy open/close quotes + // @todo something happeened when I changed to double quotes that causes this to match things like ,"0.8"\n"2", as one when it should be two + $patterns[] = "/{$delim} ?{$quote}.*?\\2\\1/ms"; // ,"something", - anything but whitespace or quotes followed by a possible space followed by a quote followed by anything followed by same quote, followed by same anything but whitespace + $patterns[] = "/(?:^|{$lineTerminator}){$quote}.*?\\1{$delim} ?/ms"; // 'something', - beginning of line or line break, followed by quote followed by anything followed by quote followed by anything but whitespace or quotes + $patterns[] = "/{$delim} ?{$quote}.*?\\2(?:$|{$lineTerminator})/ms"; // ,'something' - anything but whitespace or quote followed by possible space followed by quote followed by anything followed by quote, followed by end of line + $patterns[] = "/(?:^|{$lineTerminator}){$quote}.*?\\2(?:$|{$lineTerminator})/ms"; // 'something' - beginning of line followed by quote followed by anything followed by quote followed by same quote followed by end of line + foreach ($patterns as $pattern) { + // @todo I had to add the error suppression char here because it was + // causing undefined offset errors with certain data sets. strange... + if (preg_match_all($pattern, $data, $matches) && $matches) { + break; + } + } + if ($matches) { + try { + return collect($matches) + ->kintersect(array_flip(['quoteChar', 'delim'])) + ->map(function($val) { + return collect($val)->frequency()->sort()->reverse()->getKeyAt(1); + }) + ->ksort() + ->reverse() + ->values() + ->toArray(); + } catch (RuntimeException $e) { + // eat this exception and let the sniffer exception below be thrown instead... + } + } + throw new SnifferException('quoteChar and delimiter cannot be determined', SnifferException::ERR_QUOTE_AND_DELIM); + } +} \ No newline at end of file diff --git a/src/CSVelte/Sniffer/SniffQuoteStyle.php b/src/CSVelte/Sniffer/SniffQuoteStyle.php new file mode 100644 index 0000000..d47c8ad --- /dev/null +++ b/src/CSVelte/Sniffer/SniffQuoteStyle.php @@ -0,0 +1,100 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelte\Sniffer; + +use CSVelte\Dialect; + +use function Noz\collect; +use function Stringy\create as s; + +class SniffQuoteStyle extends AbstractSniffer +{ + /** + * Guess quoting style + * + * The quoting style refers to which types of columns are quoted within a csv dataset. The dialect class defines + * four possible quoting styles; all, none, minimal, or non-numeric. This class attempts to determine which of those + * four it is by analyzing the content within each quoted value. + * + * @param string $data The data to analyze + * + * @return int + */ + public function sniff($data) + { + $styles = collect([ + Dialect::QUOTE_NONE => true, + Dialect::QUOTE_ALL => true, + Dialect::QUOTE_MINIMAL => true, + Dialect::QUOTE_NONNUMERIC => true + ]); + + $delimiter = $this->getOption('delimiter'); + $lineTerminator = $this->getOption('lineTerminator') ?: "\n"; + $quoted = collect(); + collect(explode($lineTerminator, $this->replaceQuotedSpecialChars($data, $delimiter, $lineTerminator))) + ->each(function($line, $line_no) use (&$styles, $quoted, $delimiter) { + $values = explode($delimiter, $line); + foreach ($values as $value) { + if ($this->isQuoted($value)) { + // remove surrounding quotes + $value = $this->unQuote($value); + $styles[Dialect::QUOTE_NONE] = false; + if (s($value)->containsAny([static::PLACEHOLDER_DELIM, static::PLACEHOLDER_NEWLINE, '"', "'"])) { + $quoted->add(Dialect::QUOTE_MINIMAL); + } elseif (!is_numeric((string) $value)) { + $quoted->add(Dialect::QUOTE_NONNUMERIC); + } else { + $quoted->add(Dialect::QUOTE_ALL); + } + } else { + $styles[Dialect::QUOTE_ALL] = false; + } + } + }); + + // @todo the following can almost certainly be cleaned up considerably + if ($styles[Dialect::QUOTE_ALL]) { + return Dialect::QUOTE_ALL; + } elseif ($styles[Dialect::QUOTE_NONE]) { + return Dialect::QUOTE_NONE; + } + + $types = $quoted->distinct(); + + if ($types->contains(Dialect::QUOTE_NONNUMERIC) && $types->contains(Dialect::QUOTE_MINIMAL)) { + // if both non-numeric and minimal then it isn't minimal + $types = $types->filter(function($type) { + return $type !== Dialect::QUOTE_MINIMAL; + }); + } + + if ($types->count() == 1) { + return $types->getValueAt(1); + } + + return Dialect::QUOTE_MINIMAL; + } + + /** + * Determine whether a particular string of data has quotes around it. + * + * @param string $data The data to check + * + * @return bool + */ + protected function isQuoted($data) + { + return preg_match('/^([\'"])[^\1]*\1$/', $data); + } +} \ No newline at end of file diff --git a/src/CSVelte/Table/AbstractRow.php b/src/CSVelte/Table/AbstractRow.php deleted file mode 100755 index 0f1def8..0000000 --- a/src/CSVelte/Table/AbstractRow.php +++ /dev/null @@ -1,265 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Table; - -use ArrayAccess; -use Countable; -use CSVelte\Collection\AbstractCollection; -use CSVelte\Exception\ImmutableException; - -use InvalidArgumentException; -use Iterator; - -use function CSVelte\collect; - -/** - * Table row abstract base class - * Represents a row of tabular data (represented by CSVelte\Table\Data objects). - * - * @package CSVelte - * @subpackage CSVelte\Table - * - * @since v0.1 - * - * @todo On all of the ArrayAccess methods, the docblocks say that $offset can be - * either an integer offset or a string index, but that isn't true, they must - * be an integer offset. Fix docblocks. - */ -abstract class AbstractRow implements Iterator, Countable, ArrayAccess -{ - /** - * An collection of fields for this row. - * - * @var AbstractCollection - */ - protected $fields; - - /** - * Iterator position. - * - * @var int - */ - protected $position; - - /** - * Class constructor. - * - * @param array|Iterator An array (or anything that looks like one) of data (fields) - * @param mixed $fields - */ - public function __construct($fields) - { - $this->setFields($fields) - ->rewind(); - } - - /** - * Return a string representation of this object. - * - * @return string - */ - public function __toString() - { - return $this->join(); - } - - /** - * Join fields together using specified delimiter. - * - * @param string The delimiter character - * @param mixed $delimiter - * - * @return string - */ - public function join($delimiter = ',') - { - return $this->fields->join($delimiter); - } - - /** - * Convert object to an array. - * - * @return array representation of the object - */ - public function toArray() - { - return $this->fields->toArray(); - } - - // Begin SPL Countable Interface Method - - /** - * Count fields within the row. - * - * @return int The amount of fields - */ - public function count() - { - return count($this->fields); - } - - // Begin SPL Iterator Interface Methods - - /** - * Get the current column's data object. - * - * @return string - */ - public function current() - { - return $this->fields->getValueAtPosition($this->position); - } - - /** - * Get the current key (column number or header, if available). - * - * @return string The "current" key - * - * @todo Figure out if this can return a CSVelte\Table\HeaderData object so long as it - * has a __toString() method that generated the right key... - */ - public function key() - { - return $this->fields->getKeyAtPosition($this->position); - } - - /** - * Advance the internal pointer to the next column's data object - * Also returns the next column's data object if there is one. - * - * @return mixed The "next" column's data - */ - public function next() - { - $this->position++; - if ($this->valid()) { - return $this->current(); - } - } - - /** - * Return the internal pointer to the first column and return that object. - * - * @return null|mixed|AbstractRow - */ - public function rewind() - { - $this->position = 0; - if ($this->valid()) { - return $this->current(); - } - } - - /** - * Is the current position within the row's data fields valid? - * - * @return bool - */ - public function valid() - { - return $this->fields->hasPosition($this->position); - } - - // Begin SPL ArrayAccess Methods - - /** - * Is there an offset at specified position. - * - * @param mixed $offset The offset to check existence of - * - * @return bool - */ - public function offsetExists($offset) - { - return $this->fields->offsetExists($offset); - } - - /** - * Retrieve offset at specified position or by header name. - * - * @param mixed $offset The offset to get - * - * @return mixed The data at the specified position - */ - public function offsetGet($offset) - { - return $this->fields->offsetGet($offset); - } - - /** - * Set offset at specified position. - * - * @param mixed $offset The array offset to set - * @param mixed $value The value to set $offset to - * - * @throws ImmutableException - */ - public function offsetSet($offset, $value) - { - // fields are immutable, cannot be set - $this->raiseImmutableException(); - } - - /** - * Unset offset at specified position/index. - * - * @param mixed $offset The offset to unset - * - * @throws ImmutableException - * - * @todo I'm not sure if these objects will stay immutable or not yet... - */ - public function offsetUnset($offset) - { - $this->raiseImmutableException(); - } - - /** - * Set the row fields. - * - * Using either an array or iterator, set the fields for this row. - * - * @param array|Iterator $fields An array or iterator with the row's fields - * - * @return $this - */ - protected function setFields($fields) - { - if (!is_array($fields)) { - if (is_object($fields) && method_exists($fields, 'toArray')) { - $fields = $fields->toArray(); - } elseif ($fields instanceof Iterator) { - $fields = iterator_to_array($fields); - } else { - throw new InvalidArgumentException(__CLASS__ . ' requires an array, got: ' . gettype($fields)); - } - } - $this->fields = collect($fields)->values(); - - return $this; - } - - /** - * Raise (throw) immutable exception. - * - * @param string $msg The message to pass to the exception - * - * @throws ImmutableException - */ - protected function raiseImmutableException($msg = null) - { - // fields are immutable, cannot be set - throw new ImmutableException($msg ?: 'Cannot change immutable column data'); - } -} diff --git a/src/CSVelte/Table/HeaderRow.php b/src/CSVelte/Table/HeaderRow.php deleted file mode 100755 index 871ce7f..0000000 --- a/src/CSVelte/Table/HeaderRow.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Table; - -/** - * Table Header Row - * A specialized version of CSVelte\Table\Row that represents a header row. - * - * @todo Notes about implementation of headers as row indexes... - * Because property names adhere to a much stricter naming scheme, any array key - * that can not be used, directly, as a property name must be converted to a - * proper property name. Also, because data in this library comes from one of the - * most notoriously malformed of the data formats (CSV), you have to account for - * the possibility of things such as duplicate header names. So if the row parser - * should encounter two identical header names, it should make some attempt to - * differentiate them before passing them in as array keys. - * - * @package CSVelte\Reader - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @todo This may need its own toArray() method so that it doesnt return an - * array with itself as keys - */ -class HeaderRow extends AbstractRow -{ -} diff --git a/src/CSVelte/Table/Row.php b/src/CSVelte/Table/Row.php deleted file mode 100755 index 7a4e2cb..0000000 --- a/src/CSVelte/Table/Row.php +++ /dev/null @@ -1,119 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte\Table; - -use CSVelte\Exception\HeaderException; -use OutOfBoundsException; - -use function CSVelte\collect; - -/** - * Table Row Class - * Represents a row of tabular data (CSVelte\Table\Cell objects). - * - * @package CSVelte - * @subpackage CSVelte\Table - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @todo May need to put toArray() method in here so that it uses headers - * as keys here - */ -class Row extends AbstractRow -{ - /** - * Get the current key (column number or header, if available). - * - * @return string The "current" key - * - * @todo Figure out if this can return a CSVelte\Table\HeaderData object so long as it - * has a __toString() method that generated the right key... - */ - public function key() - { - try { - return $this->fields->getKeyAtPosition($this->position); - } catch (OutOfBoundsException $e) { - return parent::key(); - } - } - - /** - * Set the header row (so that it can be used to index the row). - * - * @param AbstractRow|HeaderRow $headers Header row to set - * - * @throws HeaderException - */ - public function setHeaderRow(AbstractRow $headers) - { - if (!($headers instanceof HeaderRow)) { - $headers = new HeaderRow($headers->toArray()); - } - $headerArray = $headers->toArray(); - if (($hcount = $headers->count()) !== ($rcount = $this->count())) { - if ($hcount > $rcount) { - // header count is long, could be an error, but lets just fill in the short row with null values... - $this->fields = $this->fields->pad($hcount); - } else { - // @todo This is too strict. I need a way to recover from this a little better... - // header count is short, this is likely an error... - throw new HeaderException("Header count ({$hcount}) does not match column count ({$rcount}).", HeaderException::ERR_HEADER_COUNT); - } - } - $this->fields = collect(array_combine( - $headerArray, - $this->fields->toArray() - )); - } - - /** - * Is there an offset at specified position? - * - * @param mixed $offset - * - * @return bool - * - * @internal param Offset $integer - */ - public function offsetExists($offset) - { - try { - $this->fields->get($offset, null, true); - } catch (\OutOfBoundsException $e) { - return parent::offsetExists($offset); - } - - return true; - } - - /** - * Retrieve data at specified column offset. - * - * @param mixed $offset The offset to get - * - * @return mixed The value at $offset - */ - public function offsetGet($offset) - { - try { - $val = $this->fields->get($offset, null, true); - } catch (\OutOfBoundsException $e) { - return parent::offsetGet($offset); - } - - return $val; - } -} diff --git a/src/CSVelte/Taster.php b/src/CSVelte/Taster.php deleted file mode 100755 index 2068b42..0000000 --- a/src/CSVelte/Taster.php +++ /dev/null @@ -1,840 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte; - -use CSVelte\Collection\AbstractCollection; -use CSVelte\Collection\CharCollection; -use CSVelte\Collection\Collection; -use CSVelte\Collection\NumericCollection; -use CSVelte\Collection\TabularCollection; -use CSVelte\Contract\Streamable; -use CSVelte\Exception\TasterException; - -use DateTime; -use Exception; -use OutOfBoundsException; - -use function CSVelte\collect; - -/** - * CSVelte\Taster - * Given CSV data, Taster will "taste" the data and provide its buest guess at - * its "flavor". In other words, this class inspects CSV data and attempts to - * auto-detect various CSV attributes such as line endings, quote characters, etc.. - * - * @package CSVelte - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @todo There are a ton of improvements that could be made to this class. - * I'll do a refactor on this fella once I get at least one test - * passing for each of its public methods. - * @todo Should I have a lickEscapeChar method? The python version doesn't - * have one. But then why does it even bother including one in its - * flavor class? - * @todo Examine each of the public methods in this class and determine - * whether it makes sense to ask for the data as a param rather than - * just pulling it from source. I don't think it makes sense... it - * was just easier to write the methods that way during testing. - * @todo There are at least portions of this class that could use the - * Reader class rather than working directly with data. - * @todo Refactor all of the anonymous functions used as callbacks. Rather - * than passing $this all over, use $closure->bindTo() instead... - * Actually, write a method called getBoundClosure() or something... - * maybe even make it a trait I don't know yet. But here it would - * allow me to bind any anon function to $this and give me a certain - * set of commonly needed values ($delim, $eol, etc.) - */ -class Taster -{ - /** - * End-of-line constants. - */ - const EOL_UNIX = 'lf'; - const EOL_TRS80 = 'cr'; - const EOL_WINDOWS = 'crlf'; - - /** - * ASCII character codes for "invisibles". - */ - const HORIZONTAL_TAB = 9; - const LINE_FEED = 10; - const CARRIAGE_RETURN = 13; - const SPACE = 32; - - /** - * Data types -- Used within the lickQuotingStyle method. - */ - const DATA_NONNUMERIC = 'nonnumeric'; - const DATA_SPECIAL = 'special'; - const DATA_UNKNOWN = 'unknown'; - - /** - * Placeholder strings -- hold the place of newlines and delimiters contained - * within quoted text so that the explode method doesn't split incorrectly. - */ - const PLACEHOLDER_NEWLINE = '[__NEWLINE__]'; - const PLACEHOLDER_DELIM = '[__DELIM__]'; - - /** - * Recommended data sample size. - */ - const SAMPLE_SIZE = 2500; - - /** - * Column data types -- used within the lickHeader method to determine - * whether the first row contains different types of data than the rest of - * the rows (and thus, is likely a header row). - */ - // +-987 - const TYPE_NUMBER = 'number'; - // +-12.387 - const TYPE_DOUBLE = 'double'; - // I am a string. I can contain all kinds of stuff. - const TYPE_STRING = 'string'; - // 2010-04-23 04:23:00 - const TYPE_DATETIME = 'datetime'; - // 10-Jul-15, 9/1/2007, April 1st, 2006, etc. - const TYPE_DATE = 'date'; - // 10:00pm, 5pm, 13:08, etc. - const TYPE_TIME = 'time'; - // $98.96, ¥12389, £6.08, €87.00 - const TYPE_CURRENCY = 'currency'; - // 12ab44m1n2_asdf - const TYPE_ALNUM = 'alnum'; - // abababab - const TYPE_ALPHA = 'alpha'; - - /** @var Contract\Streamable The source of data to examine */ - protected $input; - - /** @var string Sample of CSV data to use for tasting (determining CSV flavor) */ - protected $sample; - - /** @var CharCollection Possible delimiter characters in (roughly) the order of likelihood */ - protected $delims; - - /** - * Class constructor--accepts a CSV input source. - * - * @param Contract\Streamable The source of CSV data - * - * @throws TasterException - * - * @todo It may be a good idea to skip the first line or two for the sample - * so that the header line(s) don't throw things off (with the exception - * of lickHeader() obviously) - */ - public function __construct(Streamable $input) - { - $this->delims = collect([',', "\t", ';', '|', ':', '-', '_', '#', '/', '\\', '$', '+', '=', '&', '@']); - $this->input = $input; - if (!$this->sample = $input->read(self::SAMPLE_SIZE)) { - throw new TasterException('Invalid input, cannot read sample.', TasterException::ERR_INVALID_SAMPLE); - } - } - - /** - * "Invoke" magic method. - * - * Called when an object is invoked as if it were a function. So, for instance, - * This is simply an alias to the lick method. - * - * @throws TasterException - * - * @return Flavor A flavor object - */ - public function __invoke() - { - return $this->lick(); - } - - /** - * Examine the input source and determine what "Flavor" of CSV it contains. - * The CSV format, while having an RFC (https://tools.ietf.org/html/rfc4180), - * doesn't necessarily always conform to it. And it doesn't provide meta such as the delimiting character, quote character, or what types of data are quoted. - * such as the delimiting character, quote character, or what types of data are quoted. - * are quoted. - * - * @throws TasterException - * - * @return Flavor The metadata that the CSV format doesn't provide - * - * @todo Implement a lickQuote method for when lickQuoteAndDelim method fails - * @todo Should there bea lickEscapeChar method? the python module that inspired - * this library doesn't include one... - * @todo This should cache the results and only regenerate if $this->sample - * changes (or $this->input) - */ - public function lick() - { - $lineTerminator = $this->lickLineEndings(); - try { - list($quoteChar, $delimiter) = $this->lickQuoteAndDelim(); - } catch (TasterException $e) { - if ($e->getCode() !== TasterException::ERR_QUOTE_AND_DELIM) { - throw $e; - } - $quoteChar = '"'; - $delimiter = $this->lickDelimiter($lineTerminator); - } - /** - * @todo Should this be null? Because doubleQuote = true means this = null - */ - $escapeChar = '\\'; - $quoteStyle = $this->lickQuotingStyle($delimiter, $lineTerminator); - $header = $this->lickHeader($delimiter, $lineTerminator); - - return new Flavor(compact('quoteChar', 'escapeChar', 'delimiter', 'lineTerminator', 'quoteStyle', 'header')); - } - - /** - * Examines the contents of the CSV data to make a determination of whether - * or not it contains a header row. To make this determination, it creates - * an array of each column's (in each row)'s data type and length and then - * compares them. If all of the rows except the header look similar, it will - * return true. This is only a guess though. There is no programmatic way to - * determine 100% whether a CSV file has a header. The format does not - * provide metadata such as that. - * - * @param string $delim The CSV data's delimiting char (can be a variety of chars but) - * typically is either a comma or a tab, sometimes a pipe) - * @param string $eol The CSV data's end-of-line char(s) (\n \r or \r\n) - * - * @return bool True if the data (most likely) contains a header row - * - * @todo This method needs a total refactor. It's not necessary to loop twice - * You could get away with one loop and that would allow for me to do - * something like only examining enough rows to get to a particular - * "hasHeader" score (+-100 for instance) & then just return true|false - * @todo Also, break out of the first loop after a certain (perhaps even a - * configurable) amount of lines (you only need to examine so much data ) - * to reliably make a determination and this is an expensive method) - * @todo I could remove the need for quote, delim, and eol by "licking" the - * data sample provided in the first argument. Also, I could actually - * create a Reader object to read the data here. - */ - public function lickHeader($delim, $eol) - { - // this will be filled with the type and length of each column and each row - $types = new TabularCollection(); - - // callback to build the aforementioned collection - $buildTypes = function ($line, $line_no) use ($types, $delim, $eol) { - if ($line_no > 2) { - return; - } - $line = str_replace(self::PLACEHOLDER_NEWLINE, $eol, $line); - $getType = function ($field, $colpos) use ($types, $line, $line_no, $delim) { - $field = str_replace(self::PLACEHOLDER_DELIM, $delim, $field); - $fieldMeta = [ - 'value' => $field, - 'type' => $this->lickType($this->unQuote($field)), - 'length' => strlen($field), - ]; - // @todo TabularCollection should have a way to set a value using [row,column] - try { - $row = $types->get($line_no); - } catch (OutOfBoundsException $e) { - $row = []; - } - $row[$colpos] = $fieldMeta; - $types->set($line_no, $row); - }; - collect(explode($delim, $line))->walk($getType->bindTo($this)); - }; - - collect(explode( - $eol, - $this->replaceQuotedSpecialChars($this->sample, $delim) - )) - ->walk($buildTypes->bindTo($this)); - - $hasHeader = new NumericCollection(); - $possibleHeader = collect($types->shift()); - $types->walk(function (AbstractCollection $row) use ($hasHeader, $possibleHeader) { - $row->walk(function (AbstractCollection $fieldMeta, $col_no) use ($hasHeader, $possibleHeader) { - try { - $col = collect($possibleHeader->get($col_no, null, true)); - if ($fieldMeta->get('type') == self::TYPE_STRING) { - // use length - if ($fieldMeta->get('length') != $col->get('length')) { - $hasHeader->push(1); - } else { - $hasHeader->push(-1); - } - } else { - // use data type - if ($fieldMeta->get('type') != $col->get('type')) { - $hasHeader->push(1); - } else { - $hasHeader->push(-1); - } - } - } catch (OutOfBoundsException $e) { - // failure... - return; - } - }); - }); - - return $hasHeader->sum() > 0; - } - - /** - * Replaces all quoted columns with a blank string. I was using this method - * to prevent explode() from incorrectly splitting at delimiters and newlines - * within quotes when parsing a file. But this was before I wrote the - * replaceQuotedSpecialChars method which (at least to me) makes more sense. - * - * @param string $data The string to replace quoted strings within - * - * @return string The input string with quoted strings removed - * - * @todo Replace code that uses this method with the replaceQuotedSpecialChars - * method instead. I think it's cleaner. - */ - protected function removeQuotedStrings($data) - { - return preg_replace($pattern = '/(["\'])(?:(?=(\\\\?))\2.)*?\1/sm', $replace = '', $data); - } - - /** - * Examine the input source to determine which character(s) are being used - * as the end-of-line character. - * - * @return string The end-of-line char for the input data - * @credit pulled from stackoverflow thread *tips hat to username "Harm"* - * - * @todo This should throw an exception if it cannot determine the line ending - * @todo I probably will make this method protected when I'm done with testing... - * @todo If there is any way for this method to fail (for instance if a file ) - * is totally empty or contains no line breaks), then it needs to throw - * a relevant TasterException - * @todo Use replaceQuotedSpecialChars rather than removeQuotedStrings() - */ - protected function lickLineEndings() - { - $str = $this->removeQuotedStrings($this->sample); - $eols = [ - self::EOL_WINDOWS => "\r\n", // 0x0D - 0x0A - Windows, DOS OS/2 - self::EOL_UNIX => "\n", // 0x0A - - Unix, OSX - self::EOL_TRS80 => "\r", // 0x0D - - Apple ][, TRS80 - ]; - - $curCount = 0; - // @todo This should return a default maybe? - $curEol = PHP_EOL; - foreach ($eols as $k => $eol) { - if (($count = substr_count($str, $eol)) > $curCount) { - $curCount = $count; - $curEol = $eol; - } - } - - return $curEol; - } - - /** - * The best way to determine quote and delimiter characters is when columns - * are quoted, often you can seek out a pattern of delim, quote, stuff, quote, delim - * but this only works if you have quoted columns. If you don't you have to - * determine these characters some other way... (see lickDelimiter). - * - * @throws TasterException - * - * @return array A two-row array containing quotechar, delimchar - * - * @todo make protected - * @todo This should throw an exception if it cannot determine the delimiter - * this way. - * @todo This should check for any line endings not just \n - */ - protected function lickQuoteAndDelim() - { - /** - * @var array An array of pattern matches - */ - $matches = null; - /** - * @var array An array of patterns (regex) - */ - $patterns = []; - // delim can be anything but line breaks, quotes, alphanumeric, underscore, backslash, or any type of spaces - $antidelims = implode(["\r", "\n", "\w", preg_quote('"', '/'), preg_quote("'", '/'), preg_quote(chr(self::SPACE), '/')]); - $delim = '(?P[^' . $antidelims . '])'; - $quote = '(?P"|\'|`)'; // @todo I think MS Excel uses some strange encoding for fancy open/close quotes - $patterns[] = '/' . $delim . ' ?' . $quote . '.*?\2\1/ms'; // ,"something", - anything but whitespace or quotes followed by a possible space followed by a quote followed by anything followed by same quote, followed by same anything but whitespace - $patterns[] = '/(?:^|\n)' . $quote . '.*?\1' . $delim . ' ?/ms'; // 'something', - beginning of line or line break, followed by quote followed by anything followed by quote followed by anything but whitespace or quotes - $patterns[] = '/' . $delim . ' ?' . $quote . '.*?\2(?:^|\n)/ms'; // ,'something' - anything but whitespace or quote followed by possible space followed by quote followed by anything followed by quote, followed by end of line - $patterns[] = '/(?:^|\n)' . $quote . '.*?\2(?:$|\n)/ms'; // 'something' - beginning of line followed by quote followed by anything followed by quote followed by same quote followed by end of line - foreach ($patterns as $pattern) { - // @todo I had to add the error suppression char here because it was - // causing undefined offset errors with certain data sets. strange... - if (@preg_match_all($pattern, $this->sample, $matches) && $matches) { - break; - } - } - if ($matches) { - $qcad = array_intersect_key($matches, array_flip(['quoteChar', 'delim'])); - if (!empty($matches['quoteChar']) && !empty($matches['delim'])) { - try { - return [ - collect($qcad['quoteChar'])->frequency()->sort()->reverse()->getKeyAtPosition(0), - collect($qcad['delim'])->frequency()->sort()->reverse()->getKeyAtPosition(0), - ]; - } catch (OutOfBoundsException $e) { - // eat this exception and let the taster exception below be thrown instead... - } - } - } - throw new TasterException('quoteChar and delimiter cannot be determined', TasterException::ERR_QUOTE_AND_DELIM); - } - - /** - * Take a list of likely delimiter characters and find the one that occurs - * the most consistent amount of times within the provided data. - * - * @param string $eol The character(s) used for newlines - * - * @return string One of four Flavor::QUOTING_* constants - * - * @see Flavor for possible quote style constants - * - * @todo Refactor this method--It needs more thorough testing against a wider - * variety of CSV data to be sure it works reliably. And I'm sure there - * are many performance and logic improvements that could be made. This - * is essentially a first draft. - * @todo Can't use replaceQuotedSpecialChars rather than removeQuotedStrings - * because the former requires u to know the delimiter - */ - protected function lickDelimiter($eol = "\n") - { - $frequencies = collect(); - $consistencies = new NumericCollection(); - - // build a table of characters and their frequencies for each line. We - // will use this frequency table to then build a table of frequencies of - // each frequency (in 10 lines, "tab" occurred 5 times on 7 of those - // lines, 6 times on 2 lines, and 7 times on 1 line) - collect(explode($eol, $this->removeQuotedStrings($this->sample))) - ->walk(function ($line, $line_no) use ($frequencies) { - collect(str_split($line)) - ->filter(function ($c) { - return collect($this->delims)->contains($c); - }) - ->frequency() - ->sort() - ->reverse() - ->walk(function ($count, $char) use ($frequencies, $line_no) { - try { - $char_counts = $frequencies->get($char, null, true); - } catch (OutOfBoundsException $e) { - $char_counts = []; - } - $char_counts[$line_no] = $count; - $frequencies->set($char, $char_counts); - }); - }) - // the above only finds frequencies for characters if they exist in - // a given line. This will go back and fill in zeroes where a char - // didn't occur at all in a given line (needed to determine mode) - ->walk(function ($line, $line_no) use ($frequencies) { - $frequencies->walk(function ($counts, $char) use ($line_no, $frequencies) { - try { - $char_counts = $frequencies->get($char, null, true); - } catch (OutOfBoundsException $e) { - $char_counts = []; - } - if (!array_key_exists($line_no, $char_counts)) { - $char_counts[$line_no] = 0; - } - $frequencies->set($char, $char_counts); - }); - }); - - // now determine the mode for each char to decide the "expected" amount - // of times a char (possible delim) will occur on each line... - $modes = new NumericCollection([]); - foreach ($frequencies as $char => $freq) { - $modes->set($char, (new NumericCollection($freq))->mode()); - } - $frequencies->walk(function ($f, $chr) use ($modes, $consistencies) { - collect($f)->walk(function ($num) use ($modes, $chr, $consistencies) { - if ($expected = $modes->get($chr)) { - if ($num == $expected) { - // met the goal, yay! - $cc = $consistencies->get($chr, 0); - $consistencies->set($chr, ++$cc); - } - } - }); - }); - - $max = $consistencies->max(); - $dups = $consistencies->duplicates(); - if ($dups->has($max)) { - // if more than one candidate, then look at where the character appeared - // in the data. Was it relatively evenly distributed or was there a - // specific area that the character tended to appear? Dates will have a - // consistent format (e.g. 04-23-1986) and so may easily provide a false - // positive for delimiter. But the dash will be focused in that one area, - // whereas the comma character is spread out. You can determine this by - // finding out the number of chars between each occurrence and getting - // the average. If the average is wildly different than any given distance - // than bingo you probably aren't working with a delimiter there... - - // another option to find the delimiter if there is a tie, is to build - // a table of character position within each line. Then use that to - // determine if one character is consistently in the same position or - // at least the same general area. Use the delimiter that is the most - // consistent in that way... - - /** - * @todo Add a method here to figure out where duplicate best-match - * delimiter(s) fall within each line and then, depending on - * which one has the best distribution, return that one. - */ - $decision = $dups->get($max); - try { - return $this->guessDelimByDistribution($decision, $eol); - } catch (TasterException $e) { - // if somehow we STILL can't come to a consensus, then fall back to a - // "preferred delimiters" list... - foreach ($this->delims as $key => $chr) { - if (collect($decision)->contains($chr)) { - return $chr; - } - } - } - } - - return $consistencies - ->sort() - ->reverse() - ->getKeyAtPosition(0); - } - - /** - * Compare positional consistency of several characters to determine the - * probable delimiter character. The idea behind this is that the delimiter - * character is likely more consistently distributed than false-positive - * delimiter characters produced by lickDelimiter(). For instance, consider - * a series of rows similar to the following:. - * - * 1,luke,visinoni,luke.visinoni@gmail.com,(530) 413-3076,04-23-1986 - * - * The lickDelimiter() method will often not be able to determine whether the - * delimiter is a comma or a dash because they occur the same number of times - * on just about every line (5 for comma, 3 for dash). The difference is - * obvious to you, no doubt. But us humans are pattern-recognition machines! - * The difference between the comma and the dash are that the comma is dist- - * ributed almost evenly throughout the line. The dash characters occur - * entirely at the end of the line. This method accepts any number of possible - * delimiter characters and returns the one that is distributed - * - * If delim character cannot be determined by lickQuoteAndDelim(), taster - * tries lickDelimiter(). When that method runs into a tie, it will use this - * as a tie-breaker. - * - * @param array $delims Possible delimiter characters (method chooses from - * this array of characters) - * @param string $eol The end-of-line character (or set of characters) - * - * @throws TasterException - * - * @return string The probable delimiter character - */ - protected function guessDelimByDistribution(array $delims, $eol = "\n") - { - try { - // @todo Write a method that does this... - $lines = collect(explode($eol, $this->removeQuotedStrings($this->sample))); - - return $delims[collect($delims)->map(function ($delim) use (&$distrib, $lines) { - $linedist = collect(); - $lines->walk(function ($line, $line_no) use (&$linedist, $delim) { - if (!strlen($line)) { - return; - } - $sectstot = 10; - $sectlen = (int) (strlen($line) / $sectstot); - $sections = collect(str_split($line, $sectlen)) - ->map(function ($section) use ($delim) { - return substr_count($section, $delim); - }) - ->filter(function ($count) { - return (bool) $count; - }); - if (is_numeric($count = $sections->count())) { - $linedist->set($line_no, $count / $sectstot); - } - }); - - return $linedist; - })->map(function ($dists) { - return $dists->average(); - })->sort() - ->reverse() - ->getKeyAtPosition(0)]; - } catch (Exception $e) { - throw new TasterException('delimiter cannot be determined by distribution', TasterException::ERR_DELIMITER); - } - } - - /** - * Determine the "style" of data quoting. The CSV format, while having an RFC - * (https://tools.ietf.org/html/rfc4180), doesn't necessarily always conform - * to it. And it doesn't provide metadata such as the delimiting character, - * quote character, or what types of data are quoted. So this method makes a - * logical guess by finding which columns have been quoted (if any) and - * examining their data type. Most often, CSV files will only use quotes - * around columns that contain special characters such as the dilimiter, - * the quoting character, newlines, etc. (we refer to this style as ) - * QUOTE_MINIMAL), but some quote all columns that contain nonnumeric data - * (QUOTE_NONNUMERIC). Then there are CSV files that quote all columns - * (QUOTE_ALL) and those that quote none (QUOTE_NONE). - * - * @param string $delim The character used as the column delimiter - * @param string $eol The character used for newlines - * - * @return string One of four "QUOTING_" constants defined above--see this - * method's description for more info. - * - * @todo Refactor this method--It needs more thorough testing against a wider - * variety of CSV data to be sure it works reliably. And I'm sure there - * are many performance and logic improvements that could be made. This - * is essentially a first draft. - */ - protected function lickQuotingStyle($delim, $eol) - { - $quoting_styles = collect([ - Flavor::QUOTE_ALL => true, - Flavor::QUOTE_NONE => true, - Flavor::QUOTE_MINIMAL => true, - Flavor::QUOTE_NONNUMERIC => true, - ]); - - $lines = collect(explode($eol, $this->replaceQuotedSpecialChars($this->sample, $delim))); - $freq = collect() - ->set('quoted', collect()) - ->set('unquoted', collect()); - - // walk through each line from the data sample to determine which fields - // are quoted and which aren't - $qsFunc = function ($line) use (&$quoting_styles, &$freq, $eol, $delim) { - $line = str_replace(self::PLACEHOLDER_NEWLINE, $eol, $line); - $qnqaFunc = function ($field) use (&$quoting_styles, &$freq, $delim) { - $field = str_replace(self::PLACEHOLDER_DELIM, $delim, $field); - if ($this->isQuoted($field)) { - $field = $this->unQuote($field); - $freq->get('quoted')->push($this->lickDataType($field)); - // since we know there's at least one quoted field, - // QUOTE_NONE can be ruled out - $quoting_styles->set(Flavor::QUOTE_NONE, false); - } else { - $freq->get('unquoted')->push($this->lickDataType($field)); - // since we know there's at least one unquoted field, - // QUOTE_ALL can be ruled out - $quoting_styles->set(Flavor::QUOTE_ALL, false); - } - }; - collect(explode($delim, $line)) - ->walk($qnqaFunc->bindTo($this)); - }; - $lines->walk($qsFunc->bindTo($this)); - - $types = $freq->get('quoted')->unique(); - $quoting_styles = $quoting_styles->filter(function ($val) { - return (bool) $val; - }); - // if quoting_styles still has QUOTE_ALL or QUOTE_NONE, then return - // whichever of them it is, we don't need to do anything else - if ($quoting_styles->has(Flavor::QUOTE_ALL)) { - return Flavor::QUOTE_ALL; - } - if ($quoting_styles->has(Flavor::QUOTE_NONE)) { - return Flavor::QUOTE_NONE; - } - if (count($types) == 1) { - $style = $types->getValueAtPosition(0); - if ($quoting_styles->has($style)) { - return $style; - } - } else { - if ($types->contains(self::DATA_NONNUMERIC)) { - // allow for a SMALL amount of error here - $counts = collect([self::DATA_SPECIAL => 0, self::DATA_NONNUMERIC => 0]); - $freq->get('quoted')->walk(function ($type) use (&$counts) { - $counts->increment($type); - }); - // @todo is all this even necessary? seems unnecessary to me... - if ($most = $counts->max()) { - $least = $counts->min(); - $err_margin = $least / $most; - if ($err_margin < 1) { - return Flavor::QUOTE_NONNUMERIC; - } - } - } - } - - return Flavor::QUOTE_MINIMAL; - } - - /** - * Remove quotes around a piece of text (if there are any). - * - * @param string $data The data to "unquote" - * - * @return string The data passed in, only with quotes stripped (off the edges) - */ - protected function unQuote($data) - { - return preg_replace('/^(["\'])(.*)\1$/', '\2', $data); - } - - /** - * Determine whether a particular string of data has quotes around it. - * - * @param string $data The data to check - * - * @return bool Whether the data is quoted or not - */ - protected function isQuoted($data) - { - return preg_match('/^([\'"])[^\1]*\1$/', $data); - } - - /** - * Determine what type of data is contained within a variable - * Possible types: - * - nonnumeric - only numbers - * - special - contains characters that could potentially need to be quoted (possible delimiter characters) - * - unknown - everything else - * This method is really only used within the "lickQuotingStyle" method to - * help determine whether a particular column has been quoted due to it being - * nonnumeric or because it has some special character in it such as a delimiter - * or newline or quote. - * - * @param string $data The data to determine the type of - * - * @return string The type of data (one of the "DATA_" constants above) - * - * @todo I could probably eliminate this method and use an anonymous function - * instead. It isn't used anywhere else and its name could be misleading. - * Especially since I also have a lickType method that is used within the - * lickHeader method. - */ - protected function lickDataType($data) - { - // @todo make this check for only the quote and delim that are actually being used - // that will make the guess more accurate - if (preg_match('/[\'",\t\|:;-]/', $data)) { - return self::DATA_SPECIAL; - } elseif (preg_match('/[^0-9]/', $data)) { - return self::DATA_NONNUMERIC; - } - - return self::DATA_UNKNOWN; - } - - /** - * Replace all instances of newlines and whatever character you specify (as - * the delimiter) that are contained within quoted text. The replacements are - * simply a special placeholder string. This is done so that I can use the - * very unsmart "explode" function and not have to worry about it exploding - * on delimiters or newlines within quotes. Once I have exploded, I typically - * sub back in the real characters before doing anything else. Although - * currently there is no dedicated method for doing so I just use str_replace. - * - * @param string $data The string to do the replacements on - * @param string $delim The delimiter character to replace - * - * @return string The data with replacements performed - * - * @todo I could probably pass in (maybe optionally) the newline character I - * want to replace as well. I'll do that if I need to. - */ - protected function replaceQuotedSpecialChars($data, $delim) - { - return preg_replace_callback('/([\'"])(.*)\1/imsU', function ($matches) use ($delim) { - $ret = preg_replace("/([\r\n])/", self::PLACEHOLDER_NEWLINE, $matches[0]); - $ret = str_replace($delim, self::PLACEHOLDER_DELIM, $ret); - - return $ret; - }, $data); - } - - /** - * Determine the "type" of a particular string of data. Used for the lickHeader - * method to assign a type to each column to try to determine whether the - * first for is different than a consistent column type. - * - * @todo As I'm writing this method I'm beginning ot realize how expensive - * the lickHeader method is going to end up being since it has to apply all - * these regexes (potentially) to every column. I may end up writing a much - * simpler type-checking method than this if it proves to be too expensive - * to be practical. - * - * @param string $data The string of data to check the type of - * - * @return string One of the TYPE_ string constants above - */ - protected function lickType($data) - { - if (preg_match('/^[+-]?[\d\.]+$/', $data)) { - return self::TYPE_NUMBER; - } elseif (preg_match('/^[+-]?[\d]+\.[\d]+$/', $data)) { - return self::TYPE_DOUBLE; - } elseif (preg_match('/^[+-]?[¥£€$]\d+(\.\d+)$/', $data)) { - return self::TYPE_CURRENCY; - } elseif (preg_match('/^[a-zA-Z]+$/', $data)) { - return self::TYPE_ALPHA; - } - try { - $year = '([01][0-9])?[0-9]{2}'; - $month = '([01]?[0-9]|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'; - $day = '[0-3]?[0-9]'; - $sep = '[\/\.\-]?'; - $time = '([0-2]?[0-9](:[0-5][0-9]){1,2}(am|pm)?|[01]?[0-9](am|pm))'; - $date = '(' . $month . $sep . $day . $sep . $year . '|' . $day . $sep . $month . $sep . $year . '|' . $year . $sep . $month . $sep . $day . ')'; - $dt = new DateTime($data); - $dt->setTime(0, 0, 0); - $now = new DateTime(); - $now->setTime(0, 0, 0); - $diff = $dt->diff($now); - $diffDays = (int) $diff->format('%R%a'); - if ($diffDays === 0) { - // then this is most likely a time string... - if (preg_match("/^{$time}$/i", $data)) { - return self::TYPE_TIME; - } - } - if (preg_match("/^{$date}$/i", $data)) { - return self::TYPE_DATE; - } elseif (preg_match("/^{$date} {$time}$/i")) { - return self::TYPE_DATETIME; - } - } catch (\Exception $e) { - // now go on checking remaining types - if (preg_match('/^\w+$/', $data)) { - return self::TYPE_ALNUM; - } - } - - return self::TYPE_STRING; - } -} diff --git a/src/CSVelte/Traits/IsReadable.php b/src/CSVelte/Traits/IsReadable.php index 3c7681e..0d041d6 100644 --- a/src/CSVelte/Traits/IsReadable.php +++ b/src/CSVelte/Traits/IsReadable.php @@ -1,15 +1,14 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Traits; @@ -19,14 +18,6 @@ * IO IsReadable Trait. * * Read methods shared between CSVelte\IO classes. - * - * @package CSVelte - * @subpackage CSVelte\Traits - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 */ trait IsReadable { @@ -61,14 +52,14 @@ public function readLine($eol = PHP_EOL, $maxLength = null) $buffer .= $byte; // Break when a new line is found or the max length - 1 is reached if (array_reduce($eol, function ($carry, $eol) use ($buffer) { - if (!$carry) { - $eollen = 0 - strlen($eol); + if (!$carry) { + $eollen = 0 - strlen($eol); - return substr($buffer, $eollen) === $eol; - } + return substr($buffer, $eollen) === $eol; + } - return true; - }, false) || ++$size === $maxLength - 1) { + return true; + }, false) || ++$size === $maxLength - 1) { break; } } diff --git a/src/CSVelte/Traits/IsSeekable.php b/src/CSVelte/Traits/IsSeekable.php index 127c1a7..5bbdfa9 100644 --- a/src/CSVelte/Traits/IsSeekable.php +++ b/src/CSVelte/Traits/IsSeekable.php @@ -1,15 +1,14 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Traits; @@ -20,14 +19,6 @@ * IO IsSeekable Trait. * * Seek methods shared between CSVelte\IO classes. - * - * @package CSVelte - * @subpackage CSVelte\Traits - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 */ trait IsSeekable { @@ -46,7 +37,12 @@ trait IsSeekable */ public function seekLine($offset, $whence = SEEK_SET, $eol = PHP_EOL) { - throw new NotYetImplementedException('This method not yet implemented.'); + throw new NotYetImplementedException(sprintf( + 'This method not yet implemented.', + $offset, + $whence, + $eol // these are simply here to satisfy my code analysis tools + )); } abstract public function isSeekable(); diff --git a/src/CSVelte/Traits/IsWritable.php b/src/CSVelte/Traits/IsWritable.php index 99772ad..79cda1c 100644 --- a/src/CSVelte/Traits/IsWritable.php +++ b/src/CSVelte/Traits/IsWritable.php @@ -1,15 +1,14 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte\Traits; @@ -19,14 +18,6 @@ * IO IsWritable Trait. * * Write methods shared between CSVelte\IO classes. - * - * @package CSVelte - * @subpackage CSVelte\Traits - * - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * - * @since v0.2 */ trait IsWritable { @@ -62,4 +53,4 @@ protected function assertIsWritable() throw new IOException('Stream not writable', IOException::ERR_NOT_WRITABLE); } } -} +} \ No newline at end of file diff --git a/src/CSVelte/Writer.php b/src/CSVelte/Writer.php old mode 100755 new mode 100644 index 172b323..475ff2a --- a/src/CSVelte/Writer.php +++ b/src/CSVelte/Writer.php @@ -1,294 +1,153 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte; -use ArrayIterator; - use CSVelte\Contract\Streamable; -use CSVelte\Exception\WriterException; -use CSVelte\Table\AbstractRow; -use CSVelte\Table\HeaderRow; -use CSVelte\Table\Row; +use Noz\Collection\Collection; -use InvalidArgumentException; -use Iterator; +use function Noz\collect; +use function Stringy\create as s; +use Traversable; -/** - * CSVelte Writer Base Class - * A PHP CSV utility library (formerly PHP CSV Utilities). - * - * @package CSVelte - * - * @copyright (c) 2016 Luke Visinoni - * @author Luke Visinoni - * - * @todo Buffer write operations so that you can call things like setHeaderRow() - * and change flavor and all that jivey divey goodness at any time. - */ class Writer { - /** - * The flavor (format) of CSV to write. - * - * @var Flavor - */ - protected $flavor; - - /** - * The output stream to write to. - * - * @var Contract\Streamable - */ + /** @var Streamable The output stream to write to */ protected $output; - /** - * The header row. - * - * @var \Iterator - */ - protected $headers; - - /** - * Number of lines written so far (not including header). - * - * @var int - */ - protected $written = 0; + /** @var Dialect The *dialect* of CSV to write */ + protected $dialect; - /** - * Class Constructor. - * - * @param Contract\Streamable $output An output source to write to - * @param Flavor|array $flavor A flavor or set of formatting params - */ - public function __construct(Streamable $output, $flavor = null) - { - if (!($flavor instanceof Flavor)) { - $flavor = new Flavor($flavor); - } - $this->flavor = $flavor; - $this->output = $output; - } + /** @var Collection The header row */ + protected $header; /** - * Get the CSV flavor (or dialect) for this writer. - * - * @param void - * - * @return Flavor - */ - public function getFlavor() - { - return $this->flavor; - } - - /** - * Sets the header row - * If any data has been written to the output, it is too late to write the - * header row and an exception will be thrown. Later implementations will - * likely buffer the output so that this may be called after writeRows(). - * - * @param \Iterator|array A list of header values - * @param mixed $headers + * Writer constructor. * - * @throws Exception\WriterException + * Although this is the constructor, I don't envision it being used much in userland. I think much more common + * methods of creating writers will be available within CSVelte base class such as CSVelte::toSplFileObject, + * CSVelte::toPath(), CSVelte::toOutputBuffer(), etc. * - * @return $this + * @param Streamable $output The destination streamable being written to + * @param Dialect $dialect The dialect being written */ - public function setHeaderRow($headers) + public function __construct(Streamable $output, Dialect $dialect = null) { - if ($this->written) { - throw new WriterException('Cannot set header row once data has already been written. '); + if (is_null($dialect)) { + $dialect = new Dialect; } - if (is_array($headers)) { - $headers = new ArrayIterator($headers); - } - $this->headers = $headers; - - return $this; + $this->setOutputStream($output) + ->setDialect($dialect); } /** - * Write a single row. + * Set the CSV dialect * - * @param \Iterator|array $row The row to write to source + * @param Dialect $dialect The *dialect* of CSV to use * - * @return int The number or bytes written + * @return self */ - public function writeRow($row) + public function setDialect(Dialect $dialect) { - $eol = $this->getFlavor()->lineTerminator; - $delim = $this->getFlavor()->delimiter; - if (!$this->written && $this->headers) { - $headerRow = new HeaderRow((array) $this->headers); - $this->writeHeaderRow($headerRow); - } - if (is_array($row)) { - $row = new ArrayIterator($row); - } - $row = $this->prepareRow($row); - if ($count = $this->output->writeLine($row->join($delim), $eol)) { - $this->written++; - - return $count; - } - - return 0; - } - - /** - * Write multiple rows. - * - * @param \Iterator|array $rows List of \Iterable|array - * - * @return int number of lines written - */ - public function writeRows($rows) - { - if (is_array($rows)) { - $rows = new ArrayIterator($rows); - } - if (!($rows instanceof Iterator)) { - throw new InvalidArgumentException('First argument for ' . __METHOD__ . ' must be iterable'); - } - $written = 0; - if ($rows instanceof Reader) { - $this->writeHeaderRow($rows->header()); - } - foreach ($rows as $row) { - if ($this->writeRow($row)) { - $written++; - } - } - - return $written; - } - - /** - * Write the header row. - * - * @param HeaderRow $row - * - * @return int|false - */ - protected function writeHeaderRow(HeaderRow $row) - { - $eol = $this->getFlavor()->lineTerminator; - $delim = $this->getFlavor()->delimiter; - $row = $this->prepareRow($row); - - return $this->output->writeLine($row->join($delim), $eol); + $this->dialect = $dialect; + return $this; } /** - * Prepare a row of data to be written - * This means taking an array of data, and converting it to a Row object. - * - * @param \Iterator $row of data items + * Get dialect * - * @return AbstractRow + * @return Dialect */ - protected function prepareRow(Iterator $row) + public function getDialect() { - $items = []; - foreach ($row as $data) { - $items []= $this->prepareData($data); - } - $row = new Row($items); - - return $row; + return $this->dialect; } /** - * Prepare a cell of data to be written (convert to Data object). + * Set output stream * - * @param string $data A string containing cell data + * @param Streamable $stream The output stream to write to * - * @return string quoted string data + * @return self */ - protected function prepareData($data) + protected function setOutputStream(Streamable $stream) { - // @todo This can't be properly implemented until I get Data and DataType right... - // it should be returning a Data object but until I get that working properly - // it's just going to have to return a string - return $this->quoteString($data); + $this->output = $stream; + return $this; } /** - * Enclose a string in quotes. + * Insert a single record into CSV output * - * Accepts a string and returns it with quotes around it. + * Returns total bytes written to the output stream. * - * @param string $str The string to wrap in quotes + * @param array|Traversable $data A row of data to write to the CSV output * - * @return string + * @return false|int */ - protected function quoteString($str) + public function insertRow($data) { - $flvr = $this->getFlavor(); - // Normally I would make this a method on the class, but I don't intend - // to use it for very long, in fact, once I finish writing the Data class - // it is gonezo! - $hasSpecialChars = function ($s) use ($flvr) { - $specialChars = preg_quote($flvr->lineTerminator . $flvr->quoteChar . $flvr->delimiter); - $pattern = "/[{$specialChars}]/m"; - - return preg_match($pattern, $s); - }; - switch ($flvr->quoteStyle) { - case Flavor::QUOTE_ALL: - $doQuote = true; - break; - case Flavor::QUOTE_NONNUMERIC: - $doQuote = !is_numeric($str); - break; - case Flavor::QUOTE_MINIMAL: - $doQuote = $hasSpecialChars($str); - break; - case Flavor::QUOTE_NONE: - default: - // @todo I think that if a cell is not quoted, newlines and delimiters should still be escaped by the escapeChar... no? - $doQuote = false; - break; - } - $quoteChar = ($doQuote) ? $flvr->quoteChar : ''; - - return sprintf('%s%s%s', - $quoteChar, - $this->escapeString($str, $doQuote), - $quoteChar - ); + $d = $this->getDialect(); + $data = collect($data) + ->map(function($field) use ($d) { + if ($qstyle = $d->getQuoteStyle()) { + $wrap = false; + switch ($qstyle) { + case Dialect::QUOTE_ALL: + $wrap = true; + break; + case Dialect::QUOTE_MINIMAL: + if (s($field)->containsAny([$d->getQuoteChar(), $d->getDelimiter(), $d->getLineTerminator()])) { + $wrap = true; + } + break; + case Dialect::QUOTE_NONNUMERIC: + if (is_numeric((string) $field)) { + $wrap = true; + } + break; + } + if ($wrap) { + $field = s($field); + if ($field->contains($d->getQuoteChar())) { + $escapeChar = $d->isDoubleQuote() ? $d->getQuoteChar() : '\\' /*$d->getEscapeChar()*/; + $field = $field->replace($d->getQuoteChar(), $d->getQuoteChar() . $d->getQuoteChar()); + } + $field = $field->surround($d->getQuoteChar()); + } + } + return (string) $field; + }); + $str = s($data->join($d->getDelimiter())) + ->append($d->getLineTerminator()); + + return $this->output->write((string) $str); } /** - * Escape a string. + * Write multiple rows to CSV output * - * Return a string with all special characters escaped. + * Returns total bytes written to the output stream. * - * @param string $str The string to escape - * @param bool $isQuoted True if string is quoted + * @param array|Traversable $data An array of rows of data to write to the CSV output * - * @return string + * @return int */ - protected function escapeString($str, $isQuoted = true) + public function insertAll($data) { - $flvr = $this->getFlavor(); - $escapeQuote = ''; - if ($isQuoted) { - $escapeQuote = ($flvr->doubleQuote) ? $flvr->quoteChar : $flvr->escapeChar; - } - // @todo Not sure what else, if anything, I'm supposed to be escaping here.. - return str_replace($flvr->quoteChar, $escapeQuote . $flvr->quoteChar, $str); + return collect($data) + ->map(function($row, $lineNo, $i) { + return $this->insertRow($row); + }) + ->sum(); } -} +} \ No newline at end of file diff --git a/src/CSVelte/functions.php b/src/CSVelte/functions.php index c53ec85..5ca5ec2 100644 --- a/src/CSVelte/functions.php +++ b/src/CSVelte/functions.php @@ -1,35 +1,23 @@ + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) + * @license See LICENSE file (MIT license) */ namespace CSVelte; -/* - * Library Functions - * - * @package CSVelte - * @subpackage functions - * @since v0.2.1 - */ - -use CSVelte\Collection\AbstractCollection; -use CSVelte\Collection\Collection; +use Iterator; +use InvalidArgumentException; use CSVelte\Contract\Streamable; -use CSVelte\IO\IteratorStream; use CSVelte\IO\Stream; use CSVelte\IO\StreamResource; -use InvalidArgumentException; - -use Iterator; +use CSVelte\IO\IteratorStream; /** * Stream - streams various types of values and objects. @@ -44,9 +32,9 @@ * * @return Streamable * - * @since v0.2.1 + * @since v0.3 */ -function streamize($obj = '') +function to_stream($obj = '') { if ($obj instanceof Streamable) { return $obj; @@ -60,6 +48,7 @@ function streamize($obj = '') return new Stream(new StreamResource($obj)); } + // @todo this is currently how SplFileObject objects are handled; there's gotta be a better way if ($obj instanceof Iterator) { return new IteratorStream($obj); } @@ -83,163 +72,4 @@ function streamize($obj = '') __FUNCTION__, gettype($obj) )); -} - -/** - * StreamResource factory. - * - * This method is just a shortcut to create a stream resource object using - * a stream URI string. - * - * @param string $uri A stream URI - * @param string $mode The access mode string - * @param array|resource $context An array or resource with stream context options - * @param bool $lazy Whether to lazy-open - * - * @return StreamResource - * - * @since v0.2.1 - */ -function stream_resource( - $uri, - $mode = null, - $context = null, - $lazy = true -) { - if (is_array($context)) { - if (!isset($context['options'])) { - $context['options'] = []; - } - if (!isset($context['params'])) { - $context['params'] = []; - } - $context = stream_context_create($context['options'], $context['params']); - } - $res = (new StreamResource($uri, $mode, null, true)) - ->setContextResource($context); - if (!$lazy) { - $res->connect(); - } - - return $res; -} - -/** - * Stream factory. - * - * This method is just a shortcut to create a stream object using a URI. - * - * @param string $uri A stream URI to open - * @param string $mode The access mode string - * @param array|resource $context An array or stream context resource of options - * @param bool $lazy Whether to lazy-open - * - * @return Stream - * - * @since v0.2.1 - */ -function stream( - $uri, - $mode = null, - $context = null, - $lazy = true -) { - $res = stream_resource($uri, $mode, $context, $lazy); - - return $res(); -} - -/** - * "Taste" a stream object. - * - * Pass any class that implements the "Streamable" interface to this function - * to auto-detect "flavor" (formatting attributes). - * - * @param Contract\Streamable Any streamable class to analyze - * - * @return Flavor A flavor representing stream's formatting attributes - * - * @since v0.2.1 - */ -function taste(Streamable $str) -{ - $taster = new Taster($str); - - return $taster(); -} - -/** - * Does dataset being streamed by $str have a header row? - * - * @param Contract\Streamable $str Stream object - * - * @return bool Whether stream dataset has header - * - * @since v0.2.1 - */ -function taste_has_header(Streamable $str) -{ - $taster = new Taster($str); - $flv = $taster(); - - return $taster->lickHeader( - $flv->delimiter, - $flv->lineTerminator - ); -} - -/** - * Collection factory. - * - * Simply an alias to (new Collection($in)). Allows for a little more concise and - * simpler instantiation of a collection. Also I plan to eventually support - * additional input types that will make this function more flexible and forgiving - * than simply instantiating a Collection object, but for now the two are identical. - * - * @param array|Iterator $in Either an array or an iterator of data - * - * @return AbstractCollection A collection object containing data from $in - * - * @since v0.2.1 - * @see AbstractCollection::__construct() (alias) - */ -function collect($in = null) -{ - return Collection::factory($in); -} - -/** - * Invoke a callable and return result. - * - * Pass in a callable followed by whatever arguments you want passed to - * it and this function will invoke it with your arguments and return - * the result. - * - * @param callable $callback The callback function to invoke - * @param array ...$args The args to pass to your callable - * - * @return mixed The result of your invoked callable - * - * @since v0.2.1 - */ -function invoke(callable $callback, ...$args) -{ - return $callback(...$args); -} - -/** - * Determine if data is traversable. - * - * Pass in any variable and this function will tell you whether or not it - * is traversable. Basically this just means that it is either an array or an iterator. - * This function was written simply because I was tired of if statements that checked - * whether a variable was an array or a descendant of \Iterator. So I wrote this guy. - * - * @param mixed $input The variable to determine traversability - * - * @return bool True if $input is an array or an Iterator - */ -function is_traversable($input) -{ - return is_array($input) || $input instanceof Iterator; -} +} \ No newline at end of file diff --git a/src/autoload.php b/src/autoload.php deleted file mode 100644 index 8c123bb..0000000 --- a/src/autoload.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelte; - -/** - * Add this file's parent directory to list of search paths and register autoloader. - * - * @var Autoloader used to autoload classes for users not taking advantage of the - * benefits of either Composer (see getcomposer.org) or PSR-4 (see php-fig.org) - * - * @since v0.2 This file was added in version 0.2, after removing it from the - * Autoloader class definition file. A file should contain either function/ - * class definitions or it should contain code such as this. Not both. - */ -$autoloader = new Autoloader(); -$autoloader->addPath(__DIR__); -$autoloader->register(); diff --git a/styleci.yml b/styleci.yml index 9417f7b..fb6efa7 100644 --- a/styleci.yml +++ b/styleci.yml @@ -44,7 +44,6 @@ enabled: - no_trailing_whitespace - no_trailing_whitespace_in_comment - no_unneeded_control_parentheses - - no_unreachable_default_argument_value - no_unused_imports - no_useless_else - no_useless_return @@ -70,7 +69,6 @@ enabled: - phpdoc_types - property_visibility_required - return_type_declaration - - self_accessor - semicolon_after_instruction - short_array_syntax - short_scalar_cast diff --git a/tests/CSVelte/AutoloaderTest.php b/tests/CSVelte/AutoloaderTest.php deleted file mode 100644 index 22c7909..0000000 --- a/tests/CSVelte/AutoloaderTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @author Luke Visinoni - */ -class AutoloaderTest extends UnitTestCase -{ - public function testAddPathsUsingConstructor() - { - $dir = __DIR__ . '/../../src'; - $paths = array( - $dir - ); - $auto = new Autoloader($paths); - $realdir = realpath($dir); - $this->assertContains($realdir, $auto->getPaths()); - } - - public function testAddPathsUsingAddPath() - { - $dir = __DIR__ . '/../../src'; - $auto = new Autoloader(); - $auto->addPath($dir); - $realdir = realpath($dir); - $this->assertContains($realdir, $auto->getPaths()); - } - - public function testNonExistantPathFailsQuietly() - { - $dir = __DIR__ . '/../../sourrc'; - $auto = new Autoloader(); - $auto->addPath($dir); - $realdir = realpath($dir); - $this->assertFalse($realdir); - $this->assertContains($dir, $auto->getPaths()); - } - - public function testRegisterAddsPathsToIncludePath() - { - $dir = __DIR__ . '/../../src'; - $fakedir = '../../fakedir'; - $paths = array( - $dir, - $fakedir - ); - $auto = new Autoloader($paths); - $auto->register(); - $realdir = realpath($dir); - $includepaths = explode(PATH_SEPARATOR, get_include_path()); - $this->assertContains($realdir, $includepaths); - $this->assertContains($fakedir, $includepaths); - } - - public function testLoadClassReturnsNullIfClassExists() - { - $auto = new Autoloader; - $this->assertNull($auto->load('CSVelte\Flavor')); - } - - public function testLoadClassReturnsNullIfClassDoesntExistAtAll() - { - $auto = new Autoloader; - $this->assertNull($auto->load('CSVelte\Foo')); - } - - public function testLoadClassLoadsClassIfItHasntBeenLoaded() - { - $auto = new Autoloader; - $this->assertNull($auto->load($classname = 'CSVelte\Table\Row')); - $this->assertTrue(class_exists($classname)); - } - - public function testRequireSrcAutoloadClass() - { - // set include path to something meaningless first, just to make sure that - // including the autoload.php class fixes things... - $meaningless = __DIR__; - set_include_path($meaningless); - $this->assertEquals($meaningless, get_include_path(), "Just a control test to make sure that include path was messed up first"); - require_once $meaningless . "/../../src/autoload.php"; - $this->assertEquals($meaningless . PATH_SEPARATOR . realpath(__DIR__ . '/../../src'), get_include_path(), "Test that including the autoload file adds CSVelte's src directory to the include path"); - } -} diff --git a/tests/CSVelte/CSVelteTest.php b/tests/CSVelte/CSVelteTest.php deleted file mode 100755 index 0f3364a..0000000 --- a/tests/CSVelte/CSVelteTest.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @author Luke Visinoni - */ - -use CSVelte\CSVelte; -use CSVelte\Reader; -use CSVelte\Flavor; - -class CSVelteTest extends UnitTestCase -{ - protected $dummydata = array( - array('foo','bar','baz'), - array('1','luke','visinoni'), - array('2','margaret','kelly'), - array('3','jerry','rafferty') - ); - - public function testGenerateReaderObject() - { - $reader = CSVelte::reader($this->getFilePathFor('veryShort')); - $this->assertInstanceOf(Reader::class, $reader); - } - - public function testGenerateReaderObjectWithCustomFlavor() - { - $flavor = new Flavor(array('delimiter' => '!', 'header' => false)); - $reader = CSVelte::reader($this->getFilePathFor('veryShort'), $flavor); - $this->assertInstanceOf(Flavor::class, $flavor); - $this->assertSame($flavor, $reader->getFlavor()); - } - - /** - * @expectedException CSVelte\Exception\IOException - */ - public function testGenerateReaderWillThrowExceptionIfFileDoesNotExist() - { - $reader = CSVelte::reader($this->getFilePathFor('veryShort') . 'asdf'); - } - - public function testCSVelteReaderCanBeUsedDirectlyInsideOfAForeachLoop() - { - $rows = 0; - foreach (CSVelte::reader($this->getFilePathFor('commaNewlineHeader')) as $row) { - $rows++; - } - $this->assertEquals(29, $rows); - } - - public function testCSVelteReaderString() - { - $string = "foo,bar,baz\ncolbert,4,prez\nyou,2,silly\ntoocool,4,school\n"; - $reader = CSVelte::stringReader($string, new Flavor(array('lineTerminator' => "\n", 'header' => true))); - $this->assertEquals($reader->current()->offsetGet('foo'), 'colbert'); - } - - // .. WRITER ... - - public function testCSVelteWriterCreatesFile() - { - $filename = $this->root->url() . '/deleteme.csv'; - $writer = CSVelte::writer($filename); - $this->assertEquals(4, $writer->writeRows($this->dummydata)); - } - - public function testCSVelteWriterCreatesFileWithFlavor() - { - $filename = $this->root->url() . '/deleteme.csv'; - $writer = CSVelte::writer($filename, $flavor = new Flavor(array('delimiter' => "\t", 'lineTerminator' => "\n"))); - $this->assertEquals(4, $writer->writeRows($this->dummydata)); - $this->assertEquals("foo\tbar\tbaz\n1\tluke\tvisinoni\n2\tmargaret\tkelly\n3\tjerry\trafferty\n", file_get_contents($filename)); - } - - public function testExportMethod() - { - $filename = $this->root->url() . '/deleteme.csv'; - $this->assertEquals(4, CSVelte::export($filename, $this->dummydata)); - $this->assertEquals("foo,bar,baz\r\n1,luke,visinoni\r\n2,margaret,kelly\r\n3,jerry,rafferty\r\n", file_get_contents($filename)); - } - - public function testExportMethodWithFlavor() - { - $filename = $this->root->url() . '/deleteme.csv'; - $this->assertEquals(4, CSVelte::export($filename, $this->dummydata, $flavor = new Flavor(array('delimiter' => "\t", 'lineTerminator' => "\n")))); - $this->assertEquals("foo\tbar\tbaz\n1\tluke\tvisinoni\n2\tmargaret\tkelly\n3\tjerry\trafferty\n", file_get_contents($filename)); - } - - /** - * @expectedException CSVelte\Exception\IOException - * @expectedExceptionCode CSVelte\Exception\IOException::ERR_FILE_PERMISSION_DENIED - */ - public function testNonExistantFileForReaderThrowsException() - { - $file = $this->root->url() . '/permission-denied.csv'; - touch($file); - chmod($file, 0000); - $reader = CSVelte::reader($file); - } - -} diff --git a/tests/CSVelte/Collection/AbstractCollectionTest.php b/tests/CSVelte/Collection/AbstractCollectionTest.php deleted file mode 100644 index d0a755a..0000000 --- a/tests/CSVelte/Collection/AbstractCollectionTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelteTest\Collection; - -use CSVelte\Collection\Collection; -use CSVelte\Collection\MultiCollection; -use CSVelte\Collection\TabularCollection; -use CSVelteTest\UnitTestCase; -use Faker; - -class AbstractCollectionTest extends UnitTestCase -{ - protected $testdata = []; - - public function setUp() - { - parent::setUp(); - $faker = Faker\Factory::create(); - $faker->seed(19860423); - $this->testdata[MultiCollection::class] = [ - 'names' => [], - 'addresses' => [], - 'cities' => [], - 'dates' => [], - 'numeric' => [], - 'words' => [], - 'userAgent' => [] - ]; - for ($i = 0; $i < 10; $i++) { - $this->testdata[MultiCollection::class]['names'][] = $faker->name; - $this->testdata[MultiCollection::class]['addresses'][] = $faker->streetAddress; - $this->testdata[MultiCollection::class]['cities'][] = $faker->city; - $this->testdata[MultiCollection::class]['dates'][] = $faker->date; - $this->testdata[MultiCollection::class]['numeric'][] = $faker->randomNumber; - $this->testdata[MultiCollection::class]['words'][] = $faker->words; - $this->testdata[MultiCollection::class]['userAgent'][] = $faker->userAgent; - } - $this->testdata[TabularCollection::class] = [ - 'user' => [], - 'profile' => [] - ]; - for($t = 1; $t <= 5; $t++) { - $created = $faker->dateTimeThisYear->format('YmdHis'); - $profile_id = $t + 125; - $this->testdata[TabularCollection::class]['user'][] = [ - 'id' => $t, - 'profile_id' => $profile_id, - 'email' => $faker->email, - 'password' => sha1($faker->asciify('**********')), - 'role' => $faker->randomElement(['user','admin','user','user','user','user','user','moderator','moderator']), - 'is_active' => $faker->boolean, - 'created' => $created, - 'modified' => $created - ]; - $this->testdata[TabularCollection::class]['profile'][] = [ - 'id' => $profile_id, - 'address' => $faker->streetAddress, - 'city' => $faker->city, - 'state' => $faker->stateAbbr, - 'zipcode' => $faker->postcode, - 'phone' => $faker->phoneNumber, - 'bio' => $faker->paragraph, - 'created' => $created, - 'modified' => $created - ]; - } - $this->testdata[Collection::class] = $faker->words(15); - } -} \ No newline at end of file diff --git a/tests/CSVelte/Collection/CharCollectionTest.php b/tests/CSVelte/Collection/CharCollectionTest.php deleted file mode 100644 index bcd31ba..0000000 --- a/tests/CSVelte/Collection/CharCollectionTest.php +++ /dev/null @@ -1,252 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelteTest\Collection; - -use CSVelte\Collection\CharCollection; - -class CharCollectionTest extends AbstractCollectionTest -{ - public function testCharCollectionAcceptsString() - { - $chars = new CharCollection($exp = 'A collection of chars'); - $this->assertEquals($exp, (string) $chars); - $this->assertEquals(str_split($exp), $chars->toArray()); - } - - public function testContainsReturnsTrueIFCharInCollection() - { - $chars = new CharCollection($exp = 'A collection of chars'); - $this->assertTrue($chars->contains('c')); - $this->assertFalse($chars->contains('Z')); - } - - public function testMapRunsFuncForEveryChar() - { - $chars = new CharCollection($exp = 'A collection of chars'); - $nl = $chars->map(function($char){ - if ($char == ' ') { - return "\n"; - } - return $char; - }); - $this->assertEquals("A\ncollection\nof\nchars", (string) $nl); - } - - public function testCountReturnsCharCount() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertEquals(strlen($exp), $chars->count()); - $this->assertEquals(strlen($exp), count($chars)); - } - - public function testHasReturnsTrueIfOffsetExists() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertTrue($chars->has(5)); - $this->assertFalse($chars->has(50)); - $this->assertFalse($chars->has('c')); - } - - /** - * @todo add method to grab a slice of a collection - */ - public function testGetReturnsCharacterAtGivenOffset() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertEquals('r', $chars->get(5)); - $this->assertEquals('A', $chars->get(0)); - $this->assertNull($chars->get(15)); - } - - public function testSetReplacesCharacterAtGivenOffset() - { - $chars = new CharCollection($exp = 'A character set'); - $chars->set('5', 'p'); - $this->assertEquals('p', $chars->get(5)); - // @todo I'm not sure this is the behavior I want - $chars->set('3', 'poo'); - $this->assertEquals('poo', $chars->get(3)); - $this->assertEquals('A cpooapacter set', (string) $chars); - } - - public function testDeleteRemovesCharacterAtGivenOffset() - { - $chars = new CharCollection($exp = 'A character set'); - $chars->delete(5); - $this->assertEquals('A chaacter set', (string) $chars); - } - - /** - * @todo Merge and a few other methods aren't really relevant to a character set. Probably should remove them from abstract collection? - */ -// public function testMergeDoesWhateverItDoes() -// { -// $chars = new CharCollection($exp = 'A character set'); -// // $chars->merge([12 => 'p', 15 => ' sandwich']); -// $chars->merge('foo'); -// $this->assertEquals('A character pet sandwich', (string) $chars); -// } - - public function testContainsChecksForTheExistenceOfCharacter() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertTrue($chars->contains('a')); - $this->assertFalse($chars->contains('b')); - $this->assertTrue($chars->contains('A')); - $this->assertFalse($chars->contains('C')); - } - - public function testContainsChecksForTheExistenceOfCharacterAtGivenPosition() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertTrue($chars->contains('a', 4)); - $this->assertFalse($chars->contains('a', 0)); - $this->assertTrue($chars->contains('A', 0)); - $this->assertFalse($chars->contains('S', 12)); - } - - public function testContainsChecksForTheExistenceOfCharacterViaCallback() - { - $chars = new CharCollection($exp = 'A character set'); - $this->assertTrue($chars->contains(function($val) { - return $val > 's'; - })); - $this->assertTrue($chars->contains(function($val) { - return $val > 'S'; - })); - $this->assertFalse($chars->contains(function($val) { - return $val > 8; - })); - $this->assertFalse($chars->contains(function($char) { - return is_numeric($char); - })); - } - - public function testPopRemovesTheLastChar() - { - $me = new CharCollection($exp = 'Luke Visinoni'); - $this->assertEquals('i', $me->pop()); - $this->assertEquals('n', $me->pop()); - $this->assertEquals('Luke Visino', (string) $me); - } - - public function testShiftRemovesTheFirstChar() - { - $me = new CharCollection($exp = 'Luke Visinoni'); - $this->assertEquals('L', $me->shift()); - $this->assertEquals('u', $me->shift()); - $this->assertEquals('ke Visinoni', (string) $me); - } - - public function testPushAddsCharToEnd() - { - $me = new CharCollection($exp = 'Luke Visinoni'); - $notme = $me->push('i'); - $this->assertEquals('Luke Visinonii', (string) $notme); - } - - public function testUnshiftAddsCharToBeginning() - { - $me = new CharCollection($exp = 'Luke Visinoni'); - $notme = $me->unshift('i'); - $this->assertEquals('iLuke Visinoni', (string) $notme); - } - - public function testPadRepeatsCharacterXTimes() - { - $str = new CharCollection('-'); - $str = $str->pad(10, '-'); - $this->assertEquals('----------', (string) $str); - } - - /** - * @todo When map callback returns str w/more than a single char - * it causes problems. Come back to that. - */ - public function testMapUsesCallbackOnEachChar() - { - $str = new CharCollection($exp = 'abcdefghijklmnopqrstuvwxyz'); - $newstr = $str->map(function($char){ - return ord($char) < 110 ? '-' : $char; - }); - $this->assertEquals('-------------nopqrstuvwxyz', (string) $newstr); - } - - public function testWalkUsesCallbackOnEachChar() - { - $str = new CharCollection($exp = 'abcdefghijklmnopqrstuvwxyz'); - $exp = []; - $str->walk(function($val, $key, $extra) use (&$exp) { - if ($key % 2 == 0) { - $exp[] = $val . $extra[0]; - } else { - $exp[] = $val . $extra[1]; - } - }, ['foo','bar']); - $this->assertEquals("afoo", $exp[0]); - $this->assertEquals("bbar", $exp[1]); - } - - public function testReduceUsesCallbackToReturnSingleValue() - { - $str = new CharCollection($exp = 'abcdefghijklmnopqrstuvwxyz'); - $isstr = $str->reduce(function($carry, $elem){ - return (is_string($elem) && $carry); - }, true); - $this->assertTrue($isstr); - $isnum= $str->reduce(function($carry, $elem){ - return (is_numeric($elem) && $carry); - }, true); - $this->assertFalse($isnum); - } - - public function testFilterRemovesCorrectChars() - { - $str = new CharCollection($exp = 'abcdefghijklmnopqrstuvwxyz'); - $newstr = $str->filter(function($val) { - return ($val > 'm'); - }); - $this->assertEquals('nopqrstuvwxyz', (string) $newstr); - } - - public function testFirstReturnsFirstMatchingValue() - { - $str = new CharCollection($exp = 'I like char collections.'); - $char = $str->first(function($val) { - return ($val > 'm'); - }); - $this->assertEquals('r', $char); - } - - public function testLastReturnsLastMatchingValue() - { - $str = new CharCollection($exp = 'I like char collections.'); - $char = $str->last(function($val) { - return ($val > 'm'); - }); - $this->assertEquals('s', $char); - } - - public function testReverse() - { - $str = new CharCollection($exp = 'I like char collections.'); - $this->assertEquals(strrev($exp), (string) $str->reverse()); - } - - public function testUnique() - { - $str = new CharCollection($exp = 'I like char collections.'); - $this->assertEquals('I likecharotns.', (string) $str->unique()); - } -} \ No newline at end of file diff --git a/tests/CSVelte/Collection/CollectionTest.php b/tests/CSVelte/Collection/CollectionTest.php deleted file mode 100644 index 98ced84..0000000 --- a/tests/CSVelte/Collection/CollectionTest.php +++ /dev/null @@ -1,754 +0,0 @@ - - * @author Luke Visinoni - * @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) - */ -namespace CSVelteTest\Collection; - -use ArrayAccess; -use Countable; -use CSVelte\Collection\CharCollection; -use CSVelte\Collection\MultiCollection; -use CSVelte\Collection\NumericCollection; -use CSVelte\Collection\TabularCollection; -use \Iterator; -use \ArrayIterator; -use CSVelte\Collection\AbstractCollection; -use CSVelte\Collection\Collection; -//use CSVelte\Contract\Collectable; -use function CSVelte\is_traversable; - -class CollectionTest extends AbstractCollectionTest -{ -// public function testCollectFactoryReturnsCollectable() -// { -// $coll = Collection::factory(); -// $this->assertInstanceOf(Collectable::class, $coll); -// } - - public function testCollectFactoryReturnsBasicCollectionByDefault() - { - $coll = Collection::factory(); - $this->assertInstanceOf(Collection::class, $coll); - } - - // @todo write this test - public function testCollectFactoryThrowsExceptionOnInvalidForceClassType() - { - - } - - public function testCollectionFactoryPassesInputToCollection() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertEquals($in, $coll->toArray()); - } - - public function testCollectionFactoryReturnsTabularCollectionForTabularDataset() - { - $in = [ - [ - 'foo' => 'far', - 'woo' => 'war', - 'too' => 'tar', - 'coo' => 'car', - 'roo' => 'rar' - ], - [ - 'foo' => 'yay', - 'woo' => 'bay', - 'too' => 'day', - 'coo' => 'fay', - 'roo' => 'stay' - ], - [ - 'foo' => 'poo', - 'woo' => 'doo', - 'too' => 'soo', - 'coo' => 'woo', - 'roo' => 'coo' - ], - [ - 'foo' => '---', - 'woo' => '...', - 'too' => '===', - 'coo' => '~~~', - 'roo' => ',,,' - ] - ]; - $table = Collection::factory($in); - $this->assertInstanceOf(TabularCollection::class, $table); - } - - public function testCollectionFactoryReturnsMultiCollectionForMultiDimensionalDataset() - { - $in = [ - [ - 'foo' => 'far', - 'woo' => 'war', - 'too' => 'tar', - 'coo' => 'car', - 'roo' => 'rar' - ], - [ - 'foo' => 'yay', - 'woo' => 'bay', - 'too' => 'day', - 'coo' => 'fay', - 'roo' => 'stay' - ], - [ - 'foo' => 'poo', - 'woo' => 'doo', - 'too' => 'soo', - 'coo' => 'woo', - ], - [ - 'foo' => '---', - 'woo' => '...', - 'too' => '===', - 'coo' => '~~~', - 'roo' => ',,,' - ] - ]; - $multi = Collection::factory($in); - $this->assertInstanceOf(MultiCollection::class, $multi); - $in = [ - [ - 'foo' => 'far', - 'woo' => 'war', - 'too' => 'tar', - 'coo' => 'car', - 'roo' => 'rar' - ], - [1,2,3,4,5], - 'foobar', - [ - 'foo' => 'yay', - 'woo' => 'bay', - ], - [ - 'foo' => '---', - 'woo' => '...', - 'too' => '===', - 'coo' => '~~~', - 'roo' => ',,,' - ] - ]; - $multi = Collection::factory($in); - $this->assertInstanceOf(MultiCollection::class, $multi); - $in = [ - [ - 'foo' => 'far', - 'woo' => 'war', - 'too' => 'tar', - 'coo' => 'car', - 'roo' => 'rar' - ], - 1,2,3,4,5 - ]; - $multi = Collection::factory($in); - $this->assertInstanceOf(MultiCollection::class, $multi); - } - - public function testCollectionFactoryReturnsNumericCollectionForNumericDataset() - { - $in = [1,2,3,4,5]; - $numeric = Collection::factory($in); - $this->assertInstanceOf(NumericCollection::class, $numeric); - $in = [1,2.5,3,4,5]; - $numeric = Collection::factory($in); - $this->assertInstanceOf(NumericCollection::class, $numeric); - $in = [0,0,0,0,'0']; - $numeric = Collection::factory($in); - $this->assertInstanceOf(NumericCollection::class, $numeric); - $in = ['0','123',10]; - $numeric = Collection::factory($in); - $this->assertInstanceOf(NumericCollection::class, $numeric); - $in = [1,]; - $numeric = Collection::factory($in); - $this->assertInstanceOf(NumericCollection::class, $numeric); - } - - public function testCollectionFactoryReturnsCharCollectionForCharacterSet() - { - $chars = 'a set of characters'; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(CharCollection::class, $charColl); - $chars = '000'; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(CharCollection::class, $charColl); - $chars = 0; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(CharCollection::class, $charColl); - $chars = 12345; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(CharCollection::class, $charColl); - } - - public function testCollectionFactoryReturnsCollectionForEverythingElse() - { - $chars = ['a set of characters']; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(Collection::class, $charColl); - $chars = ['000', 0, 'zero']; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(Collection::class, $charColl); - $chars = [0,1,2,3,4,5,'six']; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(Collection::class, $charColl); - $chars = [12345, null, true, false]; - $charColl = Collection::factory($chars); - $this->assertInstanceOf(Collection::class, $charColl); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testCollectionThrowsExceptionIfPassedInvalidData() - { - $in = false; - Collection::factory($in); - } - - public function testCollectionAcceptsArrayOrIterator() - { - $arr = ['foo' => 'bar', 'baz' => 'bin']; - $arrColl = Collection::factory($arr); - $this->assertEquals($arr, $arrColl->toArray()); - - $iter = new ArrayIterator($arr); - $iterColl = Collection::factory($iter); - $this->assertEquals(iterator_to_array($iter), $iterColl->toArray()); - } - - public function testCollectionHas() - { - $arr = ['foo' => 'bar', 'baz' => 'bin']; - $arrColl = Collection::factory($arr); - $this->assertTrue($arrColl->has('foo')); - $this->assertFalse($arrColl->has('poo')); - } - - public function testCollectionHasWorksOnNumericKeys() - { - $arr = ['foo', 'bar', 'baz', 'bin']; - $arrColl = Collection::factory($arr); - $this->assertTrue($arrColl->has(0)); - $this->assertFalse($arrColl->has(5)); - } - - public function testCollectionGetReturnsValueAtIndex() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertEquals('bar', $coll->get('foo')); - } - - public function testCollectionGetReturnsDefaultIfIndexNotFound() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertEquals('woo!', $coll->get('poo', 'woo!')); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testCollectionGetThrowsExceptionIfIndexNotFoundAndThrowIsTrue() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $coll->get('poo', null, true); - } - - public function testCollectionSetValue() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertNull($coll->get('poo')); - $this->assertInstanceOf(AbstractCollection::class, $coll->set('poo', 'woo!')); - $this->assertEquals('woo!', $coll->get('poo')); - } - - public function testCollectionDeleteValue() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertNotNull($coll->get('foo')); - $this->assertInstanceOf(AbstractCollection::class, $coll->delete('foo')); - $this->assertNull($coll->get('foo')); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testCollectionDeleteValueThrowsExceptionIfThrowIsTrue () - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $coll->delete('boo', true); - } - - public function testCollectionToArrayCallsToArrayRecursively() - { - $in1 = ['foo' => 'bar', 'baz' => 'bin']; - $in2 = ['boo' => 'far', 'biz' => 'ban']; - $in3 = ['doo' => 'dar', 'diz' => 'din']; - $coll1 = Collection::factory($in1); - $coll2 = Collection::factory($in2); - $coll2->set('coll1', $coll1); - $coll3 = Collection::factory($in3); - $coll3->set('coll2', $coll2); - $this->assertEquals([ - 'doo' => 'dar', 'diz' => 'din', - 'coll2' => [ - 'boo' => 'far', 'biz' => 'ban', - 'coll1' => [ - 'foo' => 'bar', 'baz' => 'bin' - ] - ] - ], $coll3->toArray()); - } - - public function testCollectionKeysReturnsCollectionOfKeys() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertEquals(['foo','baz'], $coll->keys()->toArray()); - } - - public function testCollectionValuesReturnsCollectionOfValues() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $this->assertEquals(['bar','bin'], $coll->values()->toArray()); - } - - public function testCollectionMergeMergesDataIntoCollection() - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $mergeIn = ['baz' => 'bone', 'boo' => 'hoo']; - $this->assertEquals([ - 'foo' => 'bar', - 'baz' => 'bone', - 'boo' => 'hoo' - ], $coll->merge($mergeIn)->toArray()); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testCollectionMergeThrowsExceptionOnInvalidDataType () - { - $in = ['foo' => 'bar', 'baz' => 'bin']; - $coll = Collection::factory($in); - $coll->merge('boo'); - } - - public function testCollectionContainsReturnsTrueIfRequestedValueInCollection() - { - $coll = Collection::factory([ - 'foo' => 'bar', - 'boo' => 'far', - 'goo' => 'czar' - ]); - $this->assertTrue($coll->contains('bar')); - $this->assertFalse($coll->contains('tar')); - - // can also check key - $this->assertTrue($coll->contains('bar', 'foo'), "Ensure Container::contains() can pass a second param for key. "); - $this->assertFalse($coll->contains('far', 'poo')); - - // can also accept a callable to determine if collection contains user-specified criteria - $this->assertTrue($coll->contains(function($val) { - return strlen($val) > 3; - })); - $this->assertFalse($coll->contains(function($val) { - return strlen($val) < 3; - })); - $this->assertFalse($coll->contains(function($val) { - return $val instanceof Iterator; - })); - - // can also return true only for given index(es) - $this->assertTrue($coll->contains(function($val, $key) { - return strlen($val) > 3; - }, 'goo')); - $this->assertFalse($coll->contains(function($val, $key) { - return strlen($val) > 3; - }, 'boo')); - - // check that $key can be used for truthiness checking... - $this->assertTrue($coll->contains(function($val, $key) { - if (is_string($key)) { - return strlen($val) > 3; - } - return false; - }, 'goo')); - $this->assertFalse($coll->contains(function($val, $key) { - if (is_numeric($key)) { - return strlen($val) > 3; - } - return false; - }, 'boo')); - } - - public function testCollectionContainsAcceptsArrayForIndexParam() - { - $coll = Collection::factory([ - 'foo' => 'bar', - 'boo' => 'far', - 'goo' => 'czar' - ]); - - // pass an array of possible indexes - $this->assertTrue($coll->contains('bar', ['foo','boo'])); - $this->assertFalse($coll->contains('bar', ['goo','too'])); - - // we also need to make sure this works with callables - $this->assertTrue($coll->contains(function($val, $key) { - return strlen($val) > 3; - }, ['goo','boo'])); - $this->assertFalse($coll->contains(function($val, $key) { - return strlen($val) > 3; - }, ['foo','boo'])); - - // check that $key can be used for truthiness checking... - $this->assertFalse($coll->contains(function($val, $key) { - if (is_string($key)) { - return strlen($val) > 3; - } - return false; - }, ['foo','boo'])); - $this->assertFalse($coll->contains(function($val, $key) { - if (is_numeric($key)) { - return strlen($val) > 3; - } - return false; - }, ['goo','boo'])); - } - - public function testPopReturnsAnItemAndRemovesItFromEnd() - { - $coll = Collection::factory(['a','b','c','d',$expected = 'pop goes the weasel']); - $this->assertEquals($expected, $coll->pop()); - $this->assertEquals(['a','b','c','d'], $coll->toArray()); - $this->assertEquals('d', $coll->pop()); - $this->assertEquals(['a','b','c'], $coll->toArray()); - } - - public function testShiftReturnsAnItemAndRemovesItFromBeginning() - { - $coll = Collection::factory([$expected = 'a','b','c','d','pop goes the weasel']); - $this->assertEquals($expected, $coll->shift()); - $this->assertEquals(['b','c','d','pop goes the weasel'], $coll->toArray()); - $this->assertEquals('b', $coll->shift()); - $this->assertEquals(['c','d','pop goes the weasel'], $coll->toArray()); - } - - public function testPushItemsOntoCollectionAddsToEnd() - { - $coll = Collection::factory(['a','b','c','d']); - $coll->push('e'); - $this->assertEquals(['a','b','c','d','e'], $coll->toArray()); - $this->assertEquals(['a','b','c','d','e','f','g',['h','i','j'], 'k'], $coll->push('f', 'g', ['h', 'i', 'j'], 'k')->toArray()); - } - - public function testUnshiftAddsToBeginningOfCollection() - { - $coll = Collection::factory(['a','b','c','d']); - $coll->unshift('e'); - $this->assertEquals(['e','a','b','c','d'], $coll->toArray()); - $this->assertEquals(['f','g',['h','i','j'],'k','e','a','b','c','d'], $coll->unshift('f', 'g', ['h', 'i', 'j'], 'k')->toArray()); - } - - public function testMapReturnsANewCollectionContainingValuesAfterCallback() - { - $coll = Collection::factory([0,1,2,3,4,5,6,7,8,9]); - $coll2 = $coll->map(function($val){ - return $val + 1; - }); - $this->assertInstanceOf(AbstractCollection::class, $coll2); - $this->assertEquals([1,2,3,4,5,6,7,8,9,10], $coll2->toArray()); - } - - public function testCollectionWalkCallbackModifyInPlace() - { - $coll = Collection::factory([1,2,3,4,5,6,7,8,9,0]); - $context = [ - 'extra_context' => 'foobar', - 'more_context' => 'boofar' - ]; - $coll->walk(function (&$value, $key, $udata) { - if ($key %2 == 0) $value++; - else $value--; - $value .= $udata['extra_context']; - }, $context); - $this->assertEquals([ - '2foobar', - '1foobar', - '4foobar', - '3foobar', - '6foobar', - '5foobar', - '8foobar', - '7foobar', - '10foobar', - '-1foobar' - ], $coll->toArray()); - } - - public function testCollectionReduceReturnsSingleValueUsingCallback() - { - $coll = Collection::factory([ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool' - ]); - $this->assertEquals('really cool guy', $coll->reduce(function($carry, $item) { - if (strlen($item) >= strlen($carry)) { - return $item; - } - return $carry; - }, null)); - - } - - public function testCollectionFilterReturnsCollectionFilteredUsingCallback() - { - $coll = Collection::factory([ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool' - ]); - $this->assertEquals([ - 'mk' => 'lady', - 'terry' => 'what a fool' - ], $coll->filter(function($v, $k) { - return strpos($v, 'e') === false; - })->toArray()); - } - - public function testCollectionIsIterable() - { - $coll = Collection::factory($exp = [ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool', - ]); - $this->assertInstanceOf(Iterator::class, $coll); - $this->assertEquals('mk', $coll->key()); - $this->assertEquals('lady', $coll->current()); - $this->assertTrue($coll->valid()); - $this->assertEquals('sweet', $coll->next()); - $this->assertEquals('lorrie', $coll->key()); - $this->assertEquals('sweet', $coll->current()); - $this->assertTrue($coll->valid()); - $this->assertEquals('really cool guy', $coll->next()); - $this->assertEquals('luke', $coll->key()); - $this->assertEquals('really cool guy', $coll->current()); - $this->assertTrue($coll->valid()); - $this->assertEquals('what a fool', $coll->next()); - $this->assertEquals('terry', $coll->key()); - $this->assertEquals('what a fool', $coll->current()); - $this->assertTrue($coll->valid()); - $this->assertNull($coll->next()); - $this->assertFalse($coll->valid()); - $this->assertEquals('lady', $coll->rewind()); - - foreach ($coll as $key => $val) { - $this->assertEquals($exp[$key], $val); - } - } - - public function testSPLIteratorFunctionsWorkOnCollection() - { - $coll = Collection::factory($exp = [ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool', - ]); - $arr = iterator_to_array($coll); - $this->assertEquals($exp, $arr); - $this->assertEquals($arr, $coll->toArray()); - } - - //public function testToArrayUsesIteratorMethods() - //{ - // @todo Need to stub the collection and change the "current" method to return something different - // so I can test that foreach always returns the value that current returns - //} - - public function testCollectionReturnsTrueForIsTraversable() - { - $coll = Collection::factory($exp = [ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool', - ]); - $this->assertTrue(is_traversable($coll)); - } - - public function testOffsetMethodsForCollectionArrayAccess() - { - $coll = Collection::factory($exp = [ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool', - ]); - $this->assertInstanceOf(ArrayAccess::class, $coll); - $this->assertTrue($coll->offsetExists('mk')); - $this->assertFalse($coll->offsetExists('mom')); - $this->assertEquals('lady', $coll->offsetGet('mk')); - $this->assertNull($coll->offsetSet('mk', 'wife')); - $this->assertEquals('wife', $coll->offsetGet('mk')); - $coll->offsetSet('mom', 'saint'); - $this->assertTrue($coll->offsetExists('mom')); - $this->assertEquals('saint', $coll->offsetGet('mom')); - $this->assertNull($coll->offsetUnset('mom')); - $this->assertFalse($coll->offsetExists('mom')); - - // now we can test that array syntax works (it will) - $this->assertTrue(isset($coll['mk'])); - $this->assertEquals('wife', $coll['mk']); - unset($coll['mk']); - $this->assertFalse(isset($coll['mk'])); - $coll['foo'] = 'var'; - $this->assertTrue(isset($coll['foo'])); - $this->assertEquals('var', $coll['foo']); - } - - public function testCollectionIsCountable() - { - $coll = Collection::factory($exp = [ - 'mk' => 'lady', - 'lorrie' => 'sweet', - 'luke' => 'really cool guy', - 'terry' => 'what a fool', - ]); - $this->assertInstanceOf(Countable::class, $coll); - $this->assertEquals(4, $coll->count()); - } - - public function testPairsReturnsKeyValPairs() - { - $coll = Collection::factory([ - 'foo' => 'bar', - 'bin' => 'baz', - 'boo' => 'far', - ]); - $this->assertEquals([ - ['foo', 'bar'], - ['bin', 'baz'], - ['boo', 'far'], - ], $coll->pairs()->toArray()); - } - - public function testHasPositionReturnsNumericPositionRegardlessOfKeyType() - { - $coll = Collection::factory([ - 'foo' => 'bar', - 0 => 'baz', - 'test' => 'best', - 10 => 'ten', - 'fifth' => 'this is the fifth' - ]); - $this->assertTrue($coll->hasPosition(0)); - $this->assertTrue($coll->hasPosition(1)); - $this->assertTrue($coll->hasPosition(2)); - $this->assertTrue($coll->hasPosition(3)); - $this->assertTrue($coll->hasPosition(4)); - $this->assertFalse($coll->hasPosition(5)); - } - - public function testEachWithAlwaysFalseCallbackGivesEmptyIterator() - { - $coll = new Collection($this->testdata[Collection::class]); - $callback = function ($val, $key) { - return false; - }; - $this->assertInstanceOf(AbstractCollection::class, $coll->each($callback)); - $this->assertCount(0, $coll->each($callback)); - $this->assertEquals([ - ], $coll->each($callback)->toArray()); - } - - public function testEachIteratesWithCallbackAsFilter() - { - $coll = new Collection($this->testdata[Collection::class]); - $callback = function ($val, $key) { - return strlen($val) >= 5; - }; - $this->assertInstanceOf(AbstractCollection::class, $coll->each($callback)); - $this->assertCount(7, $coll->each($callback)); - $this->assertEquals([ - 2 => 'voluptatem', - 4 => 'quisquam', - 5 => 'voluptatibus', - 7 => 'veniam', - 8 => 'omnis', - 10 => 'cupiditate', - 13 => 'numquam' - ], $coll->each($callback)->toArray()); - } - - public function testEachIteratesWithCallbackAsFilterForKey() - { - $coll = new Collection($this->testdata[Collection::class]); - $callback = function ($val, $key) { - return $key %2 == 0; - }; - $this->assertInstanceOf(AbstractCollection::class, $coll->each($callback)); - $this->assertCount(8, $coll->each($callback)); - $this->assertEquals([ - 0 => 'et', - 2 => 'voluptatem', - 4 => 'quisquam', - 6 => 'modi', - 8 => 'omnis', - 10 => 'cupiditate', - 12 => 'ipsa', - 14 => 'est' - ], $coll->each($callback)->toArray()); - } - - public function testEachIsAutomaticallyBoundToCollection() - { - $coll = new Collection($this->testdata[Collection::class]); - $callback = function ($val, $key) use ($coll) { - return ($this === $coll); - }; - $this->assertInstanceOf(AbstractCollection::class, $coll->each($callback)); - $this->assertCount(15, $coll->each($callback)); - } - - public function testEachCanBeBoundToArbitraryObject() - { - $coll1 = new Collection([1,2,3]); - $coll2 = new Collection([4,5,6]); - $coll3 = new Collection([7,8,9]); - $callback = function ($val, $key) { - return ($this->contains(4)); - }; - $this->assertInstanceOf(AbstractCollection::class, $coll1->each($callback)); - $this->assertCount(3, $coll1->each($callback, $coll2)); - $this->assertCount(0, $coll1->each($callback, $coll3)); - } -} \ No newline at end of file diff --git a/tests/CSVelte/Collection/MultiCollectionTest.php b/tests/CSVelte/Collection/MultiCollectionTest.php deleted file mode 100644 index 9ddf5e9..0000000 --- a/tests/CSVelte/Collection/MultiCollectionTest.php +++ /dev/null @@ -1,133 +0,0 @@ - [ - 'test', - 'this', - 'that', - 'boo', - 'zoo', - 'mpo' - ], - 'test' => [ - 'a','b','c','d','e','f','g' - ], - 'another' => 'this is a string', - [ - 'an', - 'array', - 'of', - 'strings' - ] - ]; - $coll = Collection::factory($input); - $this->assertInstanceOf(MultiCollection::class, $coll); - $coll2 = Collection::factory($this->testdata[MultiCollection::class]); - $this->assertInstanceOf(MultiCollection::class, $coll2); - } - - public function testToArrayReturnsMultiDimensionalArray() - { - $coll = Collection::factory($this->testdata[MultiCollection::class]); - $arr = $coll->toArray(); - $this->assertArrayHasKey('names', $arr); - $this->assertArrayHasKey('addresses', $arr); - $this->assertArrayHasKey('cities', $arr); - $this->assertArrayHasKey('userAgent', $arr); - $this->assertEquals(3, count($arr['words'][0])); - } - - public function testMergeMultiCollectionCanMergeArray() - { - $coll = Collection::factory($this->testdata[MultiCollection::class]); - $arr = [ - 'names' => [ - 'Nobody', - 'Somebody', - 'Thembody' - ], - 'words' => [ - 'I', - 'like', - 'stuff' - ], - 'cities' => 'This is not a city' - ]; - $merged = $coll->merge($arr); - $this->assertEquals(array_merge($coll->toArray(), $arr), $merged->toArray()); - } - - public function testMergeMultiCollectionCanMergeCollection() - { - $coll = Collection::factory($this->testdata[MultiCollection::class]); - $arr = [ - 'names' => [ - 'Nobody', - 'Somebody', - 'Thembody' - ], - 'words' => [ - 'I', - 'like', - 'stuff' - ], - 'cities' => 'This is not a city' - ]; - $mergeme = Collection::factory($arr); - $merged = $coll->merge($mergeme); - $this->assertEquals(array_merge($coll->toArray(), $arr), $merged->toArray()); - } - - public function testMultiContainsSearchesThroughoutAllDimensions() - { - $coll = Collection::factory($this->testdata[MultiCollection::class]); - //dd($coll); - $this->assertTrue($coll->contains('Mrs. Aaliyah Paucek Jr.')); - $this->assertFalse($coll->contains('Mrs. Aaliyah Paucek Jr.', 8)); - $this->assertTrue($coll->contains('Mrs. Aaliyah Paucek Jr.', 7)); - $this->assertFalse($coll->contains('Mrs. Aaliyah Paucek Jr.', [1,2,3])); - $this->assertTrue($coll->contains('Mrs. Aaliyah Paucek Jr.', [6,7,8,9])); - $this->assertTrue($coll->contains('praesentium')); - $this->assertTrue($coll->contains('praesentium', 1)); - $this->assertFalse($coll->contains('praesentium',2)); - $this->assertFalse($coll->contains('praesentium', [0,2])); - $this->assertTrue($coll->contains('praesentium', [0,1])); - - $func = function($val, $key) { - if ($key == 'cities') { - if (is_traversable($val)) { - foreach ($val as $k => $v) { - if (strpos($v, 'Waldo') !== false) { - return true; - } - } - } - } - return false; - }; - $this->assertTrue($coll->contains($func)); - - // @todo Come back to this... - // $this->assertFalse($coll->contains($func), 'names'); - } - - public function testMapRunsCallbackAgainstEachItem() - { - $coll = Collection::factory($this->testdata[MultiCollection::class]['addresses']); - $coll2 = $coll->map(function($val) { - return strlen($val); - }); - $this->assertEquals([18,22,19,21,20,30,27,14,32,24], $coll2->toArray()); - } - -} \ No newline at end of file diff --git a/tests/CSVelte/Collection/NumericCollectionTest.php b/tests/CSVelte/Collection/NumericCollectionTest.php deleted file mode 100644 index 843c0a0..0000000 --- a/tests/CSVelte/Collection/NumericCollectionTest.php +++ /dev/null @@ -1,106 +0,0 @@ -assertInstanceOf(NumericCollection::class, $coll); - $coll->increment($zero); - $this->assertEquals(11, $coll->get($zero)); - $coll->increment($zero); - $coll->increment($zero); - $coll->increment($zero); - $coll->increment($zero); - $this->assertEquals(15, $coll->get($zero)); - $coll->decrement($zero); - $this->assertEquals(14, $coll->get($zero)); - $coll->decrement($zero); - $coll->decrement($zero); - $this->assertEquals(12, $coll->get($zero)); - } - - public function testIncrementDecrementWithIntervalAddsSubtractsIntervalFromGivenKey() - { - $coll = Collection::factory([10,15,20,25,50,100]); - $zero = 0; - $this->assertInstanceOf(NumericCollection::class, $coll); - $coll->increment($zero, 5); - $this->assertEquals(15, $coll->get($zero)); - $coll->increment($zero, 100); - $this->assertEquals(115, $coll->get($zero)); - $coll->decrement($zero, 2); - $this->assertEquals(113, $coll->get($zero)); - $coll->decrement($zero, 1000); - $coll->decrement($zero); - $this->assertEquals(-888, $coll->get($zero)); - } - - - public function testSumMethodSumsCollection() - { - $coll = Collection::factory([10,20,30,100,60,80]); - $this->assertEquals(300, $coll->sum()); - } - - public function testAverageMethodAveragesCollection() - { - $coll = Collection::factory([10,20,30,100,60,80]); - $this->assertEquals(50, $coll->average()); - } - - public function testModeMethodReturnsCollectionMode() - { - $coll = Collection::factory([10,20,30,100,60,80,10,20,100,10,50,40,10,20,50,60,80]); - $this->assertEquals(10, $coll->mode()); - } - - public function testMedianMethodReturnsCollectionMedian() - { - $coll = Collection::factory([1,10,20,30,100,60,80,10,20,100,10,50,40,10,20,50,60,80]); - $this->assertEquals(35, $coll->median()); - - $coll = Collection::factory([1,20,300,4000]); - $this->assertEquals(160, $coll->median()); - - // $coll = Collection::factory(['one','two','three','four','five']); - // $this->assertEquals('four', $coll->median()); - - // @todo Maybe for strings median should work with string length? - // $coll = Collection::factory(['hello','world','this','will','do','weird','stuff','yes','it','will']); - // $this->assertEquals(0, $coll->median()); - - $coll = Collection::factory([1]); - $this->assertEquals(1, $coll->median()); - - $coll = Collection::factory([1,2]); - $this->assertEquals(1.5, $coll->median()); - } - - public function testCountsReturnsCollectionOfCounts() - { - $data = [1,1,1,2,0,2,2,3,3,3,3,3,3,3,4,5,6,6,7,8,9,0]; - $coll = Collection::factory($data); - $this->assertInstanceOf(NumericCollection::class, $coll); - $counts = $coll->counts(); - $this->assertInstanceOf(NumericCollection::class, $counts); - $this->assertEquals([ - 1 => 3, - 2 => 3, - 3 => 7, - 4 => 1, - 5 => 1, - 6 => 2, - 7 => 1, - 8 => 1, - 9 => 1, - 0 => 2 - ], $counts->toArray()); - } - -} \ No newline at end of file diff --git a/tests/CSVelte/Collection/TabularCollectionTest.php b/tests/CSVelte/Collection/TabularCollectionTest.php deleted file mode 100644 index a160260..0000000 --- a/tests/CSVelte/Collection/TabularCollectionTest.php +++ /dev/null @@ -1,221 +0,0 @@ - 10, - 'words' => 'some words' - ], - [ - 'numbers' => 17, - 'words' => 'some words' - ], - [ - 'numbers' => 18, - 'words' => 'some words' - ], - [ - 'numbers' => 11, - 'words' => 'no words' - ], - [ - 'numbers' => 20, - 'words' => 'all words' - ], - [ - 'numbers' => 15, - 'words' => 'word up' - ], - [ - 'numbers' => 15, - 'words' => 'word up' - ], - [ - 'numbers' => 15, - 'words' => 'word down' - ], - [ - 'numbers' => 20, - 'words' => 'all words' - ], - [ - 'numbers' => 5, - 'words' => 'all words' - ], - ]; - - public function testFactoryReturnsTabularCollection() - { - $coll = Collection::factory($this->testdata[TabularCollection::class]['user']); - $this->assertInstanceOf(TabularCollection::class, $coll); - $coll2 = Collection::factory($this->testdata[TabularCollection::class]['profile']); - $this->assertInstanceOf(TabularCollection::class, $coll2); - } - - public function testMapTabularCollection() - { - $coll = Collection::factory($this->testdata[TabularCollection::class]['user']); - $func = function($row) { - $this->assertInstanceOf(AbstractCollection::class, $row); - return $row->get('email'); - }; - $newcoll = $coll->map($func->bindTo($this)); - $this->assertEquals([ - 'ohauck@bahringer.info', - 'larry.emard@pacocha.com', - 'jaylin.mueller@yahoo.com', - 'gfriesen@hotmail.com', - 'verla.ohara@dibbert.com' - ], $newcoll->toArray()); - } - - public function testContainsWorksWithTabular() - { - $coll = Collection::factory($this->testdata[TabularCollection::class]['user']); - $this->assertTrue($coll->contains('gfriesen@hotmail.com')); - $this->assertTrue($coll->contains('gfriesen@hotmail.com', 'email')); - $this->assertFalse($coll->contains('gfriesen@hotmail.com', 'role')); - $this->assertTrue($coll->contains('gfriesen@hotmail.com', ['id','email','created'])); - } - - public function testContainsWithCallbackWorksWithTabular() - { - $coll = Collection::factory($this->testdata[TabularCollection::class]['user']); - $this->assertTrue($coll->contains(function($val) { - return $val['is_active']; - })); - $this->assertTrue($coll->contains(function($val) { - return !$val['is_active']; - })); - $this->assertTrue($coll->contains(function($val) { - return is_array($val) && array_key_exists('email', $val); - })); - $this->assertFalse($coll->contains(function($val) { - return is_array($val) && array_key_exists('username', $val); - })); - } - - public function testCollectionHasRow() - { - $coll = new TabularCollection($this->testdata[TabularCollection::class]['user']); - $this->assertTrue($coll->hasRow(0)); - $this->assertTrue($coll->hasRow(1)); - $this->assertTrue($coll->hasRow(2)); - $this->assertTrue($coll->hasRow(3)); - $this->assertTrue($coll->hasRow(4)); - $this->assertFalse($coll->hasRow(5)); - } - - public function testCollectionGetRow() - { - $coll = new TabularCollection($this->testdata[TabularCollection::class]['user']); - $firstrow = $coll->getRow(0); - unset($firstrow['created']); - unset($firstrow['modified']); - $this->assertEquals([ - 'id' => 1, - 'profile_id' => 126, - 'email' => 'ohauck@bahringer.info', - 'password' => '60a2409dea624661573516a31e3a1ea412076237', - 'role' => 'moderator', - 'is_active' => false - ], $firstrow); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testCollectionGetRowThrowsExceptionForMissingRow() - { - $coll = new TabularCollection($this->testdata[TabularCollection::class]['user']); - $firstrow = $coll->getRow(10); - } - - public function testHasColumn() - { - $coll = new TabularCollection($this->testdata[TabularCollection::class]['user']); - $this->assertTrue($coll->hasColumn('email')); - $this->assertFalse($coll->hasColumn('foobar')); - } - - public function testGetColumn() - { - $coll = new TabularCollection($this->testdata[TabularCollection::class]['user']); - $this->assertInstanceOf(AbstractCollection::class, $coll->getColumn('email')); - $this->assertEquals([1,2,3,4,5], $coll->getColumn('id')->toArray()); - } - - public function testAverageColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(14.6, $coll->average('numbers')); - } - - public function testSumColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(146, $coll->sum('numbers')); - } - - public function testModeColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(15, $coll->mode('numbers')); - } - - public function testMedianColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(15, $coll->median('numbers')); - } - - public function testMaxColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(20, $coll->max('numbers')); - } - - public function testMinColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals(5, $coll->min('numbers')); - } - - public function testCountsColumn() - { - $coll = new TabularCollection($this->moretestdata); - $this->assertEquals([ - 10 => 1, - 17 => 1, - 18 => 1, - 11 => 1, - 20 => 2, - 15 => 3, - 5 => 1 - ], $coll->counts('numbers')->toArray()); - } - - /** - * @expectedException BadMethodCallException - * @expectedExceptionMessage Method does not exist: CSVelte\Collection\TabularCollection::nonExistantMethod() - */ - public function testTabularCollectionThrowsBadMethodCallExceptionOnBadMethodCall() - { - $coll = new TabularCollection([ - ['id' => 2, 'name' => 'Luke', 'email' => 'luke.visinoni@gmail.com'], - ['id' => 3, 'name' => 'Dave', 'email' => 'dave.mason@gmail.com'], - ['id' => 5, 'name' => 'Joe', 'email' => 'joe.rogan@gmail.com'], - ]); - $coll->nonExistantMethod('foo','bar'); - } -} \ No newline at end of file diff --git a/tests/CSVelte/DialectTest.php b/tests/CSVelte/DialectTest.php new file mode 100644 index 0000000..7439e88 --- /dev/null +++ b/tests/CSVelte/DialectTest.php @@ -0,0 +1,105 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelteTest; + +use CSVelte\Dialect; + +class DialectTest extends UnitTestCase +{ + public function testDefaultDialectAttributesAreConsistentWithCSVW() + { + $dialect = new Dialect; + $this->assertSame("#", $dialect->getCommentPrefix()); + $this->assertSame(",", $dialect->getDelimiter()); + $this->assertTrue($dialect->isDoubleQuote()); + $this->assertSame("utf-8", $dialect->getEncoding()); + $this->assertTrue($dialect->hasHeader()); + $this->assertSame("\n", $dialect->getLineTerminator()); + $this->assertSame('"', $dialect->getQuoteChar()); + $this->assertFalse($dialect->isSkipBlankRows()); + $this->assertSame(0, $dialect->getSkipColumns()); + $this->assertFalse($dialect->isSkipInitialSpace()); + $this->assertSame(0, $dialect->getSkipRows()); + $this->assertSame(Dialect::TRIM_ALL, $dialect->getTrim()); + $this->assertSame(Dialect::QUOTE_MINIMAL, $dialect->getQuoteStyle()); + } + + public function testDefaultDialectAllowsOverwritingAttributesWithSetters() + { + $dialect = new Dialect; + $dialect->setCommentPrefix('//') + ->setDelimiter("\t") + ->setIsDoubleQuote(false) + ->setEncoding("utf-16") + ->setHasHeader(false) + ->setHeaderRowCount(2) + ->setLineTerminator("\r") + ->setQuoteChar("'") + ->setIsSkipBlankRows(true) + ->setSkipColumns(1) + ->setIsSkipInitialSpace(true) + ->setSkipRows(1) + ->setTrim(Dialect::TRIM_NONE) + ->setQuoteStyle(Dialect::QUOTE_ALL); + + $this->assertSame('//', $dialect->getCommentPrefix()); + $this->assertSame("\t", $dialect->getDelimiter()); + $this->assertFalse($dialect->isDoubleQuote()); + $this->assertSame("utf-16", $dialect->getEncoding()); + $this->assertFalse($dialect->hasHeader()); + $this->assertSame(2, $dialect->getHeaderRowCount()); + $this->assertSame("\r", $dialect->getLineTerminator()); + $this->assertSame("'", $dialect->getQuoteChar()); + $this->assertTrue($dialect->isSkipBlankRows()); + $this->assertSame(1, $dialect->getSkipColumns()); + $this->assertTrue($dialect->isSkipInitialSpace()); + $this->assertSame(1, $dialect->getSkipRows()); + $this->assertSame(Dialect::TRIM_NONE, $dialect->getTrim()); + $this->assertSame(Dialect::QUOTE_ALL, $dialect->getQuoteStyle()); + } + + public function testDialectAllowsAllAttributesToBeSetFromConstructor() + { + $dialect = new Dialect([ + 'commentPrefix' => "//", + 'delimiter' => "\t", + 'doubleQuote' => false, + 'encoding' => "utf-16", + 'header' => false, + 'headerRowCount' => 2, + 'lineTerminator' => "\r", + 'quoteChar' => "'", + 'skipBlankRows' => true, + 'skipColumns' => 1, + 'skipInitialSpace' => true, + 'skipRows' => 1, + 'trim' => Dialect::TRIM_START, + 'quoteStyle' => Dialect::QUOTE_ALL + ]); + + $this->assertSame('//', $dialect->getCommentPrefix()); + $this->assertSame("\t", $dialect->getDelimiter()); + $this->assertFalse($dialect->isDoubleQuote()); + $this->assertSame("utf-16", $dialect->getEncoding()); + $this->assertFalse($dialect->hasHeader()); + $this->assertSame(2, $dialect->getHeaderRowCount()); + $this->assertSame("\r", $dialect->getLineTerminator()); + $this->assertSame("'", $dialect->getQuoteChar()); + $this->assertTrue($dialect->isSkipBlankRows()); + $this->assertSame(1, $dialect->getSkipColumns()); + $this->assertTrue($dialect->isSkipInitialSpace()); + $this->assertSame(1, $dialect->getSkipRows()); + $this->assertSame(Dialect::TRIM_START, $dialect->getTrim()); + $this->assertSame(Dialect::QUOTE_ALL, $dialect->getQuoteStyle()); + } +} \ No newline at end of file diff --git a/tests/CSVelte/FlavorTest.php b/tests/CSVelte/FlavorTest.php deleted file mode 100755 index 405c814..0000000 --- a/tests/CSVelte/FlavorTest.php +++ /dev/null @@ -1,204 +0,0 @@ - - * @author Luke Visinoni - */ -class FlavorTest extends UnitTestCase -{ - /** - * This is just a really simple test to get things started... - */ - public function testCSVelteFlavor() - { - $flavor = new Flavor(array('delimiter' => "\tab!!")); - $this->assertInstanceOf($expected = 'CSVelte\Flavor', new Flavor); - } - - /** - * Test that CSVelte\Flavor provides reasonable default values for its attributes - */ - public function testCSVelteFlavorDefaults() - { - $flavor = new Flavor; - $this->assertEquals($delimiter = ",", $flavor->delimiter); - $this->assertEquals($quoteChar = "\"", $flavor->quoteChar); - $this->assertEquals($escapeChar = "\\", $flavor->escapeChar); - $this->assertEquals($lineTerminator = "\r\n", $flavor->lineTerminator); - $this->assertEquals($quoting = Flavor::QUOTE_MINIMAL, $flavor->quoteStyle); - $this->assertTrue($flavor->doubleQuote); - //$this->assertFalse($flavor->skipInitialSpace); - $this->assertNull($flavor->header); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testCSVelteFlavorGetNonExistAttributeThrowsException() - { - $flavor = new Flavor; - $foo = $flavor->foo; - } - - /** - * These objects are immutable, so any attempt to set an attribute should - * result in an exception being thrown. - * @expectedException CSVelte\Exception\ImmutableException - */ - public function testCSVelteFlavorSetAttributeThrowsImmutableException() - { - $flavor = new Flavor; - $flavor->foo = 'bar'; - } - - /** - * - */ - public function testInitializeFlavorUsingAssociativeArray() - { - $attribs = [ - 'delimiter' => "\t", - 'quoteChar' => "'", - 'escapeChar' => "'", - 'lineTerminator' => "\r\n", - 'quoteStyle' => Flavor::QUOTE_MINIMAL - ]; - $flavor = new Flavor($attribs); - $this->assertEquals($attribs['delimiter'], $flavor->delimiter); - $this->assertEquals($attribs['quoteChar'], $flavor->quoteChar); - $this->assertEquals($attribs['escapeChar'], $flavor->escapeChar); - $this->assertEquals($attribs['lineTerminator'], $flavor->lineTerminator); - $this->assertEquals($attribs['quoteStyle'], $flavor->quoteStyle); - } - - public function testFlavorCanCopyItself() - { - $flavor = new Flavor($exp_attribs = array('delimiter' => '|', 'quoteChar' => "'", 'escapeChar' => '&', 'doubleQuote' => true, /*'skipInitialSpace' => true,*/ 'quoteStyle' => Flavor::QUOTE_NONE, 'lineTerminator' => "\r", 'header' => null)); - $this->assertEquals($flavor, $flavor->copy()); - $this->assertNotSame($flavor, $flavor->copy()); - } - - public function testFlavorCanCopyItselfWithAlteredAttribs() - { - $flavor = new Flavor($attribs = array('delimiter' => '|', 'quoteChar' => "'", 'escapeChar' => '&', 'doubleQuote' => true, /*'skipInitialSpace' => true,*/ 'quoteStyle' => Flavor::QUOTE_NONE, 'lineTerminator' => "\r", 'header' => null)); - - $new_attribs = array( - 'header' => true, - 'lineTerminator' => "\n", - 'delimiter' => "\t" - ); - $exp_attribs = array_merge($attribs, $new_attribs); - - $this->assertEquals(new Flavor($exp_attribs), $flavor->copy($new_attribs)); - } - - // public function testFlavorCreateFactoryByName() - // { - // $excel = Flavor::create('excel-tab'); - // $this->assertInstanceOf(Excel::class, $excel); - // } - // - // public function testConcreteFlavorClasses() - // { - // $expected = array( - // // this is the RFC standard for CSV according to https://tools.ietf.org/html/rfc4180 - // 'excel' => array( - // 'delimiter' => ',', - // 'quoteChar' => '"', - // 'doubleQuote' => true, - // 'skipInitialSpace' => false, - // 'lineTerminator' => "\r\n", - // 'quoteStyle' => Flavor::QUOTE_MINIMAL - // ), - // // these will be all the same as excel with the exception of delimiter - // 'excel-tab' => array( - // 'delimiter' => "\t" - // ), - // // I'm not sure if this is actually the correct set of parameters I just guessed - // 'unix' => array( - // 'delimiter' => ',', - // 'quoteChar' => '"', - // 'escapeChar' => '\\', - // 'doubleQuote' => false, - // 'skipInitialSpace' => false, - // 'lineTerminator' => "\n", - // 'quoteStyle' => Flavor::QUOTE_NONNUMERIC - // ), - // 'unix-tab' => array( - // 'delimiter' => "\t", - // ) - // ); - // - // $excel = Flavor::create('excel'); - // $excel_tab = Flavor::create('excel-tab'); - // $unix = Flavor::create('unix'); - // $unix_tab = Flavor::create('unix-tab'); - // - // $excel_flavor = new CSVelte\Flavor\Excel; - // $exceltab_flavor = new CSVelte\Flavor\ExcelTab; - // // $unix_flavor = new CSVelte\Flavor\Unix; - // // $unixtab_flavor = new CSVelte\Flavor\UnixTab; - // - // $this->assertEquals($excel_flavor, $excel); - // $this->assertEquals($exceltab_flavor, $excel_tab); - // $this->assertEquals($unix_flavor, $unix); - // $this->assertEquals($unixtab_flavor, $unix_tab); - // - // $this->assertEquals($excel_flavor, $excel->copy($expected['excel'])); - // $this->assertEquals($exceltab_flavor, $excel_tab->copy($expected['excel-tab'])); - // $this->assertEquals($unix_flavor, $unix->copy($expected['unix'])); - // $this->assertEquals($unixtab_flavor, $unix_tab->copy($expected['unix-tab'])); - // } - - public function testConcreteFlavors() - { - $excel = new Excel; - $this->assertEquals("\r\n", $excel->lineTerminator); - $this->assertEquals('"', $excel->quoteChar); - $this->assertEquals(",", $excel->delimiter); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $excel->quoteStyle); - $this->assertTrue($excel->doubleQuote); - //$this->assertFalse($excel->skipInitialSpace); - $this->assertNull($excel->escapeChar); - } - - public function testFlavorToArray() - { - $flavor = new Flavor(); - $arr = $flavor->toArray(); - $this->assertEquals(",", $arr['delimiter']); - $this->assertEquals('"', $arr['quoteChar']); - $this->assertEquals('\\', $arr['escapeChar']); - $this->assertTrue($arr['doubleQuote']); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $arr['quoteStyle']); - $this->assertEquals("\r\n", $arr['lineTerminator']); - $this->assertNull($arr['header']); - } - - public function testInstantiateWithInvalidArg() - { - $flavor = new Flavor("fooey gooey bars!"); - $this->assertEquals(new Flavor, $flavor); - } - - public function testHasHeader() - { - $headerNull = new Flavor(); - $this->assertFalse($headerNull->hasHeader()); - $headerTrue = new Flavor(['header' => true]); - $this->assertTrue($headerTrue->hasHeader()); - $headerFalse = new Flavor(['header' => false]); - $this->assertFalse($headerFalse->hasHeader()); - } - -} diff --git a/tests/CSVelte/FunctionsTest.php b/tests/CSVelte/FunctionsTest.php deleted file mode 100644 index 5d41bd2..0000000 --- a/tests/CSVelte/FunctionsTest.php +++ /dev/null @@ -1,293 +0,0 @@ - - * @author Luke Visinoni - */ -class FunctionsTest extends UnitTestCase -{ - public function testStreamizeReturnsStreamObjectForOpenStreamResource() - { - $handle = fopen($this->getFilePathFor('veryShort'), $mode = 'r+b'); - $this->assertInstanceOf(Stream::class, $stream = streamize($handle)); - $this->assertEquals($handle, $stream->getResource()->getHandle()); - $this->assertEquals($mode, $stream->getResource()->getMode()); - } - - public function testStreamizeReturnsStreamObjectForPHPString() - { - $string = "All your base are belong to us"; - $this->assertInstanceOf(Stream::class, $stream = streamize($string)); - $res = $stream->getResource(); - $this->assertTrue(is_resource($res->getHandle())); - $this->assertTrue($res->isConnected()); - $this->assertEquals(strlen($string), $stream->getSize()); - $this->assertEquals($string, (string) $stream); - $this->assertEquals('r+', $stream->getResource()->getMode()); - } - - public function testStreamizeReturnsStreamObjectForPHPStringableObject() - { - // Create a stub for non-existant StringableClass. - $csv_obj = $this->getMockBuilder('StringableClass') - ->setMethods(['__toString']) - ->getMock(); - - // configure the stub to return test string... - $string = "All your base are belong to us"; - $csv_obj->method('__toString') - ->willReturn($string); - - // test it... - $this->assertInstanceOf(Stream::class, $stream = streamize($csv_obj)); - $res = $stream->getResource(); - $this->assertTrue(is_resource($res->getHandle())); - $this->assertTrue($res->isConnected()); - $this->assertEquals(strlen($string), $stream->getSize()); - $this->assertEquals($string, (string) $stream); - $this->assertEquals('r+', $stream->getResource()->getMode()); - } - - // this will work for any Iterator class, not just SplFileObject - public function testStreamizeReturnsStreamObjectForSplFileObject() - { - $file_obj = new SplFileObject($this->getFilePathFor('commaNewlineHeader')); - $stream = streamize($file_obj); - $this->assertEquals("Bank Name,City,ST,CERT,Acquiring", $stream->read(32)); - $this->assertEquals(" Institution,Closing Date,Update", $stream->read(32)); - $this->assertEquals("d Date\nFirst CornerStone Bank,\"K", $stream->read(32)); - } - - public function testStreamFunctionReturnsStream() - { - $stream = stream($this->getFilePathFor('veryShort')); - $this->assertInstanceOf(Stream::class, $stream); - } - - public function testStreamFunctionReturnsLazyStreamByDefault() - { - $stream = stream($this->getFilePathFor('veryShort')); - $this->assertInstanceOf(Stream::class, $stream); - $this->assertTrue($stream->getResource()->isLazy()); - $this->assertFalse($stream->getResource()->isConnected()); - $this->assertEquals("vfs://root/testfiles/veryShort.csv", $stream->getUri()); - $this->assertEquals("foo,b", $stream->read(5)); - } - - public function testStreamFunctionReturnsOpenStreamWhenRequested() - { - $stream = stream($this->getFilePathFor('veryShort'), 'r+b', null, false); - $this->assertInstanceOf(Stream::class, $stream); - // @todo this is a bug, needs to be fixed - //$this->assertFalse($stream->getResource()->isLazy()); - $this->assertTrue($stream->getResource()->isConnected()); - $this->assertEquals("vfs://root/testfiles/veryShort.csv", $stream->getUri()); - $this->assertEquals("foo,b", $stream->read(5)); - } - - public function testTasteFunctionIsAliasForTasterInvoke() - { - $stream = Stream::open($this->getFilePathFor('headerDoubleQuote')); - $flavor = taste($stream); - $this->assertInstanceOf(Flavor::class, $flavor); - $this->assertEquals(",", $flavor->delimiter); - $this->assertEquals("\n", $flavor->lineTerminator); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $flavor->quoteStyle); - $this->assertTrue($flavor->doubleQuote); - $this->assertEquals('"', $flavor->quoteChar); - } - - public function testTasterInvokeWithSplFileObject() - { - $fileObj = new SplFileObject($this->getFilePathFor('headerTabSingleQuotes')); - $flavor = taste(streamize($fileObj)); - $this->assertInstanceOf(Flavor::class, $flavor); - $this->assertEquals("\t", $flavor->delimiter); - $this->assertEquals("\n", $flavor->lineTerminator); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $flavor->quoteStyle); - $this->assertTrue($flavor->doubleQuote); - $this->assertEquals("'", $flavor->quoteChar); - } - - public function testHasHeaderFunction() - { - $stream = Stream::open($this->getFilePathFor('veryShort')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('shortQuotedNewlines')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('commaNewlineHeader')); - $this->assertTrue(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('headerDoubleQuote')); - $this->assertTrue(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('headerTabSingleQuotes')); - $this->assertTrue(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('noHeaderCommaNoQuotes')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('noHeaderCommaQuoteAll')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('headerCommaQuoteNonnumeric')); - $this->assertTrue(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('noHeaderCommaNoQuotes')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('noHeaderCommaNoQuotes')); - $this->assertFalse(taste_has_header($stream)); - - $stream = Stream::open($this->getFilePathFor('noHeaderCommaNoQuotes')); - $this->assertFalse(taste_has_header($stream)); - } - - public function testStreamResourceFunctionReturnsResourceObject() - { - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b', - $ctxt = stream_context_create($ctxarr = [ - 'vfs' => ['foo' => 'bar'] - ]) - ); - $this->assertInstanceOf(StreamResource::class, $res); - $this->assertFalse($res->isConnected()); - $this->assertTrue($res->isLazy()); - $this->assertEquals($ctxt, $res->getContext()); - $this->assertEquals($ctxarr, stream_context_get_options($res->getContext())); - } - - public function testStreamResourceFunctionAcceptsArray() - { - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b', - [ - 'options' => [ - 'http' => ['method' => 'post'] - ], - 'params' => [ - ] - ] - ); - $this->assertInstanceOf(StreamResource::class, $res); - } - - public function testStreamResourceFunctionAcceptsArrayWithoutExpectedKeys() - { - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b', - [ - 'params' => [ - ] - ] - ); - $this->assertInstanceOf(StreamResource::class, $res); - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b', - [ - 'options' => [ - 'http' => ['method' => 'post'] - ], - ] - ); - $this->assertInstanceOf(StreamResource::class, $res); - $this->assertEquals([ - 'http' => [ - 'method' => 'post' - ] - ], stream_context_get_options($res->getContext())); - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b' - ); - $this->assertInstanceOf(StreamResource::class, $res); - } - - public function testStreamResourceInvokeReturnsAStreamObject() - { - $res = stream_resource( - $this->getFilePathFor('headerDoubleQuote'), - 'c+b', - $ctxt = stream_context_create($ctxarr = [ - 'vfs' => ['foo' => 'bar'] - ]) - ); - $this->assertInstanceOf(StreamResource::class, $res); - $this->assertInstanceOf(Stream::class, $res()); - } - - public function testCollectionFactoryFunctionUsingArray() - { - $coll = collect($arr = [0,1,2,3,4,5,6,7,8,9]); - $this->assertEquals($arr, $coll->toArray()); - } - - public function testCollectFluidMethods() - { - $coll = collect($arr = [ - 'f' => 'a', - 1 => '', - 'a' => '', - 2 => 'a', - 3 => 'foobar' - ])->unique(); - $this->assertEquals(['f' => 'a', 1 => '', 3 => 'foobar'], $coll->toArray()); - } - - // // @todo Create a collection object that works on a string so that you - // // can call a function for every character in a string and various other - // // functionality - // public function testCollectFunctionAcceptsString() - // { - // - // } - - public function testGetValueAcceptsCallbackAndVariadicArguments() - { - $this->assertEquals('Hello, Luke Visinoni!', invoke(function($first, $last) { - return "Hello, {$first} {$last}!"; - }, 'Luke', 'Visinoni')); - } - - public function testStreamizeAcceptsIOResourceObject() - { - $reg = new StreamResource($this->getFilePathFor('veryShort'), null, false); - $this->assertTrue($reg->isConnected()); - $regstream = streamize($reg); - $this->assertInstanceOf(Stream::class, $regstream); - $this->assertTrue($reg->isConnected()); - - $lazy = new StreamResource($this->getFilePathFor('veryShort'), null, true); - $this->assertFalse($lazy->isConnected()); - $lazystream = streamize($lazy); - $this->assertInstanceOf(Stream::class, $lazystream); - $this->assertFalse($lazy->isConnected()); - $this->assertEquals("foo,bar,ba", $lazystream->read(10)); - $this->assertTrue($lazy->isConnected()); - } - -} diff --git a/tests/CSVelte/IO/IOTest.php b/tests/CSVelte/IO/IOTest.php index 59844c8..944ee02 100644 --- a/tests/CSVelte/IO/IOTest.php +++ b/tests/CSVelte/IO/IOTest.php @@ -1,38 +1,20 @@ + * @license See LICENSE file (MIT license) */ +namespace CSVelteTest\IO; + use CSVelteTest\UnitTestCase; use CSVelteTest\StreamWrapper\HttpStreamWrapper; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\visitor\vfsStreamPrintVisitor; -/** - * CSVelte\IO Tests - * - * @package CSVelte Unit Tests - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - */ + class IOTest extends UnitTestCase { public function setUp() @@ -44,10 +26,9 @@ public function setUp() HttpStreamWrapper::class ) or die('Failed to register protocol'); } - public function tearDown() { parent::tearDown(); stream_wrapper_restore('http'); } -} +} \ No newline at end of file diff --git a/tests/CSVelte/IO/ResourceTest.php b/tests/CSVelte/IO/StreamResourceTest.php similarity index 90% rename from tests/CSVelte/IO/ResourceTest.php rename to tests/CSVelte/IO/StreamResourceTest.php index 6316be1..030730d 100644 --- a/tests/CSVelte/IO/ResourceTest.php +++ b/tests/CSVelte/IO/StreamResourceTest.php @@ -1,22 +1,23 @@ + * @license See LICENSE file (MIT license) + */ namespace CSVelteTest\IO; -use CSVelte\Exception\IOException; use CSVelte\IO\Stream; use CSVelte\IO\StreamResource; +use function CSVelte\to_stream; use InvalidArgumentException; -/** - * CSVelte\IO\Stream Tests. - * This tests the new IO\Stream class that will be replacing CSVelte\Input\Stream - * and CSVelte\Output\Stream - * - * @package CSVelte Unit Tests - * @copyright (c) 2016, Luke Visinoni - * @author Luke Visinoni - * @coversDefaultClass CSVelte\IO\Stream - */ -class ResourceTest extends IOTest +class StreamResourceTest extends IOTest { public function testInstantiateStreamResource() { @@ -150,8 +151,8 @@ public function testInstantiateVariousModes() $sr->setIsPlus(true); $this->assertEquals("r+b", $sr->getMode()); $sr->setBaseMode('x') - ->setIsPlus(true) - ->setIsText(true); + ->setIsPlus(true) + ->setIsText(true); $this->assertFalse($sr->isBinary()); $this->assertTrue($sr->isText()); $this->assertFalse($sr->isCursorPositionedAtEnd()); @@ -161,7 +162,7 @@ public function testInstantiateVariousModes() $this->assertTrue($sr->rejectsExistingFiles()); $this->assertFalse($sr->appendsWriteOps()); $sr->setBaseMode('a') - ->connect(); + ->connect(); $this->assertEquals('a+t', $sr->getMode()); $this->assertTrue($sr->isCursorPositionedAtEnd()); } @@ -297,4 +298,26 @@ public function testInstantiateIOResourceAcceptsOpenResourceHandle() $this->assertFalse($resource->isCursorPositionedAtEnd()); $this->assertTrue($resource->isTruncated()); } + + public function testToStreamAcceptsStreamResource() + { + $resource = fopen('php://temp', 'r+'); + $stream = to_stream($resource); + $this->assertInstanceOf(Stream::class, $stream); + } + + public function testToStreamAcceptsStreamResourceObject() + { + $resource = new StreamResource('php://temp', 'r+'); + $stream = to_stream($resource); + $this->assertInstanceOf(Stream::class, $stream); + } + + public function testStreamResourceAcceptsStringyObjectAsUri() + { + $stringy = Stream::open('php://temp', 'r+'); + $stringy->write('php://temp'); + $resource = new StreamResource($stringy, 'r+'); + $this->assertEquals('php://temp', $resource->getUri()); + } } diff --git a/tests/CSVelte/IO/StreamTest.php b/tests/CSVelte/IO/StreamTest.php index a3407c7..48d71c4 100644 --- a/tests/CSVelte/IO/StreamTest.php +++ b/tests/CSVelte/IO/StreamTest.php @@ -1,16 +1,27 @@ + * @license See LICENSE file (MIT license) + */ namespace CSVelteTest\IO; -use \ArrayIterator; -use CSVelte\Exception\NotYetImplementedException; -use \SplFileObject; +use ArrayIterator; use CSVelte\Contract\Streamable; -use CSVelte\IO\Stream; use CSVelte\IO\BufferStream; use CSVelte\IO\IteratorStream; +use InvalidArgumentException; +use SplFileObject; +use CSVelte\IO\Stream; use CSVelte\IO\StreamResource; -use function CSVelte\streamize; +use function CSVelte\to_stream; /** * CSVelte\IO\Stream Tests. @@ -93,7 +104,7 @@ public function testInstantiateIOStreamAcceptsStreamURI() } /** - * @expectedException \InvalidArgumentException + * @expectedException InvalidArgumentException */ public function testInstantiateWithContextNotArrayThrowsException() { @@ -115,8 +126,8 @@ public function testInstantiateStreamWithContextOptionsAndStringURI() } /** - * @expectedException CSVelte\Exception\IOException - * @expectedExceptionCode CSVelte\Exception\IOException::ERR_STREAM_CONNECTION_FAILED + * @expectedException \CSVelte\Exception\IOException + * @expectedExceptionCode \CSVelte\Exception\IOException::ERR_STREAM_CONNECTION_FAILED */ public function testInstantiateThrowsExceptionIfInvalidStreamURI() { @@ -124,7 +135,7 @@ public function testInstantiateThrowsExceptionIfInvalidStreamURI() } /** - * @expectedException \InvalidArgumentException + * @expectedException InvalidArgumentException */ public function testInstantiateThrowsExceptionIfInvalidStreamResource() { @@ -196,8 +207,8 @@ public function testReadGetsCorrectNumChars() } /** - * @expectedException CSVelte\Exception\IOException - * @expectedExceptionCode CSVelte\Exception\IOException::ERR_NOT_READABLE + * @expectedException \CSVelte\Exception\IOException + * @expectedExceptionCode \CSVelte\Exception\IOException::ERR_NOT_READABLE */ public function testReadThrowsExceptionIfNotReadable() { @@ -352,64 +363,64 @@ public function testSeekableStreamsReturnTrueOnIsWritable() $this->assertFalse($nonWritableStream->isWritable()); } -// /** -// * @expectedException NotYetImplementedException -// */ -// public function testSeekableStreamsThrowNotYetImplementedOnSeekLineMethod() -// { -// $stream = Stream::open($this->getFilePathFor('veryShort')); -// $this->assertTrue($stream->isSeekable()); -// $stream->seekLine(1); -// } + /** + * @expectedException \CSVelte\Exception\NotYetImplementedException + */ + public function testSeekableStreamsThrowNotYetImplementedOnSeekLineMethod() + { + $stream = Stream::open($this->getFilePathFor('veryShort')); + $this->assertTrue($stream->isSeekable()); + $stream->seekLine(1); + } - public function testStreamCanConvertStringIntoStreamWithStreamize() + public function testStreamCanConvertStringIntoStreamWithToStream() { $csv_string = $this->getFileContentFor('veryShort'); - $csv_stream = streamize($csv_string); + $csv_stream = to_stream($csv_string); $this->assertEquals($csv_string, $csv_stream->read(37)); } - public function testStreamCanConvertEmptyStringIntoStreamWithStreamizeWithNoParams() + public function testStreamCanConvertEmptyStringIntoStreamWithToStreamWithNoParams() { - $csv_stream = streamize(); + $csv_stream = to_stream(); $this->assertEquals('', $csv_stream->read(10)); } - public function testStreamCanConvertObjectWithToStringMethodIntoStreamWithStreamize() + public function testStreamCanConvertObjectWithToStringMethodIntoStreamWithToStream() { // Create a stub for non-existant StreamableClass. $csv_obj = $this->getMockBuilder('StreamableClass') - ->setMethods(['__toString']) - ->getMock(); + ->setMethods(['__toString']) + ->getMock(); // Configure the stub. $csv_obj->method('__toString') - ->willReturn($csv_string = $this->getFileContentFor('veryShort')); + ->willReturn($csv_string = $this->getFileContentFor('veryShort')); // test it - $csv_stream = streamize($csv_obj); + $csv_stream = to_stream($csv_obj); $this->assertEquals($csv_string, $csv_stream->read(37)); } - public function testStreamizeCanStreamSplFileObject() + public function testToStreamCanStreamSplFileObject() { $fileObj = new SplFileObject($fn = $this->getFilePathFor('headerCommaQuoteNonnumeric')); - $this->assertInstanceOf(Streamable::class, $stream = streamize($fileObj)); + $this->assertInstanceOf(Streamable::class, $stream = to_stream($fileObj)); $this->assertEquals(file_get_contents($fn), $stream->__toString()); } - // public function testStreamizeCanStreamSplFileObjectAndSetCorrectPosition() - // { - // $fileObj = new SplFileObject($fn = $this->getFilePathFor('headerCommaQuoteNonnumeric')); - // $fileObj->read($pos = 25); - // $stream = streamize($fileObj); - // $this->assertEquals($pos, $stream->tell()); - // //$this->assertEquals($pos, $fileObj->ftell()); - // } +// public function testToStreamCanStreamSplFileObjectAndSetCorrectPosition() +// { +// $fileObj = new SplFileObject($fn = $this->getFilePathFor('headerCommaQuoteNonnumeric')); +// $fileObj->fread($pos = 25); +// $stream = to_stream($fileObj); +// $this->assertEquals($pos, $stream->tell()); +// //$this->assertEquals($pos, $fileObj->ftell()); +// } /** - * @expectedException CSVelte\Exception\IOException - * @expectedExceptionCode CSVelte\Exception\IOException::ERR_NOT_WRITABLE + * @expectedException \CSVelte\Exception\IOException + * @expectedExceptionCode \CSVelte\Exception\IOException::ERR_NOT_WRITABLE */ public function testWriteToNonWritableStreamThrowsIOException() { @@ -418,7 +429,7 @@ public function testWriteToNonWritableStreamThrowsIOException() } /** - * @expectedException \InvalidArgumentException + * @expectedException InvalidArgumentException */ public function testStreamThrowsExceptionIfContextIsNotAnArray() { @@ -522,7 +533,7 @@ public function testStreamDetachRemovesStreamFromUnderlyingStreamResourceLeaving } /** - * @expectedException CSVelte\Exception\IOException + * @expectedException \CSVelte\Exception\IOException */ public function testDetachedStreamThrowsExceptionOnRead() { @@ -532,7 +543,7 @@ public function testDetachedStreamThrowsExceptionOnRead() } /** - * @expectedException CSVelte\Exception\IOException + * @expectedException \CSVelte\Exception\IOException */ public function testDetachedStreamThrowsExceptionOnWrite() { @@ -542,7 +553,7 @@ public function testDetachedStreamThrowsExceptionOnWrite() } /** - * @expectedException CSVelte\Exception\IOException + * @expectedException \CSVelte\Exception\IOException */ public function testDetachedStreamThrowsExceptionOnSeek() { @@ -552,26 +563,26 @@ public function testDetachedStreamThrowsExceptionOnSeek() } /** - * @expectedException \InvalidArgumentException + * @expectedException InvalidArgumentException */ - public function testStreamizeWithIntegerThrowsInvalidArgumentException() + public function testToStreamWithIntegerThrowsInvalidArgumentException() { $intval = 1; - streamize($intval); + to_stream($intval); } /** - * @expectedException \InvalidArgumentException + * @expectedException InvalidArgumentException */ - public function testStreamizeWithNonStringObjectThrowsInvalidArgumentException() + public function testToStreamWithNonStringObjectThrowsInvalidArgumentException() { $obj = new \stdClass; - streamize($obj); + to_stream($obj); } - public function testStreamizeWithNoArguments() + public function testToStreamWithNoArguments() { - $stream = streamize(); + $stream = to_stream(); $this->assertTrue($stream->isReadable()); $this->assertTrue($stream->isWritable()); $this->assertTrue($stream->isSeekable()); @@ -581,10 +592,10 @@ public function testStreamizeWithNoArguments() $this->assertEquals("helloworld", (string) $stream); } - public function testStreamizeStream() + public function testToStreamStream() { - $stream = streamize("helloworld"); - $streamcopy = streamize($stream); + $stream = to_stream("helloworld"); + $streamcopy = to_stream($stream); $this->assertEquals((string) $stream, (string) $streamcopy); } @@ -738,6 +749,15 @@ public function testBufferStreamThrowsExceptionIfPassedBadSecondArgument() ); } + public function testBufferStreamToStringReturnsStreamContents() + { + $buffer = new BufferStream(); + $buffer->write('This is the string that is being buffered.'); + $buffer->read(10); + $this->assertSame("e string that is being buffered.", (string) $buffer); + $this->assertSame("", (string) $buffer, "Calling __toString() again should return an empty string since it is the equivalent of calling read on the entire buffer."); + } + public function testIteratorStreamUsingArrayIterator() { $array = explode("\n", $this->getFileContentFor('noHeaderCommaNoQuotes')); @@ -877,4 +897,4 @@ public function testIteratorStreamWriteAlwaysReturnsFalse() } -} +} \ No newline at end of file diff --git a/tests/CSVelte/ReaderTest.php b/tests/CSVelte/ReaderTest.php index 054c067..9c1987f 100644 --- a/tests/CSVelte/ReaderTest.php +++ b/tests/CSVelte/ReaderTest.php @@ -1,192 +1,264 @@ + * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV + * standardization efforts, CSVelte was written in an effort to take all the + * suck out of working with CSV. + * + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @todo Move all of the tests from OldReaderTest.php into this class - * @coversDefaultClass CSVelte\Reader + * @license See LICENSE file (MIT license) */ +namespace CSVelteTest; + +use CSVelte\Dialect; +use CSVelte\Reader; + +use function CSVelte\to_stream; +use function Noz\collect; + class ReaderTest extends UnitTestCase { - public function testReaderCanAcceptArrayForFlavor() + public function testInstantiateReaderWithoutDialectUsesDefault() { - $flavorArr = (new Flavor())->toArray(); - $flavorArr['delimiter'] = "\t"; - $flavorArr['lineTerminator'] = "\n"; - $flavorArr['quoteChar'] = "'"; - $flavorArr['quoteStyle'] = Flavor::QUOTE_ALL; - $flavorArr['header'] = false; - $reader = new Reader($this->getFilePathFor('veryShort'), $flavorArr); - $this->assertEquals($flavorArr, $reader->getFlavor()->toArray()); + $source = fopen($this->getFilePathFor('veryShort'), 'r+'); + $reader = new Reader(to_stream($source)); + $this->assertInstanceOf(Dialect::class, $reader->getDialect()); } - /** - * @covers ::__construct() - */ - public function testReaderCanUseIOStreamForFileReadable() + public function testInstantiateReaderWithCustomDialectUsesCustomDialect() { - $readable = Stream::open($this->getFilePathFor('shortQuotedNewlines')); - $reader = new Reader($readable); - $this->assertEquals(['foo','bar','baz'], $reader->current()->toArray()); - $this->assertEquals(['bin',"boz,bork\nlib,bil,ilb",'bon'], $reader->next()->toArray()); + $source = fopen($this->getFilePathFor('veryShort'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source), $dialect); + $this->assertInstanceOf(Dialect::class, $reader->getDialect()); + $this->assertSame($dialect, $reader->getDialect()); + $this->assertFalse($reader->getDialect()->hasHeader()); } - /** - * @covers ::__construct() - */ - public function testReaderCanUseStraightPHPString() + public function testSetDialectDoesTheSameAsSettingItInConstructor() { - $readable = $this->getFileContentFor('shortQuotedNewlines'); - $reader = new Reader($readable); - $this->assertEquals(['foo','bar','baz'], $reader->current()->toArray()); - $this->assertEquals(['bin',"boz,bork\nlib,bil,ilb",'bon'], $reader->next()->toArray()); + $source = fopen($this->getFilePathFor('veryShort'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source)); + $this->assertInstanceOf(Dialect::class, $reader->getDialect()); + $this->assertNotSame($dialect, $reader->getDialect()); + $this->assertTrue($reader->getDialect()->hasHeader()); + $reader->setDialect($dialect); + $this->assertSame($dialect, $reader->getDialect()); + $this->assertFalse($reader->getDialect()->hasHeader()); } - public function testReaderTreatsQuotedNewlinesAsOneLine() + public function testSetDialectRewindsAndResetsReader() { - $flavor = new Flavor(array('quoteStyle' => Flavor::QUOTE_MINIMAL, 'lineTerminator' => "\n"), array('hasHeader' => false)); - $reader = new Reader($this->getFileContentFor('commaNewlineHeader'), $flavor); - $line = $reader->current(); - $this->assertEquals($expected = "First CornerStone Bank,King of\nPrussia,PA,35312,First-Citizens Bank & Trust Company,6-May-16,25-May-16", $line->join(",")); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source), $dialect); + $this->assertSame([ + 0 => 'Bank Name', + 1 => 'City', + 2 => 'ST', + 3 => 'CERT', + 4 => 'Acquiring Institution', + 5 => 'Closing Date', + 6 => 'Updated Date' + ], $reader->current()); + + $newdialect = new Dialect(['header' => true]); + $reader->setDialect($newdialect); + $this->assertSame([ + 'Bank Name' => 'First CornerStone Bank', + 'City' => "King of\nPrussia", + 'ST' => 'PA', + 'CERT' => '35312', + 'Acquiring Institution' => 'First-Citizens Bank & Trust Company', + 'Closing Date' => '6-May-16', + 'Updated Date' => '25-May-16' + ], $reader->current()); } - public function testReaderWillAutomaticallyDetectFlavorIfNoneProvided() + public function testFetchRowReturnsCurrentRowAndAdvancesPointerToNextLine() { - $reader = new Reader($this->getFileContentFor('headerTabSingleQuotes')); - $expected = new Flavor(array( - 'delimiter' => "\t", - 'quoteChar' => "'", - 'quoteStyle' => Flavor::QUOTE_MINIMAL, - 'escapeChar' => '\\', - 'lineTerminator' => "\n", - 'header' => true - )); - $this->assertInstanceOf(Flavor::class, $flavor = $reader->getFlavor()); - $this->assertEquals($expected, $flavor); + $source = to_stream(fopen($this->getFilePathFor('commaNewlineHeader'), 'r+')); + $reader = new Reader($source); + $this->assertEquals(1, $reader->key()); + $this->assertSame([ + 'Bank Name' => 'First CornerStone Bank', + 'City' => "King of\nPrussia", + 'ST' => 'PA', + 'CERT' => '35312', + 'Acquiring Institution' => 'First-Citizens Bank & Trust Company', + 'Closing Date' => '6-May-16', + 'Updated Date' => '25-May-16' + ], $reader->fetchRow()); + $this->assertEquals(2, $reader->key()); + $this->assertSame([ + 'Bank Name' => 'Trust Company Bank', + 'City' => 'Memphis', + 'ST' => 'TN', + 'CERT' => '9956', + 'Acquiring Institution' => 'The Bank of Fayette County', + 'Closing Date' => '29-Apr-16', + 'Updated Date' => '25-May-16' + ], $reader->fetchRow()); + $this->assertEquals(3, $reader->key()); + $this->assertSame([ + 'Bank Name' => 'North Milwaukee State Bank', + 'City' => 'Milwaukee', + 'ST' => 'WI', + 'CERT' => '20364', + 'Acquiring Institution' => 'First-Citizens Bank & Trust Company', + 'Closing Date' => '11-Mar-16', + 'Updated Date' => '16-Jun-16' + ], $reader->fetchRow()); + $this->assertEquals(4, $reader->key()); } - // it is useful for a CSV reader class to have a method for determining - // whether or not its source input contains a header column, so this provides - // one for convenience, although it is just a proxy to Taster with a sort of - // cache so that the expensive Taster::lickHeader method is only ran when it - // has to be (when input source changes or something) - public function testReaderHasHeader() + public function testFetchRowReturnsFalseIfAtEndOfInput() { - $no_header_reader = new Reader($this->getFileContentFor('noHeaderCommaNoQuotes')); - $this->assertFalse($no_header_reader->hasHeader()); - $header_reader = new Reader($this->getFileContentFor('headerDoubleQuote')); - $this->assertTrue($header_reader->hasHeader()); + $source = to_stream(fopen($this->getFilePathFor('commaNewlineHeader'), 'r+')); + $reader = new Reader($source); + $source->seek($source->getSize()); + $this->assertFalse($reader->fetchRow()); } - public function testReaderStillRunsLickHeaderIfFlavorWasPassedInWithNullHasHeaderProperty() + /** BEGIN: SPL implementation method tests */ + + public function testCurrentReturnsCurrentLineFromInput() { - $flavor = new Flavor(['header' => null, 'lineTerminator' => "\n"]); - $in = Stream::open($this->getFilePathFor('headerDoubleQuote')); - $reader = new Reader($in, $flavor); - $this->assertTrue($reader->hasHeader()); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source), $dialect); + $this->assertSame([ + 0 => 'Bank Name', + 1 => 'City', + 2 => 'ST', + 3 => 'CERT', + 4 => 'Acquiring Institution', + 5 => 'Closing Date', + 6 => 'Updated Date' + ], $reader->current()); } - public function testReaderCurrent() + public function testNextMovesInputToNextLineAndLoadsItIntoMemory() { - $flavor = new Flavor(array('header' => false, 'lineTerminator' => "\n")); - $reader = new Reader($this->getFileContentFor('noHeaderCommaNoQuotes'), $flavor); - $this->assertInstanceOf($expected = Row::class, $reader->current()); - $this->assertEquals($expected = array("1","Eldon Base for stackable storage shelf platinum","Muhammed MacIntyre","3","-213.25","38.94","35","Nunavut","Storage & Organization","0.8"), $reader->current()->toArray()); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source), $dialect); + $this->assertSame([ + 0 => 'Bank Name', + 1 => 'City', + 2 => 'ST', + 3 => 'CERT', + 4 => 'Acquiring Institution', + 5 => 'Closing Date', + 6 => 'Updated Date' + ], $reader->current()); + $this->assertSame($reader, $reader->next()); + $this->assertSame([ + 'First CornerStone Bank', + "King of\nPrussia", + 'PA', + '35312', + 'First-Citizens Bank & Trust Company', + '6-May-16', + '25-May-16' + ], $reader->current()); } - public function testReaderToArray() + public function testKeyReturnsLineNumber() { - $reader = new Reader($this->getFileContentFor('veryShort')); - $this->assertInternalType("array", $arr = $reader->toArray()); - // "foo,bar,baz\nbin,boz,bork\nlib,bil,ilb\n" - $this->assertEquals([ - 1 => ["foo","bar","baz"], - 2 => ["bin","boz","bork"], - 3 => ["lib","bil","ilb"] - ], $arr); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $dialect = new Dialect([ + 'header' => false, + ]); + $reader = new Reader(to_stream($source), $dialect); + $this->assertSame([ + 0 => 'Bank Name', + 1 => 'City', + 2 => 'ST', + 3 => 'CERT', + 4 => 'Acquiring Institution', + 5 => 'Closing Date', + 6 => 'Updated Date' + ], $reader->current()); + $this->assertSame(1, $reader->key()); } - // if you need to get a stream for a uri/filename you need to use Stream::open() - // or instantiate a resource manually - // public function testReaderConstructorWillTreatAllTextAsCSVData() - // { - // $reader = new Reader($this->getFilePathFor('veryShort')); - // $this->assertEquals(["vfs:","","root",'testfiles','veryShort.csv'], $reader->current()->toArray()); - // $reader = new Reader("i,am,a\nvry,short,csv\nfile,yes,sir\n"); - // $this->assertEquals(["i","am","a"], $reader->current()->toArray()); - // } - - public function testReaderFilteredIterator() + public function testKeyReturnsLineNumberNotIncludingHeaderLine() { - $reader = new Reader($this->getFileContentFor('commaNewlineHeader')); - $reader->addFilter(function($row){ - return $row['CERT'] > 55000; - })->addFilter(function($row){ - return stripos($row['Bank Name'], 'bank') !== false; - }); - foreach ($reader->filter() as $line_no => $row) { - $this->assertGreaterThan(55000, $row['CERT'], "Ensure \"CERT\" field from row #{$line_no} is greater than 55000."); - $this->assertContains('bank', $row['Bank Name'], "Ensure \"Bank Name\" field from row #{$line_no} contains the word \"bank\".", true); - } - $this->assertCount(4, iterator_to_array($reader->filter())); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $reader = new Reader(to_stream($source)); + $this->assertSame(1, $reader->key()); + $this->assertSame([ + 'Bank Name' => 'First CornerStone Bank', + 'City' => "King of\nPrussia", + 'ST' => 'PA', + 'CERT' => '35312', + 'Acquiring Institution' => 'First-Citizens Bank & Trust Company', + 'Closing Date' => '6-May-16', + 'Updated Date' => '25-May-16' + ], $reader->current()); } - public function testReaderFilteredIteratorWithMultipleFiltersAddedAtOnce() + public function testValidReturnsFalseIfInputIsAtEOF() { - $reader = new Reader($this->getFileContentFor('commaNewlineHeader')); - $reader->addFilters([function($row){ - return $row['CERT'] > 55000; - }, function($row){ - return stripos($row['Bank Name'], 'bank') !== false; - }]); - foreach ($reader->filter() as $line_no => $row) { - $this->assertGreaterThan(55000, $row['CERT'], "Ensure \"CERT\" field from row #{$line_no} is greater than 55000."); - $this->assertContains('bank', $row['Bank Name'], "Ensure \"Bank Name\" field from row #{$line_no} contains the word \"bank\".", true); - } - $this->assertCount(4, iterator_to_array($reader->filter())); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $stream = to_stream($source); + $reader = new Reader($stream); + $this->assertFalse($stream->eof()); + $this->assertTrue($reader->valid()); + $stream->seek($stream->getSize()+1); + $this->assertTrue($stream->eof()); + $this->assertFalse($reader->valid()); } - public function testFilteredIteratorHasToArrayMethod() + public function testRewindResetsReaderToBeginning() { - $reader = CSVelte::reader($this->getFilePathFor('commaNewlineHeader')); - $reader->addFilter(function($row){ - return ($row['CERT'] < 40000); - }); - $this->assertInternalType("array", $reader->filter()->toArray()); - $reader->addFilter(function($row){ - return $row['Bank Name'] == "Northern Star Bank"; - }); - $this->assertInternalType("array", $arr = $reader->filter()->toArray()); - $this->assertInternalType("array", current($arr)); - $this->assertCount(1, $oneArr = $reader->filter()->toArray()); - $this->assertEquals([13 => [ - "Bank Name" => "Northern Star Bank", - "City" => "Mankato", - "ST" => "MN", - "CERT" => "34983", - "Acquiring Institution" => "BankVista", - "Closing Date" => "19-Dec-14", - "Updated Date" => "6-Jan-16" - ]], $oneArr); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $stream = to_stream($source); + $reader = new Reader($stream); + $this->assertSame([ + 'Bank Name' => 'Trust Company Bank', + 'City' => 'Memphis', + 'ST' => 'TN', + 'CERT' => '9956', + 'Acquiring Institution' => 'The Bank of Fayette County', + 'Closing Date' => '29-Apr-16', + 'Updated Date' => '25-May-16' + ], $reader->next()->current()); + $this->assertSame($reader, $reader->rewind()); + $this->assertSame([ + 'Bank Name' => 'First CornerStone Bank', + 'City' => "King of\nPrussia", + 'ST' => 'PA', + 'CERT' => '35312', + 'Acquiring Institution' => 'First-Citizens Bank & Trust Company', + 'Closing Date' => '6-May-16', + 'Updated Date' => '25-May-16' + ], $reader->current()); } - public function testReaderKeyReturnsLine() + public function testCountReturnsNumberOfLines() { - $reader = new Reader($this->getFileContentFor('commaNewlineHeader')); - // @todo This should be 1 since the first row was the header, but ill get to that later - $this->assertEquals(2, $reader->key()); + $dialect = new Dialect(['header' => false]); + $source = fopen($this->getFilePathFor('commaNewlineHeader'), 'r+'); + $reader = new Reader(to_stream($source), $dialect); + $this->assertEquals(29, $reader->count()); + $this->assertEquals(29, count($reader)); + $reader->setDialect(new Dialect(['header' => true])); + $this->assertEquals(28, $reader->count()); + $this->assertEquals(28, count($reader)); } - -} +} \ No newline at end of file diff --git a/tests/CSVelte/SnifferTest.php b/tests/CSVelte/SnifferTest.php new file mode 100644 index 0000000..18dc122 --- /dev/null +++ b/tests/CSVelte/SnifferTest.php @@ -0,0 +1,137 @@ + + * @license See LICENSE file (MIT license) + */ +namespace CSVelteTest; + +use CSVelte\Dialect; +use CSVelte\Sniffer; + +use CSVelte\Sniffer\SniffDelimiterByConsistency; +use CSVelte\Sniffer\SniffDelimiterByDistribution; +use CSVelte\Sniffer\SniffHeaderByDataType; +use CSVelte\Sniffer\SniffLineTerminatorByCount; +use CSVelte\Sniffer\SniffQuoteAndDelimByAdjacency; +use CSVelte\Sniffer\SniffQuoteStyle; +use function CSVelte\to_stream; +use function Noz\collect; + +class SnifferTest extends UnitTestCase +{ + public function testInstantiateWithCustomDelimiterSet() + { + $sniffer = new Sniffer(to_stream()); + $this->assertSame([',', "\t", ';', '|', ':', '-', '_', '#', '/', '\\', '$', '+', '=', '&', '@'], $sniffer->getPossibleDelimiters()); + $sniffer->setPossibleDelimiters($delims = [',', "\t", '|']); + $this->assertSame($delims, $sniffer->getPossibleDelimiters()); + $sniffer = new Sniffer(to_stream(), $delims); + $this->assertSame($delims, $sniffer->getPossibleDelimiters()); + } + + public function testSniffLineTerminatorByCount() + { + $nl = to_stream($this->getFileContentFor('commaNewlineHeader')); + $nlcr = to_stream(str_replace("\n", "\r\n", $this->getFileContentFor('commaNewlineHeader'))); + $cr = to_stream(str_replace("\n", "\r", $this->getFileContentFor('commaNewlineHeader'))); + + $sniffer = new SniffLineTerminatorByCount; + $this->assertSame("\n", $sniffer->sniff($nl->read(1500))); + $this->assertSame("\r\n", $sniffer->sniff($nlcr->read(1500))); + $this->assertSame("\r", $sniffer->sniff($cr->read(1500))); + } + + public function testSniffQuoteAndDelimByAdjacency() + { + $data = $this->getFileContentFor('noHeaderCommaQuoteAll'); + $sniffer = new SniffQuoteAndDelimByAdjacency([ + 'lineTerminator' => "\n" + ]); + $this->assertSame(['"', ','], $sniffer->sniff($data)); + } + + public function testSniffDelimiterByConsistency() + { + $data = $this->getFileContentFor('headerTabSingleQuotes'); + $sniffer = new SniffDelimiterByConsistency([ + 'lineTerminator' => "\n", + 'delimiters' => [',', "\t", ';', '|', ':', '-', '_', '#', '/', '\\', '$', '+', '=', '&', '@'] + ]); + $this->assertSame(["\t"], $sniffer->sniff($data)); + } + + public function testSniffDelimiterByConsistencyReturnsTie() + { + $data = $this->getFileContentFor('commaDelimTie'); + $sniffer = new SniffDelimiterByConsistency([ + 'lineTerminator' => "\n", + 'delimiters' => [',', "\t", ';', '|', ':', '-', '_', '#', '/', '\\', '$', '+', '=', '&', '@'] + ]); + $this->assertSame([',',':','/','@'], $sniffer->sniff($data)); + } + + public function testSniffDelimiterByDistribution() + { + $data = $this->getFileContentFor('commaDelimTie'); + $sniffer = new SniffDelimiterByDistribution([ + 'lineTerminator' => "\n", + 'delimiters' => [',', ':', '/', '@'] + ]); + $this->assertSame(',', $sniffer->sniff($data)); + } + + public function testSniffQuoteStyle() + { + $all = $this->getFileContentFor('noHeaderCommaQuoteAll'); + $none = $this->getFileContentFor('noHeaderCommaNoQuotes'); + $min = $this->getFileContentFor('noHeaderCommaQuoteMinimal'); + $nan = $this->getFileContentFor('headerCommaQuoteNonnumeric'); + $sniffer = new SniffQuoteStyle([ + 'delimiter' => ',', + 'lineTerminator' => "\n" + ]); + $this->assertSame(Dialect::QUOTE_ALL, $sniffer->sniff($all)); + $this->assertSame(Dialect::QUOTE_NONE, $sniffer->sniff($none)); + $this->assertSame(Dialect::QUOTE_MINIMAL, $sniffer->sniff($min)); + $this->assertSame(Dialect::QUOTE_NONNUMERIC, $sniffer->sniff($nan)); + } + + public function testSniffHeaderByDataType() + { + $no1 = $this->getFileContentFor('noHeaderCommaQuoteAll'); + $no2 = $this->getFileContentFor('noHeaderCommaNoQuotes'); + $no3 = $this->getFileContentFor('noHeaderCommaQuoteMinimal'); + $no4 = $this->getFileContentFor('commaDelimTie'); + $no5 = $this->getFileContentFor('veryShort'); + $yes1 = $this->getFileContentFor('commaNewlineHeader'); + $yes2 = $this->getFileContentFor('headerDoubleQuote'); + $yes3 = $this->getFileContentFor('headerCommaQuoteNonnumeric'); + $sniffer = new SniffHeaderByDataType([ + 'delimiter' => ',' + ]); + $this->assertFalse($sniffer->sniff($no1)); + $this->assertFalse($sniffer->sniff($no2)); + $this->assertFalse($sniffer->sniff($no3)); + $this->assertFalse($sniffer->sniff($no4)); + $this->assertTrue($sniffer->sniff($yes1)); + $this->assertTrue($sniffer->sniff($yes2)); + $this->assertTrue($sniffer->sniff($yes3)); + } + + // @todo finish this later by calling sniff(), then removing the top line, calling sniff again, remove top line again, until you get false. + // @note it's actually not quite that simple. If I want to support multiple headers, there needs to be a way to specify which header represents the + // column names, allow for some headers to have different amount(s) of columns, and all kinds of other stuff that I'm + // just not ready to support yet. Come back to this after everything else is supported. I created an issue on github + // for this here: +// public function testSniffHeaderCanSniffMultipleHeaders() +// { +// +// } +} \ No newline at end of file diff --git a/tests/CSVelte/StreamWrapper/HttpStreamWrapper.php b/tests/CSVelte/StreamWrapper/HttpStreamWrapper.php index 2996927..504e464 100644 --- a/tests/CSVelte/StreamWrapper/HttpStreamWrapper.php +++ b/tests/CSVelte/StreamWrapper/HttpStreamWrapper.php @@ -99,4 +99,4 @@ public function stream_stat() { public function stream_tell() { return $this->position; } -} +} \ No newline at end of file diff --git a/tests/CSVelte/Table/RowTest.php b/tests/CSVelte/Table/RowTest.php deleted file mode 100644 index 3c30d15..0000000 --- a/tests/CSVelte/Table/RowTest.php +++ /dev/null @@ -1,316 +0,0 @@ - - * @author Luke Visinoni - * @todo Move all of the tests from OldReaderTest.php into this class - */ -class RowTest extends UnitTestCase -{ - public function testInitializeRowWithStrings() - { - $row = new Row($expected = array(1, 'foo', 'bar', 'baz', 'biz', 25)); - $this->assertEquals($expected, $row->toArray()); - } - - public function testNewReaderRowAcceptsArray() - { - $row = new Row($expected = array(1, 'foo', 'bar', 'baz', 'biz', 25)); - $this->assertEquals($expected, $row->toArray()); - } - - public function testRowIsCountable() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertEquals(count($expected), $row->count()); - $this->assertEquals(count($expected), count($row)); - } - - public function testRowGetCurrentColumn() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertEquals($expected[0], $row->current()); - } - - public function testRowGetKey() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertSame(0, $row->key()); - } - - public function testRowNextReturnsNextAndMovesToNextColumn() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertSame($expected[1], $row->next()); - } - - public function testRowRewindResetsPointerToBeginningAndReturnsValue() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertSame($expected[0], $row->rewind()); - } - - public function testRowValidChecksWhetherCurrentIsValid() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertSame(true, $row->valid()); - } - - public function testIteratorImplementationIsWorking() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - foreach($row as $col) { - $this->assertEquals(current($expected), $col); - next($expected); - } - } - - public function testIteratorWhileLoop() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - while ($row->valid()) { - $this->assertEquals(current($expected), $row->current()); - next($expected); - $row->next(); - } - } - - public function testJoinRow() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertEquals(implode(",", $expected), $row->join(",")); - } - - public function testOffsetExists() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertTrue($row->offsetExists($expected = 0)); - $this->assertTrue($row->offsetExists($expected = 1)); - $this->assertTrue($row->offsetExists($expected = 2)); - $this->assertFalse($row->offsetExists($expected = 3)); - } - - public function testOffsetGet() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertEquals('foo', $row->offsetGet(0)); - $this->assertEquals('bar', $row->offsetGet(1)); - $this->assertEquals('baz', $row->offsetGet(2)); - } - - /** - * @expectedException OutOfBoundsException - */ - public function testOffsetGetThrowsExceptionOnUnknownOffset() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $row->offsetGet(3); - } - - /** - * @expectedException CSVelte\Exception\ImmutableException - */ - public function testOffsetSetThrowsImmutableException() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $row->offsetSet(0, 'cannotchangeme!'); - // $this->assertFalse($row->offsetExists(3)); - // // create new offset - // $row->offsetSet(3, 'beez'); - // $this->assertTrue($row->offsetExists(3)); - // $this->assertEquals($expected = 'beez', $row->offsetGet(3)); - // // overwrite existing offset - // $row->offsetSet(1, 'eats you'); - // $this->assertEquals($row->offsetGet(1), 'eats you'); - } - - /** - * @expectedException CSVelte\Exception\ImmutableException - */ - public function testOffsetUnsetThrowsImmutableException() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $row->offsetUnset(0); - // $this->assertTrue($row->offsetExists(1)); - // $row->offsetUnset(1); - // $this->assertFalse($row->offsetExists(1)); - // $this->assertTrue($row->offsetExists(0)); - // $row->offsetUnset(0); - // $this->assertFalse($row->offsetExists(0)); - } - - public function testReaderRowIsAccessableAsArray() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $this->assertEquals($expected[0], $row[0]); - $this->assertEquals($expected[1], $row[1]); - $this->assertEquals($expected[2], $row[2]); - } - - public function testRowDoesntAllowAssociativeIndexesAndReIndexesNumericallyIfYouAttemptToUseThem() - { - $row = new Row($expected = array('foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo')); - $this->assertFalse($row->offsetExists('foo')); - $this->assertFalse($row->offsetExists('bar')); - $this->assertFalse($row->offsetExists('baz')); - $this->assertEquals($expected['foo'], $row[0]); - $this->assertEquals($expected['bar'], $row[1]); - $this->assertEquals($expected['baz'], $row[2]); - } - - /** - * @expectedException CSVelte\Exception\ImmutableException - */ - public function testRowValuesAreImmutable() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $row[0] = 'boo'; - } - - /** - * @expectedException CSVelte\Exception\HeaderException - * @expectedExceptionCode CSVelte\Exception\HeaderException::ERR_HEADER_COUNT - */ - public function testIncorrectHeaderCountThrowsException() - { - $row = new Row($expected = array('foo', 'bar', 'baz')); - $row->setHeaderRow(new HeaderRow(array('poop'))); - } - - // public function testRowsCanBeIndexedByBothOffsetAndColumnHeaderName() - // { - // $header = new HeaderRow($headers = array('first name', 'last name', 'address1', '2nd address line', 'city', 'state', 'zipcode', 'phone', 'email', 'state', 'startdate', 'enddate')); - // $row = new Row($values = array('Luke', 'Visinoni', '1424 Some St.', 'Apt. #26', 'Chico', 'CA', '95926', '(530) 413-1234', 'luke.visinoni@gmail.com', '423', '12-28-2015', '04-21-2016')); - // $row->setHeaderRow($header); - // $this->assertEquals('Luke', $row->first_name); - // $this->assertEquals('Visinoni', $row->last_name); - // // if column starts with a number it will be converted to its "word" version - // $this->assertEquals('Apt. #26', $row->twondaddress_line); - // // if the header row contains duplicates, each one is appended with its column's index number (from 1) - // $this->assertEquals('CA', $row->state6); - // $this->assertEquals('423', $row->state10); - // // you can get all of the values (indexed by header names) by calling toAssoc() - // $this->assertEquals(array(), $row->toAssoc()); - // // you can get all of the values (numerically indexed) by calling toArray() - // $this->assertEquals(array(), $row->toArray()); - // } - - // @todo handle duplicate header names - public function testRowsCanBeIndexedByBothOffsetAndColumnHeaderName() - { - $header = new HeaderRow($headers = array('first name', 'last name', 'address1', '2nd address line', 'city', 'state', 'zipcode', 'phone', 'email', 'id', 'start-date', 'end [date]')); - $row = new Row($values = array('Luke', 'Visinoni', '1424 Some St.', 'Apt. #26', 'Chico', 'CA', '95926', '(530) 413-1234', 'luke.visinoni@gmail.com', '423', '12-28-2015', '04-21-2016')); - $row->setHeaderRow($header); - $this->assertEquals('Luke', $row['first name']); - $this->assertEquals('Visinoni', $row['last name']); - $this->assertEquals('1424 Some St.', $row['address1']); - $this->assertEquals('Apt. #26', $row['2nd address line']); - $this->assertEquals('Chico', $row['city']); - $this->assertEquals('CA', $row['state']); - $this->assertEquals('95926', $row['zipcode']); - $this->assertEquals('(530) 413-1234', $row['phone']); - $this->assertEquals('luke.visinoni@gmail.com', $row['email']); - $this->assertEquals('423', $row['id']); - $this->assertEquals('12-28-2015', $row['start-date']); - $this->assertEquals('04-21-2016', $row['end [date]']); - } - - public function testShortRowGetsNullValuesForExtraColumns() - { - $expected_header = array('first','second','third','fourth','fifth','sixth'); - $expected_values = array('one','two','three','four','five','six'); - $expected_shortvalues = array_slice(array_combine($expected_header, $expected_values), 0, 3); - $expected_shortvalues = array( - 'first' => 'one', - 'second' => 'two', - 'third' => 'three', - 'fourth' => null, - 'fifth' => null, - 'sixth' => null - ); - - $hrow = new HeaderRow($expected_header); - $shortrow = new Row($expected_shortvalues); - $shortrow->setHeaderRow($hrow); - $this->assertEquals($expected_shortvalues, $shortrow->toArray()); - } - - public function testRowCanBeCastToString() - { - $row = new Row($cols = array('one', 'two', 'three')); - $this->assertEquals($expected = "one,two,three", (string) $row); - } - - // public function testSetHeadersDirectlyInConstructorArray() - // { - // // test that you can set the headers within the array you pass to Row() itself - // } - - public function testCastToArrayUsesHeadersIfAvailable() - { - $row = new Row($vals = array('aone','atwo','athree','anda','four')); - $headers = new HeaderRow($keys = array('first','second','third','fourth','fifth')); - $row->setHeaderRow($headers); - $this->assertEquals($expected = array_combine($keys, $vals), $row->toArray()); - } - - public function testSetHeadersUsingNonHeaderRow() - { - $row = new Row($vals = array('aone','atwo','athree','anda','four')); - $headers = new Row($keys = array('first','second','third','fourth','fifth')); - $row->setHeaderRow($headers); - $this->assertEquals($expected = array_combine($keys, $vals), $row->toArray()); - } - - public function testSetHeadersUsingShortRow() - { - $row = new Row($vals = array('aone','atwo','athree')); - $headers = new HeaderRow($keys = array('first','second','third','fourth','fifth')); - $row->setHeaderRow($headers); - $this->assertEquals($expected = array_combine($keys, array('aone','atwo','athree', null, null)), $row->toArray()); - } - - public function testInstantiateRowWithAnyObjectThatHasToArray() - { - // Create a stub for non-existant StreamableClass. - $array_obj = $this->getMockBuilder('ArrClass') - ->setMethods(['toArray']) - ->getMock(); - - // Configure the stub. - $array_obj->method('toArray') - ->willReturn($arr = ['won', 'to', 'three', 'fore']); - - $row = new Row($array_obj); - $this->assertEquals($arr, $row->toArray()); - } - - public function testInstantiateRowWithAnythingIterable() - { - $arr = ['won', 'to', 'free', 'for']; - $iter = new ArrayIterator($arr); - $row = new Row($iter); - $this->assertEquals($arr, $row->toArray()); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testInstantiateRowWithAnythingElseWillCauseAnException() - { - $str = 'won,to,free,for'; - $row = new Row($str); - } -} diff --git a/tests/CSVelte/TasterTest.php b/tests/CSVelte/TasterTest.php deleted file mode 100644 index 529f56c..0000000 --- a/tests/CSVelte/TasterTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @author Luke Visinoni - * @todo Move all of the tests from OldReaderTest.php into this class - * @coversDefaultClass CSVelte\Taster - */ -class TasterTest extends UnitTestCase -{ - public function testLickQuotingStyleDoesntNeedSampleInParams() - { - $stream = streamize($this->getFileContentFor('headerDoubleQuote')); - $taster = new Taster($stream); - $flavor = $taster->lick(); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $flavor->quoteStyle); - - $stream = streamize($this->getFileContentFor('noHeaderCommaQuoteAll')); - $taster = new Taster($stream); - $flavor = $taster->lick(); - $this->assertEquals(Flavor::QUOTE_ALL, $flavor->quoteStyle); - - $stream = streamize($this->getFileContentFor('headerCommaQuoteNonnumeric')); - $taster = new Taster($stream); - $flavor = $taster->lick(); - $this->assertEquals(Flavor::QUOTE_NONNUMERIC, $flavor->quoteStyle); - - $stream = streamize($this->getFileContentFor('noHeaderCommaNoQuotes')); - $taster = new Taster($stream); - $flavor = $taster->lick(); - $this->assertEquals(Flavor::QUOTE_NONE, $flavor->quoteStyle, sprintf( - 'Assert that %s guesses quoting style to be %s for %s', - 'Taster::lick()', - 'Flavor::QUOTE_NONE', - $this->getFilePathFor('noHeaderCommaNoQuotes') - )); - } - - /** - * @covers ::lickHeader() - */ - public function testLickHeaderNowAcceptsReader() - { - $header_stream = streamize($this->getFileContentFor('headerDoubleQuote')); - $no_header_stream = $stream = streamize($this->getFileContentFor('noHeaderCommaNoQuotes')); - - $header_taster = new Taster($header_stream); - $header_flavor = $header_taster->lick(); - $this->assertTrue($header_flavor->header); - - $no_header_taster = new Taster($no_header_stream); - $no_header_flavor = $no_header_taster->lick(); - $this->assertFalse($no_header_flavor->header); - } - - /** - * @expectedException CSVelte\Exception\TasterException - * @expectedExceptionCode CSVelte\Exception\TasterException::ERR_INVALID_SAMPLE - */ - public function testTasterThrowsExceptionIfPassedInputWithNoData() - { - $input = streamize(''); - $taster = new Taster($input); - } - - public function testTasterInvokeMethodIsAliasToLickMethod() - { - $stream = Stream::open($this->getFilePathFor('headerDoubleQuote')); - $taster = new Taster($stream); - $flavor = $taster(); - $this->assertInstanceOf(Flavor::class, $flavor); - } - - public function testTasterInvokeWithSplFileObject() - { - $fileObj = new SplFileObject($this->getFilePathFor('headerTabSingleQuotes')); - $taster = new Taster(streamize($fileObj)); - $flavor = $taster(); - $this->assertEquals("\t", $flavor->delimiter); - $this->assertEquals("\n", $flavor->lineTerminator); - $this->assertEquals(Flavor::QUOTE_MINIMAL, $flavor->quoteStyle); - $this->assertTrue($flavor->doubleQuote); - $this->assertEquals("'", $flavor->quoteChar); - } -} diff --git a/tests/CSVelte/UnitTestCase.php b/tests/CSVelte/UnitTestCase.php index af49887..314c0b1 100644 --- a/tests/CSVelte/UnitTestCase.php +++ b/tests/CSVelte/UnitTestCase.php @@ -1,4 +1,15 @@ + * @license See LICENSE file (MIT license) + */ namespace CSVelteTest; use PHPUnit\Framework\TestCase; @@ -28,12 +39,15 @@ public function setUp() $strings = array( 'veryShort' => "foo,bar,baz\nbin,boz,bork\nlib,bil,ilb\n", 'shortQuotedNewlines' => "foo,bar,baz\nbin,\"boz,bork\nlib,bil,ilb\",bon\nbib,bob,\"boob\nboober\"\ncool,pool,wool\n", + 'shortQuotedNewlines2' => "bin,\"boz,\"\"bork\"\"\nlib,bil,ilb\",bon\nfoo,bar,baz\nbib,bob,\"boob\nboober\"\ncool,pool,wool\n", 'commaNewlineHeader' => "Bank Name,City,ST,CERT,Acquiring Institution,Closing Date,Updated Date\nFirst CornerStone Bank,\"King of\nPrussia\",PA,35312,First-Citizens Bank & Trust Company,6-May-16,25-May-16\nTrust Company Bank,Memphis,TN,9956,The Bank of Fayette County,29-Apr-16,25-May-16\nNorth Milwaukee State Bank,Milwaukee,WI,20364,First-Citizens Bank & Trust Company,11-Mar-16,16-Jun-16\nHometown National Bank,Longview,WA,35156,Twin City Bank,2-Oct-15,13-Apr-16\nThe Bank of Georgia,Peachtree City,GA,35259,Fidelity Bank,2-Oct-15,13-Apr-16\nPremier Bank,Denver,CO,34112,\"United Fidelity \r\n \r \r \n \r\n Bank, fsb\",10-Jul-15,17-Dec-15\nEdgebrook Bank,Chicago,IL,57772,Republic Bank of Chicago,8-May-15,2-Jun-16\nDoral Bank,San Juan,PR,32102,Banco Popular de Puerto Rico,27-Feb-15,13-May-15\nCapitol\t City Bank & Trust: Company,Atlanta,GA,33938,First-Citizens Bank & Trust: Company,13-Feb-15,21-Apr-15\nHighland: Community Bank,Chicago,IL,20290,\"United Fidelity Bank, fsb\",23-Jan-15,21-Apr-15\nFirst National Bank of Crestview ,Crestview,FL,17557,First NBC Bank,16-Jan-15,15-Jan-16\nNorthern Star Bank,Mankato,MN,34983,BankVista,19-Dec-14,6-Jan-16\n\"Frontier Bank, FSB D/B/A El Paseo Bank\",Palm Desert,CA,34738,\"Bank of Southern California, N.A.\",7-Nov-14,6-Jan-16\nThe National Republic Bank of Chicago,Chicago,IL,916,State Bank of Texas,24-Oct-14,6-Jan-16\nNBRS Financial,Rising Sun,MD,4862,Howard Bank,17-Oct-14,26-Mar-15\n\"GreenChoice Bank, fsb\",Chicago,IL,28462,\"Providence Bank, LLC\",25-Jul-14,28-Jul-15\nEastside Commercial Bank,Conyers,GA,58125,Community: Southern Bank,18-Jul-14,28-Jul-15\nThe Freedom State Bank ,Freedom,OK,12483,Alva State Bank & Trust Company,27-Jun-14,25-Mar-16\nValley Bank,Fort Lauderdale,FL,21793,\"Landmark Bank, National Association\",20-Jun-14,29-Jun-15\nValley Bank,Moline,IL,10450,Great Southern Bank,20-Jun-14,26-Jun-15\nSlavie Federal Savings Bank,Bel Air,MD,32368,\"Bay Bank, FSB\",30-May-14,15-Jun-15\nColumbia Savings Bank,Cincinnati,OH,32284,\"United Fidelity Bank, fsb\",23-May-14,28-May-15\nAztecAmerica Bank ,Berwyn,IL,57866,Republic Bank of Chicago,16-May-14,18-Jul-14\nAllendale County Bank,Fairfax,SC,15062,Palmetto State Bank,25-Apr-14,18-Jul-14\nVantage Point Bank,Horsham,PA,58531,First Choice Bank,28-Feb-14,3-Mar-15\n\"Millennium Bank, National\n Association\",Sterling,VA,35096,WashingtonFirst Bank,28-Feb-14,3-Mar-15\nSyringa Bank,Boise,ID,34296,Sunwest Bank,31-Jan-14,12-Apr-16\nThe Bank of Union,El Reno,OK,17967,BancFirst,24-Jan-14,25-Mar-16\nDuPage National Bank,West Chicago,IL,5732,Republic Bank of Chicago,17-Jan-14,19-F\n", 'headerDoubleQuote' => "Bank Name,City,ST,CERT,Acquiring Institution,Closing Date,Updated Date\nFirst CornerStone Bank,\"King of\n\"\"Prussia\"\"\",PA,35312,First-Citizens Bank & Trust Company,6-May-16,25-May-16\nTrust Company Bank,Memphis,TN,9956,The Bank of Fayette County,29-Apr-16,25-May-16\nNorth Milwaukee State Bank,Milwaukee,WI,20364,First-Citizens Bank & Trust Company,11-Mar-16,16-Jun-16\nHometown National Bank,Longview,WA,35156,Twin City Bank,2-Oct-15,13-Apr-16\nThe Bank of Georgia,Peachtree City,GA,35259,Fidelity Bank,2-Oct-15,13-Apr-16\nPremier Bank,Denver,CO,34112,\"United Fidelity \r\n \r \r \n \r\n Bank, fsb\",10-Jul-15,17-Dec-15\nEdgebrook Bank,Chicago,IL,57772,Republic Bank of Chicago,8-May-15,2-Jun-16\nDoral Bank,San Juan,PR,32102,Banco Popular de Puerto Rico,27-Feb-15,13-May-15\nCapitol\t City Bank & Trust: Company,Atlanta,GA,33938,First-Citizens Bank & Trust: Company,13-Feb-15,21-Apr-15\n\"Highland: \"\"Community\"\" Bank\",Chicago,IL,20290,\"United Fidelity Bank, fsb\",23-Jan-15,21-Apr-15\nFirst National Bank of Crestview ,Crestview,FL,17557,First NBC Bank,16-Jan-15,15-Jan-16\nNorthern Star Bank,Mankato,MN,34983,BankVista,19-Dec-14,6-Jan-16\n\"Frontier Bank, FSB D/B/A El Paseo Bank\",Palm Desert,CA,34738,\"Bank of Southern California, N.A.\",7-Nov-14,6-Jan-16\nThe National Republic Bank of Chicago,Chicago,IL,916,State Bank of Texas,24-Oct-14,6-Jan-16\nNBRS Financial,Rising Sun,MD,4862,Howard Bank,17-Oct-14,26-Mar-15\n\"GreenChoice Bank, fsb\",Chicago,IL,28462,\"Providence Bank, LLC\",25-Jul-14,28-Jul-15\nEastside Commercial Bank,Conyers,GA,58125,Community: Southern Bank,18-Jul-14,28-Jul-15\nThe Freedom State Bank ,Freedom,OK,12483,Alva State Bank & Trust Company,27-Jun-14,25-Mar-16\nValley Bank,Fort Lauderdale,FL,21793,\"Landmark Bank, National Association\",20-Jun-14,29-Jun-15\nValley Bank,Moline,IL,10450,Great Southern Bank,20-Jun-14,26-Jun-15\nSlavie Federal Savings Bank,Bel Air,MD,32368,\"Bay Bank, FSB\",30-May-14,15-Jun-15\nColumbia Savings Bank,Cincinnati,OH,32284,\"United Fidelity Bank, fsb\",23-May-14,28-May-15\nAztecAmerica Bank ,Berwyn,IL,57866,Republic Bank of Chicago,16-May-14,18-Jul-14\nAllendale County Bank,Fairfax,SC,15062,Palmetto State Bank,25-Apr-14,18-Jul-14\nVantage Point Bank,Horsham,PA,58531,First Choice Bank,28-Feb-14,3-Mar-15\n\"Millennium Bank, National\n Association\",Sterling,VA,35096,WashingtonFirst Bank,28-Feb-14,3-Mar-15\nSyringa Bank,Boise,ID,34296,Sunwest Bank,31-Jan-14,12-Apr-16\nThe Bank of Union,El Reno,OK,17967,BancFirst,24-Jan-14,25-Mar-16\nDuPage National Bank,West Chicago,IL,5732,\"Republic \"\"Bank\"\" of Chicago\",17-Jan-14,19-F\n", 'headerTabSingleQuotes' => "Bank Name\tCity\tST\tCERT\tAcquiring Institution\tClosing Date\tUpdated Date\nFirst CornerStone Bank\tKing of Prussia\tPA\t35312\tFirst-Citizens Bank & Trust Company\t6-May-16\t25-May-16\nTrust Company Bank\tMemphis\tTN\t9956\tThe Bank of Fayette County\t29-Apr-16\t25-May-16\nNorth Milwaukee State Bank\tMilwaukee\tWI\t20364\tFirst-Citizens Bank & Trust Company\t11-Mar-16\t16-Jun-16\nHometown National Bank\tLongview\tWA\t35156\tTwin City Bank\t2-Oct-15\t13-Apr-16\nThe Bank of Georgia\tPeachtree City\tGA\t35259\tFidelity Bank\t2-Oct-15\t13-Apr-16\nPremier Bank\tDenver\tCO\t34112\t'United Fidelity \r\n \r \r \n \r\n Bank\t fsb'\t10-Jul-15\t17-Dec-15\nEdgebrook Bank\tChicago\tIL\t57772\tRepublic Bank of Chicago\t8-May-15\t2-Jun-16\nDoral Bank\tSan Juan\tPR\t32102\tBanco Popular de Puerto Rico\t27-Feb-15\t13-May-15\nCapitol City Bank & Trust Company\tAtlanta\tGA\t33938\tFirst-Citizens Bank & Trust Company\t13-Feb-15\t21-Apr-15\nHighland Community Bank\tChicago\tIL\t20290\t'United Fidelity Bank, fsb'\t23-Jan-15\t21-Apr-15\nFirst National Bank of Crestview \tCrestview\tFL\t17557\tFirst NBC Bank\t16-Jan-15\t15-Jan-16\nNorthern Star Bank\tMankato\tMN\t34983\tBankVista\t19-Dec-14\t6-Jan-16\n'Frontier\'s Bank, FSB D/B/A El Paseo Bank'\tPalm Desert\tCA\t34738\t'Bank of Southern California, N.A.'\t7-Nov-14\t6-Jan-16\nThe National Republic Bank of Chicago\tChicago\tIL\t916\tState Bank of Texas\t24-Oct-14\t6-Jan-16\nNBRS Financial\tRising Sun\tMD\t4862\tHoward Bank\t17-Oct-14\t26-Mar-15\n'GreenChoice\'s Bank, fsb'\tChicago\tIL\t28462\t'Providence Bank, LLC'\t25-Jul-14\t28-Jul-15\nEastside Commercial Bank\tConyers\tGA\t58125\tCommunity & Southern Bank\t18-Jul-14\t28-Jul-15\nThe Freedom State Bank \tFreedom\tOK\t12483\tAlva State Bank & Trust Company\t27-Jun-14\t25-Mar-16\nValley Bank\tFort Lauderdale\tFL\t21793\t'Landmark Bank, National Association'\t20-Jun-14\t29-Jun-15\nValley Bank\tMoline\tIL\t10450\tGreat Southern Bank\t20-Jun-14\t26-Jun-15\nSlavie Federal Savings Bank\tBel Air\tMD\t32368\t'Bay Bank, FSB'\t30-May-14\t15-Jun-15\nColumbia Savings Bank\tCincinnati\tOH\t32284\t'United Fidelity Bank, fsb'\t23-May-14\t28-May-15\nAztecAmerica Bank \tBerwyn\tIL\t57866\tRepublic Bank of Chicago\t16-May-14\t18-Jul-14\nAllendale County Bank\tFairfax\tSC\t15062\tPalmetto State Bank\t25-Apr-14\t18-Jul-14\nVantage Point Bank\tHorsham\tPA\t58531\tFirst Choice Bank\t28-Feb-14\t3-Mar-15\n'Millennium Bank, National\n Association'\tSterling\tVA\t35096\tWashingtonFirst Bank\t28-Feb-14\t3-Mar-15\nSyringa Bank\tBoise\tID\t34296\tSunwest Bank\t31-Jan-14\t12-Apr-16\nThe Bank of Union\tEl Reno\tOK\t17967\tBancFirst\t24-Jan-14\t25-Mar-16\nDuPage National Bank\tWest Chicago\tIL\t5732\tRepublic Bank of Chicago\t17-Jan-14\t19-F\n", 'noHeaderCommaNoQuotes' => "1,Eldon Base for stackable storage shelf platinum,Muhammed MacIntyre,3,-213.25,38.94,35,Nunavut,Storage & Organization,0.8\n2,1.7 Cubic Foot Compact Office Refrigerators,Barry French,293,457.81,208.16,68.02,Nunavut,Appliances,0.58\n3,Cardinal Slant-DÆ Ring Binder Heavy Gauge Vinyl,Barry French,293,46.71,8.69,2.99,Nunavut,Binders and Binder Accessories,0.39\n4,R380,Clay Rozendal,483,1198.97,195.99,3.99,Nunavut,Telephones and Communication,0.58\n5,Holmes HEPA Air Purifier,Carlos Soltero,515,30.94,21.78,5.94,Nunavut,Appliances,0.5\n6,G.E. Longer-Life Indoor Recessed Floodlight Bulbs,Carlos Soltero,515,4.43,6.64,4.95,Nunavut,Office Furnishings,0.37\n7,Angle-D Binders with Locking Rings Label Holders,Carl Jackson,613,-54.04,7.3,7.72,Nunavut,Binders and Binder Accessories,0.38\n8,SAFCO Mobile Desk Side File Wire Frame,Carl Jackson,613,127.70,42.76,6.22,Nunavut,Storage & Organization,\n9,SAFCO Commercial Wire Shelving Black,Monica Federle,643,-695.26,138.14,35,Nunavut,Storage & Organization,\n10,Xerox 198,Dorothy Badders,678,-226.36,4.98,8.33,Nunavut,Paper,0.38", 'noHeaderCommaQuoteAll' => "\"1\",\"Eldon Base for stackable storage shelf platinum\",\"Muhammed MacIntyre\",\"3\",\"-213.25\",\"38.94\",\"35\",\"Nunavut\",\"Storage & Organization\",\"0.8\"\n\"2\",\"1.7 Cubic Foot Compact Office Refrigerators\",\"Barry French\",\"293\",\"457.81\",\"208.16\",\"68.02\",\"Nunavut\",\"Appliances\",\"0.58\"\n\"3\",\"Cardinal Slant-DÆ Ring Binder Heavy Gauge Vinyl\",\"Barry French\",\"293\",\"46.71\",\"8.69\",\"2.99\",\"Nunavut\",\"Binders and Binder Accessories\",\"0.39\"\n\"4\",\"R380\",\"Clay Rozendal\",\"483\",\"1198.97\",\"195.99\",\"3.99\",\"Nunavut\",\"Telephones and Communication\",\"0.58\"\n\"5\",\"Holmes HEPA Air Purifier\",\"Carlos Soltero\",\"515\",\"30.94\",\"21.78\",\"5.94\",\"Nunavut\",\"Appliances\",\"0.5\"\n\"6\",\"G.E. Longer-Life Indoor Recessed Floodlight Bulbs\",\"Carlos Soltero\",\"515\",\"4.43\",\"6.64\",\"4.95\",\"Nunavut\",\"Office Furnishings\",\"0.37\"\n\"7\",\"Angle-D Binders with Locking Rings Label Holders\",\"Carl Jackson\",\"613\",\"-54.04\",\"7.3\",\"7.72\",\"Nunavut\",\"Binders and Binder Accessories\",\"0.38\"\n\"8\",\"SAFCO Mobile Desk Side File Wire Frame\",\"Carl Jackson\",\"613\",\"127.70\",\"42.76\",\"6.22\",\"Nunavut\",\"Storage & Organization\",\"\"\n\"9\",\"SAFCO Commercial Wire Shelving Black\",\"Monica Federle\",\"643\",\"-695.26\",\"138.14\",\"35\",\"Nunavut\",\"Storage & Organization\",\"\"\n\"10\",\"Xerox 198\",\"Dorothy Badders\",\"678\",\"-226.36\",\"4.98\",\"8.33\",\"Nunavut\",\"Paper\",\"0.38\"", - 'headerCommaQuoteNonnumeric' => "\"policyID\",\"statecode\",\"county\",\"eq_site_limit\",\"hu_site_limit\",\"fl_site_limit\",\"fr_site_limit\",\"tiv_2011,tiv_2012\",\"eq_site_deductible\",\"hu_site_deductible\",\"fl_site_deductible\",\"fr_site_deductible\",\"point_latitude\",\"point_longitude\",\"line\",\"construction\",\"point_granularity\"\n119736,\"FL\",\"CLAY COUNTY\",498960,498960,498960,498960,498960,792148.9,0,9979.2,0,0,30.102261,-81.711777,\"Residential\",\"Masonry\",1\n448094,\"FL\",\"CLAY COUNTY\",1322376.3,1322376.3,1322376.3,1322376.3,1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,\"Residential\",\"Masonry\",3\n206893,\"FL\",\"CLAY COUNTY\",190724.4,190724.4,190724.4,190724.4,190724.4,192476.78,0,0,0,0,30.089579,-81.700455,\"Residential\",\"Wood\",1\n333743,\"FL\",\"CLAY COUNTY\",0,79520.76,0,0,79520.76,86854.48,0,0,0,0,30.063236,-81.707703,\"Residential\",\"Wood\",3\n172534,\"FL\",\"CLAY COUNTY\",0,254281.5,0,254281.5,254281.5,246144.49,0,0,0,0,30.060614,-81.702675,\"Residential\",\"Wood\",1\n785275,\"FL\",\"CLAY COUNTY\",0,515035.62,0,0,515035.62,884419.17,0,0,0,0,30.063236,-81.707703,\"Residential\",\"Masonry\",3\n995932,\"FL\",\"CLAY COUNTY\",0,19260000,0,0,19260000,20610000,0,0,0,0,30.102226,-81.713882,\"Commercial\",\"Reinforced Concrete\",1\n223488,\"FL\",\"CLAY COUNTY\",328500,328500,328500,328500,328500,348374.25,0,16425,0,0,30.102217,-81.707146,\"Residential\",\"Wood\",1\n433512,\"FL\",\"CLAY COUNTY\",315000,315000,315000,315000,315000,265821.57,0,15750,0,0,30.118774,-81.704613,\"Residential\",\"Wood\",1\n142071,\"FL\",\"CLAY COUNTY\",705600,705600,705600,705600,705600,1010842.56,14112,35280,0,0,30.100628,-81.703751,\"Residential\",\"Masonry\",1\n253816,\"FL\",\"CLAY COUNTY\",831498.3,831498.3,831498.3,831498.3,831498.3,1117791.48,0,0,0,0,30.10216,-81.719444,\"Residential\",\"Masonry\",1\n894922,\"FL\",\"CLAY COUNTY\",0,24059.09,0,0,24059.09,33952.19,0,0,0,0,30.095957,-81.695099,\"Residential\",\"Wood\",1\n422834,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,66755.39,0,0,0,0,30.100073,-81.739822,\"Residential\",\"Wood\",1\n582721,\"FL\",\"CLAY COUNTY\",0,28869.12,0,0,28869.12,42826.99,0,0,0,0,30.09248,-81.725167,\"Residential\",\"Wood\",1\n842700,\"FL\",\"CLAY COUNTY\",0,56135.64,0,0,56135.64,50656.8,0,0,0,0,30.101356,-81.726248,\"Residential\",\"Wood\",1\n874333,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,67905.07,0,0,0,0,30.113743,-81.727463,\"Residential\",\"Wood\",1\n580146,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,66938.9,0,0,0,0,30.121655,-81.732391,\"Residential\",\"Wood\",3\n456149,\"FL\",\"CLAY COUNTY\",0,80192.49,0,0,80192.49,86421.04,0,0,0,0,30.109537,-81.741661,\"Residential\",\"Wood\",1\n767862,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,73798.5,0,0,0,0,30.11824,-81.745335,\"Residential\",\"Wood\",3\n353022,\"FL\",\"CLAY COUNTY\",0,60946.79,0,0,60946.79,62467.29,0,0,0,0,30.065799,-81.717416,\"Residential\",\"Wood\",1\n367814,\"FL\",\"CLAY COUNTY\",0,28869.12,0,0,28869.12,42727.74,0,0,0,0,30.082993,-81.710581,\"Residential\",\"Wood\",1\n671392,\"FL\",\"CLAY COUNTY\",0,13410000,0,0,13410000,11700000,0,0,0,0,30.091921,-81.711929,\"Commercial\",\"Reinforced Concrete\",3\n772887,\"FL\",\"CLAY COUNTY\",0,1669113.93,0,0,1669113.93,2099127.76,0,0,0,0,30.117352,-81.711884,\"Residential\",\"Masonry\",1\n983122,\"FL\",\"CLAY COUNTY\",0,179562.23,0,0,179562.23,211372.57,0,0,0,0,30.095783,-81.713181,\"Residential\",\"Wood\",3\n934215,\"FL\",\"CLAY COUNTY\",0,177744.16,0,0,177744.16,157171.16,0,0,0,0,30.110518,-81.727478,\"Residential\",\"Wood\",1\n385951,\"FL\",\"CLAY COUNTY\",0,17757.58,0,0,17757.58,16948.72,0,0,0,0,30.10288,-81.705719,\"Residential\",\"Wood\",1\n716332,\"FL\",\"CLAY COUNTY\",0,130129.87,0,0,130129.87,101758.43,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n751262,\"FL\",\"CLAY COUNTY\",0,42854.77,0,0,42854.77,63592.88,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n633663,\"FL\",\"CLAY COUNTY\",0,785.58,0,0,785.58,662.18,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n105851,\"FL\",\"CLAY COUNTY\",0,170361.91,0,0,170361.91,177176.38,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n710400,\"FL\",\"CLAY COUNTY\",0,1430.89,0,0,1430.89,1861.41,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1" + 'headerCommaQuoteNonnumeric' => "\"policyID\",\"statecode\",\"county\",\"eq_site_limit\",\"hu_site_limit\",\"fl_site_limit\",\"fr_site_limit\",\"tiv_2011\",\"tiv_2012\",\"eq_site_deductible\",\"hu_site_deductible\",\"fl_site_deductible\",\"fr_site_deductible\",\"point_latitude\",\"point_longitude\",\"line\",\"construction\",\"point_granularity\"\n119736,\"FL\",\"CLAY COUNTY\",498960,498960,498960,498960,498960,792148.9,0,9979.2,0,0,30.102261,-81.711777,\"Residential\",\"Masonry\",1\n448094,\"FL\",\"CLAY COUNTY\",1322376.3,1322376.3,1322376.3,1322376.3,1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,\"Residential\",\"Masonry\",3\n206893,\"FL\",\"CLAY COUNTY\",190724.4,190724.4,190724.4,190724.4,190724.4,192476.78,0,0,0,0,30.089579,-81.700455,\"Residential\",\"Wood\",1\n333743,\"FL\",\"CLAY COUNTY\",0,79520.76,0,0,79520.76,86854.48,0,0,0,0,30.063236,-81.707703,\"Residential\",\"Wood\",3\n172534,\"FL\",\"CLAY COUNTY\",0,254281.5,0,254281.5,254281.5,246144.49,0,0,0,0,30.060614,-81.702675,\"Residential\",\"Wood\",1\n785275,\"FL\",\"CLAY COUNTY\",0,515035.62,0,0,515035.62,884419.17,0,0,0,0,30.063236,-81.707703,\"Residential\",\"Masonry\",3\n995932,\"FL\",\"CLAY COUNTY\",0,19260000,0,0,19260000,20610000,0,0,0,0,30.102226,-81.713882,\"Commercial\",\"Reinforced Concrete\",1\n223488,\"FL\",\"CLAY COUNTY\",328500,328500,328500,328500,328500,348374.25,0,16425,0,0,30.102217,-81.707146,\"Residential\",\"Wood\",1\n433512,\"FL\",\"CLAY COUNTY\",315000,315000,315000,315000,315000,265821.57,0,15750,0,0,30.118774,-81.704613,\"Residential\",\"Wood\",1\n142071,\"FL\",\"CLAY COUNTY\",705600,705600,705600,705600,705600,1010842.56,14112,35280,0,0,30.100628,-81.703751,\"Residential\",\"Masonry\",1\n253816,\"FL\",\"CLAY COUNTY\",831498.3,831498.3,831498.3,831498.3,831498.3,1117791.48,0,0,0,0,30.10216,-81.719444,\"Residential\",\"Masonry\",1\n894922,\"FL\",\"CLAY COUNTY\",0,24059.09,0,0,24059.09,33952.19,0,0,0,0,30.095957,-81.695099,\"Residential\",\"Wood\",1\n422834,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,66755.39,0,0,0,0,30.100073,-81.739822,\"Residential\",\"Wood\",1\n582721,\"FL\",\"CLAY COUNTY\",0,28869.12,0,0,28869.12,42826.99,0,0,0,0,30.09248,-81.725167,\"Residential\",\"Wood\",1\n842700,\"FL\",\"CLAY COUNTY\",0,56135.64,0,0,56135.64,50656.8,0,0,0,0,30.101356,-81.726248,\"Residential\",\"Wood\",1\n874333,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,67905.07,0,0,0,0,30.113743,-81.727463,\"Residential\",\"Wood\",1\n580146,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,66938.9,0,0,0,0,30.121655,-81.732391,\"Residential\",\"Wood\",3\n456149,\"FL\",\"CLAY COUNTY\",0,80192.49,0,0,80192.49,86421.04,0,0,0,0,30.109537,-81.741661,\"Residential\",\"Wood\",1\n767862,\"FL\",\"CLAY COUNTY\",0,48115.94,0,0,48115.94,73798.5,0,0,0,0,30.11824,-81.745335,\"Residential\",\"Wood\",3\n353022,\"FL\",\"CLAY COUNTY\",0,60946.79,0,0,60946.79,62467.29,0,0,0,0,30.065799,-81.717416,\"Residential\",\"Wood\",1\n367814,\"FL\",\"CLAY COUNTY\",0,28869.12,0,0,28869.12,42727.74,0,0,0,0,30.082993,-81.710581,\"Residential\",\"Wood\",1\n671392,\"FL\",\"CLAY COUNTY\",0,13410000,0,0,13410000,11700000,0,0,0,0,30.091921,-81.711929,\"Commercial\",\"Reinforced Concrete\",3\n772887,\"FL\",\"CLAY COUNTY\",0,1669113.93,0,0,1669113.93,2099127.76,0,0,0,0,30.117352,-81.711884,\"Residential\",\"Masonry\",1\n983122,\"FL\",\"CLAY COUNTY\",0,179562.23,0,0,179562.23,211372.57,0,0,0,0,30.095783,-81.713181,\"Residential\",\"Wood\",3\n934215,\"FL\",\"CLAY COUNTY\",0,177744.16,0,0,177744.16,157171.16,0,0,0,0,30.110518,-81.727478,\"Residential\",\"Wood\",1\n385951,\"FL\",\"CLAY COUNTY\",0,17757.58,0,0,17757.58,16948.72,0,0,0,0,30.10288,-81.705719,\"Residential\",\"Wood\",1\n716332,\"FL\",\"CLAY COUNTY\",0,130129.87,0,0,130129.87,101758.43,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n751262,\"FL\",\"CLAY COUNTY\",0,42854.77,0,0,42854.77,63592.88,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n633663,\"FL\",\"CLAY COUNTY\",0,785.58,0,0,785.58,662.18,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n105851,\"FL\",\"CLAY COUNTY\",0,170361.91,0,0,170361.91,177176.38,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1\n710400,\"FL\",\"CLAY COUNTY\",0,1430.89,0,0,1430.89,1861.41,0,0,0,0,30.068468,-81.71624,\"Residential\",\"Wood\",1", + 'noHeaderCommaQuoteMinimal' => "1,\"Eldon Base for stackable, storage, shelf, and platinum\",Muhammed MacIntyre,3,-213.25,38.94,35,Nunavut,\"Storage, Cleaning & Organization\",0.8\n2,1.7 Cubic Foot Compact Office Refrigerators,Barry French,293,457.81,208.16,68.02,Nunavut,Appliances,0.58\n3,\"Cardinal Slant-D, Ring Binder, Heavy Gauge, and Vinyl\",Barry French,293,46.71,8.69,2.99,Nunavut,Binders and Binder Accessories,0.39\n4,R380,Clay Rozendal,483,1198.97,195.99,3.99,Nunavut,Telephones and Communication,0.58\n5,Holmes HEPA Air Purifier,Carlos Soltero,515,30.94,21.78,5.94,Nunavut,\"Appliances, Construction, and Other stuff\",0.5\n6,G.E. Longer-Life Indoor Recessed Floodlight Bulbs,Carlos Soltero,515,4.43,6.64,4.95,Nunavut,Office Furnishings,0.37\n7,Angle-D Binders with Locking Rings Label Holders,Carl Jackson,613,-54.04,7.3,7.72,Nunavut,\"Binders,\n Binder Accessories\",0.38\n8,SAFCO Mobile Desk Side File Wire Frame,Carl Jackson,613,127.70,42.76,6.22,Nunavut,Storage & Organization,\n9,\"SAFCO\nCommercial Wire Shelving Black\",Monica Federle,643,-695.26,138.14,35,Nunavut,Storage & Organization,\n10,Xerox 198,Dorothy Badders,678,-226.36,4.98,8.33,Nunavut,Paper,0.38", + 'commaDelimTie' => "1,luke.visinoni@gmail.com,Luker,Visicvoni,10/10/2018,http://www.example.com/\n2,m.visinoni@gmail.com,Lauke,Visidfanoni,10/10/2018,http://www.google.com/\n3,luke@gmail.com,Lukeaa,Visindddoni,10/10/2018,http://www.bleh.com/\n4,linoni@gmail.com,Lukasdfe,Visinoni1,10/10/2018,http://www.asdf.com/\n5,luke.visinoni@gmail.net,Lffuke,ddddddddd,10/10/2018,http://www.ffdaoo.com/\n6,lunoni@yahoo.com,Lukde,Visasdfinoni,10/10/2018,http://www.fffffoo.com/\n7,lunoni@gmail.com,Lddaduke,Visisssnoni,10/10/2018,http://www.fofffo.com/\n8,lu23456@gmail.com,Luasdke,Visin-oni,10/10/2018,http://www.foasdassao.com/\n9,lili@example.com,Luke,Visiasdfsadfnoni,10/10/2018,http://www.foo.com/\n10,lvisinoni@lili.codddm,ddLuke,Visinasdfni,10/10/2018,http://www.foo.com/\n11,visinoni@nono.com,Lukdfe,Visinocni,10/10/2018,http://www.peepee.net/\n12,goofy@disney.com,Lukde,Visinoccni,10/10/2018,http://www.foo.com/\n13,bla@nothing.com,Lukasdfe,Visiccnoni,10/10/2018,http://www.foasdfasdfao.com/\n14,lasdf@lasdkfj.com,asdfLuke,Visicccnoni,10/10/2018,http://www.foo.com/\n15,gmail@luke.visinoni.com,Lasdfuke,Visinccconi,10/10/2018,http://www.foasdfasdfasdfo.com/\n16,luke@gmail.com,Lfuke,Visinoni,10/10/2018,http://www.foo.com/\n17,luke.visinonis@gmail.com,Like,Visindddoni,10/10/2018,http://www.ffffoo.com/\n18,luke.visinoni@gmail.com,Karl,Visinoni,10/10/2018,http://www.foo.com/\n19,luasdfasdfasdfnoni@gmail.com,Dale,Visddddddinoni,10/10/2018,http://www.foo.com/\n20,luke.visinoni@gmail.com,Luke,Visinoni,10/10/2018,http://www.somewebsite.com/\n21,luke.visinoni@gmaasdfil.com,Luddddke,ddasdf,10/10/2018,http://www.foo.com/\n22,lukasdfasdfni@gmail.com,Luke,Visiasdnoni,10/10/2018,http://www.foo.com/\n23,luke_visinoni@gmail.com,Ludddke,Visinoni,10/10/2018,http://www.foo.com/\n24,luke_visinoni_@gmail.com,Luasdfke,VisiSSSnoni,10/10/2018,http://www.someawesomesite.com/\n25,luassni@gmail.com,asdf,Vi-sinoni,10/10/2018,http://www.foo.com/\n26,luke.visinoni@gmail.com,asdfasdfasdf,VisiFDnoni,10/10/2018,http://www.visionofedesign.com/\n27,luke.vi_sin_oni@gmail.com,Ldaduke,VisinaSDoni,10/10/2018,http://www.foo.com/\n28,luke.v24567isinoni@gmail.com,Liuke,Visinoytrni,10/10/2018,http://www.foo.com/\n29,luke.vnoni@gmail.com,Lukie,Visipoinoni,10/10/2018,http://www.foo.com/\n30,lukasdfasdfasdfnoni@gmail.com,Lukiiiie,Visiddddnoni,10/10/2018,http://www.somethingyouknowandlove.com/\n31,lukisinoni@gmail.com,Lukeii,Visidddnoni,10/10/2018,http://www.foo.com/\n32,lukevisinoni@gmail.com,Lukesadf,Visidddnoni,10/10/2018,http://www.thevirginmaryandhergrandma.com/\n33,luke.visi@gmail.com,Luked,Visinonid,10/10/2018,http://www.poopootape.com/\n34,luke.vinoni@gmail.com,Luke,Visinoniocious,10/10/2018,http://www.peepeetable.com/\n" ); foreach ($strings as $filename => $content) { $file = "{$this->filedir}/{$filename}.csv"; @@ -65,4 +79,4 @@ protected function getFileContentFor($filekey) { return file_get_contents($this->getFilePathFor($filekey)); } -} +} \ No newline at end of file diff --git a/tests/CSVelte/WriterTest.php b/tests/CSVelte/WriterTest.php index d001998..96fb8f8 100644 --- a/tests/CSVelte/WriterTest.php +++ b/tests/CSVelte/WriterTest.php @@ -1,167 +1,86 @@ + * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV + * standardization efforts, CSVelte was written in an effort to take all the + * suck out of working with CSV. + * + * @copyright Copyright (c) 2018 Luke Visinoni * @author Luke Visinoni - * @todo Move all of the tests from OldReaderTest.php into this class - * @coversDefaultClass CSVelte\Writer + * @license See LICENSE file (MIT license) */ -class WriterTest extends UnitTestCase -{ - protected $sampledata = [ - ['1','luke','visinoni','luke.visinoni@gmail.com'], - ['2','bob','smith','bob.smith@yahoo.com'], - ['3','larry','johnson','ljh@johnsonhome.com'], - ]; - - public function testWriterCustomFlavor() - { - $out = Stream::open('php://memory'); - $writer = new Writer($out, $expectedFlavor = new Flavor(array('delimiter' => '|'))); - $this->assertSame($expectedFlavor, $writer->getFlavor()); - } - - public function testWriterCanAcceptArrayForFlavor() - { - $flavorArr = (new Flavor())->toArray(); - $flavorArr['delimiter'] = "\t"; - $flavorArr['lineTerminator'] = "\n"; - $flavorArr['quoteChar'] = "'"; - $flavorArr['quoteStyle'] = Flavor::QUOTE_ALL; - $writer = new Writer(Stream::open($this->getFilePathFor('veryShort'), 'a+'), $flavorArr); - $this->assertEquals($flavorArr, $writer->getFlavor()->toArray()); - } - - public function testWriterWriteWriteSingleRowUsingArray() - { - $out = Stream::open('php://memory'); - $writer = new Writer($out); - $data = array('one','two', 'three'); - $this->assertEquals(strlen(implode(',', $data)) + strlen("\r\n"), $writer->writeRow($data)); - } - - public function testWriterWriteWriteSingleRowUsingIterator() - { - $out = Stream::open('php://memory'); - $writer = new Writer($out); - $data = new ArrayIterator(array('one','two', 'three')); - $this->assertEquals(strlen(implode(',', $data->getArrayCopy())) + strlen("\r\n"), $writer->writeRow($data)); - } - - public function testWriterWritesHeaderRow() - { - $temp = Stream::open('php://temp'); - $writer = new Writer($temp); - $writer->setHeaderRow(['id','firstname','lastname','email']); - $writer->writeRows($this->sampledata); - $temp->seek(0); - $this->assertEquals("id,firstname,lastname,email\r\n1,luke,visinoni,luke.visinoni@gmail.com\r\n2,bob,smith,bob.smith@yahoo.com\r\n3,larry,johnson,ljh@johnsonhome.com\r\n", $temp->read(200)); - } - - /** - * @expectedException CSVelte\Exception\WriterException - */ - public function testWriterThrowsExceptionIfUserAttemptsToSetHeaderAfterRowsHaveBeenWritten() - { - $writer = new Writer(Stream::open('php://temp')); - $writer->writeRow(array('foo','bar','baz')); - $writer->setHeaderRow(array('this','shouldnt','work')); - } - - public function testWriterWriteWriteSingleRowUsingCSVReader() - { - $reader = new Reader(Stream::open($this->getFilePathFor('veryShort'))); - $writer = new Writer($stream = Stream::open('php://temp')); - $writer->writeRow($reader->current()); - $stream->rewind(); - $this->assertEquals("foo,bar,baz\r\n", $stream->read(15)); - } +namespace CSVelteTest; - /** - * @expectedException \InvalidArgumentException - */ - public function testWriterWriteRowsThrowsExceptionIfPassedNonIterable() - { - $out = Stream::open('php://temp'); - $writer = new Writer($out); - $writer->writeRows('foo'); - } +use CSVelte\Dialect; +use CSVelte\Reader; +use CSVelte\Writer; - public function testWriterWriteMultipleRows() - { - $out = Stream::open('php://temp'); - $writer = new Writer($out); - $reader = new Reader(Stream::open($this->getFilePathFor('commaNewlineHeader'))); - $data = array(); - $i = 0; - foreach ($reader as $row) { - if ($i == 10) break; - $data []= $row->toArray(); - $i++; - } - $written_rows = $writer->writeRows($data); - $this->assertEquals(10, $written_rows); - } +use function CSVelte\to_stream; - public function testWriterWriteRowsAcceptsReader() +class WriterTest extends UnitTestCase +{ + public function testInstantiateWriterWithNoDialectUsesDefault() { - $out = Stream::open($fname = $this->root->url() . '/reader2writer.csv', 'w'); - $writer = new Writer($out, ['lineTerminator' => "\n"]); - $in = Stream::open($this->getFilePathFor('headerDoubleQuote'), 'r+b'); - $reader = new Reader($in, ['lineTerminator' => "\n"]); - $this->assertEquals(29, $writer->writeRows($reader)); - $this->assertEquals($this->getFileContentFor('headerDoubleQuote'), file_get_contents($fname)); + $stream = to_stream(fopen('php://temp', 'w+')); + $writer = new Writer($stream); + $this->assertInstanceOf(Dialect::class, $writer->getDialect()); } - public function testWriterWriteRowsAcceptsReaderToReformat() + public function testInstantiateWriterWithDialectCanChangeDialectWithSetDialect() { - $out = Stream::open($fname = $this->root->url() . '/reformatme.csv', 'w'); - $in = Stream::open($this->getFilePathFor('headerDoubleQuote')); - $writer = new Writer($out, [ - 'delimiter' => '|', - 'quoteStyle' => Flavor::QUOTE_NONNUMERIC, - 'lineTerminator' => "\r\n", - 'doubleQuote' => false, - 'escapeChar' => '\\' - ]); - $reader = new Reader($in, ['lineTerminator' => "\n"]); - $writer->writeRows($reader); - $lines = file($fname); - $this->assertEquals("\"Bank Name\"|\"City\"|\"ST\"|\"CERT\"|\"Acquiring Institution\"|\"Closing Date\"|\"Updated Date\"\r\n", $lines[0]); - $this->assertEquals("\"First CornerStone Bank\"|\"King of\n\\\"Prussia\\\"\"|\"PA\"|35312|\"First-Citizens Bank & Trust Company\"|\"6-May-16\"|\"25-May-16\"\r\n", $lines[1] . $lines[2]); - $out->close(); - unlink($fname); + $stream = to_stream(fopen('php://temp', 'w+')); + $dialect = new Dialect(['delimiter' => "\t"]); + $writer = new Writer($stream); + $this->assertNotSame($dialect, $writer->getDialect()); + $this->assertSame($writer, $writer->setDialect($dialect)); + $this->assertSame($dialect, $writer->getDialect()); + $this->assertEquals("\t", $writer->getDialect()->getDelimiter()); } - public function testWriterWritesCorrectOutputForFlavorWithQuoteAll() + public function testWriterWritesToOutputStreamAccordingToDialect() { - $stream = Stream::open('php://temp'); - $writer = new Writer($stream, ['quoteStyle' => Flavor::QUOTE_ALL]); - $this->assertEquals(24, $writer->writeRow(['bacon','cheese','ham'])); - $this->assertEquals(3, $writer->writeRows([['monkey','lettuce','spam'],['table','hair','blam'],['chalk','talk','caulk']])); - $stream->rewind(); - $this->assertEquals("\"bacon\",\"cheese\",\"ha", $stream->read(20)); + $stream = to_stream(fopen('php://temp', 'w+')); + $dialect = new Dialect(['header' => false]); + $writer = new Writer($stream, $dialect); + $this->assertEquals(12, $writer->insertRow([ + 'foo', + 'bar', + 'baz' + ])); + $this->assertEquals("foo,bar,baz\n", (string) $stream); + $this->assertEquals(71, $writer->insertRow([ + 'test', + 'this is a test, with a comma in it', + 'this is a "quoted" field' + ])); + $this->assertEquals("foo,bar,baz\ntest,\"this is a test, with a comma in it\",\"this is a \"\"quoted\"\" field\"\n", (string) $stream); + $this->assertEquals(11, $writer->insertRow([ + 1, + '2', + '3.5556' + ])); + $this->assertEquals("foo,bar,baz\ntest,\"this is a test, with a comma in it\",\"this is a \"\"quoted\"\" field\"\n1,2,3.5556\n", (string) $stream); + $this->assertEquals(103, $writer->insertRow([ + "this field\nhas\nline breaks", + 'this field has a \' single quote', + 'this has? weird^*& characters!! ,.:?!2@' + ])); + $this->assertEquals("foo,bar,baz\ntest,\"this is a test, with a comma in it\",\"this is a \"\"quoted\"\" field\"\n1,2,3.5556\n\"this field\nhas\nline breaks\",this field has a ' single quote,\"this has? weird^*& characters!! ,.:?!2@\"\n", (string) $stream); } - public function testWriterWritesCorrectOutputForFlavorWithQuoteNone() + public function testInsertAllWritesMultipleRowsAndReturnsTotalWrittenBytes() { - $stream = Stream::open('php://temp'); - $writer = new Writer($stream, ['quoteStyle' => Flavor::QUOTE_NONE]); - $this->assertEquals(18, $writer->writeRow(['bacon','cheese','ham'])); - $this->assertEquals(3, $writer->writeRows([['monkey','lettuce','spam'],['table','hair','blam'],['chalk','talk','caulk']])); - $stream->rewind(); - $this->assertEquals("bacon,cheese,ham\r\nmo", $stream->read(20)); + $stream = to_stream(fopen('php://temp', 'w+')); + $dialect = new Dialect(['header' => false]); + $writer = new Writer($stream, $dialect); + $data = [ + ['1', 'luke@example.com', 'A short description', 'ON'], + ['2', 'bob@example.com', 'What about "bob"?', 'OFF'], + ['3', 'steve@example.com', 'The problem with steve, is steve.', 'ON'], + ['4', 'joe@example.com', 'Hey Joe, where you goin\' with that gun in yo hand?', 'ON'], + ]; + $this->assertEquals(219, $writer->insertAll($data)); + $this->assertEquals("1,luke@example.com,A short description,ON\n2,bob@example.com,\"What about \"\"bob\"\"?\",OFF\n3,steve@example.com,\"The problem with steve, is steve.\",ON\n4,joe@example.com,\"Hey Joe, where you goin' with that gun in yo hand?\",ON\n", (string) $stream); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c16a317..306a22f 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,20 +9,14 @@ */ require_once __DIR__ . '/../vendor/autoload.php'; -function dd($input, $exit = true, $label = null) -{ - if (is_null($label)) { - $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 1); - $label = 'File: '; - $label .= pathinfo($trace[0]['file'], PATHINFO_FILENAME); - $label .= ":" . $trace[0]['line']; - echo $label . "\n"; - } else { - echo $label . "\n" . implode(array_map(function($c){ return "-"; }, str_split($label))) . "\n"; +use Symfony\Component\VarDumper\VarDumper; + +if (!function_exists('dd')) { + function dd($input, $exit = true) + { + VarDumper::dump($input); + if ($exit) exit; } - var_dump($input); - echo "\n"; - if ($exit) exit; } /** @@ -37,14 +31,8 @@ function dd($input, $exit = true, $label = null) * visible versions, but it appears that json_encode does this pretty well * for me. Neato! */ -function si($in, $exit = true, $dump = true) +function si($in, $exit = true) { - $out = json_encode($in); - if ($dump) return var_dump($out); - else { - if ($exit) exit($out); - } - return $out; + dd(json_encode($in), $exit); } -function with($obj) { return $obj; }