Nombres de ciudades chinas exportados a .dbf en un PC occidental pueden volver como interrogaciones desde una exportación que reportó éxito, porque los ficheros dBASE, SYLK y DIF no tienen dónde anotar una codificación. Un writer seguro usa la página de códigos ANSI del host y comprueba cada nombre y valor contra ella antes del primer byte; HotXLS rechaza lo que esa página no pueda contener
La página de códigos del host es la única codificación que tienen estos ficheros
Los tres formatos vienen de los primeros años del PC: dBASE de Ashton-Tate, SYLK de la era Multiplan de Microsoft, DIF de Software Arts, la empresa de VisiCalc. Cada uno guarda el texto en bytes sueltos y asume que el lector comparte la página de códigos del writer — una apuesta razonable cuando todas las máquinas de la oficina corrían el mismo DOS. En Windows hoy esa página es lo que devuelva GetACP: 1252 en una instalación de Europa occidental, 936 en chino simplificado, 1251 en cirílico. dBASE IV y FoxPro pusieron después una marca de language-driver en el byte 29 de la cabecera de la tabla, pero el layout dBASE III que escribe HotXLS deja ese byte a cero, y SYLK, DIF y el texto .prn alineado con espacios no tienen ningún campo de cabecera para ella. Así que la página de códigos se acuerda fuera del fichero. HotXLS usa la página ANSI del host al escribir y al leer, y comprueba antes de escribir que cada carácter tenga un byte en esa página
| Formato | Extensión en SaveAs / Open | Codificación declarada en el fichero | Texto que la página del host no puede contener |
|---|---|---|---|
| Tabla dBASE III | .dbf, más un fichero memo .dbt cuando haga falta | No (el byte 29 se queda a cero) | EXlsDbf se lanza antes de escribir, y el .dbf / .dbt recién creados se borran |
| SYLK | .slk, .sylk | No | SaveAs devuelve -1 y no crea ningún fichero |
| DIF | .dif | No | SaveAs devuelve -1 y no crea ningún fichero |
| Texto formateado | .prn | No | SaveAs devuelve -1 y no crea ningún fichero |
Diez bytes para un nombre de campo dBASE, contados en la página de códigos
Todos los límites de tamaño de una tabla dBASE son conteos de bytes. Ahí es donde duelen las páginas de códigos multibyte. Un descriptor de campo guarda el nombre en once bytes — diez para el nombre y uno para su NUL terminador — y pone la letra de tipo justo detrás, en el offset 11, el ancho en el 16 y el conteo de decimales en el 17. Bajo la página 936 un carácter chino son dos bytes, así que un nombre chino de diez caracteres son veinte. Copia esos bytes sin medirlos y atraviesan de largo el NUL, la letra de tipo y el ancho; ningún lector puede recuperar ese descriptor después, aunque el nombre pasara una comprobación de diez caracteres. Mantén los nombres de campo dBASE en ASCII incluso en una estación CJK: las herramientas xBase, los drivers ODBC y el software GIS que lee tablas de atributos de shapefile se anclan todos en esos nombres, y un nombre que solo decodifica bajo 936 se convierte en bytes distintos en cada máquina con otra página
| Bytes | Contenido dBASE III |
|---|---|
| 0 | Versión: 0x03, u 0x83 cuando un fichero memo .dbt acompaña a la tabla |
| 1–3 | Última actualización: año desde 1900, mes, día |
| 4–7 | Conteo de registros, 32-bit little-endian |
| 8–11 | Longitud de cabecera y longitud de registro, palabras de 16-bit |
| 32 onward | Un descriptor de 32 bytes por campo, luego 0x0D; los registros siguen, cada uno tras un flag de borrado |
Los anchos también son bytes. Un campo de caracteres (C) se queda en 254 bytes como máximo, los valores N son texto alineado a la derecha con los decimales declarados, los valores D son ocho caracteres en forma YYYYMMDD, y un valor demasiado ancho para su campo se convierte en una fila de asteriscos, la convención que usa el propio dBASE. El texto más largo va a un campo memo (M) que guarda un número de bloque en el compañero .dbt: una cabecera de 512 bytes, luego bloques de 512 bytes, cada memo terminado por 0x1A 0x1A. TXLSXWorkbook.SaveAs con un nombre .dbf convierte la primera fila usada de la hoja activa en nombres de campo, infiere C, N, L o D por columna, recurre a C para las columnas mixtas y mueve el texto de más de 254 caracteres a M. Cuando los anchos tienen que controlarse en bytes, construye la tabla tú mismo con TXlsDbfTable y dimensiona cada campo C por la longitud codificada de su valor más largo
uses
Windows, SysUtils, Variants, lxDbf;
procedure WriteCustomerTable(const FileName: string);
var
Table: TXlsDbfTable;
Row: Integer;
begin
Table := TXlsDbfTable.Create;
try
// Nombres ASCII de diez bytes como máximo; los anchos son bytes en la página de códigos del host
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 en CP936
Table.SetValue(Row, 2, 1234.5);
Table.SetValue(Row, 3, EncodeDate(2026, 11, 12));
try
XlsWriteDbfFile(FileName, Table);
except
on E: EXlsDbf do
// Esperado en un host 1252: el mensaje nombra el campo "CITY" y la fila 1,
// y no queda ningún .dbf en disco
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
Lo esperado: en un host 936 la tabla se escribe y reabre con los cuatro campos. En un host 1252 XlsWriteDbfFile lanza EXlsDbf nombrando el campo y la fila base uno. XlsWriteDbfStream convierte cada nombre de campo y cada valor C o M a la página del host y de vuelta, compara y mide cada nombre contra sus diez bytes antes de que salga la cabecera, así que una tabla rechazada deja el stream intacto. La forma de fichero va un paso más allá y borra el .dbf, y cualquier .dbt, que ya hubiera creado
¿Cómo compruebas si una cadena sobrevive a la página de códigos del host?
Conviértela a AnsiString, convierte eso de vuelta a WideString y compara con el original. Un carácter que la página no tiene vuelve como un reemplazo, normalmente ?, así que la comparación falla exactamente cuando la exportación perdería datos. Los writers usan la misma conversión, así que la comprobación y el writer no pueden discrepar sobre la página de códigos. Guarda también la longitud en bytes: es el número que contrastar con un hueco de nombre de diez bytes o con un ancho de campo
uses
Variants;
// True cuando cada carácter de V sobrevive a la página ANSI del host;
// ByteLen es la longitud codificada para comparar con un ancho de campo
function FitsHostCodePage(const V: Variant; out ByteLen: Integer): Boolean;
var
W: WideString;
A: AnsiString;
begin
W := VarToWideStr(V); // VarToStr puede convertir antes de que esta comprobación vea el texto
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
El comentario sobre VarToWideStr guarda la lección de verdad. En Delphi 2007 y anteriores string es AnsiString en todas partes, y bajo Free Pascal la unidad Variants se compila con el tipo string propio de la RTL, AnsiString, use tu unidad el modo que use. En esos compiladores VarToStr ya ha puesto un ? donde iba cada carácter chino. Un guard construido sobre AnsiString(VarToStr(V)) compara entonces interrogaciones con interrogaciones y acepta todo. TStringList tiene allí el mismo tipo de problema: asigna texto wide a su propiedad Text para partir líneas y cada carácter pasa por la página de códigos del host al entrar. XlsParseSlk, XlsParseDifText y XlsParsePrnText toman entrada WideString y conservan todos sus caracteres en cualquier host. Si también compilas con Lazarus, mira con qué tipo de string compilan las unidades de HotXLS bajo Free Pascal
Registros SYLK y DIF, línea a línea
SYLK es un registro por línea, con campos separados por punto y coma. HotXLS escribe una cabecera ID;PHotXLS, un registro C;Yfila;Xcol;Kvalor por celda y un terminador E. El texto va entre comillas con las comillas incrustadas dobladas, lo que también protege un punto y coma dentro de un valor, y el lector acepta el escape ;; que usan algunos productores en su lugar. No hay Boolean. Los writers del propio formato mandan 1 y 0, así que es lo que escribe SaveAs; los errores salen como texto del estilo #N/A y las fechas como números de serie. Esas dos letras de la cabecera son cómo Excel reconoce SYLK — y por qué confunde un CSV cuya primera celda dice ID con un fichero SYLK. La construcción y el parseo trabajan ambas sobre WideString, así que el round trip en memoria de abajo da la misma respuesta en cualquier host
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 es más verboso. Cuatro grupos de cabecera, TABLE, VECTORS (conteo de columnas), TUPLES (conteo de filas) y DATA, ocupan tres líneas cada uno; cada item de datos ocupa después dos, una línea tipo,número y una línea con la cadena entre comillas o un indicador de valor. Una fila abre con -1,0 y BOT, y -1,0 con EOD cierra los datos. Un número es 0,valor más V, un lógico 0,1 más TRUE o 0,0 más FALSE, una cadena 1,0 más su texto entre comillas. Al leer, HotXLS mapea los indicadores NA y ERROR a #N/A y #VALUE! y trata NULL como blanco. Una hoja llamada Orders cuya única fila lleva Beijing, 1234.5 y True se exporta así:
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 coma decimal rompe los tres formatos
La página de códigos no es el único ajuste de máquina que se cuela en estos ficheros. En una estación con ajustes regionales alemanes o franceses, FloatToStr convierte 1234.5 en 1234,5. En DIF la línea de item se vuelve 0,1234,5 y pierde su forma tipo,valor. En SYLK, K1234,5 no es un número. En un campo N de dBASE, los lectores xBase esperan un punto. HotXLS escribe y lee los tres con TFormatSettings.Invariant: SYLK recibe el token más corto que se lee de vuelta como el mismo double, DIF un FloatToStr invariante con .0 añadido a los enteros, y los campos numéricos dBASE sus decimales declarados, alineados a la derecha. El parseo también ignora el locale, así que un fichero venido de una máquina con coma decimal se lee igual en todas partes
Leer tablas que escribieron otros programas
Abrir la tabla de otro pone la pregunta de la página de códigos en la máquina que lee. XlsReadDbfFile, y TXLSXWorkbook.Open con un nombre .dbf, aceptan los bytes de versión 0x03, 0x83, 0x8B, 0x30, 0x31 y 0x32, cubriendo tablas dBASE III y IV y de Visual FoxPro, incluidos los tipos binarios de FoxPro I (entero de 32-bit), B (double), Y (moneda escalada por 10.000) y T (día juliano más milisegundos). El texto memo viene de un .dbt junto a la tabla, o de un .fpt con el layout de bloques de FoxPro cuando no hay .dbt; la extensión decide, así que ninguno de los dos compañeros se lee por error como el otro. Los datos de caracteres se decodifican en la página del host. Una tabla dBASE III escrita por un programa DOS bajo la página 866, con el byte 29 a cero, muestra por tanto las letras equivocadas en una máquina 1252. Cuando conoces al productor, ejecuta el import donde su página de códigos sea la del sistema. SYLK, DIF y los ficheros .prn siguen la misma regla, porque Open decodifica sus bytes en la página del host antes de que el parser de texto wide los vea
¿Qué formato debería llevar texto no ASCII entre locales?
Ninguno de estos cuatro, teniendo elección. Úsalos cuando el sistema receptor insista — una pantalla de import de ERP, una aplicación xBase, un pipeline GIS — y cuando los datos sean ASCII o ambos extremos compartan página de códigos. Todo lo demás debería viajar en un formato que declare su codificación: XLSX, SpreadsheetML 2003, o CSV, que HotXLS escribe como UTF-8 con marca de orden de bytes (la exportación CSV y TSV tiene sus propios escollos de transporte). Como SaveAs elige el formato por la extensión, tu código tiene que manejar los dos estilos de rechazo desde la primera tabla: una excepción para .dbf, un resultado de -1 para los formatos de texto
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'); // la primera hoja añadida pasa a ser la activa
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 página de códigos activa no puede contener el texto: no se creó ningún .slk
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;
En un host 936 esta función produce un .slk y un .dbf que llevan ambos 北京; en un host 1252 produce un .xlsx e informa de por qué se rechazó el .dbf. Los nombres de entradas RTF, HTML y ZIP en el componente de hojas de cálculo HotXLS para Delphi siguen reglas de codificación propias