Articolo tecnico

Esportare DBF, SYLK e DIF quando il file non ha code page

Nomi di città cinesi esportati in .dbf su un PC occidentale possono tornare come punti interrogativi da un export che aveva riferito successo, perché i file dBASE, SYLK e DIF non hanno dove registrare una codifica. Un writer sicuro usa la code page ANSI della macchina ospite e controlla ogni nome e valore contro di essa prima del primo byte; HotXLS rifiuta ciò che quella pagina non può contenere

La code page della macchina ospite è l'unica codifica che questi file abbiano

Tutti e tre i formati vengono dai primi anni del PC: dBASE da Ashton-Tate, SYLK dall'era Multiplan di Microsoft, DIF da Software Arts, l'azienda di VisiCalc. Ciascuno conserva il testo come byte singoli e presume che chi legge condivida la code page di chi scrive — una scommessa equa quando ogni macchina dell'ufficio girava con lo stesso setup DOS. Su Windows oggi quella pagina è ciò che restituisce GetACP: 1252 su una installazione dell'Europa occidentale, 936 sul cinese semplificato, 1251 sul cirillico. dBASE IV e FoxPro hanno poi messo un marchio del language driver al byte 29 dell'header della tabella, ma il layout dBASE III che HotXLS scrive lascia quel byte a zero, e SYLK, DIF e il testo .prn allineato con spazi non hanno affatto un campo header per questo. Quindi la code page si concorda fuori dal file. HotXLS usa la code page ANSI della macchina ospite in scrittura e in lettura, e controlla prima di scrivere che ogni carattere abbia un byte in quella pagina

FormatoEstensione su SaveAs / OpenCodifica dichiarata nel fileTesto che la pagina ospite non può contenere
Tabella dBASE III.dbf, più un file memo .dbt quando serveNo (il byte 29 resta a zero)EXlsDbf viene sollevata prima di scrivere, e i .dbf / .dbt appena creati vengono cancellati
SYLK.slk, .sylkNoSaveAs restituisce -1 e non crea alcun file
DIF.difNoSaveAs restituisce -1 e non crea alcun file
Testo formattato.prnNoSaveAs restituisce -1 e non crea alcun file

Dieci byte per un nome di campo dBASE, contati nella code page

Ogni limite di dimensione in una tabella dBASE è un conteggio di byte, ed è lì che le code page multibyte fanno male. Un descrittore di campo tiene il nome in undici byte — dieci per il nome, uno per il NUL di terminazione — e mette la lettera del tipo subito dietro, all'offset 11, la larghezza a 16 e il conteggio dei decimali a 17. Con la code page 936 un carattere cinese è due byte, quindi un nome cinese di dieci caratteri ne fa venti. Copiate quei byte dentro senza misurarli e attraversano dritti il NUL, la lettera del tipo e la larghezza; nessun reader può recuperare quel descrittore dopo, anche se il nome aveva passato un controllo a dieci caratteri. Tenete i nomi di campo dBASE ASCII anche su una workstation CJK. Gli strumenti xBase, i driver ODBC e il software GIS che legge le tabelle di attributi degli shapefile puntano tutti su quei nomi, e un nome che si decodifica solo sotto 936 diventa byte diversi su ogni macchina che gira con un'altra pagina

Layout del descrittore di campo DBF dBASE III di HotXLS in Delphi: slot del nome da dieci byte più NUL, lettera del tipo all'offset 11, larghezza a 16 e decimali a 17, dove AddField('CITY') ci sta ma un nome cinese di dieci caratteri con la code page 936 sfora i venti byte
Dentro un descrittore di campo dBASE da 32 byte, un nome di campo cinese che passa un controllo a dieci caratteri trabocca nel NUL, nella lettera del tipo e nella larghezza con la code page 936
ByteContenuto dBASE III
0Versione: 0x03, o 0x83 quando un file memo .dbt accompagna la tabella
1–3Ultimo aggiornamento: anno dal 1900, mese, giorno
4–7Conteggio record, little-endian a 32 bit
8–11Lunghezza header e lunghezza record, parole a 16 bit
dal 32 in poiUn descrittore da 32 byte per campo, poi 0x0D; seguono i record, ciascuno dopo una flag di cancellazione

Anche le larghezze sono byte. Un campo carattere (C) si ferma a 254 byte, i valori N sono testo allineato a destra con i decimali dichiarati, i valori D sono otto caratteri in forma YYYYMMDD, e un valore troppo largo per il suo campo diventa una fila di asterischi, la convenzione che usa dBASE stesso. Il testo più lungo va in un campo memo (M) che contiene un numero di blocco nel .dbt compagno: un header da 512 byte, poi blocchi da 512 byte, ogni memo chiuso da 0x1A 0x1A. TXLSXWorkbook.SaveAs con un nome .dbf trasforma la prima riga usata del foglio attivo in nomi di campo, deduce C, N, L o D per colonna, ripiega sul C per le colonne miste e sposta il testo oltre i 254 caratteri in M. Quando le larghezze vanno controllate in byte, costruite la tabella da soli con TXlsDbfTable e dimensionate ogni campo C sulla lunghezza codificata del suo valore più lungo

