-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathMyCSV.class.php
1160 lines (1084 loc) · 38.9 KB
/
MyCSV.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/*
* LICENSE
* 1. If you like to use this class for personal purposes, it's free.
* 2. For comercial purposes, please contact me (http://maettig.com/email).
* I'll send a license to you.
* 3. When you copy the framework you must copy this notice with the source
* code. You may alter the source code, but you have to put the original
* with your altered version.
* 4. The license is for all files included in this bundle.
*
* KNOWN BUGS/LIMITATIONS/TODO
* - seek(-1, SEEK_CUR) does not work!
* - sort("... id") doesn't work properly in all cases!
* - fetch_row/fetch_array() aren't supported, use fetch_assoc/each() instead.
* - num_fields() etc. aren't supported, use count($table->fields) instead.
* - create/create_table() is not supported, use add_field() instead.
* - What about some kind of GROUP BY?
* - Add where($filter = "field='n' OR strtolower(substr(r,0,1))='x'") becomes
* eval("$data['field']=='n' OR strtolower(substr($data['r'],0,1))=='x'");
*/
/**
* For compatibility with older PHP versions before 4.4.0.
*/
if (!defined('SORT_LOCALE_STRING'))
define('SORT_LOCALE_STRING', 5);
/**
* More special sorting type flags for use in MyCSV::sort().
*/
if (!defined('SORT_NAT'))
define('SORT_NAT', 16);
if (!defined('SORT_TIME'))
define('SORT_TIME', 17);
if (!defined('SORT_NULL'))
define('SORT_NULL', 32);
/**
* A text file based database complement.
*
* This class handles standard CSV or TXT text files as they where database
* tables. It supports most benefits of both SQL tables and PHP arrays. It
* doesn't need a real database management system nor does it require any
* knowlege of the SQL language. It hides all filesystem functions so you don't
* have to deal with file pointers, field delimiters, escape sequences and so
* on. Because it uses the widespreaded standard CSV file format you are able
* to create, read and update the tables using any spreadsheet software (e.g.
* Excel). It supports user defined table sort similar to ORDER BY, auto
* incremented ID numbers, limitation and joins similar to LIMIT and LEFT OUTER
* JOIN, it's binary safe (uses work arounds for all known fgetcsv() related
* bugs) and lots more.
*
* File format restrictions by design ("it's not a bug, it's a feature"):
* - The first line of the CSV file <b>must</b> contain the column names.
* - The CSV file <b>should</b> contain a column named "id". If this column is
* missing, it is added automatically. See {@link fields()}.
* - Some critical characters (NUL, double quotes, backslashes) are replaced or
* backslashed to make the resulting CSV file compatible to all PHP versions
* (all known versions do have one or more bugs in <code>fgetcsv()</code>).
* See {@link write()}.
*
* See {@link MyCSV()}, {@link dump()}, {@link limit()} or {@link join()} for
* some examples.
*
* Don't hesitate to report bugs or feature requests.
*
* @author Thiemo Mättig (http://maettig.com/)
* @version 2009-09-02
* @package TM
* @requires PHP 4.0.5 (array_search, strcoll)
*/
class MyCSV
{
/**
* Array containing all the table field names. First have to be "id".
*
* @var array
* @see add_field(), insert()
*/
var $fields = array("id");
/**
* Two dimensional associative array containing all the table row data.
*
* @var array
* @see data(), each()
*/
var $data = array();
/**
* The field delimiter for separating values in the CSV file. Default is ","
* (default CSV style). If not, the class tries to use ";" (European/German
* CSV style), "\t" (tabulator separated values), "\0", "|", "&" (URI
* encoded/parameter style), ":" (Unix /etc/passwd style) and " " (log file
* style). Normaly you don't have to touch this variable. Simply choose your
* delimiter when creating your initial CSV file.
*
* @var string Field delimiter.
*/
var $delimiter = ",";
/**
* @var int Last insert ID.
* @access private
* @see insert_id()
*/
var $insert_id = null;
/**
* File name of the CSV table with or without the .csv file name extension.
* Don't change this cause write() will not realize you want another file.
*
* @var string File name of the .csv file.
* @access private
* @see MyCSV(), read(), tablename(), write()
*/
var $filename = "";
/**
* @var bool Resource handle to the CSV file already opened.
* @access private
*/
var $_fp = false;
/**
* @var int Number of rows to be fetched, set by limit().
* @access private
* @see limit()
*/
var $_limitRows = null;
/**
* Reads a CSV file and returns it as a MyCSV object.
*
* Reads a table into a new MyCSV object. The file name may be entered with
* or without the <code>.csv</code> file extension. If the file does not
* exist it will be created when calling {@link write()}. Set <i>length</i>
* to the maximum number of bytes per row you expect (as you did in
* fgetcsv()). Default is 10000 bytes per line. Setting this to 1000 may
* speed up the method if you'r sure there is no longer line.
*
* For example, create a file called <code>table.csv</code> with the
* following content and call the script below.
*
* <pre>id,value
* 3,Example
* 4,Another value
* 7,Blue</pre>
*
* <pre><?php
* require_once("MyCSV.class.php");
* $table = new MyCSV("table");
* while ($row = $table->each()) {
* echo $row['id'] . " is " . $row['value'] . "<br>";
* }
* ?></pre>
*
* @param tablename string
* @param length int
* @return MyCSV
*/
function MyCSV($tablename = "", $length = 10000)
{
// Warning: Constructors can not return anything.
if ($tablename) $this->read($tablename, $length);
}
/**
* @param tablename string
* @param length int
* @return bool
* @access private
*/
function read($tablename, $length = 10000)
{
$this->filename = $tablename;
// Add default file extension if missing.
if (!preg_match('/\.\w+$/', $this->filename)) $this->filename .= ".csv";
// Break if the CSV file for this table does not exist.
if (!strstr($this->filename, "://") && !file_exists($this->filename))
return false;
if (!empty($GLOBALS['_MyCSV_locked'][$this->filename]))
{
user_error(
"MyCSV::read() failed, file $this->filename is open already",
E_USER_WARNING);
$this->filename = "";
return false;
}
$GLOBALS['_MyCSV_locked'][$this->filename] = true;
// Open the CSV file for exclusive reading and writing OR reading only.
if (is_writable($this->filename))
{
$this->_fp = @fopen($this->filename, "r+b");
}
// is_writable() may fail if Windows locked the file.
if (!$this->_fp) $this->_fp = fopen($this->filename, "rb");
if (!$this->_fp) return false;
if (!strstr($this->filename, "://")) flock($this->_fp, LOCK_EX);
$this->fields = fgetcsv($this->_fp, $length, $this->delimiter);
// Try some delimiters, but use the default if nothing was found.
$delimiters = str_replace($this->delimiter, "", ",;\t\0|&: ") . $this->delimiter;
while (count($this->fields) < 2)
{
$this->delimiter = $delimiters[0];
if (!$delimiters = substr($delimiters, 1)) break;
rewind($this->_fp);
$this->fields = fgetcsv($this->_fp, $length, $this->delimiter);
}
// On what position is the ID field? Returns $i = -1 if not found.
for ($i = count($this->fields) - 1; $i > -1; $i--)
{
if (strcasecmp($this->fields[$i], "id") == 0) break;
}
$lastId = 0;
$fieldsCount = count($this->fields);
while ($row = fgetcsv($this->_fp, $length, $this->delimiter))
{
// Add missing id numbers.
$id = isset($row[$i]) ? $row[$i] : $lastId + 1;
$lastId = max($id, $lastId);
$count = min($fieldsCount, count($row));
for ($c = 0; $c < $count; ++$c)
{
// Strip "smart" backslashes. This makes the CSV files
// binary-safe and compatible to PHP >=4.3.2 (which is when Ilia
// Alshanetsky started ruining fgetcsv).
$row[$c] = strtr($row[$c], array("\\\x7F" => "\x00",
"\\\x93" => '"',
'\\\\' => '\\'));
$this->data[$id][$this->fields[$c]] = $row[$c];
}
}
// Always move the id column to the front.
unset($this->fields[$i]);
array_unshift($this->fields, "id");
return true;
}
/**
* Adds a new field (column) to the table. Returns false on failure, e.g.
* if the field already exists.
*
* @param field string
* @param afterField string
* @return bool
* @see insert(), drop_field()
*/
function add_field($field, $afterField = null)
{
// Break if the field name contains invalid characters or already exists.
if (!preg_match('/^[\w\x7F-\xFF]+$/is', $field) || in_array($field, $this->fields))
{
return false;
}
if (isset($afterField) && in_array($afterField, $this->fields))
{
$newFields = array();
foreach ($this->fields as $oldField)
{
$newFields[] = $oldField;
if (strcasecmp($oldField, $afterField) == 0) $newFields[] = $field;
}
$this->fields = $newFields;
}
else $this->fields[] = $field;
return true;
}
/**
* Moves the internal row pointer to the specified row number. This is an
* alias for <code>{@link seek}(<i>row_number</i>, SEEK_SET)</code>.
*
* @param row_number int
* @return bool
*/
function data_seek($row_number)
{
return $this->seek($row_number, SEEK_SET);
}
/**
* Deletes a table row specified by the <i>id</i>. Deletes all rows if no
* <i>id</i> is given.
*
* @param id mixed
* @return void
*/
function delete($id = null)
{
// If delete(array('id' => 3)) is called, delete row 3.
if (is_array($id) && isset($id['id'])) $id = $id['id'];
if (isset($id))
{
// Delete one row if a valid id is given.
if (!is_array($id)) unset($this->data[$id]);
}
else
{
// Delete all rows if no id was given (or id is null).
$this->data = array();
// Do not reset the ID numbers cause they where used already.
++$this->insert_id;
}
}
/**
* Deletes a field/column from the table.
*
* Returns false on failure, e.g. if <i>field</i> does not exists. Rewinds
* the internal array pointer to the first element on success.
*
* @param field string
* @return bool
*/
function drop_field($field)
{
if (is_array($field) || strcasecmp($field, "id") == 0) return false;
$offset = array_search($field, $this->fields);
if ($offset === false || $offset === null) return false;
array_splice($this->fields, $offset, 1);
while (list($id) = each($this->data)) unset($this->data[$id][$field]);
reset($this->data);
return true;
}
/**
* Clears the table. Remove all columns and all fields too.
*
* @return void
*/
function drop_table()
{
$this->fields = array("id");
$this->data = array();
$this->insert_id = null;
}
/**
* Gets the current data row and increase the internal pointer. This is an
* alias for {@link each()}.
*
* @return array
*/
function fetch_assoc()
{
return $this->each();
}
/**
* Inserts a new table row using the next free auto incremented ID number.
*
* @param data array
* @return void
*/
function insert($data)
{
if (!is_array($data)) return false;
// If data contains an unused id number, use it.
if (isset($data['id']) && strlen($data['id']))
{
$this->insert_id = $data['id'];
}
// First auto increment id is always 1, but only for the initial row.
elseif (!isset($this->insert_id) && empty($this->data))
{
$this->insert_id = 1;
}
// Don't use ++ because "x"++ returns "y" and that's not what we want.
if (isset($this->data[$this->insert_id])) $this->insert_id += 1;
if (!isset($this->insert_id) || isset($this->data[$this->insert_id]))
{
$this->insert_id = max(array_keys($this->data)) + 1;
}
$this->data[$this->insert_id] = $data;
// Fetch missing field/column names from the first data row if needed.
// This can be used instead of add_field().
if (empty($this->fields) || count($this->fields) < 2)
{
unset($data['id']);
$this->fields = array_merge(array("id"), array_keys($data));
}
}
/**
* Gets the ID generated from the previous insert() call.
*
* @return int
*/
function insert_id()
{
return isset($this->insert_id) ? $this->insert_id : false;
}
/**
* Performs a left outer join with another table.
*
* The tables are merged using a foreign key of the left table and the
* primary key of the right table. This adds temporary columns to the left
* table (temporary means, they aren't stored using {@link write()}). A
* slightly complex example:
*
* <pre>echo "<pre>";
* $rightTable = new MyCSV();
* $rightTable->insert(array('id' => 7, 'color' => "red"));
* $rightTable->insert(array('id' => 8, 'color' => "yellow"));
* $rightTable->dump();
* echo "\n";
* $leftTable = new MyCSV();
* $leftTable->insert(array('thing' => "Table", 'color_id' => 7));
* $leftTable->insert(array('thing' => "Chair", 'color_id' => 8));
* $leftTable->insert(array('thing' => "Lamp", 'color_id' => 7));
* $leftTable->dump();
* echo "\n";
* $leftTable->join($rightTable, "color_id");
* while ($row = $leftTable->each()) {
* echo $row['thing'] . " is " . $row['color'] . "\n";
* }</pre>
*
* @param rightTable array
* @param foreignKey string
* @return void
*/
function join(&$rightTable, $foreignKey)
{
if (is_array($rightTable)) $rightData = $rightTable;
else
{
$rightData = $rightTable->data;
// If filename is empty, prefix is empty too and not used below.
$prefix = preg_replace('/\.\w+$/', '', basename($rightTable->filename));
}
reset($this->data);
while (list($id) = each($this->data))
{
if (strcasecmp($foreignKey, "id") == 0) $fid = $id;
else $fid = $this->data[$id][$foreignKey];
if (isset($rightData[$fid]))
{
// Right table is modified here and used as some kind of cache.
if (!empty($prefix) && !isset($rightData[$fid][$prefix . ".id"]))
{
foreach ($rightData[$fid] as $field => $value)
{
$rightData[$fid][$prefix . "." . $field] = &$rightData[$fid][$field];
}
}
// Duplicate keys are used from the left (original) table.
$this->data[$id] += $rightData[$fid];
}
}
// Reset the internal pointer.
reset($this->data);
}
/**
* Limits the number of rows to be fetched.
*
* Use <code>limit(2)</code> to fetch the first two rows only when calling
* {@link each()} (or {@link fetch_assoc()}). Use <code>limit(2, $id)</code>
* to fetch the next two rows, where <code>$id</code> is calculated using
* <code>{@link first()}</code> for the first page and using <code>{@link
* next}($id, 2)</code>, <code>next($id, 4)</code> and so on for all other
* pages. Example:
*
* <pre>$table = new MyCSV("table");
* for ($i = 10; $i < 21; $i++) {
* $table->insert(array('text' => "Text $i"));
* }
* // Order the table first because limit() depends on this.
* $table->sort("text DESC");
* // Limit to 5 rows starting from a specific id.
* $rows = 5;
* $id = isset($_REQUEST['id']) ? $_REQUEST['id'] : $table->first();
* $table->limit($rows, $id);
* while ($row = $table->each()) {
* echo "ID $row[id]: $row[text]<br>";
* }
* // Calculate and display the link targets for paging.
* $first = $table->first();
* $prev = $table->prev($id, $rows);
* $next = $table->next($id, $rows);
* $last = $table->prev($table->last(), ($table->count() - 1) % $rows);
* if (strcmp($first, $id)) echo "<a href=\"$PHP_SELF?id=$first\">First</a> ";
* if ($prev) echo "<a href=\"$PHP_SELF?id=$prev\">Prev</a> ";
* if ($next) echo "<a href=\"$PHP_SELF?id=$next\">Next</a> ";
* if (strcmp($last, $id)) echo "<a href=\"$PHP_SELF?id=$last\">Last</a>";</pre>
*
* Call <code>limit()</code> (or <code>limit(0)</code> or something like
* that) to reset the limitation.
*
* <i>Warning! The limitation has no effect on {@link delete()},
* {@link update()} and so on! All following method calls like {@link sort()}
* or {@link join()} that {@link seek sets} or {@link reset resets} the
* internal pointer will change the starting ID (but not the number of rows)
* set by limit().</i>
*
* @param rows int
* @param id mixed
* @param whence int
* @return bool
* @see seek()
*/
function limit($rows = null, $id = null, $whence = null)
{
// Number of rows < 1 resets the limitation.
$this->_limitRows = $rows > 0 ? $rows : null;
return isset($id) ? $this->seek($id, $whence) : $this->reset();
}
/**
* Gets the number of rows in the table.
*
* @return int
* @see count()
*/
function num_rows()
{
return count($this->data);
}
/**
* Gets the table name without the default .csv file extension.
*
* The path returned can be used in {@link MyCSV()} without any change.
* Directories are not removed from the string, if present.
*
* @return string
*/
function tablename()
{
return preg_replace('{^\./|\.csv$}', '', $this->filename);
}
/**
* Updates a table row with some new field/value pairs.
*
* Examples:
*
* <pre>$table->update(array(...), 3);
* $table->update(array('id' => 3, ...));
* $table->update(array('id' => 7, ...), 3); // Moves ID 3 to ID 7</pre>
*
* @param data array
* @param id mixed
* @return bool
*/
function update($data, $id = null)
{
if (!is_array($data)) return false;
// update(array(...)) without an ID doesn't make sense.
if (!isset($data['id']) && !isset($id)) return false;
// update(array(...), 3) becomes update(array('id' => 3, ...), 3)
if (!isset($data['id'])) $data['id'] = $id;
// update(array('id' => 7, ...)) becomes update(array('id' => 7, ...), 7)
elseif (!isset($id)) $id = $data['id'];
// update(array('id' => 7, ...), 3) if forbidden if ID 7 already exists.
elseif (strcmp($data['id'], $id) != 0 && isset($this->data[$data['id']]))
{
return false;
}
// Duplicate keys will be used from the new row. Due to the cast
// update() does an insert() if required but will cause a warning.
$this->data[$data['id']] = $data + (array)$this->data[$id];
// update(array('id' => 7, ...), 3) moves ID 3 to 7, so ID 3 is killed.
if (strcmp($data['id'], $id) != 0) unset($this->data[$id]);
return true;
}
/**
* Gets the number of rows in the table. This is an alias for
* {@link num_rows()}.
*
* @return int
*/
function count()
{
return count($this->data);
}
/**
* Gets the current data row and increases the internal pointer. See
* {@link MyCSV()} for an example.
*
* @return array
*/
function each()
{
// Don't return more rows if the limit() is reached.
if (isset($this->_limitRows) && --$this->_limitRows < 0) return false;
if (!list($id, $data) = each($this->data)) return false;
return array('id' => $id) + $data;
}
/**
* Sets the internal pointer to the last data row. Returns the last data
* row.
*
* @return array
* @see reset(), last()
*/
function end()
{
return end($this->data);
}
/**
* Checks if the data row specified by the ID exists.
*
* @param id mixed
* @return bool
* @see row_exists()
*/
function id_exists($id)
{
return isset($this->data[$id]);
}
/**
* Gets an array containing all the IDs of the table.
*
* @return array
* @see min(), max(), first(), last(), prev(), next(), rand()
*/
function ids()
{
return array_keys($this->data);
}
/**
* Sorts the table rows by ID. This is identical to
* <code>{@link sort}("id")</code> but a bit faster.
*
* @param sort_flags int
* @return void
*/
function ksort($sort_flags = 0)
{
return ksort($this->data, $sort_flags);
}
/**
* Sorts the table rows by ID in reverse order. This is identical to
* <code>{@link sort}("id DESC")</code> but a bit faster.
*
* @param sort_flags int
* @return void
*/
function krsort($sort_flags = 0)
{
return krsort($this->data, $sort_flags);
}
/**
* Gets the smallest ID number used in the table. Typically, this is 1.
*
* @return int
*/
function min()
{
if (!$this->data) return false;
return min(array_keys($this->data));
}
/**
* Gets the biggest ID number used in the table. This is often the same as
* {@link insert_id()} which returns the last inserted ID. But unlike that,
* max() doesn't depend on a previous call of {@link insert()}.
*
* @return int
*/
function max()
{
if (!$this->data) return false;
return max(array_keys($this->data));
}
/**
* Gets the first ID number from the table. This depends on how's the table
* sorted and isn't identical to {@link min()} in all cases.
*
* @return int
* @see last(), prev(), reset()
*/
function first()
{
if (!$this->data) return false;
return array_shift(array_keys($this->data));
}
/**
* Gets the last ID number used in the table. This depends on how's the
* table sorted and isn't identical to {@link max()} in all cases.
*
* @return int
* @see first(), next(), end()
*/
function last()
{
if (!$this->data) return false;
return array_pop(array_keys($this->data));
}
/**
* Gets the previous ID number. Use <i>offset</i> to get another ID near to
* the row specified by <i>id</i>. Default is 1 (one backward). Returns
* false if there is no row at this position.
*
* @param id mixed
* @param offset int
* @return int
* @see next(), first()
*/
function prev($id, $offset = 1)
{
return $this->next($id, -$offset);
}
/**
* Gets the next ID number. Use <i>offset</i> to get another ID near to the
* row specified by <i>id</i>. Default is 1 (one forward). Returns false if
* there is no row at this position.
*
* @param id mixed
* @param offset int
* @return int
* @see prev(), last()
*/
function next($id, $offset = 1)
{
$ids = array_keys($this->data);
//- Add sort(ids) to return the nearest smaller/bigger ID numbers.
$i = array_search($id, $ids) + $offset;
return isset($ids[$i]) ? $ids[$i] : false;
}
/**
* Picks one or more random ID numbers out of the table.
*
* @param num_req int
* @return int
* @see ids()
*/
function rand($num_req = 1)
{
return empty($this->data) ? false : array_rand($this->data, $num_req);
}
/**
* Sets the internal pointer to the first data row. Returns the first data
* row.
*
* @return array
* @see end(), each(), first()
*/
function reset()
{
return reset($this->data);
}
/**
* Looks if a data row is already in the table.
*
* @param search array
* @return bool
* @see id_exists()
*/
function row_exists($search)
{
reset($this->data);
// foreach() destroyed the array in PHP 5.2.5.
while (list($id, $row) = each($this->data))
{
reset($search);
while (list($key, $value) = each($search))
{
if (!isset($row[$key]) || $row[$key] != $value) continue 2;
}
return true;
}
reset($this->data);
return false;
}
/**
* Orders the table rows by one or more columns.
*
* Sorting order flags:
* - ASC or SORT_ASC - Sort in ascending order (default).
* - DESC or SORT_DESC - Sort in descending order.
*
* Sorting type flags:
* - SORT_REGULAR - Compare items normally (default).
* - SORT_NUMERIC - Compare items numerically.
* - SORT_STRING - Compare items as strings.
* - SORT_LOCALE_STRING - Compare items as strings, based on the current
* locale. Don't forget to use setlocale() before.
* - SORT_NAT - Compare items using a "natural order" algorithm.
* - SORT_TIME - Compare items as date and time values. This uses
* strtotime() to convert the strings from the CSV file (everything in a
* CSV file is a string) into timestamps and compares the timestamps.
*
* Special condition flag: SORT_NULL - Move empty elements to the end.
*
* No two sorting flags of the same type can be specified after each field.
* Some examples:
*
* <pre>setlocale(LC_ALL, "de_DE@euro", "de_DE", "deu_deu");
* $table->sort("a, b DESC");
* $table->sort("a b DESC"); // Same as above
* $table->sort("a", "b", SORT_DESC); // Same as above
* $table->sort("a SORT_LOCALE_STRING SORT_NULL b SORT_NULL");
* $table->sort("a SORT_NAT, b SORT_NAT, c");</pre>
*
* @param sort_flags mixed
* @return void
*/
function sort($sort_flags)
{
// sort() can be called using array_multisort()-like multiple
// parameters or a SQL-like string argument.
if (func_num_args() > 1) $sort_flags = func_get_args();
else $sort_flags = preg_split('/[,\s]+/s', trim($sort_flags));
// trim(..., ", \t\n\r;") would be better but works in PHP 4.1.0+ only.
// Reset the _cmpFields array first.
$this->_cmpFields = array();
$p = -1;
// Calculate the _cmpFields array for use in _cmp().
foreach ($sort_flags as $f)
{
$f = preg_replace('/^(A|DE)SC$/i', 'SORT_\0', $f);
// Always use the integer values of predefined constants if available.
if (defined(strtoupper($f))) $f = constant(strtoupper($f));
// Ignore ascending order but store everything else in the associative array.
if ($f == SORT_ASC) continue;
elseif ($f == SORT_DESC) $this->_cmpFields[$p]['order'] = -1;
elseif (is_int($f)) $this->_cmpFields[$p]['type'] |= $f;
else
{
++$p;
$this->_cmpFields[] = array('field' => $f, 'order' => 1, 'type' => 0);
}
}
if (strcasecmp($this->_cmpFields[0]['field'], "id") == 0)
{
if ($this->_cmpFields[0]['order'] > 0) ksort($this->data);
else krsort($this->data);
}
else
{
// Call uasort() using the _cmp() function in the class.
uasort($this->data, array(&$this, '_cmp'));
}
// Reset the internal pointer.
reset($this->data);
}
/**
* Callback for use in uasort() called in sort().
*
* @param a array
* @param b array
* @return bool
* @access private
*/
function _cmp(&$a, &$b)
{
foreach ($this->_cmpFields as $f)
{
// Using this sorting type, empty elements always move to the end.
if ($f['type'] & SORT_NULL)
{
if (strlen($a[$f['field']]) <= 0 || strlen($b[$f['field']]) <= 0)
$f['order'] = -1;
}
switch ($f['type'] & ~SORT_NULL)
{
case SORT_NUMERIC:
// Take both arguments as numbers and return their difference.
$result = ($a[$f['field']] - $b[$f['field']]) * $f['order'];
break;
case SORT_STRING:
// Take both arguments as strings and use strcasecmp() for comparing.
$result = strcasecmp($a[$f['field']], $b[$f['field']]) * $f['order'];
break;
case SORT_LOCALE_STRING:
// Locale based string comparison.
$result = strcoll(strtolower($a[$f['field']]), strtolower($b[$f['field']])) * $f['order'];
break;
case SORT_NAT:
$result = strnatcasecmp($a[$f['field']], $b[$f['field']]) * $f['order'];
break;
case SORT_TIME:
$result = (strtotime($a[$f['field']]) - strtotime($b[$f['field']])) * $f['order'];
break;
default:
// By default, thrust in PHP's automatic type conversion.
$result = ($a[$f['field']] == $b[$f['field']]) ? 0 :
($a[$f['field']] > $b[$f['field']] ? $f['order'] : -$f['order']);
break;
}
// Continue (and maybe return 0) if both arguments are equal.
if ($result != 0)
return $result;
}
}
/**
* Gets a table row including their ID number. Returns false if the row does
* not exist.
*
* @param id mixed
* @return array
*/
function data($id)
{
return isset($this->data[$id]) ? array('id' => $id) + $this->data[$id]
: false;
}
/**
* Dumps the table to screen.
*
* Example:
*
* <pre><?php
* require_once("MyCSV.class.php");
* $table = new MyCSV("people");
* $table->insert(array('name' => "Adam", 'age' => 23));
* $table->insert(array('name' => "Bill", 'age' => 19));
* echo "<pre>";
* $table->dump();
* ?></pre>
*
* @return void
* @see export()
*/
function dump()
{
echo $this->export();
}
/**
* Checks if the CSV file for this table already exists.
*
* @return bool
*/
function exists()
{
return file_exists($this->filename);
}
/**
* Returns a complete CSV dump of the table.
*
* @return string
* @see write(), dump()
*/
function export()
{
$count_fields = count($this->fields);
$tr_from = array('"', "\x00");
$tr_to = array('""', "\\\x7F");
$csv = implode($this->delimiter, $this->fields) . "\r\n";
reset($this->data);
while (list($id, $row) = each($this->data))
{
if (strpos($id, $this->delimiter) === false &&
strpos($id, '"') === false)
{
$csv .= $id;
}
else
{
$csv .= '"' . str_replace('"', '""', $id) . '"';
}
for ($c = 1; $c < $count_fields; ++$c)
{