dbfreader

Dbfreader is a tcl-only package to read dbase III files. It is used as a component in the tclshapefile package. It is based on code published by Vitus Wagner in the 1990ies.


 # $Id: dbfreader.tcl,v 1.3 2012/01/02 16:17:40 joheid Exp $
 # dbase III file reader package
 # returns the data as dict, keys are the record numbers
 # based on the original code of Vitus Wagner
 # rewritten as snit component
 # (C) Vitus Wagner, Joachim Heidemeier 
 # published under the Tcl License
 # 
 # public methods
 # readData read the datafile into the dict
 # 
 #
 package require snit
 package provide dbasereader 0.5
 snit::type dbasefile {
 #
 # daten as list of lists
   variable fData
 # array holding information like nrow ncol
    variable info
    variable fh "none" ;# filehandle, wird in OpenDbf gesetzt
 #
 # normally used as component
    option -partof
 #
    option -filename {}
 # encoding
    option -encoding -default iso8859-1 -validatemethod encodingAvailable
 #   
    constructor {args} {
        set info(read) 0
        $self configurelist $args
    }
 #
    method encodingAvailable {option value} {
        if {[lsearch -exact [encoding names] $value] == -1} { 
            error "requested encoding $value not available"
        }
    }
 #
 # local methods
 # file == filehandle
 #
 method ReadHeader {} {
 fconfigure $fh -translation binary
 set rec [read $fh 32]
 binary scan $rec "cccciss" info(version) year month day info(nRow) info(offset) info(reclen)
 catch {set info(datestamp) [clock scan "$month/$day/[expr 1900 + $year]"]}
 set info(nCol) [expr {($info(offset)-1)/32-1}]
 set recoffset 1
 for {set i 1} {$i<=$info(nCol)} {incr i} {
   set rec [read $fh 32]
   binary scan $rec "A11Ax4cc" info(name,$i) info(type,$i) info(len,$i) \
          info(dec,$i) 
 # 
 # convert info(len) into unsigned int
 #
    set info(len,$i) [expr $info(len,$i) & 0xff]
   set info(ofs,$i) $recoffset
   incr recoffset $info(len,$i)
 }
 read $fh 1
 }
 #
 # Opens DBF file with given name.
 # Fills all neccesary information
 # sets file descriptor
 # and reads file header
 #
 method  OpenDbf {} {
    set info(read) 0
    if {[catch {open $options(-filename)} res]} {
       return -code error "$res"
    }
   set fh $res
   $self ReadHeader
   return
    }

 #
 # Position dbf file to given record
 # 
 method Dbfseek {recno} {
        if {![array exists info]||![info exists info(version)]} {
            return -code error "No structure information exists for $info(fh)"
        }
        if {$recno>$info(nRow)||$recno<0} {
            return -code error "No such record: $recno"
        }
        seek $fh [expr {$info(offset)+$info(reclen)*(int($recno)-1)}]
    }

 #
 # Get and decode next record
 #
 method GetNextRec {} {
 if {![array exists info]||![info exists info(version)]} {
  return -code error "No structure information exists for $info(fh)"
 }
 set rec [read $fh $info(reclen)]
 set first 1
 set last 0
 set reclist {}
 for {set i 1} {$i<=$info(nCol)} {incr i} {
     set len $info(len,$i)
     incr last $len
     set tmp [string trim [string range $rec $first $last]]
     incr first $len
     switch "$info(type,$i)" {
         "D" { 
             if {[regexp {([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])} \
                  $tmp jnk y m d]} {
                 set tmp "$m/$d/$y"
             }
         }
         "N" { 
             if {![string length $tmp]} {
                 set tmp 0
             }
         }
         "L" { 
             if {[string length $tmp]} {
                 set tmp [regexp {^[YyTt1]} $tmp]
             }
         }
         "C" { 

             set tmp [encoding convertfrom $options(-encoding) $tmp]
         }  
     }
     lappend reclist $tmp 
 }
     return $reclist
 }

   #
   # checkdbf - checks if structure matches given list of fields and types
   # Raises error and returns message if something doesn't match
   #
   method checkdbf {fieldlist  typelist } {
       set i 1 
       foreach f $fieldlist t $typelist {
           if {![info exists info(name,$i)]} {
               return -code error "Not enough fields. $f expected"
           } 
           if {"$info(name,$i)"!="$f"} {
               return -code error "Field # $i doesn't match: $info(name,$i) while $f expected"
           }
           if {"$info(type,$i)"!="[lindex $t 0]"} {
               return -code error "Field $f type mismatch $info(type,$i) found [lindex $t 0] expected."
      }
      if {$info(len,$i)!=[lindex $t 1]} {
         return -code error "Field $f size mismatch $info(len,$i) found [lindex $t 1] expected."
      }
      if {"$info(type,$i)"=="N"&&[lindex $t 2]!=$info(dec,$i)} {
         return -code error "Field $f decimal places mismatch $info(dec,$i) found [lindex $t 2] expected."
      }
           incr i
       } 
       if {$i<=$info(nCol)} {
           return -code error "Extra field $info(name,$i)"
       }
       return ""
   }

   #
   # closedbf - closes file and frees data structure
   #
   method CloseDbf {} {

    if {(![info exists fh]) || ($fh ne $fh)}  {
        return -code error "$info(fh) is not a dBase file"
    }
    if {[catch {close $fh} msg]} {
        unset fh
        return -code error -errorcode $errorCode $msh
    }
 }
   method AddRow {n datalist}  {
       dict append fData  $n $datalist
   }
   #
   #
   # readData
   # reads all files
   # and all field values to it.

   method readData {} {
    if {!$info(read)} {
    set fData [dict create] 
    if {$fh eq "none"} {$self OpenDbf}    
    $self Dbfseek 1 
    set n 1
    while {$n<=$info(nRow)} {
        $self AddRow  $n [$self GetNextRec]
        incr n
    }
    $self CloseDbf
    set info(read) 1
    return 0    
    } else {
        return {file already read}
    }
    
 }
 # schreibt Spalten und Zeileninfos sowie Datum raus
 method dbfInfo {} {
    if {!$info(read)} {
        return {no dbf-file read}
    } else {
    set colInfo [list]
    for {set i 1} {$i <= $info(nCol)} {incr i} {
        lappend colInfo [list "col $i" $info(name,$i) $info(type,$i) $info(len,$i) \
          $info(dec,$i)] 
    }
    return [list $info(nRow) $info(nCol) $colInfo $info(datestamp) ]
    }
 }
 # return one or all records
 method data {{recnr all}} {
    if {"$recnr" eq "all"} {
        return $fData
    } elseif {[string is integer $recnr] && $recnr <= $info(nRow)} {
        return [dict filter $fData key $recnr]
    } else {error "Record $recnr not available"}
 }
 }
 # testscript
 if {0} {
 dbasefile d1 -filename [tk_getOpenFile]
 d1 readData
 set x [d1 dbfInfo]
 set y [d1 data]
 }