Artículo técnico

Fracciones de Excel: convergentes de fracción continua

Da a 3.14159 la máscara de fracción # ??/?? y Excel muestra 3 1/7, aunque 3 14/99 está más de siete veces más cerca, de modo que una exportación Delphi que busca la fracción más cercana imprime un valor que el usuario jamás ve. La razón es que Excel solo acepta convergentes de la fracción continua; en HotXLS, TXLSXCell.FormattedText devuelve el mismo 3 1/7, espacios de relleno incluidos

Excel se detiene en los convergentes de la fracción continua

Toda máscara de fracción aproximadora le da a Excel el mismo trabajo: encontrar un numerador y un denominador para la parte tras el entero, con el denominador limitado por el número de huecos de dígito tras la barra. Excel lo hace desarrollando el valor como fracción continua y quedándose solo con los convergentes, las fracciones que obtienes cortando ese desarrollo tras cada término. Para 0.14159 el desarrollo empieza con [0; 7, 15, 1, ...], que rinde 1/7, luego 15/106, luego 16/113. El entero 3 no cambia nada. Con él, 3.14159 pasa por los mismos 22/7, 333/106 y 355/113 que aparecen en toda tabla de aproximaciones a pi. Como 106 ya desborda un denominador de dos dígitos, Excel se queda con 1/7. El más cercano 14/99 es un semiconvergente, una de las fracciones intermedias que se sientan entre dos convergentes vecinos. Excel jamás las visita. No necesitas código a medida para topar con esto: los tipos Hasta un dígito y Hasta dos dígitos del cuadro de diálogo Formato de celdas son los IDs de formato integrados 12 y 13, # ?/? y # ??/??, en la lista de formatos numéricos predefinidos de ECMA-376 Parte 1 §18.8.30

Recta numérica de los convergentes de la fracción continua de 3.14159: 22/7, 333/106 y 355/113 en ejes con zoom, con los topes de denominador de la máscara de fracción decidiendo qué convergente conserva Excel
cada corte del desarrollo cae más cerca de 3.14159, y el número de dígitos tras la barra decide qué convergente sobrevive al tope

La brecha no se limita a entradas parecidas a pi. La mejor aproximación, típicamente un recorrido Stern-Brocot o un barrido por cada denominador permitido, pregunta qué fracción es la más cercana, mientras que el bucle de convergentes pregunta qué fracción alcanza antes el desarrollo mientras el denominador aún quepa. Con entradas ordinarias de dos decimales las dos reglas divergen a menudo lo suficiente para importar, y el 0.27 del código de abajo es un valor así. Si tu código escribe fracciones en una factura PDF, un informe HTML o un CSV que alguien va a confrontar con el libro de trabajo, copia allí la regla de Excel, aunque sea la menos exacta. Quien vea 3 14/99 sobre el papel y 3 1/7 en Excel no concluye que tu exportación sea más precisa. Concluye que una de las dos está rota. No va a ser Excel

Recta numérica con zoom para 0.27 mostrando el convergente 10/37 que muestra Excel frente al 17/63 más cercano de un barrido de fracción más próxima, con muestras en desacuerdo y de acuerdo
con valores ordinarios de dos decimales el bucle de convergentes y el barrido de fracción más próxima divergen a menudo lo suficiente para importar, así que las exportaciones deben copiar la regla de Excel

El bucle en sí es corto. Cuenta los marcadores de posición de dígito tras la barra, sean ?, # o 0, y el tope es 9, 99 o 999 para uno, dos o tres de ellos. Una máscara mixta como # ??/?? separa el entero y desarrolla solo la parte fraccionaria; una máscara impropia como ?/? desarrolla el valor entero, por eso 3.14159 bajo ?/? sale como 22/7. Cada paso toma la parte entera A del valor en curso B y la pliega en el siguiente convergente. El bucle se detiene en cuanto el resto B − A baja de 5e-8, de modo que una fracción simple más ruido de coma flotante sigue cayendo en esa fracción. Cuando un paso empuja el denominador más allá del tope, Excel vuelve al convergente anterior, y ese recurso puede caer en 1/1, que una máscara mixta arrastra al entero, así que 12.9999 bajo # ?/? muestra 13. Abajo el bucle está escrito en Delphi llano junto a una búsqueda por fuerza bruta de la fracción más próxima, para que veas dónde divergen las dos con tus propios números

