Completed
Push — master ( dd1269...21c3dd )
by Sam
02:11
created

Data   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Test Coverage

Coverage 85.81%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 175
c 3
b 0
f 0
dl 0
loc 301
ccs 133
cts 155
cp 0.8581
rs 4.5599
wmc 58

5 Methods

Rating   Name   Duplication   Size   Complexity  
A readOpcode() 0 8 2
A writeOpcode() 0 17 5
F read() 0 120 27
F write() 0 90 23
A toArray() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Data often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Data, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SPSS\Sav\Record;
4
5
use SPSS\Buffer;
6
use SPSS\Exception;
7
use SPSS\Sav\Record;
8
use SPSS\Utils;
9
10
class Data extends Record
11
{
12
    const TYPE = 999;
13
14
    /** No-operation. This is simply ignored. */
15
    const OPCODE_NOP = 0;
16
17
    /** End-of-file. */
18
    const OPCODE_EOF = 252;
19
20
    /** Verbatim raw data. Read an 8-byte segment of raw data. */
21
    const OPCODE_RAW_DATA = 253;
22
23
    /** Compressed whitespaces. Expand to an 8-byte segment of whitespaces. */
24
    const OPCODE_WHITESPACES = 254;
25
26
    /** Compressed sysmiss value. Expand to an 8-byte segment of SYSMISS value. */
27
    const OPCODE_SYSMISS = 255;
28
29
    /**
30
     * @var array [case_index][var_index]
31
     */
32
    public $matrix = [];
33
34
    /**
35
     * @var array Latest opcodes data
36
     */
37
    private $opcodes = [];
38
39
    /**
40
     * @var int Current opcode index
41
     */
42
    private $opcodeIndex = 0;
43
44
    /**
45
     * @param Buffer $buffer
46
     * @throws Exception
47
     */
48 5
    public function read(Buffer $buffer)
49
    {
50 5
        if ($buffer->readInt() != 0) {
51
            throw new \InvalidArgumentException('Error reading data record. Non-zero value found.');
52
        }
53 5
        if (! isset($buffer->context->variables)) {
54
            throw new \InvalidArgumentException('Variables required');
55
        }
56 5
        if (! isset($buffer->context->header)) {
57
            throw new \InvalidArgumentException('Header required');
58
        }
59 5
        if (! isset($buffer->context->info)) {
60
            throw new \InvalidArgumentException('Info required');
61
        }
62
63 5
        $compressed = $buffer->context->header->compression;
64 5
        $bias = $buffer->context->header->bias;
65 5
        $casesCount = $buffer->context->header->casesCount;
66
67
        /** @var Variable[] $variables */
68 5
        $variables = $buffer->context->variables;
69
70
        /** @var Record\Info[] $info */
71 5
        $info = $buffer->context->info;
72
73 5
        $veryLongStrings = [];
74 5
        if (isset($info[Record\Info\VeryLongString::SUBTYPE])) {
75 1
            $veryLongStrings = $info[Record\Info\VeryLongString::SUBTYPE]->toArray();
76
        }
77
78 5
        if (isset($info[Record\Info\MachineFloatingPoint::SUBTYPE])) {
79 5
            $sysmis = $info[Record\Info\MachineFloatingPoint::SUBTYPE]->sysmis;
80
        } else {
81
            $sysmis = NAN;
82
        }
83
84 5
        $this->opcodeIndex = 8;
85
86 5
        for ($case = 0; $case < $casesCount; $case++) {
87 4
            $parent = -1;
0 ignored issues
show
Unused Code introduced by
The assignment to $parent is dead and can be removed.
Loading history...
88 4
            $octs = 0;
89 4
            $varCount = count($variables);
90 4
            $varNum = 0;
91 4
            for($index = 0; $index < $varCount; $index++) {
92 4
                $var = $variables[$index];
93 4
                $isNumeric = $var->width == 0;
94 4
                $width = isset($var->write[2]) ? $var->write[2] : $var->width;
95
96 4
                if ($isNumeric) {
97 3
                    if (! $compressed) {
98
                        $this->matrix[$case][$varNum] = $buffer->readDouble();
99
                    } else {
100 3
                        $opcode = $this->readOpcode($buffer);
101
                        switch ($opcode) {
102 3
                            case self::OPCODE_NOP;
103
                                break;
104 3
                            case self::OPCODE_EOF;
105
                                throw new Exception(
106
                                    'Error reading data: unexpected end of compressed data file (cluster code 252)'
107
                                );
108
                                break;
109 3
                            case self::OPCODE_RAW_DATA;
110 2
                                $this->matrix[$case][$varNum] = $buffer->readDouble();
111 2
                                break;
112 3
                            case self::OPCODE_SYSMISS;
113
                                $this->matrix[$case][$varNum] = $sysmis;
114
                                break;
115
                            default:
116 3
                                $this->matrix[$case][$varNum] = $opcode - $bias;
117 3
                                break;
118
                        }
119
                    }
120
                } else {
121 3
                    $width = isset($veryLongStrings[$var->name]) ? $veryLongStrings[$var->name] : $width;
122 3
                    $this->matrix[$case][$varNum] = '';
123 3
                    $segmentsCount = Utils::widthToSegments($width);
124 3
                    $opcode = self::OPCODE_RAW_DATA;
125 3
                    $index = $index - 1;
126 3
                    for ($s = 0; $s < $segmentsCount; $s++) {
127 3
                        $segWidth = Utils::segmentAllocWidth($width, $s);
128 3
                        $octs = Utils::widthToOcts($segWidth);
129 3
                        $index = $index + $octs;    // Skip a few variables for this segment
130 3
                        if ($opcode === self::OPCODE_NOP || $opcode === self::OPCODE_EOF) {
131
                            // If next segments are empty too, skip
132
                            $continue;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $continue seems to be never defined.
Loading history...
133
                        }
134 3
                        for ($i = $segWidth; $i > 0; $i -= 8) {
135 3
                            if ($segWidth == 255) {
136 1
                                $chunkSize = min($i, 8);
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkSize is dead and can be removed.
Loading history...
137
                            } else {
138 3
                                $chunkSize = 8;
139
                            }
140
141 3
                            $val = '';
142 3
                            if (! $compressed) {
143
                                $val = $buffer->readString(8);
144
                            } else {
145 3
                                $opcode = $this->readOpcode($buffer);
146
                                switch ($opcode) {
147 3
                                    case self::OPCODE_NOP;
148
                                        break 2;
149 3
                                    case self::OPCODE_EOF;
150
                                        throw new Exception(
151
                                            'Error reading data: unexpected end of compressed data file (cluster code 252)'
152
                                        );
153
                                        break 2;
154 3
                                    case self::OPCODE_RAW_DATA;
155 3
                                        $val = $buffer->readString(8);
156 3
                                        break;
157 3
                                    case self::OPCODE_WHITESPACES;
158 3
                                        $val = '        ';
159 3
                                        break;
160
                                }
161
                            }
162 3
                            $this->matrix[$case][$varNum] .= $val;
163
                        }
164 3
                        $this->matrix[$case][$varNum] = rtrim($this->matrix[$case][$varNum]);
165
                    }
166
                }
167 4
                $varNum++;
168
            }
169
        }
170 5
    }
171
172
    /**
173
     * @param Buffer $buffer
174
     * @return int
175
     */
176 4
    public function readOpcode(Buffer $buffer)
177
    {
178 4
        if ($this->opcodeIndex >= 8) {
179 4
            $this->opcodes = $buffer->readBytes(8);
0 ignored issues
show
Documentation Bug introduced by
It seems like $buffer->readBytes(8) can also be of type false. However, the property $opcodes is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
180 4
            $this->opcodeIndex = 0;
181
        }
182
183 4
        return 0xFF & $this->opcodes[$this->opcodeIndex++];
184
    }
185
186
    /**
187
     * @param Buffer $buffer
188
     */
189 5
    public function write(Buffer $buffer)
190
    {
191 5
        $buffer->writeInt(self::TYPE);
192 5
        $buffer->writeInt(0);
193
194 5
        if (! isset($buffer->context->variables)) {
195
            throw new \InvalidArgumentException('Variables required');
196
        }
197 5
        if (! isset($buffer->context->header)) {
198
            throw new \InvalidArgumentException('Header required');
199
        }
200 5
        if (! isset($buffer->context->info)) {
201
            throw new \InvalidArgumentException('Info required');
202
        }
203
204 5
        $compressed = $buffer->context->header->compression;
205 5
        $bias = $buffer->context->header->bias;
206 5
        $casesCount = $buffer->context->header->casesCount;
207
208
        /** @var Variable[] $variables */
209 5
        $variables = $buffer->context->variables;
210
211
        /** @var Record\Info[] $info */
212 5
        $info = $buffer->context->info;
213
214 5
        $veryLongStrings = [];
215 5
        if (isset($info[Record\Info\VeryLongString::SUBTYPE])) {
216 5
            $veryLongStrings = $info[Record\Info\VeryLongString::SUBTYPE]->toArray();
217
        }
218
219 5
        if (isset($info[Record\Info\MachineFloatingPoint::SUBTYPE])) {
220 5
            $sysmis = $info[Record\Info\MachineFloatingPoint::SUBTYPE]->sysmis;
221
        } else {
222
            $sysmis = NAN;
223
        }
224
225 5
        $dataBuffer = Buffer::factory('', ['memory' => true]);
226
227 5
        for ($case = 0; $case < $casesCount; $case++) {
228 4
            foreach ($variables as $index => $var) {
229 4
                $value = $this->matrix[$case][$index];
230
231
                // $isNumeric = $var->width == 0;
232 4
                $isNumeric = $var->width == 0 && \SPSS\Sav\Variable::isNumberFormat($var->write[1]);
233 4
                $width = isset($var->write[2]) ? $var->write[2] : $var->width;
234
235 4
                if ($isNumeric) {
236 2
                    if (! $compressed) {
237
                        $buffer->writeDouble($value);
238
                    } else {
239 2
                        if ($value == $sysmis || $value == "") {
240
                            $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_SYSMISS);
241 2
                        } elseif ($value >= 1 - $bias && $value <= 251 - $bias && $value == (int) $value) {
242 1
                            $this->writeOpcode($buffer, $dataBuffer, $value + $bias);
243
                        } else {
244 1
                            $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_RAW_DATA);
245 2
                            $dataBuffer->writeDouble($value);
246
                        }
247
                    }
248
                } else {
249 4
                    if (! $compressed) {
250
                        $buffer->writeString($value, Utils::roundUp($width, 8));
251
                    } else {
252 4
                        $offset = 0;
253 4
                        $width = isset($veryLongStrings[$var->name]) ? $veryLongStrings[$var->name] : $width;
254 4
                        $segmentsCount = Utils::widthToSegments($width);
255 4
                        for ($s = 0; $s < $segmentsCount; $s++) {
256 4
                            $segWidth = Utils::segmentAllocWidth($width, $s);
257 4
                            for ($i = $segWidth; $i > 0; $i -= 8) {
258 4
                                if ($segWidth == 255) {
259 1
                                    $chunkSize = min($i, 8);
260
                                } else {
261 4
                                    $chunkSize = 8;
262
                                }
263 4
                                $val = substr($value, $offset, $chunkSize);  // Read 8 byte segements, don't use mbsubstr here
264 4
                                if ($val == "") {
265 4
                                    $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_WHITESPACES);
266
                                } else {
267 4
                                    $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_RAW_DATA);
268 4
                                    $dataBuffer->writeString($val, 8);
269
                                }
270 4
                                $offset += $chunkSize;
271
                            }
272
                        }
273
                    }
274
                }
