Даты числом 44898, пропавшие нули и 8.98E+18: как не испортить данные при экспорте в Excel

aVadim • 29.08.2026 15:20 •

Три классические жалобы после выкатки экспорта в XLSX:

  • «В колонке с датами какие-то числа — 44898 вместо даты»;
  • «У артикулов пропали нули: было 000123, стало 123»;
  • «Номера карт превратились в 8.98E+18, последние цифры — нули».

Все три бага — не баги вашей библиотеки и не баги Excel. Это следствие того, как Excel хранит значения. Разберём, как правильно типизировать данные при генерации XLSX с помощью avadim/fast-excel-writer — быстрой потоковой библиотеки на чистом PHP.

Всё ниже проверено на версии 6.16.1.

Как XLSX хранит значения на самом деле

Внутри XLSX у ячейки есть только примитивные типы: число, строка, булево значение, ошибка. Типа «дата» нет вообще. Дата — это обычное число: количество дней с эпохи Excel (условно 1 января 1900 года). Число 44898 — это 3 декабря 2022 года; время — дробная часть: 44898.75 — 3 декабря, 18:00. «Датой» это число становится только благодаря маске числового формата, привязанной к ячейке: YYYY-MM-DD покажет 2022-12-03, DD MMM YY — 03 Dec 22. Уберите маску — снова увидите 44898.

Отсюда главное правило работы с XLSX:

Значение и формат задаются отдельно. Значение — то, что лежит в ячейке (число, строка). Формат — то, как Excel его показывает. «Испорченные» данные почти всегда означают, что записан не тот тип значения или не задан формат.

С числами вторая засада: Excel хранит их как 64-битный float, то есть максимум 15 значащих цифр. Номер карты из 16 цифр или длинный ID в числовой ячейке будет молча округлён — последние цифры превратятся в нули, а отображение переключится на научную нотацию вида 8.98E+18. Данные теряются безвозвратно, никакой формат их не вернёт.

Как библиотека определяет типы

FastExcelWriter отталкивается от PHP-типа значения:

php
use avadim\FastExcelWriter\Excel;

$excel = Excel::create(['Report']);
$sheet = $excel->sheet();

$sheet->writeCell(123456);    // int PHP  -> число в Excel
$sheet->writeCell(12.34);     // float    -> число
$sheet->writeCell('123456');  // string   -> текст (даже если внутри цифры)

Строка '123456' по умолчанию попадёт в файл как текст — библиотека не гадает за вас. Если числовые строки из БД должны становиться числами, включите опцию: Excel::create(['Report'], ['auto_convert_number' => true]).

Второй способ управлять типом — задать формат ячейки. Формат передаётся в стиле, вторым аргументом writeCell():

php
$sheet->writeCell(12.34, ['format' => '@money']);        // денежный формат
$sheet->writeCell(date('Y-m-d'), ['format' => '@date']); // дата

Форматы с префиксом @ — встроенные сокращения, за которыми стоят обычные маски Excel:

простой формат код формата зависит от локали
@text @ нет
@string @ нет
@integer 0 нет
@percent 0% нет
@date YYYY-MM-DD да
@datetime YYYY-MM-DD HH:MM:SS да
@time HH:MM:SS да
@money [$$]0.00 да

Маски в правой колонке — это то, что получится при локали en. Даты и деньги берутся из локали, и если её не задать явно, библиотека возьмёт системную: на машине с русской локалью @date развернётся в DD.MM.YYYY, а @money — в #,##0.00 [$₽]. Файл, собранный на ноутбуке разработчика и на сервере, окажется разным. Поэтому локаль лучше фиксировать явно:

php
$excel = Excel::create(['Report'], ['locale' => 'en']);
// или уже после создания книги
$excel->setLocale('en');

Вместо сокращения всегда можно написать маску напрямую: 'format' => '#,##0.00' — она от локали не зависит вообще.

Даты: число + маска, и никак иначе

Запишете '2022-12-03' без формата — в файл уйдёт текст, который не отсортируешь как даты. Запишете time() без формата — уйдёт большое число секунд. Правильный вариант — значение плюс формат:

php
// строка с датой + формат: библиотека сама преобразует строку
// в «excel-число» и привяжет маску
$sheet->writeCell('2022-12-03', ['format' => '@date']);

// unix timestamp тоже понимается
$sheet->writeCell(time(), ['format' => '@datetime']);

Когда у ячейки датный формат, библиотека распознаёт значение сама: строки вида '1985-01-28 23:05:59', '23:05' разбираются через strtotime(), целые числа трактуются как unix timestamp. Объекты DateTimeInterface можно писать напрямую — они автоматически получают формат @datetime, если другой не задан.

Маска может быть любой, не только ISO. Фрагмент из демо-скрипта demo-09-datetime-formats.php:

php
$formats = [
    '@',           // как текст, без преобразования
    '@datetime',   // 1985-01-28 23:05:59
    '@date',       // 1985-01-28
    'DD MMM YY',   // 28 Jan 85
    'H:MM',        // 23:05
];

foreach ($formats as $format) {
    $sheet->writeCell('1985-01-28 23:05:59', ['format' => $format]);
}

