1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SPSS\Sav; |
4
|
|
|
|
5
|
|
|
use SPSS\Buffer; |
6
|
|
|
use SPSS\Exception; |
7
|
|
|
use SPSS\Sav\Record\Info; |
8
|
|
|
use SPSS\Utils; |
9
|
|
|
|
10
|
|
|
class Writer |
11
|
|
|
{ |
12
|
|
|
/** |
13
|
|
|
* @var Record\Header |
14
|
|
|
*/ |
15
|
|
|
public $header; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var Record\Variable[] |
19
|
|
|
*/ |
20
|
|
|
public $variables = []; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var Record\ValueLabel[] |
24
|
|
|
*/ |
25
|
|
|
public $valueLabels = []; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var Record\Document |
29
|
|
|
*/ |
30
|
|
|
public $document; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var InfoRecordSet |
34
|
|
|
*/ |
35
|
|
|
private $info = []; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var Record\Data |
39
|
|
|
*/ |
40
|
|
|
public $data; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var Buffer |
44
|
|
|
*/ |
45
|
|
|
protected $buffer; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Writer constructor. |
49
|
|
|
* |
50
|
|
|
* @param array $data |
51
|
|
|
* @throws \Exception |
52
|
|
|
*/ |
53
|
|
|
public function __construct($data = []) |
54
|
|
|
{ |
55
|
|
|
$this->buffer = Buffer::factory(); |
56
|
|
|
$this->buffer->context = $this; |
57
|
|
|
|
58
|
|
|
$this->info = new InfoRecordSet(); |
59
|
|
|
if (! empty($data)) { |
60
|
|
|
$this->write($data); |
61
|
|
|
} |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @param array $data |
66
|
|
|
* @throws \Exception |
67
|
|
|
*/ |
68
|
|
|
public function write($data) |
69
|
|
|
{ |
70
|
|
|
$this->header = new Record\Header($data['header']); |
71
|
|
|
$this->header->nominalCaseSize = 0; |
72
|
|
|
$this->header->casesCount = 0; |
73
|
|
|
|
74
|
|
|
$this->info->set($this->prepareInfoRecord( |
75
|
|
|
Record\Info\MachineInteger::class, |
76
|
|
|
$data |
77
|
|
|
)); |
78
|
|
|
|
79
|
|
|
$this->info->set($this->prepareInfoRecord( |
80
|
|
|
Record\Info\MachineFloatingPoint::class, |
81
|
|
|
$data |
82
|
|
|
)); |
83
|
|
|
|
84
|
|
|
$this->info->set(new Record\Info\VariableDisplayParam()); |
85
|
|
|
$this->info->set(new Record\Info\LongVariableNames()); |
86
|
|
|
$this->info->set(new Record\Info\VeryLongString()); |
87
|
|
|
$this->info->set($this->prepareInfoRecord( |
88
|
|
|
Record\Info\ExtendedNumberOfCases::class, |
89
|
|
|
$data |
90
|
|
|
)); |
91
|
|
|
$this->info->set(new Record\Info\VariableAttributes()); |
92
|
|
|
$this->info->set(new Record\Info\LongStringValueLabels()); |
93
|
|
|
$this->info->set(new Record\Info\LongStringMissingValues()); |
94
|
|
|
$this->info->set(new Record\Info\CharacterEncoding('UTF-8')); |
95
|
|
|
|
96
|
|
|
$this->data = new Record\Data(); |
97
|
|
|
|
98
|
|
|
$nominalIdx = 0; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @var bool[string] The variable names used in this SPSS file |
102
|
|
|
*/ |
103
|
|
|
$variableNames = []; |
104
|
|
|
/** @var Variable $var */ |
105
|
|
|
foreach (array_values($data['variables']) as $idx => $var) { |
106
|
|
|
if (!$var instanceof Variable) { |
107
|
|
|
throw new \InvalidArgumentException('Variables must be instance of ' . Variable::class); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
$variable = new Record\Variable(); |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @see \SPSS\Sav\Record\Variable::getSegmentName() |
114
|
|
|
* |
115
|
|
|
* Variable names in the SPSS file should be unique. If they are not, SPSS will rename them. |
116
|
|
|
* If SPSS renames them and it happens to be a long string then the segments will no longer share |
117
|
|
|
* the required prefix in the name. |
118
|
|
|
*/ |
119
|
|
|
$name = strtoupper(substr($var->getName(), 0, 8)); |
120
|
|
|
|
121
|
|
|
$counter = 0; |
122
|
|
|
/** |
123
|
|
|
* Using base convert we can encode 36^3 = 46656 variables with a common 5 character prefix in an 8 |
124
|
|
|
* character variable name. This should suffice since the current variable limit of SPSS is 32767 |
125
|
|
|
* variables. |
126
|
|
|
*/ |
127
|
|
|
while (isset($variableNames[$name])) { |
128
|
|
|
$name = strtoupper(substr($var->getName(), 0, 5) . base_convert($counter, 10, 36)); |
129
|
|
|
$counter++; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$variableNames[$name] = true; |
133
|
|
|
$variable->name = $name; |
134
|
|
|
|
135
|
|
|
if ($var->format == Variable::FORMAT_TYPE_A) { |
136
|
|
|
$variable->width = $var->getWidth(); |
137
|
|
|
} else { |
138
|
|
|
$variable->width = 0; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
$variable->label = $var->label; |
142
|
|
|
$variable->print = [ |
143
|
|
|
0, |
144
|
|
|
$var->format, |
145
|
|
|
min($var->getWidth(), 255), |
146
|
|
|
$var->decimals, |
147
|
|
|
]; |
148
|
|
|
$variable->write = [ |
149
|
|
|
0, |
150
|
|
|
$var->format, |
151
|
|
|
min($var->getWidth(), 255), |
152
|
|
|
$var->decimals, |
153
|
|
|
]; |
154
|
|
|
|
155
|
|
|
// TODO: refactory |
156
|
|
|
$shortName = $variable->name; |
157
|
|
|
$longName = $var->getName(); |
158
|
|
|
|
159
|
|
|
if ($var->attributes) { |
|
|
|
|
160
|
|
|
$this->info[Record\Info\VariableAttributes::SUBTYPE][$longName] = $var->attributes; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
if ($var->missing) { |
|
|
|
|
164
|
|
|
if ($var->getWidth() <= 8) { |
165
|
|
|
if (count($var->missing) >= 3) { |
166
|
|
|
$variable->missingValuesFormat = 3; |
167
|
|
|
} elseif (count($var->missing) == 2) { |
168
|
|
|
$variable->missingValuesFormat = -2; |
169
|
|
|
} else { |
170
|
|
|
$variable->missingValuesFormat = 1; |
171
|
|
|
} |
172
|
|
|
$variable->missingValues = $var->missing; |
173
|
|
|
} else { |
174
|
|
|
$this->info[Record\Info\LongStringMissingValues::SUBTYPE][$shortName] = $var->missing; |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
$this->variables[$idx] = $variable; |
179
|
|
|
|
180
|
|
|
if ($var->values) { |
|
|
|
|
181
|
|
|
if ($variable->width > 8) { |
182
|
|
|
$this->info[Record\Info\LongStringValueLabels::SUBTYPE][$longName] = [ |
183
|
|
|
'width' => $var->getWidth(), |
184
|
|
|
'values' => $var->values, |
185
|
|
|
]; |
186
|
|
|
} else { |
187
|
|
|
$valueLabel = new Record\ValueLabel([ |
188
|
|
|
'variables' => $this->variables, |
189
|
|
|
]); |
190
|
|
|
foreach ($var->values as $key => $value) { |
191
|
|
|
$valueLabel->labels[] = [ |
192
|
|
|
'value' => $key, |
193
|
|
|
'label' => $value, |
194
|
|
|
]; |
195
|
|
|
$valueLabel->indexes = [$nominalIdx + 1]; |
196
|
|
|
} |
197
|
|
|
$this->valueLabels[] = $valueLabel; |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
$this->info[Record\Info\LongVariableNames::SUBTYPE][$shortName] = $var->getName(); |
202
|
|
|
|
203
|
|
|
if (Record\Variable::isVeryLong($var->getWidth())) { |
204
|
|
|
$this->info[Record\Info\VeryLongString::SUBTYPE][$shortName] = $var->getWidth(); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$segmentCount = Utils::widthToSegments($var->getWidth()); |
208
|
|
|
for ($i = 0; $i < $segmentCount; $i++) { |
209
|
|
|
$this->info[Record\Info\VariableDisplayParam::SUBTYPE][] = [ |
210
|
|
|
$var->getMeasure(), |
211
|
|
|
$var->getColumns(), |
212
|
|
|
$var->getAlignment(), |
213
|
|
|
]; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
// TODO: refactory |
217
|
|
|
$dataCount = count($var->data); |
218
|
|
|
if ($dataCount > $this->header->casesCount) { |
219
|
|
|
$this->header->casesCount = $dataCount; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
foreach ($var->data as $case => $value) { |
223
|
|
|
$this->data->matrix[$case][$idx] = $value; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
$nominalIdx += $var->getOcts(); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$this->header->nominalCaseSize = $nominalIdx; |
230
|
|
|
|
231
|
|
|
// write header |
232
|
|
|
$this->header->write($this->buffer); |
233
|
|
|
|
234
|
|
|
// write variables |
235
|
|
|
foreach ($this->variables as $variable) { |
236
|
|
|
$variable->write($this->buffer); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
// write valueLabels |
240
|
|
|
foreach ($this->valueLabels as $valueLabel) { |
241
|
|
|
$valueLabel->write($this->buffer); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
// write documents |
245
|
|
|
if (! empty($data['documents'])) { |
246
|
|
|
$this->document = new Record\Document([ |
247
|
|
|
'lines' => $data['documents'], |
248
|
|
|
] |
249
|
|
|
); |
250
|
|
|
$this->document->write($this->buffer); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
foreach ($this->info as $info) { |
254
|
|
|
$info->write($this->buffer); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$this->data->write($this->buffer); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @param $file |
262
|
|
|
* @return false|int |
263
|
|
|
*/ |
264
|
|
|
public function save($file) |
265
|
|
|
{ |
266
|
|
|
return $this->buffer->saveToFile($file); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* @return \SPSS\Buffer |
271
|
|
|
*/ |
272
|
|
|
public function getBuffer() |
273
|
|
|
{ |
274
|
|
|
return $this->buffer; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* @param string $className |
279
|
|
|
* @param array $data |
280
|
|
|
* @param string $group |
281
|
|
|
* @return array |
282
|
|
|
* @throws Exception |
283
|
|
|
*/ |
284
|
|
|
private function prepareInfoRecord($className, $data, $group = 'info'): Info |
285
|
|
|
{ |
286
|
|
|
if (! class_exists($className)) { |
287
|
|
|
throw new Exception('Unknown class'); |
288
|
|
|
} |
289
|
|
|
$key = lcfirst(substr($className, strrpos($className, '\\') + 1)); |
290
|
|
|
|
291
|
|
|
return new $className( |
|
|
|
|
292
|
|
|
isset($data[$group]) && isset($data[$group][$key]) ? |
293
|
|
|
$data[$group][$key] : |
294
|
|
|
[] |
295
|
|
|
); |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.