275
            }
276
        }
277
278 5
        $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_EOF);
279 5
    }
280
281
    /**
282
     * @param Buffer $buffer
283
     * @param Buffer $dataBuffer
284
     * @param int $opcode
285
     */
286 5
    public function writeOpcode(Buffer $buffer, Buffer $dataBuffer, $opcode)
287
    {
288 5
        if ($this->opcodeIndex >= 8 || $opcode == self::OPCODE_EOF) {
289 5
            foreach ($this->opcodes as $opc) {
290 4
                $buffer->write(chr($opc));
291
            }
292 5
            $padding = max(8 - count($this->opcodes), 0);
293 5
            for ($i = 0; $i < $padding; $i++) {
294 4
                $buffer->write(chr(self::OPCODE_NOP));
295
            }
296 5
            $this->opcodes = [];
297 5
            $this->opcodeIndex = 0;
298 5
            $dataBuffer->rewind();
299 5
            $buffer->writeStream($dataBuffer->getStream());
300 5
            $dataBuffer->truncate();
301
        }
302 5
        $this->opcodes[$this->opcodeIndex++] = 0xFF & $opcode;
303 5
    }
304
305
    /**
306
     * @return array
307
     */
308 5
    public function toArray()
309
    {
310 5
        return $this->matrix;
311
    }
312
}
313