Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased
XLSX: decode dates with custom `<numFmt>` format codes (e.g. `dd/mm/yyyy`) using the workbook's own format code instead of leaking through as raw serial numbers. Removed the `raw` decoding mode in favour of always returning typed values.

Built-in date cells (numFmtId 14-17, 22) and ODS date cells now render as `yyyy-MM-dd` by default instead of an ISO timestamp like `2008-07-21T00:00:00.000`. Added a `dateFormat` parameter to `SpreadsheetDecoder.decodeBytes` and `SpreadsheetDecoder.decodeBuffer` so callers can choose any pattern (e.g. `dd/MM/yyyy`, `yyyy.MM.dd`); custom-numFmt date cells honour the caller's `dateFormat` when explicitly set, otherwise fall back to the workbook's own format code.

## 2.3.0
Update dependencies

Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,22 @@ This implementation doesn't support following features:
- hidden rows (visible in resulting tables)
- hidden columns (visible in resulting tables)

For XLSX format, this implementation only supports native Excel format for date, time and boolean type conversion.
In other words, custom format for date, time, boolean aren't supported and then file exported from LibreOffice as well.
For XLSX format, this implementation supports the native Excel formats for date, time and boolean type conversion, plus custom `<numFmt>` format codes declared in the workbook (e.g. `dd/mm/yyyy`).

Important: Excel often stores date cells as numeric serial values and only formats them for display. The decoder applies the workbook's format code to render the date as a string. If you need a fixed text representation independent of the workbook formatting, the source cell must be stored as text in the spreadsheet.

### Customising the date output format

By default, date cells (XLSX built-in numFmtIds 14-17, 22 and ODS `date` cells) are rendered as `yyyy-MM-dd` (e.g. `2008-07-21`). You can override this from the call site by passing `dateFormat`:

var decoder = SpreadsheetDecoder.decodeBytes(
bytes,
dateFormat: 'dd/MM/yyyy', // → 21/07/2008
);

Supported tokens (case-insensitive): `yyyy`, `yy`, `mm`/`m` (month — context-sensitive vs minutes), `dd`/`d`/`ddd`/`dddd`, `hh`/`h`, `ss`/`s`, `AM/PM`. Any other characters in the pattern (`-`, `/`, `.`, spaces, etc.) are emitted as literal separators.

For cells that use a custom `<numFmt>` format code declared in the workbook, the workbook's own format code is used unless you explicitly pass a non-default `dateFormat`, in which case your format wins.

## License

Expand Down
56 changes: 28 additions & 28 deletions example/spreadsheet_decoder.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dart:io';
import 'package:path/path.dart';
import 'package:spreadsheet_decoder/spreadsheet_decoder.dart';

void main(List<String> args) {
var file = 'test/files/test.xlsx';
var file = 'test/files/new_sample_file_bytco.xlsx';
var bytes = File(file).readAsBytesSync();
var decoder = SpreadsheetDecoder.decodeBytes(bytes, update: true);
var decoder = SpreadsheetDecoder.decodeBytes(bytes,
update: false, dateFormat: 'dd/MM/yyyy');
for (var table in decoder.tables.keys) {
print(table);
print(decoder.tables[table]!.maxCols);
Expand All @@ -15,31 +15,31 @@ void main(List<String> args) {
}
}

var sheet = decoder.tables.keys.first;
decoder
..updateCell(sheet, 0, 0, "L'oiseau <\"coucou\">")
..updateCell(sheet, 1, 0, 'B')
..updateCell(sheet, 2, 0, 'C')
..updateCell(sheet, 1, 1, 42.3)
..insertRow(sheet, 1)
..insertRow(sheet, 13)
..updateCell(sheet, 0, 13, 'A14')
..updateCell(sheet, 0, 12, 'A13')
..insertColumn(sheet, 0)
..removeRow(sheet, 1)
..removeColumn(sheet, 2);
// var sheet = decoder.tables.keys.first;
// decoder
// ..updateCell(sheet, 0, 0, "L'oiseau <\"coucou\">")
// ..updateCell(sheet, 1, 0, 'B')
// ..updateCell(sheet, 2, 0, 'C')
// ..updateCell(sheet, 1, 1, 42.3)
// ..insertRow(sheet, 1)
// ..insertRow(sheet, 13)
// ..updateCell(sheet, 0, 13, 'A14')
// ..updateCell(sheet, 0, 12, 'A13')
// ..insertColumn(sheet, 0)
// ..removeRow(sheet, 1)
// ..removeColumn(sheet, 2);

File(join('test/out/${basename(file)}'))
..createSync(recursive: true)
..writeAsBytesSync(decoder.encode());
// File(join('test/out/${basename(file)}'))
// ..createSync(recursive: true)
// ..writeAsBytesSync(decoder.encode());

print('************************************************************');
for (var table in decoder.tables.keys) {
print(table);
print(decoder.tables[table]!.maxCols);
print(decoder.tables[table]!.maxRows);
for (var row in decoder.tables[table]!.rows) {
print('$row');
}
}
// print('************************************************************');
// for (var table in decoder.tables.keys) {
// print(table);
// print(decoder.tables[table]!.maxCols);
// print(decoder.tables[table]!.maxRows);
// for (var row in decoder.tables[table]!.rows) {
// print('$row');
// }
// }
}
1 change: 0 additions & 1 deletion lib/spreadsheet_decoder.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
library spreadsheet_decoder;

