Nombres de ciudades chinas exportados a .dbf en una PC occidental pueden volver como signos de interrogación desde una exportación que reportó éxito, porque los archivos dBASE, SYLK y DIF no tienen dónde registrar una codificación. Un escritor seguro usa la code page ANSI del host y verifica cada nombre y valor contra ella antes del primer byte; HotXLS rechaza lo que esa página no pueda contener
La code page del host es la única codificación que tienen estos archivos
Los tres formatos vienen de los primeros años de la PC: dBASE de Ashton-Tate, SYLK de la era Multiplan de Microsoft, DIF de Software Arts, la empresa de VisiCalc. Cada uno guarda texto como bytes individuales y asume que el lector comparte la code page del escritor — una apuesta razonable cuando todas las máquinas de la oficina corrían el mismo DOS. En Windows de hoy esa página es la 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 del header de la tabla, pero el layout dBASE III que HotXLS escribe deja ese byte en cero, y SYLK, DIF y el texto .prn alineado con espacios no tienen ningún campo de header para ello. Así que la code page se acuerda fuera del archivo. HotXLS usa la code page ANSI del host al escribir y al leer, y verifica 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 archivo | Texto que la página del host no puede contener |
|---|---|---|---|
| Tabla dBASE III | .dbf, más un archivo memo .dbt cuando hace falta | No (el byte 29 queda en 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 archivo |
| DIF | .dif | No | SaveAs devuelve -1 y no crea archivo |
| Texto formateado | .prn | No | SaveAs devuelve -1 y no crea archivo |
Diez bytes para un nombre de campo dBASE, contados en la code page
Todo límite de tamaño en una tabla dBASE es un conteo de bytes, y ahí es donde duelen las code pages multibyte. Un descriptor de campo guarda el nombre en once bytes — diez para el nombre y uno para su NUL de cierre — y pone la letra de tipo justo detrás, en el offset 11, el ancho en 16 y el conteo de decimales en 17. Bajo la code page 936 un carácter chino son dos bytes, así que un nombre chino de diez caracteres son veinte. Copie 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 haya pasado una verificación de diez caracteres. Mantenga los nombres de campo dBASE en ASCII incluso en una estación de trabajo CJK: las herramientas xBase, los drivers ODBC y el software GIS que lee tablas de atributos de shapefiles dependen de esos nombres, y un nombre que solo decodifica bajo 936 se convierte en bytes distintos en cada máquina que corra otra página
| Bytes | Contenido dBASE III |
|---|---|
| 0 | Versión: 0x03, u 0x83 cuando un archivo memo .dbt acompaña a la tabla |
| 1–3 | Última actualización: año desde 1900, mes, día |
| 4–7 | Conteo de registros, little-endian de 32 bits |
| 8–11 | Longitud del header y del registro, palabras de 16 bits |
| 32 en adelante | Un descriptor de 32 bytes por campo, luego 0x0D; los registros siguen, cada uno detrás de un flag de borrado |
Los anchos también son bytes. Un campo de caracteres (C) se topa en 254 bytes, 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 vuelve una fila de asteriscos, la convención que el propio dBASE usa. El texto más largo va a un campo memo (M) que guarda un número de bloque hacia el compañero .dbt: un header 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 columnas mixtas y manda a M el texto de más de 254 caracteres. Cuando los anchos deben controlarse en bytes, construya la tabla usted mismo con TXlsDbfTable y dimensione cada campo C para 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 a lo más diez bytes; los anchos son bytes en la code page 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 el disco
raise Exception.CreateFmt('DBF export refused under code page %d: %s',
[GetACP, E.Message]);
end;
finally
Table.Free;
end;
end;
Esperado: en un host 936 la tabla se escribe y se 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 el header, así que una tabla rechazada deja el stream intacto. La forma de archivo va un paso más allá y borra el .dbf, y cualquier .dbt, que ya hubiera creado
¿Cómo probar si una cadena sobrevive a la code page del host?
Conviértala a AnsiString, convierta eso de vuelta a WideString y compare con el original. Un carácter que la página no tiene vuelve como reemplazo, normalmente ?, así que la comparación falla precisamente cuando la exportación perdería datos. Los escritores usan la misma conversión, de modo que la verificación y el escritor no pueden discrepar sobre la code page. Guarde también la longitud en bytes: es el número que se confronta contra una ranura de nombre de diez bytes o un ancho de campo
uses
Variants;
// True cuando cada carácter de V sobrevive a la code page ANSI del host;
// ByteLen es la longitud codificada para comparar con el ancho de un 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 verificación vea el texto
A := AnsiString(W);
ByteLen := Length(A);
Result := WideString(A) = W;
end;
El comentario sobre VarToWideStr trae 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, sin importar el modo de su unidad. En esos compiladores VarToStr ya puso un ? donde iba cada carácter chino. Un guard construido sobre AnsiString(VarToStr(V)) compara entonces signos de interrogación con signos de interrogación y acepta todo. TStringList tiene allí el mismo tipo de problema: asigne texto wide a su propiedad Text para partir líneas y cada carácter pasa por la code page del host de camino adentro. XlsParseSlk, XlsParseDifText y XlsParsePrnText toman entrada WideString y conservan cada uno de sus caracteres en cualquier host. Si también compila con Lazarus, vea con qué tipo de string compilan las unidades de HotXLS bajo Free Pascal
Registros SYLK y DIF, línea por línea
SYLK es un registro por línea, con campos separados por punto y coma. HotXLS escribe un header ID;PHotXLS, un record C;Yfila;Xcol;Kvalor por celda y un terminador E. El texto va entre comillas con las comillas incrustadas dobladas, lo que además protege un punto y coma dentro de un valor, y el lector acepta también el escape ;; que algunos productores usan en su lugar. No hay Boolean: los escritores propios del formato mandan 1 y 0, así que eso es lo que escribe SaveAs; los errores salen como texto tal como #N/A, y las fechas como números seriales. Esas dos letras de la cabecera son como Excel reconoce SYLK — y por qué confunde un CSV cuya primera celda dice ID con un archivo SYLK. La construcción y el parseo trabajan ambas sobre WideString, así que el viaje de ida y vuelta 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 verborreico. Cuatro grupos de header, TABLE, VECTORS (conteo de columnas), TUPLES (conteo de filas) y DATA, ocupan tres líneas cada uno; después cada dato ocupa dos, una línea tipo,número y una línea que contiene 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 es 0,1 más TRUE o 0,0 más FALSE, y una cadena es 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 vacío. Una hoja llamada Orders cuya única fila contiene 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 code page no es el único ajuste de la máquina que se cuela en estos archivos. En una estación con configuración regional alemana o francesa, FloatToStr convierte 1234.5 en 1234,5. En DIF la línea del dato 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 relee en 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 archivo de una máquina con coma decimal se lee igual en todas partes
Leer tablas que otros programas escribieron
Abrir la tabla de otro pone la pregunta de la code page en la máquina que lee. XlsReadDbfFile, y TXLSXWorkbook.Open sobre un nombre .dbf, aceptan los bytes de versión 0x03, 0x83, 0x8B, 0x30, 0x31 y 0x32, cubriendo tablas dBASE III y IV y Visual FoxPro, incluidos los tipos binarios de FoxPro I (entero de 32 bits), B (double), Y (currency escalado 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 ningún compañero se confunde con 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 code page 866, con el byte 29 en cero, muestra por tanto las letras equivocadas en una máquina 1252. Cuando usted conoce al productor, corra la importación donde su code page sea la página del sistema. Los archivos SYLK, DIF y .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, pudiendo elegir. Úselos cuando el sistema receptor insiste — una pantalla de importación de un ERP, una aplicación xBase, un pipeline GIS — y cuando los datos son ASCII o ambos extremos comparten code page. 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 propias trampas de transporte). Como SaveAs elige el formato por la extensión, su código tiene que manejar ambos 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 queda 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 code page 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 ambos contienen 北京; en un host 1252 produce un .xlsx y reporta 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