Session D-CSV

Arbeiten mit CSV-Listen in VFP

Volker Stamme
Softscript


Einführung

"CSV" ist die Abkürzung für "Comma Separated Values", also durch Kommata getrennte Werte. Der Name rührt von den ASCII Im- und Exportdateien her, die viele Programme erzeugen oder verarbeiten können, in der Form

"Name","Vorname",,,"PLZ","ORT"

Der Name CSV vermittelt aber einen falschen Eindruck, weil diese Listen sehr viel vielseitiger sein können, als es auf den ersten Blick scheint.

Der erste Gedanke ist, Funktionen zu entwickeln, die mit solchen Listen arbeiten können, um z.B. gezielt ein Element herauszugreifen, Umwandlungen vorzunehmen etc. Aber wie gesagt, diese Listen können völlig anders aussehen:

Wenn man sich beispielsweise einen Dateipfad in der Form

"C:\WINDOWS\SYSTEM32\DRIVERS\PRINTER..."

ansieht, stellt man fest, daß das Problem grundsätzlich dasselbe ist, mit dem einzigen Unterschied, daß ein anderes Trennzeichen verwendet wurde. Wenn die eingangs erwähnten Funktionen zum Bearbeiten solcher Listen also flexibel genug sind, auch andere Trennzeichen zu verwenden, kann damit auch ein Dateipfad zerlegt werden.

Wenn man ein bißchen darauf achtet, findet man solche Listen ziemlich häufig, manchmal allerdings erst auf den zweiten Blick (liegt oft an der Größe!). Eine IP-Adresse beispielsweise ist so eine Liste:

192.57.22.1

kann ganz einfach bearbeitet werden, wenn man als Separator (Trennzeichen) einen Punkt einsetzt. Eine Uhrzeit der Form 12:23:32 ist plötzlich eine Liste. Es interessiert dann auch nicht mehr, ob die Sekunden fehlen - In dem Fall ist das entsprechende Element der Liste nicht vorhanden und kann durch "00" ersetzt werden. Wenn ein Teil nur einstellig angegeben wurde ist das ebenso unbedeutend, da ein

val( <element2> )

bei "02" den gleichen Wert zurückliefert wie bei "2". Man kann natürlich auch solche Listen kombinieren: Angenommen, es kommt ein formatierter DM-Betrag zustande, etwa

12.223.443,12

das sind dann 2 Listen: die erste ist der Vor- und Nachkommateil, durch ein Komma von einander getrennt, das zweite ist dann der durch Punkte unterteilte Vorkommateil.

Allmählich wird klar, wie häufig einem solche Listen begegnen, und umso wünschenswerter sind dann entsprechende Funktionen, die flexibel mit den Listen umgehen können.

Im Folgenden werden die einzelnen Funktionen erläutert. Die Bibliothek kann mit „DO CSV.PRG“ installiert werden.

IF not ( program() $ set ( "proc" ))
set proc to ( program()) additive
endif

#define C_CSV_VALUE_AT_POSITION 1
#define C_CSV_POSITION_OF_VALUE 2
#define C_CSV_NUMBER_OF_VALUES 3

#define C_CSV_IGNORECASE .T.
#define C_CSV_MATCHCASE .F.

#define C_CSV_RAW .F. && 4->4
#define C_CSV_QUOTES .T. && 4->'4'

*!* Beispiele
? CSVGet( "a,b,c", 3 )

CSVGet

liefert über die Indexnummer ein bestimmtes Element aus der Liste zurück.

Z.B. ?CSVGet( "a,b,c", 2 ) -> "b"

Rückgabe: STRING

FUNCTION CSVGet( tcList, tnAppearance, tcDelimiter )
LOCAL lnPos1, lnPos2, lnDelimiter
IF isnull( tcList )
RETURN .NULL.
ENDIF
IF pcount() < 3
tcDelimiter = ","
ENDIF
IF pcount() < 2
tnAppearance = 1
ENDIF
lnDelimiter = len( tcDelimiter ) - 1
IF type( "tcList" ) != "C" OR tnAppearance < 1
RETURN .NULL.
ENDIF
RETURN CSVLocate( tcList, tcDelimiter, C_CSV_VALUE_AT_POSITION, ;
tnAppearance, .NULL., C_CSV_MATCHCASE, C_CSV_QUOTES, 0 )
ENDFUNC