uses
  Windows, SysUtils, Variants, lxDbf;

procedure WriteCustomerTable(const FileName: string);
var
  Table: TXlsDbfTable;
  Row: Integer;
begin
  Table := TXlsDbfTable.Create;
  try
    // Nomi ASCII di al più dieci byte; le larghezze sono byte nella code page ospite
    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 byte 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
        // Atteso su una macchina 1252: il messaggio nomina il campo "CITY" e la riga 1,
        // e nessun .dbf resta su disco
        raise Exception.CreateFmt('DBF export refused under code page %d: %s',
          [GetACP, E.Message]);
    end;
  finally
    Table.Free;
  end;
end;

Atteso: su una macchina 936 la tabella viene scritta e si riapre con tutti e quattro i campi. Su una 1252 XlsWriteDbfFile solleva EXlsDbf nominando il campo e la riga a base uno. XlsWriteDbfStream converte ogni nome di campo e ogni valore C o M nella pagina ospite e indietro, confronta, e misura ogni nome contro i suoi dieci byte prima che l'header esca, quindi una tabella rifiutata lascia lo stream intatto. La forma su file va un passo oltre e cancella il .dbf, e ogni .dbt, che aveva già creato

Come si verifica se una stringa sopravvive alla code page ospite?

Convertitela in AnsiString, riconvertitela in WideString e confrontatela con l'originale. Un carattere che la pagina non ha torna come rimpiazzo, di solito ?, quindi il confronto fallisce esattamente quando l'export perderebbe dati. I writer usano la stessa conversione, così il controllo e il writer non possono essere in disaccordo sulla code page. Tenete anche la lunghezza in byte: è il numero da far stare in uno slot da dieci byte o in una larghezza di campo

uses
  Variants;

// True quando ogni carattere di V sopravvive alla code page ANSI ospite;
// ByteLen è la lunghezza codificata da confrontare con una larghezza di campo
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
  W: WideString;
  A: AnsiString;
begin
  W := VarToWideStr(V);   // VarToStr può convertire prima che questo controllo veda il testo
  A := AnsiString(W);
  ByteLen := Length(A);
  Result := WideString(A) = W;
end;

Il commento su VarToWideStr porta con sé la lezione vera. In Delphi 2007 e prima string è AnsiString ovunque, e sotto Free Pascal l'unità Variants è compilata col tipo string della RTL, cioè AnsiString, qualunque modalità usi la vostra unità. Su quei compilatori VarToStr ha già messo un ? dove stava ogni carattere cinese: una guardia costruita su AnsiString(VarToStr(V)) confronta allora punti interrogativi con punti interrogativi e accetta tutto. TStringList ha lì lo stesso tipo di problema: assegnate testo wide alla sua proprietà Text per spezzare le righe e ogni carattere passa per la code page ospite lungo la strada. XlsParseSlk, XlsParseDifText e XlsParsePrnText prendono input WideString e ne conservano ogni carattere su qualsiasi macchina. Se compilate anche con Lazarus, vedete con quale tipo stringa compilano le unità HotXLS sotto Free Pascal

Controllo round-trip della code page ANSI ospite di HotXLS per l'export DBF, SYLK, DIF e PRN in Delphi: AnsiString e ritorno a WideString conserva il testo cinese su una macchina 936 ma produce punti interrogativi su 1252, rifiutato con EXlsDbf o SaveAs che restituisce -1
Convertire ogni valore nella code page ospite e indietro espone i caratteri persi prima che qualsiasi byte tocchi il disco, purché il testo sia stato letto con VarToWideStr anziché VarToStr

Record SYLK e DIF, riga per riga

