Guest User

Untitled

a guest
Feb 20th, 2018
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.45 KB | None | 0 0
  1. <?php
  2. /** Utility class to write complete DBF files. This is not a database interaction
  3. * layer, it treats the database file as an all-at-once output.
  4. * <pre>
  5. * Schemas:
  6. * schemas are supplied to this library as php arrays as follows:
  7. * array(
  8. * 'name' => 'COLUMN B'
  9. * 'type' => 'C',
  10. * 'size' => 4,
  11. * 'declength' => 2,
  12. * 'NOCPTRANS' => true,
  13. * );
  14. *
  15. * 'name': column name, 10 characters max, padded with null bytes if less than 10, truncated if more than 10
  16. * 'type' : a single character representing the DBF field type, the following are supported:
  17. * 'C': character data stored as 8 bit ascii
  18. * 'N': numeric character data stored as 8 bit ascii, when 'declength' is
  19. * present, declength + 1 bytes are consumed to store the decimal values
  20. * 'D': 8 character date specification (YYYYMMDD)
  21. * always 8 bytes
  22. * 'L': single character boolean, 'T' or 'F' or a space ' ' for unintialized
  23. * always 1 byte
  24. * 'T': 8 byte binary packed date stamp and milliseconds stamp
  25. * first four bytes: integer representing days since Jan 1 4713 (julian calendar)
  26. * second four bytes: integer milliseconds ellapsed since prior midnight
  27. * always 8 bytes
  28. *
  29. * 'size': number of bytes this field occupies
  30. * 'declength': only applicable to type 'N', indicates how many of the alloted spaces are for
  31. * decimal places and the decimal point
  32. * 'NOCPTRANS': Specifies the fields that should not be translated to another code page.
  33. *
  34. * Records:
  35. * records are supplied as arrays keyed by column name with values conforming to these type specs:
  36. * 'C': character data, will be truncated at field size, padded with spaces on the right to field size
  37. * 'N': numeric character data, will be truncated at field size,
  38. * padded with spaces on the left to field size converts numeric
  39. * native types to numeric character data automatically
  40. * 'D': accepts a unix timestamp, a structure resembling
  41. * <code>getdate()</code>'s return format, or a 8 length string
  42. * containing YYYYMMDD
  43. * 'L': accepts and converts ('T' and true) to 'T', ('F' and false) to
  44. * 'F' and everything else to ' '
  45. * 'T': accepts a unix timestamp, a structure resembling
  46. * <code>getdate()</code>'s return format or an array containing
  47. * 'jd' => (julian date representation)
  48. * 'js' => milleseconds elapsed since prior midnight
  49. * </pre>
  50. */
  51. class DBF {
  52. // utility function to ouput a string of binary digits for inspection
  53. private static function binout($bin) {
  54. echo "data:", $bin, "<br />";
  55. foreach(unpack('C*', $bin) as $byte) {
  56. printf('%b', $byte);
  57. }
  58. echo "<br />";
  59. return $bin;
  60. }
  61.  
  62. /** Writes a DBF file to the provided location {@link $filename}, with a given
  63. * {@link $schema} containing the DBF formatted <code>$records</code>
  64. * marked with the 'last updated' mark <code>$date</code> or a current timestamp if last
  65. * update is not provided.
  66. * @see DBF
  67. * @param string $filename a writable path to place the DBF
  68. * @param array $schema an array containing DBF field specifications for each
  69. * field in the DBF file (see <code>class DBF</code documentation)
  70. * @param array $records an array of fields given in the same order as the
  71. * field specifications given in the schema
  72. * @param array $date an array matching the return structure of <code>getdate()</code>
  73. * or null to use the current timestamp
  74. */
  75. public static function write($filename, array $schema, array $records, $date=null) {
  76. file_put_contents($filename, self::getBinary($schema, $records, $date));
  77. }
  78.  
  79. /** Gets the DBF file as a binary string
  80. * @see DBF::write()
  81. * @return string a binary string containing the DBF file.
  82. */
  83. public static function getBinary(array $schema, array $records, $pDate) {
  84. if (is_numeric($pDate)) {
  85. $date = getDate($pDate);
  86. } elseif ($pDate == null) {
  87. $date = getDate();
  88. } else {
  89. $date = $pDate;
  90. }
  91. return self::makeHeader($date, $schema, $records) . self::makeSchema($schema) . self::makeRecords($schema, $records);
  92. }
  93.  
  94. /** Convert a unix timestamp, or the return structure of the <code>getdate()</code>
  95. * function into a (binary) DBF timestamp.
  96. * @param mixed $date a unix timestamp or the return structure of the <code>getdate()</code>
  97. * @param number $milleseconds the number of milleseconds elapsed since
  98. * midnight on the day before the date in question. If omitted a second
  99. * accurate rounding will be constructed from the $date parameter
  100. * @return string a binary string containing the DBF formatted timestamp
  101. */
  102. public static function toTimeStamp($date, $milleseconds = null) {
  103. if (is_array($date)) {
  104. if (isset($date['jd'])) {
  105. $jd = $date['jd'];
  106. }
  107.  
  108. if (isset($date['js'])) {
  109. $js = $date['js'];
  110. }
  111. }
  112.  
  113. if (!isset($jd)) {
  114. $pDate = self::toDate($date);
  115. $year = substr($pDate, 0, 4);
  116. $month = substr($pDate, 4, 2);
  117. $day = substr($pDate, 6, 2);
  118. $jd = gregoriantojd(intval($month), intval($day), intval($year));
  119. }
  120.  
  121. if (!isset($js)) {
  122. if ($milleseconds === null) {
  123. if (is_numeric($date)) {
  124. $utime = getdate($date);
  125. } else {
  126. $utime = $date;
  127. }
  128. $ms = (
  129. //FIXME: grumble grumble seems to be 9 hours off,
  130. // no idea where the 9 came from
  131. $utime['hours'] * 60 * 60 * 1000 +
  132. $utime['minutes'] * 60 * 1000 +
  133. $utime['seconds'] * 1000
  134. );
  135. $js = $ms;
  136. } else {
  137. $js = $milleseconds;
  138. }
  139. }
  140.  
  141. return (pack('V', $jd)) . (pack('V', $js));
  142. }
  143.  
  144. /** Converts a unix timestamp to the type of date expected by this file writer.
  145. * @param integer $timestamp a unix timestamp, or the return format of <code>getdate()</code>
  146. * @return string a date formatted to DBF expectations (8 byte string: YYYYMMDD);
  147. */
  148. public static function toDate($timestamp) {
  149. if (empty($timestamp)) {
  150. $timestamp = 0;
  151. }
  152.  
  153. if (!is_numeric($timestamp) && !is_array($timestamp)) {
  154. throw new InvalidArgumentException('$timestamp was not in expected format(s).');
  155. }
  156.  
  157. if (is_array($timestamp) && (!isset($timestamp['year']) || !isset($timestamp['mon']) || !isset($timestamp['mday']))) {
  158. throw new InvalidArgumentException('$timestamp array did not contain expected key(s).');
  159. }
  160.  
  161. if (is_string($timestamp) && self::validate_date_string($timestamp)) {
  162. return $timestamp;
  163. }
  164.  
  165. if (!is_array($timestamp)) {
  166. $date = getdate($timestamp);
  167. } else {
  168. $date = $timestamp;
  169. }
  170.  
  171. return substr(str_pad($date['year'], 4, '0', STR_PAD_LEFT), 0, 4) .
  172. substr(str_pad($date['mon'], 2, '0', STR_PAD_LEFT), 0, 2) .
  173. substr(str_pad($date['mday'], 2, '0', STR_PAD_LEFT), 0, 2);
  174. }
  175.  
  176. /** Convert a boolean value into DBF equivalent, preserving the meaning of 'T' or 'F'
  177. * non-booleans will be converted to ' ' (unintialized) with the exception of
  178. * 'T' or 'F', which will be kept as is.
  179. * booleans will be converted to their respective meanings (true = 'T', false = 'F')
  180. *
  181. * @param mixed $value value to be converted
  182. * @return string length 1 string containing 'T', 'F' or uninitialized ' '
  183. */
  184. public static function toLogical($value) {
  185. if ($value === 'F' || $value === false) {
  186. return 'F';
  187. }
  188.  
  189. if (is_string($value)) {
  190. if (preg_match("#^\\ +$#", $value)) {
  191. return ' ';
  192. }
  193. }
  194.  
  195. if ($value === 'T' || $value === true) {
  196. return 'T';
  197. }
  198.  
  199. return ' ';
  200. }
  201.  
  202. //calculates the size of a single record
  203. private static function getRecordSize($schema) {
  204. $size = 1;//FIXME: I have no idea why this is 1 instead of 0
  205.  
  206. foreach ($schema as $field) {
  207. $size += $field['size'];
  208. }
  209.  
  210. return $size;
  211. }
  212.  
  213. //assembles a string into DBF format truncating and padding, where required
  214. private static function character($data, $fieldInfo) {
  215. return substr(str_pad(strval($data), $fieldInfo['size'], " "), 0, $fieldInfo['size']);
  216. }
  217.  
  218. private static function validate_date_string($string) {
  219. $time = mktime (
  220. 0,
  221. 0,
  222. 0,
  223. intval(substr($string, 4, 2)),
  224. intval(substr($string, 6, 2)),
  225. intval(substr($string, 0, 4))
  226. );
  227. if ($time === false || $time === -1) {
  228. return false;
  229. }
  230. return true;
  231. }
  232.  
  233. //assembles a date into DBF format
  234. private static function date($data) {
  235. if (is_int($data)) {
  236. $tmp = strval($data);
  237. if (strlen($tmp) == 8 && self::validate_date_string($tmp)) {
  238. $data = $tmp;
  239. }
  240. }
  241.  
  242. if (self::validate_date_string($tmp)) {
  243. return $data;
  244. }
  245.  
  246. return self::toDate($data);
  247. }
  248.  
  249. //assembles a number into DBF format, truncating and padding where required
  250. private static function numeric($data, $fieldInfo) {
  251. if (isset($fieldInfo['declength']) && $fieldInfo['declength'] > 0) {
  252. $cleaned = str_pad(number_format($data, $fieldInfo['declength']), $fieldInfo['size'], ' ', STR_PAD_LEFT);
  253. } else {
  254. $cleaned = str_pad(strval(intval($data)), $fieldInfo['size'], ' ', STR_PAD_LEFT);
  255. }
  256. return substr($cleaned, 0, $fieldInfo['size']);
  257. }
  258.  
  259. //assembles a boolean into DBF format or ' ' for uninitialized
  260. private static function logical($data) {
  261. return self::toLogical($data);
  262. }
  263.  
  264. //assembles a timestamp into DBF format
  265. private static function timeStamp($data) {
  266. return self::toTimeStamp($data);
  267. }
  268.  
  269. //assembles a single field
  270. private static function makeField($data, $fieldInfo) {
  271. //FIXME: support all the types (that make sense)
  272. switch ($fieldInfo['type']) {
  273. case 'C':
  274. return self::character($data, $fieldInfo);
  275. break;
  276. case 'D':
  277. return self::date($data);
  278. break;
  279. case 'N':
  280. return self::numeric($data, $fieldInfo);
  281. break;
  282. case 'L':
  283. return self::logical($data);
  284. break;
  285. case 'T':
  286. return self::timeStamp($data);
  287. break;
  288. default:
  289. return "";
  290. }
  291. }
  292.  
  293. //assembles a single record
  294. private static function makeRecord($schema, $record) {
  295. $out = " ";
  296.  
  297. //foreach($record as $column => $data) {
  298. foreach($schema as $column => $declaration) {
  299. //$out .= self::makeField($data, $schema[$column]);
  300. $out .= self::makeField($record[$column], $declaration);
  301. }
  302.  
  303. return $out;
  304. }
  305.  
  306. //assembles all the records
  307. private static function makeRecords($schema, $records) {
  308. $out = "";
  309.  
  310. foreach ($records as $record) {
  311. $out .= self::makeRecord($schema, $record);
  312. }
  313.  
  314. return $out . "\x1a"; //FIXME: I have no idea why the end of the file is marked with 0x1a
  315. }
  316.  
  317. //assembles binary field definition
  318. private static function makeFieldDef($fieldDef, &$location) {
  319. //0+11
  320. $out = substr(str_pad($fieldDef['name'], 11, "\x00"), 0 , 11);
  321. //11+1
  322. $out .= substr($fieldDef['type'], 0, 1);
  323. //12+4
  324. $out .= (pack('V', $location));
  325. //16+1
  326. $out .= (pack('C', $fieldDef['size']));
  327. //17+1
  328. $out .= (pack('C', $fieldDef['declength']));
  329. //18+1
  330. $out .= (pack('C', $fieldDef['NOCPTRANS'] === true ? 4 : 0));
  331. //19+13
  332. $out .= (pack('x13'));
  333.  
  334.  
  335. $location += $fieldDef['size'];
  336. return $out;
  337. }
  338.  
  339. //assembles binary schema header
  340. private static function makeSchema($schema) {
  341. $out = "";
  342. $location = 1;//FIXME: explain why this is 1 instead of 0
  343.  
  344. foreach ($schema as $key => $fieldDef) {
  345. $out .= self::makeFieldDef($fieldDef, $location);
  346. }
  347.  
  348. $out .= (pack('C', 13)); // marks the end of the schema portion of the file
  349.  
  350. $out .= str_repeat(chr(0), 263); //FIXME: I gues filenames are sometimes stored here
  351.  
  352. return $out;
  353. }
  354.  
  355. //makes partial file header
  356. private static function makeHeader($date, $schema, $records) {
  357. //0+1
  358. $out = (pack('C', 0x30)); // version 001; dbase 5
  359. //1+2
  360. $out .= (pack('C3', $date['year'] - 1900, $date['mon'], $date['mday']));
  361. //4+4
  362. $out .= (pack('V', count($records)));//number of records
  363. //8+2
  364. $out .= (pack('v', self::getTotalHeaderSize($schema))); //bytes in the header
  365. //10+2
  366. $out .= (pack('v', self::getRecordSize($schema))); //bytes in each record
  367. //12+17
  368. $out .= (pack('x17')); //reserved for zeros (unused)
  369. //29+1
  370. $out .= (pack('C', 3)); //FIXME: language? i have no idea
  371. //30+2
  372. $out .= (pack('x2')); //empty
  373. return $out;
  374. }
  375.  
  376. //calculates the total size of the header, given the number of columns
  377. private static function getTotalHeaderSize($schema) {
  378. //file header is 32 bytes
  379. //field definitions are 32 bytes each
  380. //end of schema definition marker is 1 byte
  381. //263 extra bytes for file name
  382. return (count($schema) * 32) + 32 + 1 + 263;
  383. }
  384.  
  385. }
Add Comment
Please, Sign In to add comment