CSVCount

liefert die Gesamtzahl der Elemente einer Liste zurück

Z.B. CSVCount( "a,b,c" ) = 3, CSVCount( "a,,,d" ) = 4

Rückgabe: INTEGER

FUNCTION CSVCount( tcList, tcDelimiter )
LOCAL lnCount, lnHalfCount
IF pcount() < 2
tcDelimiter = ","
ENDIF
IF isnull( tcList )
RETURN 0
ENDIF
IF type( "tcList" ) != "C"
RETURN -1
ENDIF
IF tcList == ""
RETURN 1
ENDIF
RETURN CSVLocate( tcList, tcDelimiter, C_CSV_NUMBER_OF_VALUES, ;
-1, "", C_CSV_MATCHCASE, C_CSV_RAW, 0 )
ENDFUNC

CSVRemove

entfernt ein Element aus der Liste. Das Element wird entweder durch den Index oder durch den Inhalt angegeben.

Z.B. CSVRemove( "a,b,c", 2 ) = "a,c", CSVRemove( "a,b", "b" ) = "a"

Rückgabe: STRING

FUNCTION CSVRemove( tcList, txString, tcDelimiter )
LOCAL lnPos1, lnPos2, lnLen, lnDelimiter, lnAppearance
lnLen = len( tcList )

IF pcount() < 3
tcDelimiter = ","
ENDIF
lnDelimiter = len( tcDelimiter )
IF type( "tcList" ) != "C"
RETURN .NULL.
ENDIF

IF type( "txString" ) = "N"
IF txString < 1
RETURN tcList
ENDIF
= CSVLocate( tcList, tcDelimiter, C_CSV_VALUE_AT_POSITION, ;
txString, "", C_CSV_MATCHCASE, C_CSV_RAW, -1, ;
@lnPos1, @lnPos2 )
lnAppearance = 1
IF lnPos1 > lnLen AND lnPos2 < lnLen
lnAppearance = 0
ENDIF
IF lnAppearance = 1 AND CSVCount( tcList, tcDelimiter ) = 1 ;
AND txString = 1
RETURN .NULL.
ENDIF
ELSE
IF tcList = txString
RETURN .NULL.
ENDIF
txString = CSVXtoC( txString, C_CSV_RAW )
lnAppearance = CSVLocate( tcList, tcDelimiter,;
C_CSV_POSITION_OF_VALUE, ;
-1, txString, C_CSV_MATCHCASE, C_CSV_RAW, -1, ;
@lnPos1, @lnPos2 )
ENDIF

DO CASE
CASE lnAppearance = 0
RETURN tcList
CASE between( lnPos2, lnLen - lnDelimiter, lnLen )
IF lnPos2 = lnLen AND lnPos1 > lnDelimiter
RETURN left( tcList, lnPos1 - 1 - lnDelimiter )
ELSE
RETURN left( tcList, lnPos1 - 1 )
ENDIF
OTHERWISE
RETURN left( tcList, lnPos1 - 1 ) ;
+ substr( tcList, lnPos2 + lnDelimiter + 1 )
ENDCASE
ENDFUNC

CSVUpdate

ersetzt das durch txString spezifizierte Element durch txNewVal. txString kann entweder eine Indexnummer oder der Textinhalt sein.

Z.B.: CSVUpdate( "a,b,c", 2, "d" ) = "a,d,c",
CSVUpdate( "a,b,c", "a", "d" ) = "d,b,c"

Rückgabe: STRING

FUNCTION CSVUpdate( tcList, txString, txNewVal, tcDelimiter )
LOCAL lnPos1, lnPos2, lnLen, lnDelimiter, lnAppearance
lnLen = len( tcList )
IF pcount() < 4
tcDelimiter = ","
ENDIF
lnDelimiter = len( tcDelimiter )
IF type( "tcList" ) != "C"
RETURN ""
ENDIF

