Back to... Excel Writer

Source file : excel_out.adb



--  References to documentation are to: http://www.openoffice.org/sc/excelfileformat.pdf
--
--  To do:
--  =====
--  - Unicode (for binary Excel: requires BIFF8, but BIFF8 is pretty difficult)
--  - border line styles (5.115 XF - Extended Format)
--  - XML-based formats support
--  - ...

with Ada.Unchecked_Conversion,
     Ada.Unchecked_Deallocation,
     Ada.Strings.Fixed;

with Interfaces;

--  Package IEEE_754 is from: Simple components for Ada by Dmitry A. Kazakov
--  http://www.dmitry-kazakov.de/ada/components.htm
with IEEE_754.Generic_Double_Precision;

package body Excel_Out is

  use Ada.Strings.Unbounded, Interfaces;

  --  Very low level part which deals with transferring data in an endian-neutral way,
  --  and floats in the IEEE format. This is needed for having Excel Writer
  --  totally portable on all systems and processor architectures.

  type Byte_buffer is array (Integer range <>) of Unsigned_8;
  empty_buffer : constant Byte_buffer := (1 .. 0 => 0);

  --  Put numbers with correct endianess as bytes:
  generic
    type Number is mod <>;
    size : Positive;
  function Intel_x86_buffer (n : Number) return Byte_buffer;
  pragma Inline (Intel_x86_buffer);

  function Intel_x86_buffer (n : Number) return Byte_buffer is
    b : Byte_buffer (1 .. size);
    m : Number := n;
  begin
    for i in b'Range loop
      b (i) := Unsigned_8 (m and 255);
      m := m / 256;
    end loop;
    return b;
  end Intel_x86_buffer;

  function Intel_32 is new Intel_x86_buffer (Unsigned_32, 4);

  function Intel_16 (n : Unsigned_16) return Byte_buffer is
    pragma Inline (Intel_16);
  begin
    return (Unsigned_8 (n and 255), Unsigned_8 (Shift_Right (n, 8)));
  end Intel_16;

  --  2.5.2 Byte Strings, 8-bit string length (BIFF2-BIFF5), p. 187
  function To_buf_8_bit_length (s : String) return Byte_buffer is
    b : Byte_buffer (s'Range);
  begin
    if s'Length > 255 then -- length doesn't fit in a byte
      raise Constraint_Error;
    end if;
    for i in b'Range loop
      b (i) := Character'Pos (s (i));
    end loop;
    return Unsigned_8 (s'Length) & b;
  end To_buf_8_bit_length;

  --  2.5.2 Byte Strings, 16-bit string length (BIFF2-BIFF5), p. 187
  function To_buf_16_bit_length (s : String) return Byte_buffer is
    b : Byte_buffer (s'Range);
  begin
    if s'Length > 2**16 - 1 then -- length doesn't fit in a 16-bit number
      raise Constraint_Error;
    end if;
    for i in b'Range loop
      b (i) := Character'Pos (s (i));
    end loop;
    return Intel_16 (s'Length) & b;
  end To_buf_16_bit_length;

  --  --  2.5.3 Unicode Strings, 16-bit string length (BIFF2-BIFF5), p. 17
  --  function To_buf_16_bit_length(s: Wide_String) return Byte_buffer is
  --    b: Byte_buffer(1 .. 2 * s'Length);
  --    j: Integer:= 1;
  --  begin
  --    if s'Length > 2**16-1 then -- length doesn't fit in a 16-bit number
  --      raise Constraint_Error;
  --    end if;
  --    for i in s'Range loop
  --      b(j)  := Unsigned_8(Unsigned_32'(Wide_Character'Pos(s(i))) and 255);
  --      b(j+1):= Unsigned_8(Shift_Right(Unsigned_32'(Wide_Character'Pos(s(i))), 8));
  --      j:= j + 2;
  --    end loop;
  --    return
  --      Intel_16(s'Length) &
  --      (1 => 1) &  --  Character compression (ccompr): 1 = Uncompressed (16-bit characters)
  --      b;
  --  end To_buf_16_bit_length;

  --  Gives a byte sequence of an IEEE 64-bit number as if taken
  --  from an Intel machine (i.e. with the same endianess).
  --
  --  http://en.wikipedia.org/wiki/IEEE_754-1985#Double-precision_64_bit
  --

   package IEEE_LF is new IEEE_754.Generic_Double_Precision (Long_Float);

  function IEEE_Double_Intel_Portable (x : Long_Float) return Byte_buffer is
    pragma Inline (IEEE_Double_Intel_Portable);
    d : Byte_buffer (1 .. 8);
    --
    f64 : constant IEEE_LF.Float_64 := IEEE_LF.To_IEEE (x);
  begin
    for i in d'Range loop
      d (i) := f64 (9 - i); -- Order is reversed
    end loop;
    --  Fully tested in Test_IEEE.adb
    return d;
  end IEEE_Double_Intel_Portable;

  --  Just spit the bytes of the long float - fast way.
  --  Of course this will work only on an Intel(-like) machine. We check this later.
  subtype Byte_buffer_8 is Byte_buffer (0 .. 7);
  function IEEE_Double_Intel_Native is new
    Ada.Unchecked_Conversion (Long_Float, Byte_buffer_8);

  x_test : constant Long_Float := -12345.0e-67;
  Can_use_native_IEEE : constant Boolean :=
    IEEE_Double_Intel_Portable (x_test) = IEEE_Double_Intel_Native (x_test);

  function IEEE_Double_Intel (x : Long_Float) return Byte_buffer is
    pragma Inline (IEEE_Double_Intel);
  begin
    if Can_use_native_IEEE then
      return IEEE_Double_Intel_Native (x);   -- Fast, non-portable
    else
      return IEEE_Double_Intel_Portable (x); -- Slower but portable
    end if;
  end IEEE_Double_Intel;

  --  Workaround for the severe xxx'Read xxx'Write performance
  --  problems in the GNAT and ObjectAda compilers (as in 2009)
  --  This is possible if and only if Byte = Stream_Element and
  --  arrays types are both packed and aligned the same way.
  --
  subtype Size_test_a is Byte_buffer (1 .. 19);
  subtype Size_test_b is Ada.Streams.Stream_Element_Array (1 .. 19);
  workaround_possible : constant Boolean :=
    Size_test_a'Size = Size_test_b'Size and
    Size_test_a'Alignment = Size_test_b'Alignment;

  procedure Block_Write (
    stream : in out Ada.Streams.Root_Stream_Type'Class;
    buffer : in     Byte_buffer
  )
  is
    pragma Inline (Block_Write);
    SE_Buffer   : Ada.Streams.Stream_Element_Array (1 .. buffer'Length);
    for SE_Buffer'Address use buffer'Address;
    pragma Import (Ada, SE_Buffer);
  begin
    if workaround_possible then
      Ada.Streams.Write (stream, SE_Buffer);
    else
      Byte_buffer'Write (stream'Access, buffer);
      --  ^ This was 30x to 70x slower on GNAT 2009
      --    Test in the Zip-Ada project.
    end if;
  end Block_Write;

  ----------------
  -- Excel BIFF --
  ----------------

  --  The original Modula-2 code counted on certain assumptions about
  --  record packing & endianess. We write data without these assumptions.

  procedure WriteBiff (
    xl     : Excel_Out_Stream'Class;
    biff_id : Unsigned_16;
    data   : Byte_buffer
  )
  is
    pragma Inline (WriteBiff);
  begin
    Block_Write (xl.xl_stream.all, Intel_16 (biff_id));
    Block_Write (xl.xl_stream.all, Intel_16 (Unsigned_16 (data'Length)));
    Block_Write (xl.xl_stream.all, data);
  end WriteBiff;

  --  5.8  BOF: Beginning of File, p.135
  procedure Write_BOF (xl : Excel_Out_Stream'Class) is

    function BOF_suffix return Byte_buffer is  --  5.8.1 Record BOF
    begin
      case xl.xl_format is
        when BIFF2 =>
          return empty_buffer;
        when BIFF3 | BIFF4 =>
          return (0, 0);  --  Not used
        --  when BIFF8 =>
        --    return (1,1,1,1);
      end case;
    end BOF_suffix;

    --  0005H = Workbook globals
    --  0006H = Visual Basic module
    --  0010H = Sheet or dialogue (see SHEETPR, S5.97)
    Sheet_or_dialogue : constant := 16#10#;
    --  0020H = Chart
    --  0040H = Macro sheet
    biff_record_identifier : constant array (Excel_type) of Unsigned_16 :=
      (BIFF2 => 16#0009#,
       BIFF3 => 16#0209#,
       BIFF4 => 16#0409#
       --  BIFF8 => 16#0809#
      );
    biff_version : constant array (Excel_type) of Unsigned_16 :=
      (BIFF2 => 16#0200#,
       BIFF3 => 16#0300#,
       BIFF4 => 16#0400#
       --  BIFF8 => 16#0600#
      );
  begin
    WriteBiff (xl,
      biff_record_identifier (xl.xl_format),
      Intel_16 (biff_version (xl.xl_format)) &
      Intel_16 (Sheet_or_dialogue) &
      BOF_suffix
    );
  end Write_BOF;

  --  5.49 FORMAT (actually, a number format; the full format is called XF (extended format))
  procedure Write_Number_Format_String (xl : Excel_Out_Stream'Class; s : String) is
  begin
    case xl.xl_format is
      when BIFF2 | BIFF3 =>
        WriteBiff (xl, 16#001E#, To_buf_8_bit_length (s));
      when BIFF4 =>
        WriteBiff (xl, 16#041E#, (0, 0) & To_buf_8_bit_length (s));
     --   when BIFF8 =>
     --     WriteBiff(xl, 16#041E#, (0, 0) &  --  should be: format index used in other records
     --       To_buf_8_bit_length(s));
    end case;
  end Write_Number_Format_String;

  --  Write built-in number formats (internal)
  procedure Write_Default_Number_Formats (xl : Excel_Out_Stream'Class) is
    sep_1000 : constant Character := ','; -- US format
    sep_deci : constant Character := '.'; -- US format
    --  ^ If there is any evidence of an issue with those built-in separators,
    --  we may make them configurable. NB: MS Excel 2002 and 2007 use only
    --  the index of built-in formats and discards the strings for BIFF2, but not for BIFF3...
  begin
    --  5.12 BUILTINFMTCOUNT
    case xl.xl_format is
      when BIFF2 =>
        WriteBiff (xl, 16#001F#, Intel_16 (Unsigned_16 (last_built_in - 5)));
      when BIFF3 =>
        WriteBiff (xl, 16#0056#, Intel_16 (Unsigned_16 (last_built_in - 3)));
      when BIFF4 =>
        WriteBiff (xl, 16#0056#, Intel_16 (Unsigned_16 (last_built_in + 1)));
      --  when BIFF8 =>
      --    null;
    end case;
    --  loop & case avoid omitting any choice
    for n in Number_format_type'First .. last_custom_number_format loop
      case n is
        when general    =>  Write_Number_Format_String (xl, "General");
        when decimal_0  =>  Write_Number_Format_String (xl, "0");
        when decimal_2  =>  Write_Number_Format_String (xl, "0" & sep_deci & "00"); -- 'Comma' built-in style
        when decimal_0_thousands_separator =>
          Write_Number_Format_String (xl, "#" & sep_1000 & "##0");
        when decimal_2_thousands_separator =>
          Write_Number_Format_String (xl, "#" & sep_1000 & "##0" & sep_deci & "00");
        when no_currency_0       =>
          if xl.xl_format >= BIFF4 then
            Write_Number_Format_String (xl, "#" & sep_1000 & "##0;-#" & sep_1000 & "##0");
          end if;
        when no_currency_red_0   =>
          if xl.xl_format >= BIFF4 then
            Write_Number_Format_String (xl, "#" & sep_1000 & "##0;-#" & sep_1000 & "##0");
          --  [Red] doesn't go with non-English versions of Excel !!
          end if;
        when no_currency_2       =>
          if xl.xl_format >= BIFF4 then
            Write_Number_Format_String (xl,  "#" & sep_1000 & "##0" & sep_deci & "00;" &
                          "-#" & sep_1000 & "##0" & sep_deci & "00");
          end if;
        when no_currency_red_2   =>
          if xl.xl_format >= BIFF4 then
            Write_Number_Format_String (xl,  "#" & sep_1000 & "##0" & sep_deci & "00;" &
                          "-#" & sep_1000 & "##0" & sep_deci & "00");
          end if;
        when currency_0       =>
          Write_Number_Format_String (xl, "$ #" & sep_1000 & "##0;$ -#" & sep_1000 & "##0");
        when currency_red_0   =>
          Write_Number_Format_String (xl, "$ #" & sep_1000 & "##0;$ -#" & sep_1000 & "##0");
          --  [Red] doesn't go with non-English versions of Excel !!
        when currency_2       =>
          Write_Number_Format_String (xl,  "$ #" & sep_1000 & "##0" & sep_deci & "00;" &
                          "$ -#" & sep_1000 & "##0" & sep_deci & "00");
        when currency_red_2   =>
          Write_Number_Format_String (xl,  "$ #" & sep_1000 & "##0" & sep_deci & "00;" &
                          "$ -#" & sep_1000 & "##0" & sep_deci & "00");
        when percent_0        =>  Write_Number_Format_String (xl, "0%");   -- 'Percent' built-in style
        when percent_2        =>  Write_Number_Format_String (xl, "0" & sep_deci & "00%");
        when scientific       =>  Write_Number_Format_String (xl, "0" & sep_deci & "00E+00");
        when fraction_1       =>
          if xl.xl_format >= BIFF3 then
            Write_Number_Format_String (xl, "#\ ?/?");
          end if;
        when fraction_2       =>
          if xl.xl_format >= BIFF3 then
            Write_Number_Format_String (xl, "#\ ??/??");
          end if;
        when dd_mm_yyyy       =>  Write_Number_Format_String (xl, "dd/mm/yyyy");
        when dd_mmm_yy        =>  Write_Number_Format_String (xl, "dd/mmm/yy");
        when dd_mmm           =>  Write_Number_Format_String (xl, "dd/mmm");
        when mmm_yy           =>  Write_Number_Format_String (xl, "mmm/yy");
        when h_mm_AM_PM       =>  Write_Number_Format_String (xl, "h:mm\ AM/PM");
        when h_mm_ss_AM_PM    =>  Write_Number_Format_String (xl, "h:mm:ss\ AM/PM");
        when hh_mm            =>  Write_Number_Format_String (xl, "hh:mm");
        when hh_mm_ss         =>  Write_Number_Format_String (xl, "hh:mm:ss");
        when dd_mm_yyyy_hh_mm =>  Write_Number_Format_String (xl, "dd/mm/yyyy\ hh:mm");
        when percent_0_plus  =>
          Write_Number_Format_String (xl, "+0%;-0%;0%");
        when percent_2_plus  =>
          Write_Number_Format_String (xl, "+0" & sep_deci & "00%;-0" & sep_deci & "00%;0" & sep_deci & "00%");
        when date_iso        => Write_Number_Format_String (xl, "yyyy\-mm\-dd");
        when date_h_m_iso    => Write_Number_Format_String (xl, "yyyy\-mm\-dd\ hh:mm");
        when date_h_m_s_iso  => Write_Number_Format_String (xl, "yyyy\-mm\-dd\ hh:mm:ss");
          --  !! Trouble: Excel (German Excel/French locale) writes yyyy, reads it,
          --    understands it and translates it into aaaa, but is unable to
          --    understand *our* yyyy
          --  Same issue as [Red] vs [Rot] above.
      end case;
    end loop;
    --  ^ Some formats in the original list caused problems, probably
    --    because of regional placeholder symbols
    case xl.xl_format is
      when BIFF2 =>
        for i in 1 .. 6 loop
          Write_Number_Format_String (xl, "@");
        end loop;
      when BIFF3 =>
        for i in 1 .. 4 loop
          Write_Number_Format_String (xl, "@");
        end loop;
      when BIFF4 =>
        null;
    end case;
    --  ^ Stuffing for having the same number of built-in and EW custom
  end Write_Default_Number_Formats;

  --  5.35 DIMENSION
  procedure Write_Dimensions (xl : Excel_Out_Stream'Class) is
    --  sheet bounds:   0 2 Index to first used row
    --                  2 2 Index to last used row, increased by 1
    --                  4 2 Index to first used column
    --                  6 2 Index to last used column, increased by 1
    --
    --  Since our row / column counts are 1-based, no need to increase by 1.
    sheet_bounds : constant Byte_buffer :=
      Intel_16 (0) &
      Intel_16 (Unsigned_16 (xl.maxrow)) &
      Intel_16 (0) &
      Intel_16 (Unsigned_16 (xl.maxcolumn));
    --  sheet_bounds_32_16: constant Byte_buffer:=
    --    Intel_32(0) &
    --    Intel_32(Unsigned_32(xl.maxrow)) &
    --    Intel_16(0) &
    --    Intel_16(Unsigned_16(xl.maxcolumn));
  begin
    case xl.xl_format is
      when BIFF2 =>
        WriteBiff (xl, 16#0000#, sheet_bounds);
      when BIFF3 | BIFF4 =>
        WriteBiff (xl, 16#0200#, sheet_bounds & (0, 0));
      --  when BIFF8 =>
      --    WriteBiff(xl, 16#0200#, sheet_bounds_32_16 & (0,0));
    end case;
  end Write_Dimensions;

  procedure Define_Number_Format
    (xl            : in out Excel_Out_Stream;
     format        :    out Number_format_type;
     format_string : in     String)
  is
  begin
    xl.number_fmt := xl.number_fmt + 1;
    case xl.xl_format is
      when BIFF2 =>
        if xl.number_fmt > 63 then
          raise Number_format_out_of_range
            with "Only 64 number formats are allowed in the BIFF2 format";
            --  Reason: see encoding in the Cell Attributes (2.5.13).
        end if;
      when BIFF3 .. BIFF4 =>
        if xl.number_fmt > 255 then
          raise Number_format_out_of_range
            with "Only 256 number formats are allowed in the BIFF3, BIFF4 formats";
        end if;
    end case;
    format := xl.number_fmt;
    Write_Number_Format_String (xl, format_string);
  end Define_Number_Format;

  procedure Write_Worksheet_header (xl : in out Excel_Out_Stream'Class) is

    procedure Define_style (fmt : Format_type; style_id : Unsigned_8) is
      Base_Level : constant := 255;
    begin
      WriteBiff (xl,
        16#0293#,
        Intel_16 (Unsigned_16 (fmt) + 16#8000#) & style_id & Base_Level
      );
    end Define_style;
    --
    Comma_Style     : constant := 3;
    Currency_Style  : constant := 4;
    Percent_Style   : constant := 5;
    font_for_styles, font_2, font_3 : Font_type;
    --
    function Encoding_code return Unsigned_16 is  --  5.17 CODEPAGE, p. 145
    begin
      case xl.encoding is
        when Windows_CP_874  => return 874;
        when Windows_CP_932  => return 932;
        when Windows_CP_936  => return 936;
        when Windows_CP_949  => return 949;
        when Windows_CP_950  => return 950;
        when Windows_CP_1250 => return 1250;
        when Windows_CP_1251 => return 1251;
        when Windows_CP_1252 =>
          case xl.xl_format is
            when BIFF2 .. BIFF3 =>
              return 16#8001#;
            when BIFF4 =>
              return 1252;
          end case;
        when Windows_CP_1253 => return 1253;
        when Windows_CP_1254 => return 1254;
        when Windows_CP_1255 => return 1255;
        when Windows_CP_1256 => return 1256;
        when Windows_CP_1257 => return 1257;
        when Windows_CP_1258 => return 1258;
        when Windows_CP_1361 => return 1361;
        when Apple_Roman     => return 10000;
      end case;
    end Encoding_code;
    --
  begin
    Write_BOF (xl);
    --  5.17 CODEPAGE, p. 145
    case xl.xl_format is
      --  when BIFF8 =>   --  UTF-16
      --    WriteBiff(xl, 16#0042#, Intel_16(16#04B0#));
      when others =>
        WriteBiff (xl, 16#0042#, Intel_16 (Encoding_code));
    end case;
    --  5.14 CALCMODE
    WriteBiff (xl, 16#000D#, Intel_16 (1)); --  1 => automatic
    --  5.85 REFMODE
    WriteBiff (xl, 16#000F#, Intel_16 (1)); --  1 => A1 mode
    --  5.28 DATEMODE
    WriteBiff (xl, 16#0022#, Intel_16 (0)); --  0 => 1900; 1 => 1904 Date system
    --  NB: the 1904 variant (Mac) is ignored by LibreOffice (<= 3.5), then wrong dates !
    --
    Define_Font (xl, "Arial",   10, xl.def_font);
    Define_Font (xl, "Arial",   10, font_for_styles); -- Used by BIFF3+'s styles
    Define_Font (xl, "Calibri", 10, font_2); -- Defined in BIFF3 files written by Excel 2002
    Define_Font (xl, "Calibri", 10, font_3); -- Defined in BIFF3 files written by Excel 2002
    Write_Default_Number_Formats (xl);
    --  5.111 WINDOWPROTECT
    WriteBiff (xl, 16#0019#, Intel_16 (0));
    --  Define default format
    Define_Format (xl, xl.def_font, general, xl.def_fmt);
    if xl.xl_format >= BIFF3 then
      --  Don't ask why we need the following useless formats, but it is as Excel 2002
      --  write formats. Additionally, the default format is turned into decimal_2
      --  when a file without those useless formats is opened in Excel (2002) !
      Define_Format (xl, font_for_styles, general, xl.def_fmt);
      Define_Format (xl, font_for_styles, general, xl.def_fmt);
      Define_Format (xl, font_2, general, xl.def_fmt);
      Define_Format (xl, font_2, general, xl.def_fmt);
      for i in 5 .. 15 loop
        Define_Format (xl, xl.def_font, general, xl.def_fmt);
      end loop;
      --  Final default format index is the last changed xl.def_fmt
    end if;
    Use_default_format (xl);
    --  Define formats for the BIFF3+ "styles":
    Define_Format (xl, font_for_styles, decimal_2, xl.cma_fmt);
    Define_Format (xl, font_for_styles, currency_0, xl.ccy_fmt);
    Define_Format (xl, font_for_styles, percent_0, xl.pct_fmt);
    --  Define styles - 5.103 STYLE p. 212
    --  NB: - it is BIFF3+ (we cheat a bit if selected format is BIFF2).
    --      - these "styles" seem to be a zombie feature of Excel 3
    --      - the whole purpose of including this is because format
    --        buttons (%)(,) in Excel 95 through 2007 are using these styles;
    --        if the styles are not defined, those buttons are not working
    --        when an Excel Writer sheet is open in MS Excel.
    Define_style (xl.cma_fmt, Comma_Style);
    Define_style (xl.ccy_fmt, Currency_Style);
    Define_style (xl.pct_fmt, Percent_Style);
    xl.dimrecpos := Index (xl);
    Write_Dimensions (xl);
    xl.is_created := True;
  end Write_Worksheet_header;

  type Font_or_Background is (for_font, for_background);
  type Color_pair is array (Font_or_Background) of Unsigned_16;
  auto_color : constant Color_pair :=
    (16#7FFF#, -- system window text colour
     16#0019#  -- system window background colour
    );

  color_code : constant array (Excel_type, Color_type) of Color_pair :=
    (BIFF2 =>
       (
         black      => (0, 0),
         white      => (1, 1),
         red        => (2, 2),
         green      => (3, 3),
         blue       => (4, 4),
         yellow     => (5, 5),
         magenta    => (6, 6),
         cyan       => (7, 7),
         others     => auto_color
        ),
      BIFF3 | BIFF4 =>
        (black      => (8, 8),
         white      => (9, 9),
         red        => (10, 10),
         green      => (11, 11),
         blue       => (12, 12),
         yellow     => (13, 13),
         magenta    => (14, 14),
         cyan       => (15, 15),
         dark_red   => (16, 16),
         dark_green => (17, 17),
         dark_blue  => (18, 18),
         olive      => (19, 19),
         purple     => (20, 20),
         teal       => (21, 21),
         silver     => (22, 22),
         grey       => (23, 23),
         automatic  => auto_color
        )
     );

  --  *** Exported procedures **********************************************

  --  5.115 XF - Extended Format
  procedure Define_Format
    (xl               : in out Excel_Out_Stream;
     font             : in     Font_type;          -- Default_font(xl), or given by Define_font
     number_format    : in     Number_format_type; -- built-in, or given by Define_number_format
     cell_format      :    out Format_type;
     -- Optional parameters --
     horizontal_align : in     Horizontal_alignment := general_alignment;
     border           : in     Cell_border := no_border;
     shaded           : in     Boolean := False;    -- Add a dotted background pattern
     background_color : in     Color_type := automatic;
     wrap_text        : in     Boolean := False;
     vertical_align   : in     Vertical_alignment := bottom_alignment;
     text_orient      : in     Text_orientation := normal)
  is
    actual_number_format : Number_format_type := number_format;
    cell_is_locked : constant := 1;
    --  ^ Means actually: cell formula protection is possible, and enabled when sheet is protected.
    procedure Define_BIFF2_XF is
      border_bits, mask : Unsigned_8;
    begin
      border_bits := 0;
      mask := 8;
      for s in Cell_border_single loop
        if border (s) then
          border_bits := border_bits + mask;
        end if;
        mask := mask * 2;
      end loop;
      --  5.115.2 XF Record Contents, p. 221 for BIFF2
      WriteBiff (
        xl,
        16#0043#, -- XF code in BIFF2
        (Unsigned_8 (font),
         --  ^ Index to FONT record
         0,
         --  ^ Not used
         Number_format_type'Pos (actual_number_format) + 16#40# * cell_is_locked,
         --  ^ Number format and cell flags
         Horizontal_alignment'Pos (horizontal_align) +
         border_bits +
         Boolean'Pos (shaded) * 128
         --  ^ Horizontal alignment, border style, and background
        )
      );
    end Define_BIFF2_XF;

    area_code : Unsigned_16;

    procedure Define_BIFF3_XF is
    begin
      --  5.115.2 XF Record Contents, p. 221 for BIFF3
      WriteBiff (
        xl,
        16#0243#, -- XF code in BIFF3
        (Unsigned_8 (font),
         --  ^ 0 - Index to FONT record
         Number_format_type'Pos (actual_number_format),
         --  ^ 1 - Number format and cell flags
         cell_is_locked,
         --  ^ 2 - XF_TYPE_PROT (5.115.1)
         16#FF#
         --  ^ 3 - XF_USED_ATTRIB
        ) &
        Intel_16 (
          Horizontal_alignment'Pos (horizontal_align) +
          Boolean'Pos (wrap_text) * 8
        ) &
        --  ^ 4 - Horizontal alignment, text break, parent style XF
        Intel_16 (area_code) &
        --  ^ 6 - XF_AREA_34
        (Boolean'Pos (border (top_single)),
           Boolean'Pos (border (left_single)),
           Boolean'Pos (border (bottom_single)),
           Boolean'Pos (border (right_single))
        )
        --  ^ 8 - XF_BORDER_34 - thin (=1) line; we could have other line styles:
        --       Thin, Medium, Dashed, Dotted, Thick, Double, Hair
      );
    end Define_BIFF3_XF;

    procedure Define_BIFF4_XF is
    begin
      --  5.115.2 XF Record Contents, p. 222 for BIFF4
      WriteBiff (
        xl,
        16#0443#, -- XF code in BIFF4
        (Unsigned_8 (font),
         --  ^ 0 - Index to FONT record
         Number_format_type'Pos (actual_number_format),
         --  ^ 1 - Number format and cell flags
         cell_is_locked, 0,
         --  ^ 2 - XF type, cell protection, and parent style XF
         Horizontal_alignment'Pos (horizontal_align) +
         Boolean'Pos (wrap_text) * 8 +
         (Vertical_alignment'Pos (vertical_align) and 3) * 16 +
         Text_orientation'Pos (text_orient) * 64,
         --  ^ 4 - Alignment (hor & ver), text break, and text orientation
         16#FF#
         --  ^ 3 - XF_USED_ATTRIB
        ) &
        --  ^ 4 - Horizontal alignment, text break, parent style XF
        Intel_16 (area_code) &
        --  ^ 6 - XF_AREA_34
        (Boolean'Pos (border (top_single)),
           Boolean'Pos (border (left_single)),
           Boolean'Pos (border (bottom_single)),
           Boolean'Pos (border (right_single))
        )
        --  ^ 8 - XF_BORDER_34 - thin (=1) line; we could have other line styles:
        --        Thin, Medium, Dashed, Dotted, Thick, Double, Hair
      );
    end Define_BIFF4_XF;

  begin
    --  2.5.12 Patterns for Cell and Chart Background Area
    --  This is for BIFF3+
    if shaded then
      area_code :=
        Boolean'Pos (shaded) * 17 +                        -- Sparse pattern, like BIFF2 "shade"
        16#40#  * color_code (BIFF3, black)(for_background) +           -- pattern colour
        16#800# * color_code (BIFF3, background_color)(for_background); -- pattern background
    elsif background_color = automatic then
      area_code := 0;
    else
      area_code :=
        1 +                                                          -- Full pattern
        16#40#  * color_code (BIFF3, background_color)(for_background) +  -- pattern colour
        16#800# * color_code (BIFF3, background_color)(for_background);   -- pattern background
    end if;
    case xl.xl_format is
      when BIFF2 =>
        case actual_number_format is
          when general .. no_currency_2 =>
            null;
          when currency_0 .. fraction_2 =>
            actual_number_format := actual_number_format - 4;
          when dd_mm_yyyy .. last_custom_number_format =>
            actual_number_format := actual_number_format - 6;
          when others =>
            null;
        end case;
        Define_BIFF2_XF;
      when BIFF3 =>
        if actual_number_format in currency_0 .. last_custom_number_format then
          actual_number_format := actual_number_format - 4;
        end if;
        Define_BIFF3_XF;
      when BIFF4 =>
        Define_BIFF4_XF;
      --  when BIFF8 =>
      --    Define_BIFF8_XF;  --  BIFF8: 16#00E0#, p. 224
    end case;
    --
    --  Now we will store the newly defined format.
    --
    xl.xfs := xl.xfs + 1;
    if xl.xfs not in XF_Range then
      raise Format_out_of_range
        with
          "Too many formats defined, maximum number of formats " &
          "(including a few pre-defined) is" &
          Integer'Image (XF_Range'Last + 1);
    end if;
    cell_format := Format_type (xl.xfs);
    xl.xf_def (xl.xfs) := (font => font, numb => number_format);
  end Define_Format;

  procedure Header (xl : Excel_Out_Stream; page_header_string : String) is
  begin
    WriteBiff (xl, 16#0014#, To_buf_8_bit_length (page_header_string)); -- 5.55 p.180
  end Header;

  procedure Footer (xl : Excel_Out_Stream; page_footer_string : String) is
  begin
    WriteBiff (xl, 16#0015#, To_buf_8_bit_length (page_footer_string)); -- 5.48 p.173
  end Footer;

  procedure Left_Margin (xl : Excel_Out_Stream; inches : Long_Float) is
  begin
    WriteBiff (xl, 16#0026#, IEEE_Double_Intel (inches));
  end Left_Margin;

  procedure Right_Margin (xl : Excel_Out_Stream; inches : Long_Float) is
  begin
    WriteBiff (xl, 16#0027#, IEEE_Double_Intel (inches));
  end Right_Margin;

  procedure Top_Margin (xl : Excel_Out_Stream; inches : Long_Float) is
  begin
    WriteBiff (xl, 16#0028#, IEEE_Double_Intel (inches));
  end Top_Margin;

  procedure Bottom_Margin (xl : Excel_Out_Stream; inches : Long_Float) is
  begin
    WriteBiff (xl, 16#0029#, IEEE_Double_Intel (inches));
  end Bottom_Margin;

  procedure Margins
    (xl : Excel_Out_Stream;
     left_inches,
     right_inches,
     top_inches,
     bottom_inches : Long_Float)
  is
  begin
    Left_Margin (xl, left_inches);
    Right_Margin (xl, right_inches);
    Top_Margin (xl, top_inches);
    Bottom_Margin (xl, bottom_inches);
  end Margins;

  procedure Print_Row_Column_Headers (xl : Excel_Out_Stream) is
  begin
    WriteBiff (xl, 16#002A#, Intel_16 (1));  --  5.81 PRINTHEADERS p.199
  end  Print_Row_Column_Headers;

  procedure Print_Gridlines (xl : Excel_Out_Stream) is
  begin
    WriteBiff (xl, 16#002B#, Intel_16 (1));  --  5.80 PRINTGRIDLINES p.199
  end Print_Gridlines;

  procedure Page_Setup (
    xl                     : Excel_Out_Stream;
    scaling_percents       : Positive := 100;
    fit_width_with_n_pages : Natural := 1; -- 0: as many as possible
    fit_height_with_n_pages : Natural := 1; -- 0: as many as possible
    orientation            : Orientation_choice := portrait;
    scale_or_fit           : Scale_or_fit_choice := scale
  )
  is
  begin
    --  5.73 PAGESETUP p.192 - this is BIFF4+ (cheat if xl.format below)!
    WriteBiff (xl,
      16#00A1#,
      Intel_16 (0) & -- paper type undefined
      Intel_16 (Unsigned_16 (scaling_percents)) &
      Intel_16 (1) & -- start page number
      Intel_16 (Unsigned_16 (fit_width_with_n_pages)) &
      Intel_16 (Unsigned_16 (fit_height_with_n_pages)) &
      Intel_16 (2 * Orientation_choice'Pos (orientation))
    );
    --  5.97 SHEETPR p.207 - this is BIFF3+ (cheat if xl.format below) !
    --  NB: this field contains other informations, should be delayed
    --        in case other preferences are to be set
    WriteBiff (xl,
      16#0081#,
      Intel_16 (256 * Scale_or_fit_choice'Pos (scale_or_fit))
    );
  end Page_Setup;

  y_scale : constant := 20; -- scaling to obtain character point (pt) units

  --  5.31 DEFAULTROWHEIGHT
  procedure Write_default_row_height (
        xl     : Excel_Out_Stream;
        height : Positive
  )
  is
    default_twips : constant Byte_buffer := Intel_16 (Unsigned_16 (height * y_scale));
    options_flags : constant Byte_buffer := (1, 0);
    --  1 = Row height and default font height do not match
  begin
    case xl.xl_format is
      when BIFF2 =>
        WriteBiff (xl, 16#0025#, default_twips);
      when BIFF3 | BIFF4 =>
        WriteBiff (xl, 16#0225#, options_flags & default_twips);
    end case;
  end Write_default_row_height;

  --  5.32 DEFCOLWIDTH
  procedure Write_default_column_width (
        xl : in out Excel_Out_Stream;
        width  : Positive)
  is
  begin
    WriteBiff (xl, 16#0055#, Intel_16 (Unsigned_16 (width)));
    xl.defcolwdth := 256 * width;
  end Write_default_column_width;

  procedure Write_column_width (
        xl     : in out Excel_Out_Stream;
        column : Positive;
        width  : Natural)
  is
  begin
    Write_column_width (xl, column, column, width);
  end Write_column_width;

  procedure Write_column_width (
    xl            : in out Excel_Out_Stream;
    first_column,
    last_column   : Positive;
    width         : Natural
  )
  is
  begin
    case xl.xl_format is
      when BIFF2 =>
        --  5.20 COLWIDTH (BIFF2 only)
        WriteBiff (xl, 16#0024#,
          Unsigned_8 (first_column - 1) &
          Unsigned_8 (last_column - 1) &
          Intel_16 (Unsigned_16 (width * 256)));
      when BIFF3 | BIFF4 =>
        --  5.18 COLINFO (BIFF3+)
        WriteBiff (xl, 16#007D#,
          Intel_16 (Unsigned_16 (first_column - 1)) &
          Intel_16 (Unsigned_16 (last_column - 1)) &
          Intel_16 (Unsigned_16 (width * 256)) &
          Intel_16 (0) & -- Index to XF record (5.115) for default column formatting
          Intel_16 (0) & -- Option flags
          (0, 0)         -- Not used
        );
        for j in first_column .. last_column loop
          xl.std_col_width (j) := False;
        end loop;
    end case;
  end Write_column_width;

  --  5.88 ROW
  --  The OpenOffice documentation tells nice stories about row blocks,
  --  but single ROW commands can also be put before in the data stream,
  --  where the column widths are set. Excel saves with blocks of ROW
  --  commands, most of them useless.

  procedure Write_row_height (
    xl     : Excel_Out_Stream;
    row    : Positive;
    height : Natural
  )
  is
    row_info_base : Byte_buffer :=
      Intel_16 (Unsigned_16 (row - 1)) &
      Intel_16 (0)   & -- col. min.
      Intel_16 (255) & -- col. max.
      Intel_16 (Unsigned_16 (height * y_scale));
    fDyZero : Unsigned_8 := 0;
  begin
    case xl.xl_format is
      when BIFF2 =>
        WriteBiff (xl, 16#0008#,
          row_info_base &
          (1 .. 3 => 0) &
          Intel_16 (0) -- offset to data
        );
      when BIFF3 | BIFF4 =>
        if height = 0 then -- proper hiding (needed with LibreOffice)
          fDyZero := 1;
          row_info_base (row_info_base'Last - 1 .. row_info_base'Last) :=
            Intel_16 (16#8000#);
        end if;
        WriteBiff (xl, 16#0208#,
          row_info_base &
          --  http://msdn.microsoft.com/en-us/library/dd906757(v=office.12).aspx
          (0, 0,  -- reserved1 (2 bytes): MUST be zero, and MUST be ignored.
           0, 0,  -- unused1 (2 bytes): Undefined and MUST be ignored.
           fDyZero *  32 +  -- D - fDyZero (1 bit): row is hidden
                 1 *  64 +  -- E - fUnsynced (1 bit): row height was manually set
                 0 * 128,   -- F - fGhostDirty (1 bit): the row was formatted
           1) &   -- reserved3 (1 byte): MUST be 1, and MUST be ignored
           Intel_16 (15)
           --  ^ ixfe_val, then 4 bits.
           --    If fGhostDirty is 0, ixfe_val is undefined and MUST be ignored.
        );
    end case;
  end Write_row_height;

  --  5.45 FONT, p.171
  procedure Define_Font
    (xl           : in out Excel_Out_Stream;
     font_name    :        String;
     height       :        Positive;
     font         :    out Font_type;
     style        :        Font_style := regular;
     color        :        Color_type := automatic)
  is
    style_bits, mask : Unsigned_16;
  begin
    style_bits := 0;
    mask := 1;
    for s in Font_style_single loop
      if style (s) then
        style_bits := style_bits + mask;
      end if;
      mask := mask * 2;
    end loop;
    xl.fonts := xl.fonts + 1;
    if xl.fonts = 4 then
      xl.fonts := 5;
      --  Anomaly! The font with index 4 is omitted in all BIFF versions (5.45).
      --  Numbering is 0, 1, 2, 3, *5*, 6,...
    end if;
    case xl.xl_format is
      when BIFF2 =>
        if xl.fonts > 3 then
          raise Font_out_of_range with "Only 4 fonts are allowed in the BIFF2 format";
          --  Reason: in the Cell Attributes (2.5.13), font index is encoded on 2 bits!
        end if;
        WriteBiff (xl, 16#0031#,
          Intel_16 (Unsigned_16 (height * y_scale)) &
          Intel_16 (style_bits) &
          To_buf_8_bit_length (font_name)
        );
        if color /= automatic then
          --  5.47 FONTCOLOR
          WriteBiff (xl, 16#0045#, Intel_16 (color_code (BIFF2, color)(for_font)));
        end if;
      when BIFF3 | BIFF4 =>  --  BIFF8 has 16#0031#, p. 171
        if xl.fonts > 255 then
          raise Font_out_of_range with "Only 256 fonts are allowed in the BIFF3, BIFF4 formats";
        end if;
        WriteBiff (xl, 16#0231#,
          Intel_16 (Unsigned_16 (height * y_scale)) &
          Intel_16 (style_bits) &
          Intel_16 (color_code (BIFF3, color)(for_font)) &
          To_buf_8_bit_length (font_name)
        );
    end case;
    font := Font_type (xl.fonts);
  end Define_Font;

  procedure Jump_to_and_store_max (xl : in out Excel_Out_Stream; r, c : Integer) is
    pragma Inline (Jump_to_and_store_max);
  begin
    if not xl.is_created then
      raise Excel_stream_not_created;
    end if;
    Jump_to (xl, r, c); -- Store and check current position
    if r > xl.maxrow then
      xl.maxrow := r;
    end if;
    if c > xl.maxcolumn then
      xl.maxcolumn := c;
    end if;
  end Jump_to_and_store_max;

  --  2.5.13 Cell Attributes (BIFF2 only)
  function Cell_attributes (xl : Excel_Out_Stream) return Byte_buffer is
  begin
    return
      (Unsigned_8 (xl.xf_in_use),
       Unsigned_8 (xl.xf_def (xl.xf_in_use).numb) + 16#40# *
       Unsigned_8 (xl.xf_def (xl.xf_in_use).font),
       0
      );
  end Cell_attributes;

  function Almost_zero (x : Long_Float) return Boolean is
  begin
    return abs x <= Long_Float'Model_Small;
  end Almost_zero;

  --  Internal
  --
  --  5.71 NUMBER
  procedure Write_as_double (
        xl     : in out Excel_Out_Stream;
        r,
        c      : Positive;
        num    : Long_Float
  )
  is
    pragma Inline (Write_as_double);
  begin
    Jump_to_and_store_max (xl, r, c);
    case xl.xl_format is
      when BIFF2 =>
        WriteBiff (xl, 16#0003#,
          Intel_16 (Unsigned_16 (r - 1)) &
          Intel_16 (Unsigned_16 (c - 1)) &
          Cell_attributes (xl) &
          IEEE_Double_Intel (num)
        );
      when BIFF3 | BIFF4 =>
        WriteBiff (xl, 16#0203#,
          Intel_16 (Unsigned_16 (r - 1)) &
          Intel_16 (Unsigned_16 (c - 1)) &
          Intel_16 (Unsigned_16 (xl.xf_in_use)) &
          IEEE_Double_Intel (num)
        );
    end case;
    Jump_to (xl, r, c + 1); -- Store and check new position
  end Write_as_double;

  --  Internal. This is BIFF2 only. BIFF format choice unchecked here.
  --
  procedure Write_as_16_bit_unsigned (
        xl : in out Excel_Out_Stream;
        r,
        c      : Positive;
        num    : Unsigned_16)
  is
    pragma Inline (Write_as_16_bit_unsigned);
  begin
    Jump_to_and_store_max (xl, r, c);
    --  5.60 INTEGER
    WriteBiff (xl, 16#0002#,
      Intel_16 (Unsigned_16 (r - 1)) &
      Intel_16 (Unsigned_16 (c - 1)) &
      Cell_attributes (xl) &
      Intel_16 (num)
    );
    Jump_to (xl, r, c + 1); -- Store and check new position
  end Write_as_16_bit_unsigned;

  --  Internal. This is BIFF3+. BIFF format choice unchecked here.
  --
  procedure Write_as_30_bit_signed (
        xl : in out Excel_Out_Stream;
        r,
        c      : Positive;
        num    : Integer_32)
  is
    pragma Inline (Write_as_30_bit_signed);
    RK_val : Unsigned_32;
    RK_code : constant := 2; -- Code for signed integer. See 2.5.5 RK Values
  begin
    if num >= 0 then
      RK_val := Unsigned_32 (num) * 4 + RK_code;
    else
      RK_val := (-Unsigned_32 (-num)) * 4 + RK_code;
    end if;
    Jump_to_and_store_max (xl, r, c);
    --  5.87 RK
    WriteBiff (xl, 16#027E#,
      Intel_16 (Unsigned_16 (r - 1)) &
      Intel_16 (Unsigned_16 (c - 1)) &
      Intel_16 (Unsigned_16 (xl.xf_in_use)) &
      Intel_32 (RK_val)
    );
    Jump_to (xl, r, c + 1); -- Store and check new position
  end Write_as_30_bit_signed;

  --
  --  Profile with floating-point number
  --
  procedure Write (
        xl     : in out Excel_Out_Stream;
        r,
        c      : Positive;
        num    : Long_Float
  )
  is
    max_16_u : constant := 2.0 ** 16 - 1.0;
    min_30_s : constant := -(2.0 ** 29);
    max_30_s : constant := 2.0 ** 29 - 1.0;
  begin
    case xl.xl_format is
      when BIFF2 =>
        if num >= 0.0 and then
           num <= max_16_u and then
           Almost_zero (num - Long_Float'Floor (num))
        then
          Write_as_16_bit_unsigned (xl, r, c, Unsigned_16 (Long_Float'Floor (num)));
        else
          Write_as_double (xl, r, c, num);
        end if;
      when BIFF3 | BIFF4 =>
        if num >= min_30_s and then
           num <= max_30_s and then
           Almost_zero (num - Long_Float'Floor (num))
        then
          Write_as_30_bit_signed (xl, r, c, Integer_32 (Long_Float'Floor (num)));
        else
          Write_as_double (xl, r, c, num);
        end if;
    end case;
  end Write;

  --
  --  Profile with integer number
  --
  procedure Write (
        xl : in out Excel_Out_Stream;
        r,
        c      : Positive;
        num    : Integer)
  is
  begin
    --  We use an integer representation (and small storage) if possible;
    --  we need to use a floating-point in all other cases
    case xl.xl_format is
      when BIFF2 =>
        if num in 0 .. 2**16 - 1 then
          Write_as_16_bit_unsigned (xl, r, c, Unsigned_16 (num));
        else
          Write_as_double (xl, r, c, Long_Float (num));
        end if;
      when BIFF3 | BIFF4 =>
        if num in -2**29 .. 2**29 - 1 then
          Write_as_30_bit_signed (xl, r, c, Integer_32 (num));
        else
          Write_as_double (xl, r, c, Long_Float (num));
        end if;
    end case;
  end Write;

  --  --  Function taken from Wasabee.Encoding.
  --  function ISO_8859_1_to_UTF_16(s: String) return Wide_String is
  --    --  This conversion is a trivial 8-bit to 16-bit copy.
  --    r: Wide_String(s'Range);
  --  begin
  --    for i in s'Range loop
  --      r(i):= Wide_Character'Val(Character'Pos(s(i)));
  --    end loop;
  --    return r;
  --  end ISO_8859_1_to_UTF_16;

  --  5.63 LABEL
  procedure Write (
        xl : in out Excel_Out_Stream;
        r,
        c      : Positive;
        str    : String)
  is
  begin
    Jump_to_and_store_max (xl, r, c);
    if str'Length > 0 then
      case xl.xl_format is
        when BIFF2 =>
          WriteBiff (xl, 16#0004#,
            Intel_16 (Unsigned_16 (r - 1)) &
            Intel_16 (Unsigned_16 (c - 1)) &
            Cell_attributes (xl) &
            To_buf_8_bit_length (str)
          );
        when BIFF3 | BIFF4 =>
          WriteBiff (xl, 16#0204#,
            Intel_16 (Unsigned_16 (r - 1)) &
            Intel_16 (Unsigned_16 (c - 1)) &
            Intel_16 (Unsigned_16 (xl.xf_in_use)) &
            To_buf_16_bit_length (str)
          );
        --  when BIFF8 =>
        --    WriteBiff(xl, 16#0204#,
        --      Intel_16(Unsigned_16(r-1)) &
        --      Intel_16(Unsigned_16(c-1)) &
        --      Intel_16(Unsigned_16(xl.xf_in_use)) &
        --      To_buf_16_bit_length(ISO_8859_1_to_UTF_16(str))
        --    );
      end case;
    end if;
    Jump_to (xl, r, c + 1); -- Store and check new position
  end Write;

  procedure Write (xl : in out Excel_Out_Stream; r, c : Positive; str : Ada.Strings.Unbounded.Unbounded_String)
  is
  begin
    Write (xl, r, c, To_String (str));
  end Write;

  --  Excel uses a floating-point type for time - ouch!
  --
  function To_Number (date : Ada.Calendar.Time) return Long_Float is
    --  1901 is the lowest year supported by Ada.Calendar.
    --  1900 is not a leap year, but Lotus 1-2-3, then Excel, consider it
    --  as a leap year. So, with 1901, we skip that issue anyway...
    --
    function Days_since_1901 (y, m, d : Integer) return Integer is
      function Is_leap (y : Integer) return Boolean is
      begin
        if y mod 4 = 0 then
          if y mod 100 = 0 then
            if y mod 400 = 0 then
              return True;
            else
              return False;
            end if;
          else
            return True;
          end if;
        else
          return False;
        end if;
      end Is_leap;
      days_of_previous_months : Integer;
      days_of_previous_years  : Integer;
      y_diff, y_diff_4, y_diff_100, y_diff_400 : Integer;
    begin
      case m is
        when 02 => days_of_previous_months := 31;
        when 03 => days_of_previous_months := 59;
        when 04 => days_of_previous_months := 90;
        when 05 => days_of_previous_months := 120;
        when 06 => days_of_previous_months := 151;
        when 07 => days_of_previous_months := 181;
        when 08 => days_of_previous_months := 212;
        when 09 => days_of_previous_months := 243;
        when 10 => days_of_previous_months := 273;
        when 11 => days_of_previous_months := 304;
        when 12 => days_of_previous_months := 334;
        when others => days_of_previous_months := 0;
      end case;
      if m > 2 and then Is_leap (y) then  --  February has 29 days in leap years.
        days_of_previous_months := days_of_previous_months + 1;
      end if;
      --
      y_diff     := (y - 1)       - 1900;
      y_diff_4   := (y - 1) / 4   - 1900 / 4;
      y_diff_100 := (y - 1) / 100 - 1900 / 100;
      y_diff_400 := (y - 1) / 400 - 1900 / 400;
      --  Add extra days of leap years from 1901 (included) to year y (excluded).
      days_of_previous_years := 365 * y_diff + y_diff_4 - y_diff_100 + y_diff_400;
      --
      return days_of_previous_years + days_of_previous_months + d - 1;
    end Days_since_1901;
    --
    use Ada.Calendar;
    sec : constant Day_Duration := Seconds (date);
  begin
    --  With GNAT and perhaps other systems, Duration's range allows the following:
    --    return Long_Float(date - Time_Of(1901, 01, 01, 0.0)) / 86_400.0 + 367.0;
    --  With ObjectAda and perhaps other systems, we need to count days since 1900 ourselves.
    return
      Long_Float (sec) / 86_400.0 +
      Long_Float (Days_since_1901 (Year (date), Month (date), Day (date))) +
      367.0;  --  Days from 1899-12-31 to 1901-01-01.
      --  Lotus 1-2-3, then Excel, are based on 1899-12-31 (and believe it is 1900-01-01).
  end To_Number;

  procedure Write (xl : in out Excel_Out_Stream; r, c : Positive; date : Ada.Calendar.Time)
  is
  begin
    Write (xl, r, c, To_Number (date));
  end Write;

  --  Ada.Text_IO - like. No need to specify row & column each time
  procedure Put (xl : in out Excel_Out_Stream; num : Long_Float) is
  begin
    Write (xl, xl.curr_row, xl.curr_col, num);
  end Put;

  procedure Put (xl    : in out Excel_Out_Stream;
                num   : in Integer;
                width : in Ada.Text_IO.Field := 0; -- ignored
                base  : in Ada.Text_IO.Number_Base := 10
            )
  is
  begin
    if base = 10 then
      Write (xl, xl.curr_row, xl.curr_col, num);
    else
      declare
        use Ada.Strings.Fixed;
        s : String (1 .. 50 + 0 * width);
        --  0*width is just to skip a warning of width being unused
        package IIO is new Ada.Text_IO.Integer_IO (Integer);
      begin
        IIO.Put (s, num, Base => base);
        Put (xl, Trim (s, Ada.Strings.Left));
      end;
    end if;
  end Put;

  procedure Put (xl : in out Excel_Out_Stream; str : String) is
  begin
    Write (xl, xl.curr_row, xl.curr_col, str);
  end Put;

  procedure Put (xl : in out Excel_Out_Stream; str : Ada.Strings.Unbounded.Unbounded_String) is
  begin
    Put (xl, To_String (str));
  end Put;

  procedure Put (xl : in out Excel_Out_Stream; date : Ada.Calendar.Time) is
  begin
    Put (xl, To_Number (date));
  end Put;

  procedure Merge (xl : in out Excel_Out_Stream; cells : Positive) is

    --  5.7 BLANK
    procedure Blank (r, c : Positive) is
    begin
      Jump_to_and_store_max (xl, r, c);
      case xl.xl_format is
        --  NB: Only with BIFF4, and only OpenOffice
        --  considers the cells really merged.
        when BIFF2 =>
          WriteBiff (xl, 16#0001#,
            Intel_16 (Unsigned_16 (r - 1)) &
            Intel_16 (Unsigned_16 (c - 1)) &
            Cell_attributes (xl)
          );
        when BIFF3 | BIFF4 =>
          WriteBiff (xl, 16#0201#,
            Intel_16 (Unsigned_16 (r - 1)) &
            Intel_16 (Unsigned_16 (c - 1)) &
            Intel_16 (Unsigned_16 (xl.xf_in_use))
          );
      end case;
      Jump_to (xl, r, c + 1); -- Store and check new position
    end Blank;
  begin
    for i in 1 .. cells loop
      Blank (xl.curr_row, xl.curr_col);
    end loop;
  end Merge;

  procedure Write_cell_comment (xl : Excel_Out_Stream; at_row, at_column : Positive; text : String) is
  begin
    if text'Length >= 2048 then
      raise Constraint_Error;
    end if;
    --  5.70 Note
    case xl.xl_format is
      --  when BIFF8 =>  --  https://msdn.microsoft.com/en-us/library/dd945371(v=office.12).aspx
      --    WriteBiff(xl, 16#001C#,
      --      Intel_16(Unsigned_16(row-1)) &
      --      Intel_16(Unsigned_16(column-1)) &
      --      (0, 0) &  --  Show / hide options
      --      (0, 0) --  idObj - it begins to be tough there...
      --    );
      when others =>
        WriteBiff (xl, 16#001C#,
          Intel_16 (Unsigned_16 (at_row - 1)) &
          Intel_16 (Unsigned_16 (at_column - 1)) &
          To_buf_16_bit_length (text)
        );
    end case;
  end Write_cell_comment;

  procedure Write_cell_comment_at_cursor (xl : Excel_Out_Stream; text : String) is
  begin
    Write_cell_comment (xl, Row (xl), Column (xl), text);
  end Write_cell_comment_at_cursor;

  procedure Put_Line (xl : in out Excel_Out_Stream; num : Long_Float) is
  begin
    Put (xl, num);
    New_Line (xl);
  end Put_Line;

  procedure Put_Line (xl : in out Excel_Out_Stream; num : Integer) is
  begin
    Put (xl, num);
    New_Line (xl);
  end Put_Line;

  procedure Put_Line (xl : in out Excel_Out_Stream; str : String) is
  begin
    Put (xl, str);
    New_Line (xl);
  end Put_Line;

  procedure Put_Line (xl : in out Excel_Out_Stream; str : Ada.Strings.Unbounded.Unbounded_String) is
  begin
    Put_Line (xl, To_String (str));
  end Put_Line;

  procedure Put_Line (xl : in out Excel_Out_Stream; date : Ada.Calendar.Time) is
  begin
    Put (xl, date);
    New_Line (xl);
  end Put_Line;

  procedure New_Line (xl : in out Excel_Out_Stream; Spacing : Positive := 1) is
  begin
    Jump_to (xl, xl.curr_row + Spacing, 1);
  end New_Line;

  function Col (xl : in Excel_Out_Stream) return Positive is
  begin
    return xl.curr_col;
  end Col;

  function Column (xl : in Excel_Out_Stream) return Positive renames Col;

  function Line (xl : in Excel_Out_Stream) return Positive is
  begin
    return xl.curr_row;
  end Line;

  function Row (xl : in Excel_Out_Stream) return Positive renames Line;

  --  Relative / absolute jumps
  procedure Jump (xl : in out Excel_Out_Stream; rows, columns : Natural) is
  begin
    Jump_to (xl, xl.curr_row + rows, xl.curr_col + columns);
  end Jump;

  procedure Jump_to (xl : in out Excel_Out_Stream; to_row, to_column : Positive) is
  begin
    if to_row < xl.curr_row then -- trying to overwrite cells ?...
      raise Decreasing_row_index;
    end if;
    if to_row = xl.curr_row and then
      to_column < xl.curr_col
    then -- trying to overwrite cells on same row ?...
      raise Decreasing_column_index;
    end if;
    if to_row > 65536 then
      raise Row_out_of_range;
    elsif to_column > 256 then
      raise Column_out_of_range;
    end if;
    xl.curr_row := to_row;
    xl.curr_col := to_column;
  end Jump_to;

  procedure Next (xl : in out Excel_Out_Stream; columns : Natural := 1) is
  begin
    Jump (xl, rows => 0, columns => columns);
  end Next;

  procedure Next_Row (xl : in out Excel_Out_Stream; rows : Natural := 1) is
  begin
    Jump (xl, rows => rows, columns => 0);
  end Next_Row;

  procedure Use_format (
    xl           : in out Excel_Out_Stream;
    format       : in     Format_type
  )
  is
  begin
    if Integer (format) in XF_Range'Range then
      xl.xf_in_use := XF_Range (format);
    else
      raise Format_out_of_range;
      --  ^ Raised only if `format` was hacked using Unchecked_Conversion.
    end if;
  end Use_format;

  procedure Use_default_format (xl : in out Excel_Out_Stream) is
  begin
    Use_format (xl, xl.def_fmt);
  end Use_default_format;

  function Default_font (xl : Excel_Out_Stream) return Font_type is
  begin
    return xl.def_font;
  end Default_font;

  function Default_format (xl : Excel_Out_Stream) return Format_type is
  begin
    return xl.def_fmt;
  end Default_format;

  procedure Freeze_Panes (xl : in out Excel_Out_Stream; at_row, at_column : Positive) is
  begin
    xl.frz_panes := True;
    xl.freeze_row := at_row;
    xl.freeze_col := at_column;
  end Freeze_Panes;

  procedure Freeze_Panes_at_cursor (xl : in out Excel_Out_Stream) is
  begin
    Freeze_Panes (xl, xl.curr_row, xl.curr_col);
  end Freeze_Panes_at_cursor;

  procedure Freeze_Top_Row (xl : in out Excel_Out_Stream) is
  begin
    Freeze_Panes (xl, 2, 1);
  end Freeze_Top_Row;

  procedure Freeze_First_Column (xl : in out Excel_Out_Stream) is
  begin
    Freeze_Panes (xl, 1, 2);
  end Freeze_First_Column;

  procedure Zoom_level (xl : in out Excel_Out_Stream; numerator, denominator : Positive) is
  begin
    xl.zoom_num := numerator;
    xl.zoom_den := denominator;
  end Zoom_level;

  procedure Reset (
    xl           : in out Excel_Out_Stream'Class;
    excel_format :        Excel_type;
    encoding     :        Encoding_type
  )
  is
    dummy_xl_with_defaults : Excel_Out_Pre_Root_Type;
  begin
    --  Check if we are trying to re-use a half-finished object (ouch!):
    if xl.is_created and not xl.is_closed then
      raise Excel_stream_not_closed;
    end if;
    --  We will reset everything with defaults, except this:
    dummy_xl_with_defaults.xl_format := excel_format;
    dummy_xl_with_defaults.encoding  := encoding;
    --  Now we reset xl:
    Excel_Out_Pre_Root_Type (xl) := dummy_xl_with_defaults;
  end Reset;

  procedure Finish (xl : in out Excel_Out_Stream'Class) is

    procedure Write_Window1 is
    begin
      --  5.109 WINDOW1, p. 215
      case xl.xl_format is
        when BIFF2 | BIFF3 | BIFF4 =>  --  NB: more options in BIFF8
          WriteBiff (xl, 16#003D#,
            Intel_16 (120)   & -- Window x
            Intel_16 (120)   & -- Window y
            Intel_16 (21900) & -- Window w
            Intel_16 (13425) & -- Window h
            Intel_16 (0)       -- Hidden
          );
      end case;
    end Write_Window1;

    procedure Write_Window2 is
    begin
      --  5.110 WINDOW2
      case xl.xl_format is
        when BIFF2 =>
          WriteBiff (xl, 16#003E#,
            (0, -- Display formulas, not results
             1, -- Show grid lines
             1, -- Show sheet headers
             Boolean'Pos (xl.frz_panes),
             1  -- Show zero values as zeros, not empty cells
            )
             &
            Intel_16 (0) & -- First visible row
            Intel_16 (0) & -- First visible column
            (1, -- Use automatic grid line colour
             0, 0, 0, 0) -- Grid line RGB colour
          );
        when BIFF3 | BIFF4 =>  --  NB: more options in BIFF8
          WriteBiff (xl, 16#023E#,
            --  http://msdn.microsoft.com/en-us/library/dd947893(v=office.12).aspx
            Intel_16 (   -- Option flags:
              0 *   1 + -- Display formulas, not results
              1 *   2 + -- Show grid lines
              1 *   4 + -- Show sheet headers
              Boolean'Pos (xl.frz_panes)
                *   8 + -- Panes are frozen
              1 *  16 + -- Show zero values as zeros, not empty cells
              1 *  32 + -- Gridlines of the window drawn in the default window foreground color
              0 *  64 + -- Right-to-left mode
              1 * 128 + -- Show outlines (guts ?!)
              0 * 256   -- Frozen, not split
            ) &
            Intel_16 (0) & -- First visible row
            Intel_16 (0) & -- First visible column
            Intel_32 (0)   -- Grid line colour
          );
      end case;
    end Write_Window2;

    procedure Write_Pane is
      active_pane : Unsigned_8;
    begin
      if xl.freeze_col = 1 then
        if xl.freeze_row = 1 then
          active_pane := 3;
        else
          active_pane := 2;
        end if;
      else
        if xl.freeze_row = 1 then
          active_pane := 1;
        else
          active_pane := 0;
        end if;
      end if;
      --  5.75 PANE
      WriteBiff (xl, 16#0041#,
        Intel_16 (Unsigned_16 (xl.freeze_col) - 1) &
        Intel_16 (Unsigned_16 (xl.freeze_row) - 1) &
        Intel_16 (Unsigned_16 (xl.freeze_row) - 1) &
        Intel_16 (Unsigned_16 (xl.freeze_col) - 1) &
        (1 => active_pane)
      );
    end Write_Pane;

    col_bits : Byte_buffer (1 .. 32) := (others => 0);
    byte_idx, bit_idx : Positive := 1;

  begin
    --  Calling Window1 and Window2 is not necessary for default settings, but without these calls,
    --  a Write_row_height call with a positive height results, on all MS Excel versions, in a
    --  completely blank row, including the header letters - clearly an Excel bug !
    Write_Window1;
    Write_Window2;
    --  5.92 SCL = Zoom, Magnification. Defined for BIFF4+ only, but works with BIFF2, BIFF3.
    WriteBiff (xl, 16#00A0#,
      Intel_16 (Unsigned_16 (xl.zoom_num)) &
      Intel_16 (Unsigned_16 (xl.zoom_den))
    );
    if xl.frz_panes and xl.xl_format > BIFF2 then
      --  Enabling PANE for BIFF2 causes a very strange behaviour on MS Excel 2002.
      Write_Pane;
    end if;
    --  5.93 SELECTION here !!
    if xl.xl_format >= BIFF4 then
      for i in 1 .. 256 loop
        col_bits (byte_idx) := col_bits (byte_idx) +
          Boolean'Pos (xl.std_col_width (i)) * (2**(bit_idx - 1));
        bit_idx := bit_idx + 1;
        if bit_idx = 9 then
          bit_idx := 1;
          byte_idx := byte_idx + 1;
        end if;
      end loop;
      --  5.51 GCW: Global Column Width - trying to get a correct display by LibreOffice
      --  Result: OK but useless on MS Excel, not working on LibreOffice :-(
      WriteBiff (xl, 16#00AB#, Intel_16 (32) & col_bits);
      --  if xl.defcolwdth > 0 then
      --    --  5.101 STANDARDWIDTH -- this confuses MS Excel...
      --    WriteBiff(xl, 16#0099#, Intel_16(Unsigned_16(xl.defcolwdth)));
      --  end if;
    end if;
    --  5.37 EOF: End of File:
    WriteBiff (xl, 16#000A#, empty_buffer);
    Set_Index (xl, xl.dimrecpos); -- Go back to overwrite the DIMENSION record with correct data
    Write_Dimensions (xl);
    xl.is_closed := True;
  end Finish;

  ----------------------
  -- Output to a file --
  ----------------------

  procedure Create (
    xl           : in out Excel_Out_File;
    file_name    :        String;
    excel_format :        Excel_type    := Default_Excel_type;
    encoding     :        Encoding_type := Default_encoding
  )
  is
    use Ada.Streams, Ada.Streams.Stream_IO;
  begin
    Reset (xl, excel_format, encoding);
    xl.xl_file := new Ada.Streams.Stream_IO.File_Type;
    Create (xl.xl_file.all, Out_File, file_name);
    xl.xl_stream := XL_Raw_Stream_Class (Stream (xl.xl_file.all));
    Write_Worksheet_header (xl);
  end Create;

  procedure Close (xl : in out Excel_Out_File) is
    procedure Dispose is new
      Ada.Unchecked_Deallocation (Ada.Streams.Stream_IO.File_Type, XL_file_acc);
  begin
    Finish (xl);
    Ada.Streams.Stream_IO.Close (xl.xl_file.all);
    Dispose (xl.xl_file);
  end Close;

  --  Set the index on the file
  procedure Set_Index (xl : in out Excel_Out_File;
                       To : Ada.Streams.Stream_IO.Positive_Count)
  is
  begin
    Ada.Streams.Stream_IO.Set_Index (xl.xl_file.all, To);
  end Set_Index;

  --  Return the index of the file
  function Index (xl : Excel_Out_File) return Ada.Streams.Stream_IO.Count
  is
  begin
    return Ada.Streams.Stream_IO.Index (xl.xl_file.all);
  end Index;

  function Is_Open (xl : in Excel_Out_File) return Boolean is
  begin
    if xl.xl_file = null then
      return False;
    end if;
    return Ada.Streams.Stream_IO.Is_Open (xl.xl_file.all);
  end Is_Open;

  ------------------------
  -- Output to a string --
  ------------------------
  --  Code reused from Zip_Streams

  procedure Read
    (Stream : in out Unbounded_Stream;
     Item   :    out Ada.Streams.Stream_Element_Array;
     Last   :    out Ada.Streams.Stream_Element_Offset)
  is
    use Ada.Streams;
  begin
    --  Item is read from the stream. If (and only if) the stream is
    --  exhausted, Last will be < Item'Last. In that case, T'Read will
    --  raise an End_Error exception.
    --
    --  Cf: RM 13.13.1(8), RM 13.13.1(11), RM 13.13.2(37) and
    --  explanations by Tucker Taft
    --
    Last := Item'First - 1;
    --  if Item is empty, the following loop is skipped; if Stream.Loc
    --  is already indexing out of Stream.Unb, that value is also appropriate
    for i in Item'Range loop
      Item (i) := Character'Pos (Element (Stream.Unb, Stream.Loc));
      Stream.Loc := Stream.Loc + 1;
      Last := i;
    end loop;
  exception
    when Ada.Strings.Index_Error =>
      null; -- what could be read has been read; T'Read will raise End_Error
  end Read;

  procedure Write
    (Stream : in out Unbounded_Stream;
     Item   : Ada.Streams.Stream_Element_Array)
  is
  begin
    for I in Item'Range loop
      if Length (Stream.Unb) < Stream.Loc then
        Append (Stream.Unb, Character'Val (Item (I)));
      else
        Replace_Element (Stream.Unb, Stream.Loc, Character'Val (Item (I)));
      end if;
      Stream.Loc := Stream.Loc + 1;
    end loop;
  end Write;

  procedure Set_Index (S : access Unbounded_Stream; To : Positive) is
  begin
    if Length (S.Unb) < To then
      for I in Length (S.Unb) .. To loop
        Append (S.Unb, ASCII.NUL);
      end loop;
    end if;
    S.Loc := To;
  end Set_Index;

  function Index (S : access Unbounded_Stream) return Integer is
  begin
    return S.Loc;
  end Index;

  --- ***

  procedure Create (
    xl           : in out Excel_Out_String;
    excel_format :        Excel_type    := Default_Excel_type;
    encoding     :        Encoding_type := Default_encoding
  )
  is
  begin
    Reset (xl, excel_format, encoding);
    xl.xl_memory := new Unbounded_Stream;
    xl.xl_memory.Unb := Null_Unbounded_String;
    xl.xl_memory.Loc := 1;
    xl.xl_stream := XL_Raw_Stream_Class (xl.xl_memory);
    Write_Worksheet_header (xl);
  end Create;

  procedure Close (xl : in out Excel_Out_String) is
  begin
    Finish (xl);
  end Close;

  function Contents (xl : Excel_Out_String) return String is
  begin
    if not xl.is_closed then
      raise Excel_stream_not_closed;
    end if;
    return To_String (xl.xl_memory.Unb);
  end Contents;

  --  Set the index on the Excel string stream
  procedure Set_Index (xl : in out Excel_Out_String;
                       To : Ada.Streams.Stream_IO.Positive_Count)
  is
  begin
    Set_Index (xl.xl_memory, Integer (To));
  end Set_Index;

  --  Return the index of the Excel string stream
  function Index (xl : Excel_Out_String) return Ada.Streams.Stream_IO.Count
  is
  begin
    return Ada.Streams.Stream_IO.Count (Index (xl.xl_memory));
  end Index;

  function "&"(a, b : Font_style) return Font_style is
  begin
    return a or b; -- "or" is predefined for sets (=array of Boolean)
  end "&";

  function "&"(a, b : Cell_border) return Cell_border is
  begin
    return a or b; -- "or" is predefined for sets (=array of Boolean)
  end "&";

end Excel_Out;


Excel Writer: Ada package writing Excel files (.xls). Ada programming.
Some news about Excel Writer and other Ada projects on Gautier's blog.