* Schemas: * schemas are supplied to this library as php arrays as follows: * array( * 'name' => 'COLUMN B' * 'type' => 'C', * 'size' => 4, * 'declength' => 2, * 'NOCPTRANS' => true, * ); * * 'name': column name, 10 characters max, padded with null bytes if less than 10, truncated if more than 10 * 'type' : a single character representing the DBF field type, the following are supported: * 'C': character data stored as 8 bit ascii * 'N': numeric character data stored as 8 bit ascii, when 'declength' is * present, declength + 1 bytes are consumed to store the decimal values * 'D': 8 character date specification (YYYYMMDD) * always 8 bytes * 'L': single character boolean, 'T' or 'F' or a space ' ' for unintialized * always 1 byte * 'T': 8 byte binary packed date stamp and milliseconds stamp * first four bytes: integer representing days since Jan 1 4713 (julian calendar) * second four bytes: integer milliseconds ellapsed since prior midnight * always 8 bytes * * 'size': number of bytes this field occupies * 'declength': only applicable to type 'N', indicates how many of the alloted spaces are for * decimal places and the decimal point * 'NOCPTRANS': Specifies the fields that should not be translated to another code page. * * Records: * records are supplied as arrays keyed by column name with values conforming to these type specs: * 'C': character data, will be truncated at field size, padded with spaces on the right to field size * 'N': numeric character data, will be truncated at field size, * padded with spaces on the left to field size converts numeric * native types to numeric character data automatically * 'D': accepts a unix timestamp, a structure resembling * getdate()'s return format, or a 8 length string * containing YYYYMMDD * 'L': accepts and converts ('T' and true) to 'T', ('F' and false) to * 'F' and everything else to ' ' * 'T': accepts a unix timestamp, a structure resembling * getdate()'s return format or an array containing * 'jd' => (julian date representation) * 'js' => milleseconds elapsed since prior midnight * */ class DBF { // utility function to ouput a string of binary digits for inspection private static function binout($bin) { echo "data:", $bin, "
"; foreach(unpack('C*', $bin) as $byte) { printf('%b', $byte); } echo "
"; return $bin; } /** Writes a DBF file to the provided location {@link $filename}, with a given * {@link $schema} containing the DBF formatted $records * marked with the 'last updated' mark $date or a current timestamp if last * update is not provided. * @see DBF * @param string $filename a writable path to place the DBF * @param array $schema an array containing DBF field specifications for each * field in the DBF file (see class DBFgetdate() * or null to use the current timestamp */ public static function write($filename, array $schema, array $records, $date=null) { file_put_contents($filename, self::getBinary($schema, $records, $date)); } /** Gets the DBF file as a binary string * @see DBF::write() * @return string a binary string containing the DBF file. */ public static function getBinary(array $schema, array $records, $pDate) { if (is_numeric($pDate)) { $date = getDate($pDate); } elseif ($pDate == null) { $date = getDate(); } else { $date = $pDate; } return self::makeHeader($date, $schema, $records) . self::makeSchema($schema) . self::makeRecords($schema, $records); } /** Convert a unix timestamp, or the return structure of the getdate() * function into a (binary) DBF timestamp. * @param mixed $date a unix timestamp or the return structure of the getdate() * @param number $milleseconds the number of milleseconds elapsed since * midnight on the day before the date in question. If omitted a second * accurate rounding will be constructed from the $date parameter * @return string a binary string containing the DBF formatted timestamp */ public static function toTimeStamp($date, $milleseconds = null) { if (is_array($date)) { if (isset($date['jd'])) { $jd = $date['jd']; } if (isset($date['js'])) { $js = $date['js']; } } if (!isset($jd)) { $pDate = self::toDate($date); $year = substr($pDate, 0, 4); $month = substr($pDate, 4, 2); $day = substr($pDate, 6, 2); $jd = gregoriantojd(intval($month), intval($day), intval($year)); } if (!isset($js)) { if ($milleseconds === null) { if (is_numeric($date) || empty($date)) { $utime = getdate(intval($date)); } else { $utime = $date; } $ms = ( $utime['hours'] * 60 * 60 * 1000 + $utime['minutes'] * 60 * 1000 + $utime['seconds'] * 1000 ); $js = $ms; } else { $js = $milleseconds; } } return (pack('V', $jd)) . (pack('V', $js)); } /** Converts a unix timestamp to the type of date expected by this file writer. * @param integer $timestamp a unix timestamp, or the return format of getdate() * @return string a date formatted to DBF expectations (8 byte string: YYYYMMDD); */ public static function toDate($timestamp) { if (empty($timestamp)) { $timestamp = 0; } if (!is_numeric($timestamp) && !is_array($timestamp)) { throw new InvalidArgumentException('$timestamp was not in expected format(s).'); } if (is_array($timestamp) && (!isset($timestamp['year']) || !isset($timestamp['mon']) || !isset($timestamp['mday']))) { throw new InvalidArgumentException('$timestamp array did not contain expected key(s).'); } if (is_string($timestamp) && strlen($timestamp) === 8 && self::validate_date_string($timestamp)) { return $timestamp; } if (!is_array($timestamp)) { $date = getdate($timestamp); } else { $date = $timestamp; } return substr(str_pad($date['year'], 4, '0', STR_PAD_LEFT), 0, 4) . substr(str_pad($date['mon'], 2, '0', STR_PAD_LEFT), 0, 2) . substr(str_pad($date['mday'], 2, '0', STR_PAD_LEFT), 0, 2); } /** Convert a boolean value into DBF equivalent, preserving the meaning of 'T' or 'F' * non-booleans will be converted to ' ' (unintialized) with the exception of * 'T' or 'F', which will be kept as is. * booleans will be converted to their respective meanings (true = 'T', false = 'F') * * @param mixed $value value to be converted * @return string length 1 string containing 'T', 'F' or uninitialized ' ' */ public static function toLogical($value) { if ($value === 'F' || $value === false) { return 'F'; } if (is_string($value)) { if (preg_match("#^\\ +$#", $value)) { return ' '; } } if ($value === 'T' || $value === true) { return 'T'; } return ' '; } //calculates the size of a single record private static function getRecordSize($schema) { $size = 1; foreach ($schema as $field) { $size += $field['size']; } return $size; } //assembles a string into DBF format truncating and padding, where required private static function character($data, $fieldInfo) { return substr(str_pad(strval($data), $fieldInfo['size'], " "), 0, $fieldInfo['size']); } private static function validate_date_string($string) { $time = mktime ( 0, 0, 0, intval(substr($string, 4, 2)), intval(substr($string, 6, 2)), intval(substr($string, 0, 4)) ); if ($time === false || $time === -1) { return false; } return true; } //assembles a date into DBF format private static function date($data) { if (is_int($data)) { $tmp = strval($data); if (strlen($tmp) == 8 && self::validate_date_string($tmp)) { $data = $tmp; } } return self::toDate($data); } //assembles a number into DBF format, truncating and padding where required private static function numeric($data, $fieldInfo) { if (isset($fieldInfo['declength']) && $fieldInfo['declength'] > 0) { $cleaned = str_pad(number_format(floatval($data), $fieldInfo['declength']), $fieldInfo['size'], ' ', STR_PAD_LEFT); } else { $cleaned = str_pad(strval(intval($data)), $fieldInfo['size'], ' ', STR_PAD_LEFT); } return substr($cleaned, 0, $fieldInfo['size']); } //assembles a boolean into DBF format or ' ' for uninitialized private static function logical($data) { return self::toLogical($data); } //assembles a timestamp into DBF format private static function timeStamp($data) { return self::toTimeStamp($data); } //assembles a single field private static function makeField($data, $fieldInfo) { switch ($fieldInfo['type']) { case 'C': return self::character($data, $fieldInfo); break; case 'D': return self::date($data); break; case 'N': return self::numeric($data, $fieldInfo); break; case 'L': return self::logical($data); break; case 'T': return self::timeStamp($data); break; default: return ""; } } //assembles a single record private static function makeRecord($schema, $record) { $out = " "; foreach($schema as $column => $declaration) { $out .= self::makeField( $record[$column], $declaration ); } return $out; } //assembles all the records private static function makeRecords($schema, $records) { $out = ""; foreach ($records as $record) { $out .= self::makeRecord($schema, $record); } return $out . "\x1a"; } //assembles binary field definition private static function makeFieldDef($fieldDef, &$location) { //0+11 $out = substr(str_pad($fieldDef['name'], 11, "\x00"), 0 , 11); //11+1 $out .= substr($fieldDef['type'], 0, 1); //12+4 $out .= (pack('V', $location)); //16+1 $out .= (pack('C', $fieldDef['size'])); //17+1 $out .= (pack('C', @$fieldDef['declength'])); //18+1 $out .= (pack('C', @$fieldDef['NOCPTRANS'] === true ? 4 : 0)); //19+13 $out .= (pack('x13')); $location += $fieldDef['size']; return $out; } //assembles binary schema header private static function makeSchema($schema) { $out = ""; $location = 1;//FIXME: explain why this is 1 instead of 0 foreach ($schema as $key => $fieldDef) { $out .= self::makeFieldDef($fieldDef, $location); } $out .= (pack('C', 13)); // marks the end of the schema portion of the file $out .= str_repeat(chr(0), 263); //FIXME: I gues filenames are sometimes stored here return $out; } //makes partial file header private static function makeHeader($date, $schema, $records) { //0+1 $out = (pack('C', 0x30)); // version 001; dbase 5 //1+2 $out .= (pack('C3', $date['year'] - 1900, $date['mon'], $date['mday'])); //4+4 $out .= (pack('V', count($records)));//number of records //8+2 $out .= (pack('v', self::getTotalHeaderSize($schema))); //bytes in the header //10+2 $out .= (pack('v', self::getRecordSize($schema))); //bytes in each record //12+17 $out .= (pack('x17')); //reserved for zeros (unused) //29+1 $out .= (pack('C', 3)); //FIXME: language? i have no idea //30+2 $out .= (pack('x2')); //empty return $out; } //calculates the total size of the header, given the number of columns private static function getTotalHeaderSize($schema) { //file header is 32 bytes //field definitions are 32 bytes each //end of schema definition marker is 1 byte //263 extra bytes for file name return (count($schema) * 32) + 32 + 1 + 263; } }