IF type( "txString" ) = "N"
IF txString < 1
RETURN tcList
ENDIF
= CSVLocate( tcList, tcDelimiter, C_CSV_VALUE_AT_POSITION, ;
txString, "", C_CSV_MATCHCASE, C_CSV_RAW, -1, ;
@lnPos1, @lnPos2 )
lnAppearance = 1
IF lnPos1 > lnLen AND lnPos2 < lnLen
lnAppearance = 0
ENDIF
ELSE
txString = CSVXtoC( txString, C_CSV_RAW )
lnAppearance = CSVLocate( tcList, tcDelimiter,;
C_CSV_POSITION_OF_VALUE, ;
-1, txString, C_CSV_MATCHCASE, C_CSV_RAW, -1, ;
@lnPos1, @lnPos2 )
ENDIF
IF lnAppearance = 0
RETURN tcList
ELSE
RETURN left( tcList, lnPos1 - 1 ) ;
+ CSVXtoC( txNewVal, C_CSV_QUOTES ) ;
+ IIF( lnPos2 < lnLen, substr( tcList, lnPos2 + 1 ), "" )
ENDIF
ENDFUNC

CSVAdd

Fügt ein neues Element in eine Liste ein. Wenn tlOptimize = .T. ist, wird der neue Wert nur dann angefügt, wenn er noch nicht in der Liste enthalten ist. Normalerweise wird ein neues Element ans Ende der Liste angehängt, wenn tlDescending .T. ist, wird das Element am Listenanfang eingefügt. Durch tlQuote kann eingestellt werden, daß das Element in Anführungszeichen übernommen wird.

Rückgabe: STRING

FUNCTION CSVAdd( tcList, tcString, tcDelimiter, tlOptimize,;
tlDescending, tlQuote )
LOCAL lnI, lnMax, lcString, lclQuote, lcrQuote
IF pcount() < 3
tcDelimiter = ","
ENDIF

lcString = CSVXtoC( tcString, C_CSV_RAW )

IF pcount() > 5 AND ! empty( tlQuote )
DO CASE
CASE ! '"' $ tcString
lclQuote = '"'
lcrQuote = '"'
CASE ! "'" $ tcString
lclQuote = "'"
lcrQuote = "'"
CASE ! '[' $ tcString
lclQuote = '['
lcrQuote = ']'
OTHERWISE
*!* Error processing; cannot use quotation marks...
ENDCASE
ELSE
lclQuote = ""
lcrQuote = ""
ENDIF

IF isnull( tcList )
IF empty( lcString )
RETURN IIF( pcount() < 4 OR ! tlOptimize, lclQuote ;
+ lcString + lcrQuote, .NULL. )
ELSE
RETURN lclQuote + lcString + lcrQuote
ENDIF
ENDIF
lcString = lclQuote + lcString + lcrQuote
IF type( "tcList" ) != "C"
tcList = ""
ENDIF
lnMax = CSVCount( tcList, tcDelimiter )
IF pcount() > 3 AND tlOptimize
FOR lnI = 1 to lnMax
IF CSVLocate( tcList, tcDelimiter, ;
C_CSV_POSITION_OF_VALUE, ;
.NULL., lcString, C_CSV_MATCHCASE, ;
C_CSV_QUOTES, 0 ) > 0
RETURN tcList
ENDIF
ENDFOR
IF pcount() > 4 AND tlDescending
RETURN IIF( ! empty( tcList ), ;
lcString + tcDelimiter + alltrim( tcList ), ;
lcString)
ELSE
RETURN IIF( ! empty( tcList ), ;
alltrim( tcList ) + tcDelimiter + lcString, ;
lcString)
ENDIF
ELSE
IF pcount() > 4 AND tlDescending
RETURN lcString + tcDelimiter + tcList
ELSE
RETURN tcList + tcDelimiter + lcString
ENDIF
ENDIF
ENDFUNC

CSVInlist

prüft, ob das angegebene Element in der Liste enthalten ist oder nicht.

Z.B. CSVInlist( "a,b,c", "d" ) = .F., CSVInlist( "a,b,c", "b" ) = .T.

Rückgabe: BOOL

FUNCTION CSVInlist( tcList, tcString, tcDelimiter )
LOCAL lnI, lnMax, lcList, lnPos, lnPos2
IF isnull( tcList )
RETURN .F.
ENDIF
IF pcount() < 3
tcDelimiter = ","
ENDIF
IF type( "tcList" ) != "C"
RETURN .F.
ENDIF
tcString = CSVXtoC( tcString, C_CSV_RAW )
RETURN CSVLocate( tcList, tcDelimiter, C_CSV_POSITION_OF_VALUE, ;
.NULL., tcString, C_CSV_MATCHCASE, C_CSV_RAW, 0 ) != 0
ENDFUNC