uses
  SysUtils;

type
  TFraction = record
    Num, Den: Int64;
  end;

// La regla de los convergentes tras las máscaras # ?/?, # ??/?? y # ???/??? de Excel.
// X es no negativo: la parte fraccionaria para una máscara mixta, el valor entero
// para una impropia. MaxDen es 9, 99 o 999.
function ExcelFraction(X: Double; MaxDen: Integer): TFraction;
var
  B, R: Double;
  A, P, Q, P1, Q1, P2, Q2: Int64;
begin
  B := X;
  P2 := 0; Q2 := 1;      // convergente k-2
  P1 := 1; Q1 := 0;      // convergente k-1
  P := 0;  Q := 1;
  while Q1 < MaxDen do
  begin
    A := Trunc(B);
    P := A * P1 + P2;
    Q := A * Q1 + Q2;
    R := B - A;
    if R < 5E-8 then
      Break;               // el resto cuenta como cero
    B := 1 / R;
    P2 := P1; Q2 := Q1;
    P1 := P;  Q1 := Q;
  end;
  if Q > MaxDen then       // superado el tope: retrocede un convergente
    if Q1 > MaxDen then
    begin
      P := P2; Q := Q2;
    end
    else
    begin
      P := P1; Q := Q1;
    end;
  Result.Num := P;
  Result.Den := Q;
end;

// Lo que devuelve una rutina de "mejor aproximación": la N/D más cercana con D <= MaxDen
function NearestFraction(X: Double; MaxDen: Integer): TFraction;
var
  D: Integer;
  N: Int64;
  Err, BestErr: Double;
begin
  Result.Num := Round(X);
  Result.Den := 1;
  BestErr := Abs(X - Result.Num);
  for D := 2 to MaxDen do
  begin
    N := Round(X * D);
    Err := Abs(X - N / D);
    if Err < BestErr then  // estricto: la forma reducida gana los empates
    begin
      BestErr := Err;
      Result.Num := N;
      Result.Den := D;
    end;
  end;
end;

procedure CompareFractionRules;
const
  Samples: array[0..3] of Double = (0.14159, 0.99, 0.27, 0.5);
var
  I: Integer;
  E, N: TFraction;
begin
  for I := Low(Samples) to High(Samples) do
  begin
    E := ExcelFraction(Samples[I], 99);
    N := NearestFraction(Samples[I], 99);
    Writeln(Format('%-8g Excel %d/%d   nearest %d/%d',
      [Samples[I], E.Num, E.Den, N.Num, N.Den]));
  end;
end;

// Esperado:
// 0.14159  Excel 1/7   nearest 14/99
// 0.99     Excel 98/99   nearest 98/99
// 0.27     Excel 10/37   nearest 17/63
// 0.5      Excel 1/2   nearest 1/2

La línea del 0.27 está en la lista porque una entrada ordinaria de dos decimales también diverge, 10/37 contra 17/63, sin nada exótico en el valor. La frecuencia con que las reglas divergen depende de tus datos: las fracciones simples de verdad, como 0.5 o 0.375, salen igual por ambos caminos; las magnitudes medidas y los cocientes calculados son mucho menos fiables. Trunc hace de Floor porque la entrada nunca es negativa. Excel maneja el signo fuera de la fracción. Tu código debería hacer lo mismo. Y el argumento tiene que ser el Double que la celda guarda de verdad, por un motivo que la sección siguiente concreta

¿Qué le pasa al 0.99 en una fracción de dos dígitos?

Excel muestra 0.99 bajo # ??/?? como ·98/99 (cada · de este artículo representa un carácter de espacio). La causa es la coma flotante binaria. El literal 0.99 no tiene ningún Double exacto, y el valor guardado se queda a un pelo por debajo. Así que 1/(1/0.99 − 1) evalúa a aproximadamente 98.9999999999994 en vez de 99, el segundo término del desarrollo pasa a ser 98, y el convergente 98/99 cabe bajo el tope. Haz el mismo desarrollo con decimales exactos y obtienes [0; 1, 99]: ese 99 empuja el denominador hasta 100, el bucle vuelve a 1/1, y la celda leería 1······. Aquí, ser más cuidadoso que Excel es el bug. Un generador de informes que guarde importes como Currency o TBcd, o que vuelva a parsear la cadena decimal de la base de datos y la desarrolle con aritmética exacta, imprime un 1 entero donde Excel imprime 98/99. Dale al formateador el Double guardado. Mantén cada intermedio en coma flotante. La fracción sigue siendo asunto de presentación, además: las fórmulas siguen viendo 0.99, y HotXLS mantiene las secciones de fracción a precisión completa incluso en un libro con precision as displayed activado

