Passed
Push — releases/v0.3 ( 7e799c...c0b0dc )
by Luke
06:36
created

Reader::parseLine()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 16
ccs 14
cts 14
cp 1
crap 3
rs 9.7333
c 0
b 0
f 0
1
<?php
2
/**
3
 * CSVelte: Slender, elegant CSV for PHP
4
 *
5
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
6
 * standardization efforts, CSVelte was written in an effort to take all the
7
 * suck out of working with CSV.
8
 *
9
 * @copyright Copyright (c) 2018 Luke Visinoni
10
 * @author    Luke Visinoni <[email protected]>
11
 * @license   See LICENSE file (MIT license)
12
 */
13
namespace CSVelte;
14
15
use Iterator;
16
use Countable;
17
use CSVelte\Contract\Streamable;
18
use Noz\Collection\Collection;
19
use Stringy\Stringy;
20
21
use function Noz\collect;
22
use function Stringy\create as s;
23
24
class Reader implements Iterator, Countable
25
{
26
    /** @var Streamable The input stream to read from */
27
    protected $input;
28
29
    /** @var Dialect The *dialect* of CSV to read */
30
    protected $dialect;
31
32
    /** @var Collection The line currently sitting in memory */
33
    protected $current;
34
35
    /** @var Collection The header row */
36
    protected $header;
37
38
    /** @var int The current line number */
39
    protected $lineNo;
40
41
    /**
42
     * Reader constructor.
43
     *
44
     * Although this is the constructor, I don't envision it being used much in userland. I think much more common
45
     * methods of creating readers will be available within CSVelte base class such as CSVelte::fromPath(),
46
     * CSVelte::fromString(), CSVelte::fromSplFileObject, CSVelte::toSplFileObject, CSVelte::toPath(), etc.
47
     *
48
     * @param Streamable $input The source being read from
49
     * @param Dialect $dialect The dialect being read
50
     */
51 11
    public function __construct(Streamable $input, Dialect $dialect = null)
52
    {
53 11
        if (is_null($dialect)) {
54 5
            $dialect = new Dialect;
55 5
        }
56 11
        $this->setInputStream($input)
57 11
            ->setDialect($dialect);
58 11
    }
59
60
    /**
61
     * Get csv data as a two-dimensional array
62
     *
63
     * @return array
64
     */
65 1
    public function toArray()
66
    {
67 1
        return iterator_to_array($this);
68
    }
69
70
    /**
71
     * Set the CSV dialect
72
     *
73
     * @param Dialect $dialect The *dialect* of CSV to read
74
     *
75
     * @return self
76
     */
77 11
    public function setDialect(Dialect $dialect)
78
    {
79 11
        $this->dialect = $dialect;
80
        // call rewind because new dialect needs to be used to re-read
81 11
        return $this->rewind();
82
    }
83
84
    /**
85
     * Get dialect
86
     *
87
     * @return Dialect
88
     */
89 11
    public function getDialect()
90
    {
91 11
        return $this->dialect;
92
    }
93
94
    /**
95
     * Set input stream
96
     *
97
     * @param Streamable $stream The input stream to read from
98
     *
99
     * @return self
100
     */
101 11
    protected function setInputStream(Streamable $stream)
102
    {
103 11
        $this->input = $stream;
104 11
        return $this;
105
    }
106
107
    /**
108
     * Loads next line into memory
109
     *
110
     * Reads from input one character at a time until a newline is reached that isn't within quotes. Once a completed
111
     * line has been loaded, it is assigned to the `$this->current` property. Subsequent calls will continue to load
112
     * successive lines until the end of the input source is reached.
113
     *
114
     * @return self
115
     */
116 11
    protected function loadLine()
117
    {
118 11
        $d = $this->getDialect();
119 11
        $line = '';
120 11
        while ($str = $this->input->readLine($d->getLineTerminator())) {
121 11
            $line .= $str;
122 11
            if (count(s($line)->split($d->getQuoteChar())) % 2) {
123 11
                break;
124
            }
125 6
        }
126 11
        $this->current = $this->parseLine($line);
127 11
        return $this;
128
    }
129
130
    /**
131
     * Parse a line of CSV into individual fields
132
     *
133
     * Accepts a line (string) of CSV data that it then splits at the delimiter character. The method is smart, in that
134
     * it knows not to split at delimiters within quotes. Ultimately, fields are placed into a collection and returned.
135
     *
136
     * @param string $line A single line of CSV data to parse into individual fields
137
     *
138
     * @return Collection
139
     */
140 11
    protected function parseLine($line)
141
    {
142 11
        $d = $this->getDialect();
143 11
        $fields = collect(s($line)
144 11
            ->trimRight($d->getLineTerminator())
145 11
            ->split($d->getDelimiter() . "(?=([^\"]*\"[^\"]*\")*[^\"]*$)"));
146 11
        if (!is_null($this->header)) {
147 7
            $fields = $fields->rekey($this->header);
148 7
        }
149 11
        return $fields->map(function(Stringy $field, $pos) use ($d) {
0 ignored issues
show
Unused Code introduced by
The parameter $pos is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
150 11
            if ($d->isDoubleQuote()) {
151 11
                $field = $field->replace('""', '"');
152 11
            }
153 11
            return (string) $field->trim($d->getQuoteChar());
154 11
        });
155
    }
156
157
    /** == BEGIN: SPL implementation methods == */
158
159
    /**
160
     * Get current row
161
     *
162
     * @return array
163
     */
164 10
    public function current()
165
    {
166 10
        return $this->current->toArray();
167
    }
168
169
    /**
170
     * Move pointer to beginning of the next line internally and then load the line
171
     *
172
     * @return self
173
     */
174 3
    public function next()
175
    {
176 3
        $this->loadLine();
177 3
        $this->lineNo++;
178 3
        return $this;
179
    }
180
181
    /**
182
     * Get current line number
183
     *
184
     * @return int
185
     */
186 3
    public function key()
187
    {
188 3
        return $this->lineNo;
189
    }
190
191
    /**
192
     * Have we reached the end of the CSV data?
193
     *
194
     * @return bool
195
     */
196 2
    public function valid()
197
    {
198 2
        return !$this->input->eof();
199
    }
200
201
    /**
202
     * Rewind to the beginning
203
     *
204
     * Rewinds the internal pointer to the beginning of the CSV data, load first line, and reset line number to 1. Also
205
     * loads the header (if one exists) and uses its values as indexes within rows.
206
     *
207
     * @return self
208
     */
209 11
    public function rewind()
210
    {
211 11
        $this->lineNo = 1;
212 11
        $this->input->rewind();
213 11
        if ($this->getDialect()->hasHeader()) {
214 7
            $this->loadLine();
215 7
            $this->header = $this->current();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->current() of type array is incompatible with the declared type object<Noz\Collection\Collection> of property $header.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
216 7
        }
217 11
        $this->loadLine();
218 11
        return $this;
219
    }
220
221
    /**
222
     * Get number of lines in the CSV data (not including header)
223
     *
224
     * @return int
225
     */
226 1
    public function count()
227
    {
228 1
        return count($this->toArray());
229
    }
230
231
    /** == END: SPL implementation methods == */
232
}