Названия месяцев в масках (MMM, MMMM) Excel локализует сам, под язык пользователя.

Запомните симптом: видите в колонке дат числа около 45000 — вы записали дату, но забыли формат. Значение верное, не хватает только маски.

Ведущие нули и длинные ID: только текст

Артикул 000123, телефон, штрихкод EAN, номер счёта или карты — всё это не числа, даже если состоит из цифр: любое числовое представление их калечит. Решение — писать такие значения строками PHP и явно помечать формат как текстовый:

php
$sheet->writeCell('000123', ['format' => '@text']);
$sheet->writeCell('8983190010004321757', ['format' => '@text']);

С форматом @text (он же @string, он же маска @) значение уходит в файл как текст независимо от содержимого. Нюанс с опцией auto_convert_number: она превращает числовые строки в числа, а для колонок с ID это ровно то, чего вы не хотите, — если опция включена глобально, обязательно задайте таким колонкам текстовый формат.

Именно здесь XLSX выигрывает у CSV. В CSV типов нет вообще: Excel при открытии сам решит, что 000123 — это число 123, а длинный номер — float, и повлиять на это со стороны генератора невозможно. В XLSX тип ячейки — часть файла, и он под вашим контролем.

Деньги и проценты

Денежные суммы — обычные float/int плюс маска. Минимум — разделители разрядов и два знака после запятой; маска может включать символ валюты и отдельное оформление отрицательных значений (до ; — формат положительных чисел, после — отрицательных):

php
$sheet->writeCell(1234567.891, ['format' => '#,##0.00']); // 1 234 567,89
$sheet->writeCell(12.34, ['format' => '@money']);         // символ валюты — из локали

$style = ['format' => '[$$]#,##0.00;[RED]-[$$]#,##0.00'];
$sheet->writeCell(-500, $style);                          // -$500.00 красным

Разделители в маске (, и .) — это роли (разряды и десятичная часть), а не литералы: пользователю с русской локалью Excel сам покажет пробелы и запятую. И не форматируйте деньги в строку на стороне PHP — number_format() превратит сумму в текст, по которому не построишь сумму по колонке.

Проценты — отдельный сюрприз. Маска % умножает значение на 100 при отображении, поэтому в ячейку пишется доля, а не готовое число процентов:

php
$sheet->writeCell(0.15, ['format' => '0%']);      // 15%
$sheet->writeCell(0.1234, ['format' => '0.00%']); // 12.34%

Если записать 15 с маской 0%, получите 1500%. Экспортируете из БД поле, где уже лежит «15» — поделите на 100 перед записью.

Формат на колонку, а не на каждую ячейку

У типичного отчёта тип определяется колонкой, поэтому задавать стиль в каждом writeCell() не нужно — форматы колонок объявляются один раз, дальше вы просто пишете строки данных.

Самый компактный способ — через writeHeader(): ключи массива становятся заголовками, значения — форматами колонок:

php
$sheet->writeHeader([
    'Дата'    => '@date',
    'Артикул' => '@text',
    'Сумма'   => '#,##0.00',
    'Скидка'  => '0%',
]);

foreach ($orders as $order) {
    $sheet->writeRow([
        $order['created_at'], // '2022-12-03' -> дата
        $order['sku'],        // '000123'     -> текст, нули на месте
        $order['amount'],
        $order['discount'],   // 0.15 -> 15%
    ]);
}

Если заголовки не нужны, есть прямые методы:

php
// одна колонка
$sheet->setColFormat('K', '@date');

// все колонки по порядку, начиная с 'A'; null — формат по умолчанию
$sheet->setColFormats([null, '@', '@', '@date', '0', '0.00', '@money']);

// формат + ширина, как в demo-09
$sheet->setColDataStyleArray([
    1 => ['format' => '@date',    'width' => 14],
    2 => ['format' => '@text',    'width' => 12],
    3 => ['format' => '#,##0.00', 'width' => 16],
]);

Формат ячейки, заданный в writeCell(), при необходимости переопределит формат колонки.

Шпаргалка

Что экспортируем Как писать
Числа PHP int/float, формат не обязателен
Числа строками из БД опция auto_convert_number или формат '0', '0.00'
Дата строка '2022-12-03', timestamp или DateTime + @date / своя маска
Дата и время то же + @datetime или 'YYYY-MM-DD HH:MM:SS'
Время @time или 'H:MM'
Артикулы с ведущими нулями PHP-строка + формат @text
Номера карт, EAN (больше 15 цифр) только PHP-строка + @text, иначе потеря цифр
Деньги float + маска '#,##0.00' или '[$$]#,##0.00;[RED]-[$$]#,##0.00'
Проценты доля (0.15, не 15) + маска '0%' / '0.00%'
Формат на колонку writeHeader(['Имя' => '@date']), setColFormat(), setColFormats()

Итого: XLSX не хранит «даты» и не знает про ведущие нули — он хранит числа, строки и маски форматов. Один раз явно решите для каждой колонки, что это — число, дата или текст, — и все три классических бага исчезнут.

bash
composer require avadim/fast-excel-writer

Документация и примеры — в репозитории на GitHub.

Комментарии (0)