CSVXtoC

konvertiert den übergebenen Wert in einen String.

Rückgabe: STRING

FUNCTION CSVXtoC( txValue, tlQuotes )
LOCAL lcString, lcType
lcType = type( "txValue" )
IF lcType != 'C' OR isnull( txValue )
DO CASE
CASE lcType = 'L'
lcString = IIF( isnull( txValue ), ".F.", ;
IIF( txValue, ".T.", ".F." ))
CASE lcType $ 'NFIB'
lcString = strtran( IIF( isnull( txValue ), "0", ;
IIF( int( txValue ) != txValue, ;
alltrim( str( txValue, 16, set( "DECIMALS" ))), ;
alltrim( str( txValue, 16 )))), ",", "." )
CASE lcType = 'D'
lcString = IIF( isnull( txValue ), dtoc( {..} ), ;
dtoc( txValue ))
CASE lcType = 'T'
lcString = IIF( isnull( txValue ), ttoc( {.. ::} ), ;
ttoc( txValue ))
CASE lcType = 'Y'
lcString = strtran( IIF( isnull( txValue ), "0", ;
alltrim( str( mton( txValue ), 16, 2 ))), ",", "." )
OTHERWISE && 'CM'
lcString = ""
ENDCASE
ELSE
RETURN txValue
ENDIF
IF pcount() > 1 AND tlQuotes = C_CSV_QUOTES
RETURN "'" + lcString + "'"
ELSE
RETURN lcString
ENDIF
ENDFUNC

CSVCtoX

konvertiert einen String in den übergebenen neuen Typ. Wird kein Typ übergeben, erwartet diese Funktion das Typkennzeichen als erstes Byte im String,

Z.B. CSVCtoX( "5", "N" ) = CSVCtoX( "N5" ) = 5.

Rückgabe: ??

FUNCTION CSVCtoX( tcValue, tcType )
LOCAL lcType, lcValue
lcValue = alltrim( tcValue )
IF pcount() < 2 AND upper( left( lcValue, 1 )) $ 'NFIBLYDTCM'
lcType = upper( left( lcValue, 1 ))
lcValue = substr( lcValue, 2 )
ELSE
lcType = upper( left( alltrim( tcType ), 1 ))
ENDIF
DO CASE
CASE lcType $ 'NFIBL'
IF lcType = "L" AND len( lcValue ) = 1
*!* Werte, die zu .T. evaluiert werden:
RETURN upper( lcValue ) $ 'TYJ1X'
ENDIF
RETURN evaluate( alltrim( lcValue ))
CASE lcType = 'Y'
RETURN evaluate( "$" + alltrim( lcValue ))
CASE lcType = 'D'
RETURN ctod( alltrim( lcValue ))
CASE lcType = 'T'
RETURN ctot( alltrim( lcValue ))
OTHERWISE
RETURN lcValue
ENDCASE
ENDFUNC

AtPos

ist das gleiche wie at(), nur kann eine Startposition angegeben werden.

Rückgabe: INTEGER

FUNCTION AtPos( tcSearch, tcSearchIn, tnStartAt )
LOCAL lnI, lnPos
lnI = 1
DO WHILE .T.
lnPos = at( tcSearch, tcSearchIn, lnI )
DO CASE
CASE lnPos = 0
RETURN 0
CASE lnPos > tnStartAt
RETURN lnPos
ENDCASE
lnI = lnI + 1
ENDDO
ENDFUNC

AtcPos

wie atc() aber mit Startposition

Rückgabe: INTEGER

FUNCTION AtcPos( tcSearch, tcSearchIn, tnStartAt )
RETURN AtPos( upper( tcSearch ), upper( tcSearchIn ), tnStartAt )
ENDFUNC

Beispiel für die Verwendung des COM-Control CSV