import 'dart:convert';
import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:xml/xml.dart';
Expand Down
18 changes: 13 additions & 5 deletions lib/src/ods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ class OdsDecoder extends SpreadsheetDecoder {
String get extension => '.ods';
final Map<String, List<String>> _styleNames = {};

OdsDecoder(Archive archive, {bool update = false}) {
OdsDecoder(Archive archive,
{bool update = false,
String dateFormat = SpreadsheetDecoder.defaultDateFormat}) {
_archive = archive;
_update = update;
_dateFormat = dateFormat;
_tables = <String, SpreadsheetTable>{};
_parseContent();
}
Expand Down Expand Up @@ -96,8 +99,11 @@ class OdsDecoder extends SpreadsheetDecoder {

void _parseContent() {
var file = _archive.findFile(contentXML);
file?.decompress();
var content = XmlDocument.parse(utf8.decode(file?.content));
if (file == null) {
throw FormatException('Missing required file: $contentXML');
}
file.decompress();
var content = XmlDocument.parse(utf8.decode(file.content));
if (_update == true) {
_archiveFiles = <String, ArchiveFile>{};
_sheets = <String, XmlElement>{};
Expand Down Expand Up @@ -185,6 +191,7 @@ class OdsDecoder extends SpreadsheetDecoder {
dynamic _readCell(XmlElement node) {
dynamic value;
var type = node.getAttribute('office:value-type');

switch (type) {
case 'float':
case 'percentage':
Expand All @@ -196,8 +203,9 @@ class OdsDecoder extends SpreadsheetDecoder {
node.getAttribute('office:boolean-value')!.toLowerCase() == 'true';
break;
case 'date':
value = DateTime.parse(node.getAttribute('office:date-value')!)
.toIso8601String();
value = _formatDateTimeWithCode(
DateTime.parse(node.getAttribute('office:date-value')!),
_dateFormat);
break;
case 'time':
value = node.getAttribute('office:time-value');
Expand Down
45 changes: 32 additions & 13 deletions lib/src/spreadsheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ String _normalizeNewLine(String text) {
return text.replaceAll('\r\n', '\n');
}

SpreadsheetDecoder _newSpreadsheetDecoder(Archive archive, bool update) {
SpreadsheetDecoder _newSpreadsheetDecoder(
Archive archive, bool update, String dateFormat) {
// Lookup at file format
String? format;

Expand All @@ -34,17 +35,26 @@ SpreadsheetDecoder _newSpreadsheetDecoder(Archive archive, bool update) {

switch (format) {
case _spreasheetOds:
return OdsDecoder(archive, update: update);
return OdsDecoder(archive, update: update, dateFormat: dateFormat);
case _spreasheetXlsx:
return XlsxDecoder(archive, update: update);
return XlsxDecoder(archive, update: update, dateFormat: dateFormat);
default:
throw UnsupportedError('Spreadsheet format unsupported');
}
}

/// Decode a spreadsheet file.
abstract class SpreadsheetDecoder {
/// Default output format used for date cells when none is provided.
/// Format tokens follow Excel-style codes (case-insensitive):
/// `yyyy`/`yy`, `mm`/`m` (month — context-sensitive),
/// `dd`/`d`/`ddd`/`dddd`, `hh`/`h`, `ss`/`s`, `AM/PM`.
/// Common intl-style patterns like `dd/MM/yyyy` also work because the
/// pattern is matched case-insensitively.
static const String defaultDateFormat = 'yyyy-MM-dd';

late bool _update;
late String _dateFormat;
late Archive _archive;
late Map<String, XmlElement> _sheets;
late Map<String, XmlDocument> _xmlFiles;
Expand All @@ -63,16 +73,25 @@ abstract class SpreadsheetDecoder {

SpreadsheetDecoder();

/// Decode an XLSX/ODS spreadsheet from raw [data] bytes.
///
/// Pass [dateFormat] to control how date cells are rendered as strings
/// (e.g. `'dd/MM/yyyy'`, `'yyyy-MM-dd'`). Defaults to
/// [defaultDateFormat] (`yyyy-MM-dd`).
factory SpreadsheetDecoder.decodeBytes(List<int> data,
{bool update = false, bool verify = false}) {
{bool update = false,
bool verify = false,
String dateFormat = defaultDateFormat}) {
var archive = ZipDecoder().decodeBytes(data, verify: verify);
return _newSpreadsheetDecoder(archive, update);
return _newSpreadsheetDecoder(archive, update, dateFormat);
}

factory SpreadsheetDecoder.decodeBuffer(InputStreamBase input,
{bool update = false, bool verify = false}) {
var archive = ZipDecoder().decodeBuffer(input, verify: verify);
return _newSpreadsheetDecoder(archive, update);
factory SpreadsheetDecoder.decodeBuffer(InputStream input,
{bool update = false,
bool verify = false,
String dateFormat = defaultDateFormat}) {
var archive = ZipDecoder().decodeStream(input, verify: verify);
return _newSpreadsheetDecoder(archive, update, dateFormat);
}

/// Dump XML content (for debug purpose)
Expand Down Expand Up @@ -169,7 +188,7 @@ abstract class SpreadsheetDecoder {
var content = utf8.encode(xml);
_archiveFiles[xmlFile] = ArchiveFile(xmlFile, content.length, content);
}
return ZipEncoder().encode(_cloneArchive(_archive)) as List<int>;
return ZipEncoder().encode(_cloneArchive(_archive));
}

/// Encode data url
Expand All @@ -188,10 +207,10 @@ abstract class SpreadsheetDecoder {
if (_archiveFiles.containsKey(file.name)) {
copy = _archiveFiles[file.name]!;
} else {
var content = file.content as Uint8List;
var compress = file.compress;
var content = file.content;
var compression = file.compression;
copy = ArchiveFile(file.name, content.length, content)
..compress = compress;
..compression = compression;
}
clone.addFile(copy);
}
Expand Down
Loading