Diagrama de dos caminos para 0.99 bajo la máscara de fracción de Excel # ??/??: el Double guardado desarrolla 98/99 mientras los decimales exactos desbordan el tope y muestran un 1 entero
el Double guardado aterriza en 98/99 mientras la aritmética decimal exacta vuelve a 1/1 — ser más cuidadoso que Excel es el bug aquí

Cómo rellena y alinea Excel la zona de la fracción

Excel coloca las fracciones igual que se coloca una columna de decimales: el numerador va alineado a la derecha en sus huecos, el denominador a la izquierda, de modo que la barra cae en la misma posición en todas las filas. Cada hueco sin usar se rellena según su marcador de posición. Un ? deja un espacio, un 0 escribe un cero, un # no escribe nada. Por eso 3.14159 bajo # ??/?? se lee 3··1/7·: un espacio es el separador de la máscara, otro llena el hueco vacío del numerador delante del 1, y otro llena el hueco vacío del denominador tras el 7. La tabla lista el texto que Excel muestra para un conjunto de entradas que cubren las reglas de disposición

Código de formatoValorExcel muestra
# ?/?0.5·1/2
# ?/?4/31·1/3
# ??/??3.141593··1/7·
# ??/??0.99·98/99
# ?/?1212····
# ?/?00····
# ?/?12.999913····
# #/#1212
0 ?/?0.50·1/2
#,##0 ?/?1234.751,234·3/4
?/?3.1415922/7
?/?00/1
??/??1212/1·

Los números enteros son donde los formateadores escritos a mano más se desvían de Excel. Bajo una máscara mixta cuyo numerador usa ?, Excel conserva los dígitos del entero, cero incluido, y cambia el separador, el numerador, la barra y el denominador por un espacio cada uno, así que 12 bajo # ?/? pasa a ser 12···· y bajo # ??/100 va seguido de siete espacios. Cambia el marcador del numerador a # y el relleno desaparece. Un entero cero funciona al revés: el hueco del # no imprime nada para 0.5 y el separador sobrevive solo como espacio, mientras que 0 ?/? fuerza el dígito, y las máscaras impropias jamás sueltan la fracción

Qué hacer con ese relleno depende de adonde vaya el texto. Consérvalo para informes de texto monoespaciado y para celdas de tabla PDF alineadas a la derecha. Alinear las barras es todo el punto ahí. Recórtalo para CSV y JSON, donde un espacio final convierte 12 en una cadena que ya no parsea como número. HTML también exige una decisión, porque el navegador colapsa las rachas de espacios salvo que la celda use white-space: pre; fija eso o recorta, pero evita un relleno medio colapsado que desalinee unas filas y otras no

Denominadores fijos como # ?/8

Un denominador escrito como dígitos que empiezan por 1 a 9 es un valor fijo en vez de un tope. La aritmética cambia por completo: Excel multiplica el valor por el denominador, redondea al numerador entero más próximo y deja la fracción sin reducir. Bajo 0/8, 0.375 muestra 3/8 y 0.64 muestra 5/8, porque 0.64 × 8 es 5.12. El tipo integrado En dieciseisavos muestra 0.5 como 8/16, que es lo que espera un mecanizador que lee un plano y lo que un formateador que reduce cada resultado convertiría en 1/2. Un numerador redondeado puede alcanzar al denominador. Entonces una máscara mixta lo arrastra al entero: se espera que 2.97 bajo # ?/8 lea 3····, ya que 0.97 × 8 = 7.76 redondea a un ocho entero. Elige entre las dos familias según lo que el denominador signifique para el lector. Los denominadores fijos encajan con todo lo que se cita en pasos fijos, como longitudes imperiales, tamaños de broca o viejos precios de acciones en octavos. Nadie que lea dimensiones de madera quiere 3/7 de pulgada. Las máscaras aproximadoras van con cocientes y magnitudes medidas donde ningún denominador concreto aporta significado

