Chinese stadsnamen geëxporteerd naar .dbf op een westers station kunnen terugkomen als vraagtekens bij een export die succes meldde, omdat dBASE-, SYLK- en DIF-bestanden nergens een encoding kunnen vastleggen. Een veilige writer gebruikt de host ANSI code page en toetst elke naam en waarde eraan voordat de eerste byte valt; HotXLS weigert wat die page niet aan kan
De host code page is de enige encoding die deze bestanden hebben
Alle drie de formaten stammen uit de vroege PC-jaren: dBASE van Ashton-Tate, SYLK uit het Multiplan-tijdperk van Microsoft, DIF van Software Arts, het VisiCalc-bedrijf. Elk bewaart tekst als losse bytes en veronderstelt dat de reader de code page van de writer deelt — een eerlijke gok toen elke machine op kantoor dezelfde DOS-setup draaide. Op Windows vandaag is die page wat GetACP teruggeeft: 1252 op een West-Europese installatie, 936 op Vereenvoudigd Chinees, 1251 op Cyrillisch. dBASE IV en FoxPro zetten later een language-driver-mark op byte 29 van de tabelheader, maar de dBASE III-layout die HotXLS schrijft laat die byte op nul, en SYLK, DIF en de space-aligned tekst .prn hebben helemaal geen header-veld ervoor. De code page wordt dus buiten het bestand afgesproken. HotXLS gebruikt de host ANSI code page bij schrijven en lezen, en controleert vóór het schrijven of elk teken een byte in die page heeft
| Formaat | Extensie bij SaveAs / Open | Encoding gedeclareerd in het bestand | Tekst die de hostpage niet aan kan |
|---|---|---|---|
| dBASE III-tabel | .dbf, plus een memo-bestand .dbt indien nodig | Nee (byte 29 blijft nul) | EXlsDbf gaat omhoog vóór het schrijven, en de zojuist gemaakte .dbf / .dbt worden verwijderd |
| SYLK | .slk, .sylk | Nee | SaveAs geeft -1 terug en maakt geen bestand |
| DIF | .dif | Nee | SaveAs geeft -1 terug en maakt geen bestand |
| Opgemaakte tekst | .prn | Nee | SaveAs geeft -1 terug en maakt geen bestand |
Tien bytes voor een dBASE-veldnaam, geteld in de code page
Elke groottelimiet in een dBASE-tabel is een bytelling. Dat is waar multibyte-code pages pijn doen. Een velddescriptor bewaart de naam in elf bytes — tien voor de naam, één voor zijn afsluitende NUL — en zet de typeletter er direct achter op offset 11, de breedte op 16 en het decimaaltal op 17. Onder code page 936 is een Chinees teken twee bytes, dus een Chinese naam van tien tekens is er twintig. Kopieer die bytes erin zonder ze te meten en ze lopen recht door de NUL, de typeletter en de breedte heen; geen enkele reader kan die descriptor daarna nog herstellen, ook al passeerde de naam een controle op tien tekens. Houd dBASE-veldnamen ASCII, ook op een CJK-werkstation. xBase-tools, ODBC-drivers en GIS-software die shapefile-attribuuttabelen lezen hangen allemaal aan die namen, en een naam die alleen onder 936 decodeert wordt op elke machine met een andere page andere bytes
| Bytes | dBASE III-inhoud |
|---|---|
| 0 | Versie: 0x03, of 0x83 wanneer een memo-bestand .dbt de tabel vergezelt |
| 1–3 | Laatste update: jaar sinds 1900, maand, dag |
| 4–7 | Recordtelling, 32-bit little-endian |
| 8–11 | Headerlengte en recordlengte, 16-bit woorden |
| Vanaf 32 | Eén descriptor van 32 bytes per veld, dan 0x0D; records volgen, elk achter een deletievlag |
Breedtes zijn ook bytes. Een karakterveld (C) stopt bij 254 bytes, N-waarden zijn rechtsuitgelijnde tekst met de opgegeven decimalen, D-waarden zijn acht tekens in de vorm YYYYMMDD, en een waarde die te breed is voor zijn veld wordt een rij sterretjes, de conventie die dBASE zelf gebruikt. Langere tekst gaat naar een memoveld (M) met een bloknummer naar de .dbt-partner: een header van 512 bytes, dan blokken van 512 bytes, elke memo afgesloten door 0x1A 0x1A. TXLSXWorkbook.SaveAs met een .dbf-naam maakt van de eerste gebruikte rij van het actieve werkblad veldnamen, leidt per kolom C, N, L of D af, valt terug op C voor gemengde kolommen en verplaatst tekst langer dan 254 tekens naar M. Moeten de breedtes in bytes worden aangestuurd, bouw dan de tabel zelf met TXlsDbfTable en maak elk C-veld op maat van de gecodeerde lengte van zijn langste waarde
uses
Windows, SysUtils, Variants, lxDbf;
procedure WriteCustomerTable(const FileName: string);
var
Table: TXlsDbfTable;
Row: Integer;
begin
Table := TXlsDbfTable.Create;
try
// ASCII-namen van ten hoogste tien bytes; breedtes zijn bytes in de host code page
Table.AddField('CUSTNO', 'N', 6, 0);
Table.AddField('CITY', 'C', 40, 0);
Table.AddField('BALANCE', 'N', 12, 2);
Table.AddField('OPENED', 'D', 8, 0);
Row := Table.AddRow;
Table.SetValue(Row, 0, 1001);
Table.SetValue(Row, 1, WideString(#$5317#$4EAC#$5E02)); // 北京市, 6 bytes in CP936
Table.SetValue(Row, 2, 1234.5);
Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));
try
XlsWriteDbfFile(FileName, Table);
except
on E: EXlsDbf do
// Verwacht op een 1252-host: de melding noemt veld "CITY" en rij 1,
// en er blijft geen .dbf op schijf achter
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
Verwacht: op een 936-host wordt de tabel geschreven en heropent hij met alle vier de velden. Op een 1252-host gooit XlsWriteDbfFile een EXlsDbf die het veld en de één-based rij benoemt. XlsWriteDbfStream converteert elke veldnaam en elke waarde C of M naar de hostpage en terug, vergelijkt, en meet elke naam tegen zijn tien bytes voordat de header naar buiten gaat, dus een geweigerde tabel laat de stream onaangeroerd. De bestandsvorm gaat een stap verder en verwijdert de .dbf, en elke .dbt, die hij al had gemaakt
Hoe toetst u of een string de host code page overleeft?
Converteer hem naar AnsiString, converteer die terug naar WideString, en vergelijk met het origineel. Een teken dat de page mist komt terug als vervanging, meestal ?, dus de vergelijking faalt precies wanneer de export data zou verliezen. De writers gebruiken dezelfde conversie, dus de check en de writer kunnen het niet oneens zijn over de code page. Bewaar ook de bytelengte: dat is het getal om tegen een naamslot van tien bytes of een veldbreedte aan te houden
uses
Variants;
// True wanneer elk teken van V de host ANSI code page overleeft;
// ByteLen is de gecodeerde lengte om tegen een veldbreedte te vergelijken
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
W: WideString;
A: AnsiString;
begin
W := VarToWideStr(V); // VarToStr kan converteren voordat deze check de tekst ziet
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
Het commentaar bij VarToWideStr draagt de echte les. In Delphi 2007 en eerder is string overal AnsiString, en onder Free Pascal wordt de unit Variants gecompileerd met het eigen string-type van de RTL, AnsiString, welke modus uw unit ook gebruikt. Op die compilers heeft VarToStr al een ? gezet waar elk Chinees teken stond. Een guard gebouwd op AnsiString(VarToStr(V)) vergelijkt dan vraagtekens met vraagtekens en accepteert alles. TStringList heeft daar dezelfde soort probleem: wijs brede tekst toe aan zijn property Text om regels te splitsen en elk teken gaat onderweg naar binnen door de host code page. XlsParseSlk, XlsParseDifText en XlsParsePrnText nemen WideString-invoer en houden elk teken ervan vast op elke host. Bouwt u ook met Lazarus, zie met welk string-type de HotXLS-units onder Free Pascal compileren
SYLK- en DIF-records, regel voor regel
SYLK is één record per regel, velden gescheiden door puntkomma's. HotXLS schrijft een header ID;PHotXLS, één record C;Yrij;Xkolom;Kwaarde per cel en een terminator E. Tekst wordt geciteerd met ingesloten quotes verdubbeld, wat ook een puntkomma binnen een waarde beschermt, en de reader accepteert de escape ;; die sommige producenten in plaats daarvan gebruiken. Er is geen Boolean. De eigen writers van het formaat sturen 1 en 0, dus dat schrijft SaveAs; fouten gaan eruit als tekst zoals #N/A, datums als serienummers. Die twee letters bovenaan zijn hoe Excel SYLK herkent — en waarom het een CSV waarvan de eerste cel ID leest voor een SYLK-bestand aanziet. Build en parse werken beide op WideString, dus de round trip in het geheugen hieronder geeft op elke host hetzelfde antwoord
uses
SysUtils, lxSlk;
function SylkRoundTripsInMemory: Boolean;
var
Cells, Back: TXLSSlkCells;
Text: WideString;
I: Integer;
begin
SetLength(Cells, 3);
Cells[0].Row := 1; Cells[0].Col := 1; Cells[0].Kind := slkText;
Cells[0].Text := WideString(#$4E0A#$6D77'; Pudong'); // 上海; Pudong
Cells[1].Row := 1; Cells[1].Col := 2; Cells[1].Kind := slkText;
Cells[1].Text := 'Pipe 12" steel';
Cells[2].Row := 2; Cells[2].Col := 1; Cells[2].Kind := slkNumber;
Cells[2].Number := 1234.5;
Text := XlsBuildSlk(Cells); // K"上海; Pudong", K"Pipe 12"" steel", K1234.5
Result := XlsParseSlk(Text, Back) = Length(Cells);
for I := 0 to High(Cells) do
if Result then
Result := (Back[I].Row = Cells[I].Row) and (Back[I].Col = Cells[I].Col) and
(Back[I].Kind = Cells[I].Kind) and (Back[I].Text = Cells[I].Text) and
((Cells[I].Kind <> slkNumber) or (Back[I].Number = Cells[I].Number));
end;
DIF is woordjie-rijker. Vier header-groepen, TABLE, VECTORS (kolomaantal), TUPLES (rijtelling) en DATA, nemen elk drie regels; elk data-item neemt er daarna twee, een regel type,nummer en een regel met de geciteerde string of een waarde-indicator. Een rij opent met -1,0 en BOT, en -1,0 met EOD sluit de data af. Een getal is 0,waarde plus V, een logische waarde 0,1 plus TRUE of 0,0 plus FALSE, een string 1,0 plus zijn geciteerde tekst. Bij het lezen zet HotXLS de indicatoren NA en ERROR om naar #N/A en #VALUE! en behandelt NULL als leeg. Een werkblad genaamd Orders waarvan de enige rij Beijing, 1234.5 en True bevat exporteert als:
TABLE
0,1
"Orders"
VECTORS
0,3
""
TUPLES
0,1
""
DATA
0,0
""
-1,0
BOT
1,0
"Beijing"
0,1234.5
V
0,1
TRUE
-1,0
EOD
Een decimale komma breekt alle drie de formaten
De code page is niet de enige machine-instelling die in deze bestanden lekt. Op een werkstation met Duitse of Franse regionale instellingen maakt FloatToStr van 1234.5 een 1234,5. In DIF wordt de itemregel 0,1234,5 en verliest hij zijn vorm type,waarde. In SYLK is K1234,5 geen getal. In een dBASE-veld N verwachten xBase-readers een punt. HotXLS schrijft en leest alle drie met TFormatSettings.Invariant: SYLK krijgt het kortste token dat terugleest naar dezelfde double, DIF een invariante FloatToStr met .0 achter hele getallen, dBASE-numerieke velden hun opgegeven decimalen, rechtsuitgelijnd. Het parsen negeert de locale ook, dus een bestand van een machine met decimale komma leest overal hetzelfde
Tabellen van andere programma's lezen
De tabel van iemand anders openen zet de code page-vraag op de lezende machine. XlsReadDbfFile, en TXLSXWorkbook.Open op een .dbf-naam, accepteren versiebytes 0x03, 0x83, 0x8B, 0x30, 0x31 en 0x32, en dekken daarmee dBASE III en IV en Visual FoxPro-tabellen, inclusief de binaire FoxPro-types I (32-bit integer), B (double), Y (valuta geschaald met 10.000) en T (Juliaanse dag plus milliseconden). Memotekst komt uit een .dbt naast de tabel, of uit een .fpt met de bloklayout van FoxPro wanneer er geen .dbt bestaat; de extensie beslist, dus geen van beide partners wordt voor de ander aangezien. Karakterdata wordt in de hostpage gedecodeerd. Een dBASE III-tabel geschreven door een DOS-programma onder code page 866, met byte 29 op nul gelaten, toont dus de verkeerde letters op een 1252-machine. Weet u de producent, draai de import dan waar diens code page de systeempage is. SYLK-, DIF- en .prn-bestanden volgen dezelfde regel, want Open decodeert hun bytes in de hostpage voordat de wide-text-parser ze ziet
Welk formaat moet non-ASCII-tekst tussen locales vervoeren?
Geen van deze vier, als er een keus is. Gebruik ze wanneer het ontvangende systeem aandringt — een ERP-importscherm, een xBase-applicatie, een GIS-pipeline — en wanneer de data ASCII is of beide kanten een code page delen. Al het andere hoort te reizen in een formaat dat zijn encoding declareert: XLSX, SpreadsheetML 2003, of CSV, dat HotXLS als UTF-8 schrijft met een byte order mark (de CSV- en TSV-export heeft eigen transportvalkuilen). Omdat SaveAs het formaat uit de extensie haalt, moet uw code vanaf de eerste tabel beide weigeringsstijlen aan: een exception voor .dbf, een resultaat van -1 voor de tekstformaten
uses
SysUtils, lxHandleX, lxDbf;
function SaveForLegacyImport(const BaseName: string): string;
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Orders'); // het eerst toegevoegde werkblad wordt actief
Sheet.Cells[1, 1].Value := 'CUSTNO';
Sheet.Cells[1, 2].Value := 'CITY';
Sheet.Cells[2, 1].Value := 1001;
Sheet.Cells[2, 2].Value := WideString(#$5317#$4EAC); // 北京
Result := BaseName + '.slk';
if Book.SaveAs(Result) <> 1 then
begin
// De actieve code page kan de tekst niet vasthouden: er werd geen .slk gemaakt
Result := BaseName + '.xlsx';
Book.SaveAs(Result);
end;
try
Book.SaveAs(BaseName + '.dbf');
except
on E: EXlsDbf do
Result := Result + ' (no .dbf: ' + E.Message + ')';
end;
finally
Book.Free;
end;
end;
Op een 936-host levert deze functie een .slk en een .dbf op die beide 北京 bevatten; op een 1252-host levert hij een .xlsx op en meldt waarom de .dbf werd geweigerd. RTF-, HTML- en ZIP-entrynamen in de HotXLS Delphi spreadsheet component volgen encoding-regels van eigen huis