diff --git a/.gitignore b/.gitignore index e5294f4..f1116c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor composer.lock .DS_Store +/nbproject/ +/build/ diff --git a/README.md b/README.md index 9a1e93f..9417f5b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Csv +[![Build Status](https://travis-ci.org/phillipsdata/csv.svg?branch=master)](https://travis-ci.org/phillipsdata/csv) [![Coverage Status](https://coveralls.io/repos/phillipsdata/csv/badge.svg)](https://coveralls.io/r/phillipsdata/csv) + A CSV reader and writer with no external dependencies, that allows just in time formatting and filtering, and operates on files as well as streams. diff --git a/src/Factory.php b/src/Factory.php index 3ffb10d..5a12715 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -23,8 +23,8 @@ public static function writer( $enclosure = '"', $escape = '\\' ) { - $file = static::fileObject($filename, 'w') - ->setCsvControl($delimiter, $enclosure, $escape); + $file = static::fileObject($filename, 'w'); + $file->setCsvControl($delimiter, $enclosure, $escape); $writer = Writer::output($file); return $writer; @@ -47,8 +47,8 @@ public static function reader( $escape = '\\', $withHeader = true ) { - $file = static::fileObject($filename) - ->setCsvControl($delimiter, $enclosure, $escape); + $file = static::fileObject($filename); + $file->setCsvControl($delimiter, $enclosure, $escape); $reader = Reader::input($file, $withHeader); return $reader; diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php new file mode 100644 index 0000000..b12dae9 --- /dev/null +++ b/tests/Unit/FactoryTest.php @@ -0,0 +1,49 @@ +assertInstanceOf( + '\PhillipsData\Csv\Writer', + Factory::writer($file) + ); + $this->assertFileExists($file); + + unlink($file); + } + + /** + * @covers ::reader + * @covers ::fileObject + * @uses \PhillipsData\Csv\AbstractCsv + * @uses \PhillipsData\Csv\Reader + */ + public function testReader() + { + $file = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' + . DIRECTORY_SEPARATOR . 'with-header.csv'; + + $this->assertInstanceOf( + '\PhillipsData\Csv\Reader', + Factory::reader($file) + ); + $this->assertFileExists($file); + } +} diff --git a/tests/Unit/Fixtures/with-broken-header.csv b/tests/Unit/Fixtures/with-broken-header.csv new file mode 100644 index 0000000..c695ee5 --- /dev/null +++ b/tests/Unit/Fixtures/with-broken-header.csv @@ -0,0 +1,4 @@ +"Heading 1","Heading 2" +"Cell 1","Cell 2","Cell 3" +"Cell 4","Cell 5","Cell 6" +"Cell 7","Cell 8","Cell 9" diff --git a/tests/Unit/Fixtures/with-header.csv b/tests/Unit/Fixtures/with-header.csv new file mode 100644 index 0000000..52bcb25 --- /dev/null +++ b/tests/Unit/Fixtures/with-header.csv @@ -0,0 +1,4 @@ +"Heading 1","Heading 2","Heading 3" +"Cell 1","Cell 2","Cell 3" +"Cell 4","Cell 5","Cell 6" +"Cell 7","Cell 8","Cell 9" diff --git a/tests/Unit/Fixtures/without-header.csv b/tests/Unit/Fixtures/without-header.csv new file mode 100644 index 0000000..5b9c491 --- /dev/null +++ b/tests/Unit/Fixtures/without-header.csv @@ -0,0 +1,3 @@ +"Cell 1","Cell 2","Cell 3" +"Cell 4","Cell 5","Cell 6" +"Cell 7","Cell 8","Cell 9" diff --git a/tests/Unit/Map/MapIteratorTest.php b/tests/Unit/Map/MapIteratorTest.php new file mode 100644 index 0000000..352f2c6 --- /dev/null +++ b/tests/Unit/Map/MapIteratorTest.php @@ -0,0 +1,36 @@ +assertEquals($expected, $actual); + } +} diff --git a/tests/Unit/ReaderTest.php b/tests/Unit/ReaderTest.php new file mode 100644 index 0000000..4c545b8 --- /dev/null +++ b/tests/Unit/ReaderTest.php @@ -0,0 +1,224 @@ +getReader(); + $this->assertInstanceOf( + 'Iterator', + $reader->fetch() + ); + } + + /** + * @dataProvider inputProvider + * @covers ::input + * @covers ::fetch + * @covers ::applyFilter + * @covers ::getAssocIterator + * @covers ::getFilterIterator + * @covers ::getFormatIterator + * @covers ::applyFormat + * @covers ::setHeader + * @covers \PhillipsData\Csv\AbstractCsv + * @uses \PhillipsData\Csv\Map\MapIterator + */ + public function testFormat($headers, $headerType) + { + $reader = $this->getReader($headers, $headerType); + // Determine defaults for a line + $defaultLines = []; + foreach ($reader as $line) { + $defaultLines[] = $line; + } + + // Format each CSV line + $reader->format(function ($line, $key, $iterator) { + $values = []; + foreach ($line as $cell) { + $values[] = $this->format($cell); + } + + return $values; + }); + + // Check each cell has been formatted + foreach ($reader->fetch() as $i => $line) { + foreach ($line as $j => $cell) { + // The values should be different until formatted + $this->assertNotEquals( + $defaultLines[$i][$j], + $cell + ); + + // The values should be identical once formatted + $this->assertEquals( + $this->format($defaultLines[$i][$j]), + $cell + ); + } + } + } + + /** + * @dataProvider inputProvider + * @covers ::input + * @covers ::fetch + * @covers ::applyFilter + * @covers ::getAssocIterator + * @covers ::getFilterIterator + * @covers ::getFormatIterator + * @covers ::applyFormat + * @covers ::setHeader + * @covers \PhillipsData\Csv\AbstractCsv + * @uses \PhillipsData\Csv\Map\MapIterator + */ + public function testFilter($headers, $headerType) + { + $reader = $this->getReader($headers, $headerType); + + $index = 1; + if ($headers) { + $index = 'Heading 2'; + } + + $reader->filter(function ($line) use ($index) { + // Only return values where the second column contains even numbers + return (preg_match('/[02468]+/', $line[$index])); + }); + + // Check that the CSV contains only the matching rows + foreach ($reader->fetch() as $i => $line) { + // Verify only rows where second column contains even numbers + $this->assertTrue((boolean) preg_match('/[02468]+/', $line[$index])); + } + } + + /** + * Checks formatting when the CSV has fewer headings than columns + * + * @covers ::getAssocIterator + * @covers ::input + * @covers ::fetch + * @covers ::getFilterIterator + * @covers ::getFormatIterator + * @covers ::applyFormat + * @covers ::applyfilter + * @covers ::setHeader + * @covers \PhillipsData\Csv\AbstractCsv + * @uses \PhillipsData\Csv\Map\MapIterator + */ + public function testFormatHeaders() + { + $reader = $this->getReader(true, 'with-broken'); + + // Determine defaults for a line + $defaultLines = []; + foreach ($reader as $line) { + $defaultLines[] = $line; + } + + // Format each CSV line + $reader->format(function ($line, $key, $iterator) { + $values = []; + foreach ($line as $cell) { + $values[] = $this->format($cell); + } + + return $values; + }); + + // Add another formatter + $reader->format(function ($line, $key, $iterator) { + $values = []; + foreach ($line as $cell) { + $values[] = $this->formatHyphens($cell); + } + + return $values; + }); + + // Check each cell has been formatted + foreach ($reader->fetch() as $i => $line) { + foreach ($line as $j => $cell) { + // The values should be different until formatted + $this->assertNotEquals( + $defaultLines[$i][$j], + $cell + ); + + $formattedCell = $this->format($defaultLines[$i][$j]); + $formattedCell = $this->formatHyphens($formattedCell); + + // The values should be identical once formatted + $this->assertEquals( + $formattedCell, + $cell + ); + } + } + } + + /** + * @param string $text Text to format + * @return string The formatted text + */ + private function format($text) + { + return strtoupper($text); + } + + /** + * + * @param string $text Text to format + * @return string The formatted text; + */ + private function formatHyphens($text) + { + return '-' . $text . '-'; + } +} diff --git a/tests/Unit/WriterTest.php b/tests/Unit/WriterTest.php new file mode 100644 index 0000000..5b73f53 --- /dev/null +++ b/tests/Unit/WriterTest.php @@ -0,0 +1,171 @@ +setFlags( + SplTempFileObject::READ_CSV + | SplTempFileObject::READ_AHEAD + | SplTempFileObject::SKIP_EMPTY + ); + + return $file; + } + + /** + * @covers ::output + * @covers \PhillipsData\Csv\AbstractCsv + */ + public function testOutput() + { + $this->assertInstanceOf( + '\PhillipsData\Csv\Writer', + Writer::output($this->getSplFixtureFile()) + ); + } + + /** + * @covers ::write + * @covers ::writeRow + * @covers ::isWritable + * @covers ::output + * @covers \PhillipsData\Csv\AbstractCsv + */ + public function testWrite() + { + $file = $this->getSplFixtureFile(); + $writer = Writer::output($file); + $this->assertInstanceOf('\PhillipsData\Csv\Writer', $writer); + + $data = [ + ['a1', 'b1'], + ['a2', 'b2'], + ['a3', 'b3'], + ['a4', 'b4'] + ]; + + $writer->write($data); + + $actual = []; + foreach ($file as $row) { + $actual[] = $row; + } + + $this->assertEquals($data, $actual); + } + + /** + * @covers ::write + * @covers ::output + * @covers \PhillipsData\Csv\AbstractCsv + * @expectedException InvalidArgumentException + */ + public function testWriteException() + { + $writer = Writer::output($this->getSplFixtureFile()); + + // Exception, string is an invalid argument + $writer->write('some data'); + } + + /** + * @dataProvider filtersProvider + * @covers ::isWritable + * @covers ::write + * @covers ::writeRow + * @covers ::output + * @covers \PhillipsData\Csv\AbstractCsv + */ + public function testFilters($data, $expected) + { + $file = $this->getSplFixtureFile(); + $writer = Writer::output($file); + $writer->filter(function ($row) { + return in_array(substr($row[0], -1), ['1', '3']); + }); + + $writer->write($data); + + $actual = []; + foreach ($file as $row) { + $actual[] = $row; + } + + $this->assertEquals($expected, $actual); + } + + /** + * Data provider for testFilters + * + * @return array + */ + public function filtersProvider() + { + return [ + [ + [['a1', 'b1'],['a2', 'b2'],['a3', 'b3'],['a4', 'b4']], + [['a1', 'b1'],['a3', 'b3']] + ] + ]; + } + + /** + * @dataProvider formatterProvider + * @covers ::isWritable + * @covers ::write + * @covers ::writeRow + * @covers ::output + * @covers \PhillipsData\Csv\AbstractCsv + */ + public function testFormatters($data, $expected) + { + $file = $this->getSplFixtureFile(); + $writer = Writer::output($file); + + $writer->format(function ($row) { + foreach ($row as $key => &$value) { + $value = strtoupper($value); + } + return $row; + }); + + $writer->write($data); + + $actual = []; + foreach ($file as $row) { + $actual[] = $row; + } + + $this->assertEquals($expected, $actual); + } + + /** + * Data provider for testFormatters + * + * @return array + */ + public function formatterProvider() + { + return [ + [ + [['a1', 'b1'],['a2', 'b2']], + [['A1', 'B1'],['A2', 'B2']] + ] + ]; + } +}