SYLK è un record per riga, con campi separati da punti e virgola. HotXLS scrive un header ID;PHotXLS, un record C;Yriga;Xcolonna;Kvalore per cella e un terminatore E. Il testo va tra virgolette, con le virgolette interne raddoppiate, il che protegge anche un punto e virgola dentro un valore, e il reader accetta l'escape ;; che alcuni producer usano al suo posto. Non c'è un Boolean: i writer del formato stesso mandano 1 e 0, quindi è ciò che scrive SaveAs; gli errori escono come testo tipo #N/A, le date come numeri seriali. Quelle due lettere in cima sono il modo in cui Excel riconosce SYLK — ed è per questo che scambia per un file SYLK un CSV la cui prima cella dice ID. Costruzione e parse lavorano entrambi su WideString, così il round trip in memoria qui sotto dà la stessa risposta su qualsiasi macchina

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 è più verboso. Quattro gruppi di header, TABLE, VECTORS (numero di colonne), TUPLES (numero di righe) e DATA, occupano tre righe ciascuno; poi ogni elemento di dati ne occupa due, una riga tipo,numero e una riga con la stringa tra virgolette o un indicatore di valore. Una riga si apre con -1,0 e BOT, e -1,0 con EOD chiude i dati. Un numero è 0,valore più V, un logico 0,1 più TRUE oppure 0,0 più FALSE, una stringa 1,0 più il proprio testo tra virgolette. In lettura HotXLS mappa gli indicatori NA e ERROR su #N/A e #VALUE! e tratta NULL come vuoto. Un foglio di nome Orders la cui unica riga contiene Beijing, 1234.5 e True si esporta così:

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

Una virgola decimale rompe tutti e tre i formati

La code page non è l'unica impostazione della macchina che si infiltra in questi file. Su una workstation con impostazioni regionali tedesche o francesi, FloatToStr trasforma 1234.5 in 1234,5. In DIF la riga dell'elemento diventa 0,1234,5 e perde la sua forma tipo,valore. In SYLK, K1234,5 non è un numero. In un campo N dBASE, i reader xBase si aspettano un punto. HotXLS scrive e legge tutti e tre con TFormatSettings.Invariant: SYLK prende il token più corto che rilegge allo stesso double, DIF un FloatToStr invariante con .0 aggiunto ai numeri interi, i campi numerici dBASE i propri decimali dichiarati, allineati a destra. Anche il parse ignora il locale, quindi un file da una macchina con virgola decimale si legge allo stesso modo ovunque

Export SYLK e DIF con HotXLS in Delphi: record C;Y;X;K con header ID;PHotXLS e Boolean come 1, coppie di elementi tipo,numero DIF fra BOT ed EOD, e TFormatSettings.Invariant che tiene 1234.5 intatto dove una virgola decimale rompe DIF, SYLK e dBASE
Scrivere la riga Orders come record SYLK e coppie di elementi DIF mostra dove un locale con virgola decimale corromperebbe ciascun formato, e perché HotXLS formatta i numeri con impostazioni invarianti

Leggere tabelle scritte da altri programmi

Aprire la tabella di qualcun altro sposta la domanda sulla code page sulla macchina che legge. XlsReadDbfFile, e TXLSXWorkbook.Open su un nome .dbf, accettano i byte di versione 0x03, 0x83, 0x8B, 0x30, 0x31 e 0x32, coprendo tabelle dBASE III e IV e Visual FoxPro, inclusi i tipi binari di FoxPro I (intero a 32 bit), B (double), Y (currency scalata di 10.000) e T (giorno giuliano più millisecondi). Il testo memo arriva da un .dbt accanto alla tabella, oppure da un .fpt col layout a blocchi di FoxPro quando non esiste un .dbt; decide l'estensione, quindi nessuno dei due compagni viene letto al posto dell'altro. I dati carattere vengono decodificati nella pagina ospite: una tabella dBASE III scritta da un programma DOS sotto la code page 866, col byte 29 lasciato a zero, mostra quindi le lettere sbagliate su una macchina 1252. Quando sapete chi è il producer, fate l'import dove la sua code page è la pagina di sistema. I file SYLK, DIF e .prn seguono la stessa regola, perché Open decodifica i loro byte nella pagina ospite prima che il parser del testo wide li veda

Quale formato dovrebbe portare testo non ASCII fra locale diversi?

Nessuno di questi quattro, se avete la scelta. Usateli quando il sistema ricevente insiste — una schermata di import ERP, un'applicazione xBase, una pipeline GIS — e quando i dati sono ASCII o le due estremità condividono una code page. Tutto il resto dovrebbe viaggiare in un formato che dichiara la propria codifica: XLSX, SpreadsheetML 2003, oppure CSV, che HotXLS scrive come UTF-8 con byte order mark (il export CSV e TSV ha le sue insidie di trasporto). Siccome SaveAs sceglie il formato dall'estensione, il vostro codice deve gestire entrambi gli stili di rifiuto dalla prima tabella: un'eccezione per .dbf, un risultato -1 per i formati testuali

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');   // il primo foglio aggiunto diventa attivo
    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
      // La code page attiva non può contenere il testo: nessun .slk è stato creato
      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;

Su una macchina 936 questa funzione produce un .slk e un .dbf che contengono entrambi 北京; su una 1252 produce un .xlsx e riferisce perché il .dbf è stato rifiutato. I nomi delle voci RTF, HTML e ZIP nel componente fogli di calcolo HotXLS per Delphi seguono regole di codifica proprie