Vor der Verwendung ist das Control zu registrieren. Das wird mit beiliegendem REGSVR32.EXE gemacht, entweder per Command_Line REGSVR32 <voller DLL-Pfad> oder so wie ich das immer mache: REGSVR32 als Default-Anwendung für *.OCX und *.DLL eintragen und die Dinger nur noch doppelclicken. Bin halt faul.

Das folgende #define sollte in das/die Include-Files, die z.B. in Klassenbibliotheken verwendet werden. Grund hierfür ist, daß sich dieser Aufruf mit einer neuen Version des Controls durchaus ändern kann und dann eben nur an einer Stelle geändert werden muß. Bei der letzten Version war es z.B. "sfcLib.cCSV", und das wird in der Konferenzversion möglicherweise auch wieder so sein. Dann gibt's auch ein Version- Property <g>.

#define GETCSV CreateObject( "cCSV" )
xx = GETCSV

*!* eine einfache Liste
xx.cslist = "a,b,c,d,e"
*!* Achtung! Die gesamte Liste wird zunächst als
*!* ein Element angesehen, bis
xx.separator = ","
*!* durch Neusetzen eines Separators eine Aufsplittung
*!* erzwungen wird

*!* diese Zeile liefert jetzt "b" zurück
? xx.list( 2 )

*!* Die Rückgabe ist 5
? xx.listcount()

*!* Werbeunterbrechung
xx.aboutbox()

*!* Element 3 wird ausgetauscht gegen Prosa
xx.list(3) = "Rhabarberquark macht Willi stark!"

*!* Zeigt die neue Liste an
? xx.cslist

*!* der neue Wert wird entfernt
xx.removeitem(3)

*!* So sieht die Liste jetzt aus
? xx.cslist

*!* suche in der Liste das "d", Rückgabe ist 3,
*!* weil das Gedicht ja nun weg ist.
? xx.finditem( "d" )

*!* Objekt wird nicht mehr gebraucht.
rele xx

Methoden/Properties in alphabetischer Reihenfolge

AboutBox() (M) schlechte Fälschung meines Passes
CSList (P) die gesamte Liste (R/W)
Clear() (M) Alles löschen
FindItem() (M) Sucht nach übergebenem String, Rückgabe ist
Position oder 0 wenn nicht gefunden
InsertItem() (M) Position, Wert; fügt neues Element an
übergebener Position ein
LastError() (M) Liefert den letzten Fehler zurück
List (P) Array (R/W) der einzelnen Elemente
ListCount (P) Anzahl Elemente (R)
RemoveItem() (M) Entfernt das Element mit dem übergebenen Index
Separator (P) Trennzeichen (R/W)

Geschwindigkeitsvergleich

Für diejenigen, die Angst haben, ein COM-Control wäre zu langsam, ist hier der Beweis für's Gegenteil: Übrigens wächst das Verhältnis mit steigender Anzahl proportional an!

#define ANZAHL 100
#define GETCSV CreateObject( "cCSV" )

LOCAL aTest, lnI, lnT

DECLARE aTest[ANZAHL]

lnT = seconds()
FOR lnI = 1 TO ANZAHL
aTest[lnI] = CreateObject( "Custom" )
ENDFOR
lnT = seconds() - lnT
? allt( str( ANZAHL )) + " Customs erzeugen dauert " + str( lnT, 6, 2 ) + " Sekunden."
FOR lnI = 1 TO ANZAHL
aTest[lnI] = NULL
ENDFOR

lnT = seconds()
FOR lnI = 1 TO ANZAHL
aTest[lnI] = GETCSV
ENDFOR
lnT = seconds() - lnT
? allt( str( ANZAHL )) + " CSV's erzeugen dauert " + str( lnT, 6, 2 ) + " Sekunden."
FOR lnI = 1 TO ANZAHL
aTest[lnI] = NULL
ENDFOR

Abschließendes:

Nobody is perfect, ich auch nicht. Sollten wider Erwarten Fehler auftreten oder irgendwelche ungeheuer wichtigen Funktionen fehlen, bitte ich um entsprechende Mitteilung wahlweise per Compuserve 100305,1103 oder per Internet vstamme@softscript.de. Anregungen sind immer erwünscht, weil nur durch gute Ideen sowas wie dieses Control besser und/oder vielseitiger werden. Das ist auch die einzige Nutzungsbedingung, gelegentliches Nachdenken, was sich noch verbessern ließe.