Leer el texto de fracción de Excel en Delphi con FormattedText

En HotXLS casi nunca necesitas el bucle por tu cuenta. TXLSXCell.FormattedText renderiza el valor guardado a través del NumberFormat de la celda usando TXLSFormater.GetDisplayValue de lxStandard.pas, así que la elección del convergente, el relleno y cualquier texto literal llegan juntos en una sola cadena. El procedimiento de abajo asigna pares de valor y máscara a celdas e imprime cada resultado entre corchetes para que los espacios finales sigan visibles. Un TXLSXWorkbook nuevo empieza sin hojas, por eso el código llama a Sheets.Add antes de tocar una celda; Sheets.Items y Cells[Row, Col] empiezan ambas en 1

uses
  SysUtils, lxHandleX;

type
  TFractionCase = record
    Mask: string;
    Value: Double;
  end;

const
  Cases: array[0..8] of TFractionCase = (
    (Mask: '# ??/??';        Value: 3.14159),
    (Mask: '# ??/??';        Value: 0.99),
    (Mask: '# ?/?';          Value: 12),
    (Mask: '# ?/?';          Value: 12.9999),
    (Mask: '# #/#';          Value: 12),
    (Mask: '?/?';            Value: 3.14159),
    (Mask: '0/8';            Value: 0.64),
    (Mask: '"x"# ?/?" y"';   Value: -12),
    (Mask: '# ?/?;(# ?/?)';  Value: -1.5));

procedure ShowFractionText;
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Cell: TXLSXCell;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Fractions');   // un libro nuevo todavía no tiene hojas
    for I := Low(Cases) to High(Cases) do
    begin
      Cell := Sheet.Cells[I + 1, 1];          // filas y columnas empiezan en 1
      Cell.Value := Cases[I].Value;
      Cell.NumberFormat := Cases[I].Mask;
      Writeln(Format('%-16s %9g  [%s]',
        [Cases[I].Mask, Cases[I].Value, Cell.FormattedText]));
    end;
  finally
    Book.Free;
  end;
end;

// Esperado:
// # ??/??            3.14159  [3  1/7 ]
// # ??/??               0.99  [ 98/99]
// # ?/?                   12  [12    ]
// # ?/?              12.9999  [13    ]
// # #/#                   12  [12]
// ?/?                3.14159  [22/7]
// 0/8                   0.64  [5/8]
// "x"# ?/?" y"           -12  [-x12     y]
// # ?/?;(# ?/?)         -1.5  [(1 1/2)]

Los dos últimos casos muestran lo que rodea a la fracción. El texto entre comillas y los escapes con barra invertida se imprimen como caracteres planos, y el relleno de un valor entero cae antes del sufijo, así que "x"# ?/?" y" convierte -12 en -x12·····y. El signo menos va delante de todo, prefijo incluido, salvo que una sección negativa explícita como (# ?/?) escriba el signo por sí misma; las fracciones siguen las mismas reglas de secciones y signo menos que cualquier otro formato numérico de Excel. Cuando el valor viene de un conjunto de datos y no de una celda, sáltate el libro: TXLSFormater tiene un constructor sin parámetros, y GetDisplayValue(Value, NumberFormat) es la llamada que FormattedText hace para valores numéricos

uses
  SysUtils, lxStandard;

function FractionText(const AValue: Double; const AMask: WideString): WideString;
var
  Fmt: TXLSFormater;
begin
  Fmt := TXLSFormater.Create;
  try
    Result := Fmt.GetDisplayValue(AValue, AMask);
  finally
    Fmt.Free;
  end;
end;

// FractionText(0.5, '# ?/?')                Esperado: ' 1/2'
// FractionText(1234.75, '#,##0 ?/?')        Esperado: '1,234 3/4'
// Trim(FractionText(12, '# ?/?'))           Esperado: '12'  (compatible con CSV)

Toda fracción que tu código Delphi imprima fuera de Excel debería salir del bucle de convergentes o directamente de FormattedText, alimentado con el Double de la propia celda. Entonces el 3 1/7 del papel es el 3 1/7 de la cuadrícula, y con el HotXLS Delphi spreadsheet component consigues ese texto sin escribir el bucle