Technisch artikel

DBF, SYLK en DIF exporteren zonder code page in het bestand

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

FormaatExtensie bij SaveAs / OpenEncoding gedeclareerd in het bestandTekst die de hostpage niet aan kan
dBASE III-tabel.dbf, plus een memo-bestand .dbt indien nodigNee (byte 29 blijft nul)EXlsDbf gaat omhoog vóór het schrijven, en de zojuist gemaakte .dbf / .dbt worden verwijderd
SYLK.slk, .sylkNeeSaveAs geeft -1 terug en maakt geen bestand
DIF.difNeeSaveAs geeft -1 terug en maakt geen bestand
Opgemaakte tekst.prnNeeSaveAs 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

HotXLS dBASE III DBF-velddescriptor-layout in Delphi: een naamslot van tien bytes plus NUL, typeletter op offset 11, breedte op 16 en decimalen op 17, waar AddField('CITY') past maar een Chinese naam van tien tekens onder code page 936 twintig bytes overstroomt
Binnen een dBASE-velddescriptor van 32 bytes loopt een Chinese veldnaam die een controle op tien tekens passeert onder code page 936 de NUL, typeletter en breedte in
BytesdBASE III-inhoud
0Versie: 0x03, of 0x83 wanneer een memo-bestand .dbt de tabel vergezelt
1–3Laatste update: jaar sinds 1900, maand, dag
4–7Recordtelling, 32-bit little-endian
8–11Headerlengte en recordlengte, 16-bit woorden
Vanaf 32Eé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

HotXLS host ANSI code page round-trip-controle voor DBF-, SYLK-, DIF- en PRN-export in Delphi: AnsiString en terug naar WideString houdt Chinese tekst vast op een 936-host maar levert vraagtekens op 1252, geweigerd met EXlsDbf of SaveAs die -1 teruggeeft
Elke waarde naar de host code page converteren en terug legt verloren tekens bloot voordat er ook maar een byte schijf bereikt, mits de tekst met VarToWideStr in plaats van VarToStr werd gelezen

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

HotXLS SYLK- en DIF-export in Delphi: C;Y;X;K-records met header ID;PHotXLS en Boolean als 1, DIF type,nummer-itemparen tussen BOT en EOD, en TFormatSettings.Invariant dat 1234.5 intact houdt waar een decimale komma DIF, SYLK en dBASE breekt
De Orders-rij als SYLK-records en DIF-itemparen weggeschreven laat zien waar een locale met decimale komma elk formaat zou corrumperen, en waarom HotXLS getallen met invariante instellingen formatteert

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