diff --git a/Amalgamation/SheetReader.cs b/Amalgamation/SheetReader.cs index b1dc936..496b8a3 100644 --- a/Amalgamation/SheetReader.cs +++ b/Amalgamation/SheetReader.cs @@ -23,8 +23,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* -AssemblyVersion: 3.2.1.0 -AssemblyFileVersion: 3.2.1.0 +AssemblyVersion: 3.4.0.0 +AssemblyFileVersion: 3.4.0.0 */ using Extensions = SheetReader.Utilities.Extensions; using SheetReader.Utilities; @@ -419,18 +419,21 @@ public override IEnumerable EnumerateRows() JsonElement? cells = null; if (Format.CellsPropertyName == null) { - if (Element.TryGetProperty("cells", out var element) && element.ValueKind == JsonValueKind.Array) + if (Element.ValueKind == JsonValueKind.Object) { - cells = element; - } - else if (Element.TryGetProperty("Cells", out element) && element.ValueKind == JsonValueKind.Array) - { - cells = element; + if (Element.TryGetProperty("cells", out var element) && element.ValueKind == JsonValueKind.Array) + { + cells = element; + } + else if (Element.TryGetProperty("Cells", out element) && element.ValueKind == JsonValueKind.Array) + { + cells = element; + } } } else { - if (Element.TryGetProperty(Format.CellsPropertyName, out var element) && element.ValueKind == JsonValueKind.Array) + if (Element.ValueKind == JsonValueKind.Object && Element.TryGetProperty(Format.CellsPropertyName, out var element) && element.ValueKind == JsonValueKind.Array) { cells = element; } @@ -452,11 +455,11 @@ public override IEnumerable EnumerateRows() { rows = Element; } - else if (Element.TryGetProperty("rows", out var element) && element.ValueKind == JsonValueKind.Array) + else if (Element.ValueKind == JsonValueKind.Object && Element.TryGetProperty("rows", out var element) && element.ValueKind == JsonValueKind.Array) { rows = element; } - else if (Element.TryGetProperty("Rows", out element) && element.ValueKind == JsonValueKind.Array) + else if (Element.ValueKind == JsonValueKind.Object && Element.TryGetProperty("Rows", out element) && element.ValueKind == JsonValueKind.Array) { rows = element; } @@ -485,7 +488,7 @@ public override IEnumerable EnumerateRows() } else { - if (!Element.TryGetProperty(Format.RowsPropertyName, out var element) || element.ValueKind != JsonValueKind.Array) + if (Element.ValueKind != JsonValueKind.Object || !Element.TryGetProperty(Format.RowsPropertyName, out var element) || element.ValueKind != JsonValueKind.Array) yield break; rows = element; @@ -976,6 +979,11 @@ public virtual IEnumerable EnumerateDeclaredRows() var row = CreateRow(); if (row != null) { + if (Format.LoadOptions.HasFlag(LoadOptions.FirstRowDefinesColumns)) + { + rowIndex--; + } + row.Index = rowIndex - 1; var hidden = Reader.GetAttribute("hidden"); if (hidden != null && Extensions.EqualsIgnoreCase(hidden, "true") || hidden == "1") @@ -1431,6 +1439,18 @@ static bool isDefaultJsonValue(object? value) if (value is decimal dec && dec == 0m) return true; + if (value is DateTime dt && dt == DateTime.MinValue) + return true; + + if (value is DateTimeOffset dto && dto == DateTimeOffset.MinValue) + return true; + + if (value is TimeSpan ts && ts == TimeSpan.Zero) + return true; + + if (value is Guid g && g == Guid.Empty) + return true; + if (value is uint ui && ui == 0) return true; @@ -1442,15 +1462,19 @@ static bool isDefaultJsonValue(object? value) void writePositionedCell(BookDocumentCell? cell, int rowIndex, int columnIndex) { - // don't output null values - if (cell == null || cell.Value == null || Convert.IsDBNull(cell.Value)) - return; - writer.WriteStartObject(); writer.WriteNumber("r", rowIndex); writer.WriteNumber("c", columnIndex); writer.WritePropertyName("value"); + // don't output null values + if (cell == null || cell.Value == null || Convert.IsDBNull(cell.Value)) + { + writer.WriteNullValue(); + writer.WriteEndObject(); + return; + } + if (cell.IsError) { if (cell.Value != null) @@ -1526,6 +1550,20 @@ void writePositionedCell(BookDocumentCell? cell, int rowIndex, int columnIndex) return; } + if (cell.Value is DateTime dt) + { + writer.WriteStringValue(dt.ToString("u")); + writer.WriteEndObject(); + return; + } + + if (cell.Value is DateTimeOffset dto) + { + writer.WriteStringValue(dto.ToString("u")); + writer.WriteEndObject(); + return; + } + if (cell.Value is byte[] bytes) { writer.WriteBase64StringValue(bytes); @@ -1638,6 +1676,18 @@ void writeCell(BookDocumentCell? cell) return; } + if (cell.Value is DateTime dt) + { + writer.WriteStringValue(dt.ToString("u")); + return; + } + + if (cell.Value is DateTimeOffset dto) + { + writer.WriteStringValue(dto.ToString("u")); + return; + } + if (cell.Value is byte[] bytes) { writer.WriteBase64StringValue(bytes); @@ -1699,7 +1749,7 @@ void writeRowAsObjects(BookDocumentSheet sheet, BookDocumentRow? row, Dictionary row?.Cells.TryGetValue(columnIndex, out cell); if (!options.HasFlag(ExportOptions.JsonNoDefaultCellValues) || (cell != null && !isDefaultJsonValue(cell.Value))) { - string colName = colNames[columnIndex]; + var colName = colNames[columnIndex]; writer.WritePropertyName(colName); writeCell(cell); } @@ -1749,17 +1799,13 @@ void writeSheet(BookDocumentSheet sheet) writer.WriteStartObject(); writeSheetHeader(sheet); - if (!sheet.ColumnsHaveBeenGenerated) - { - writeColumns(sheet); - } + writeColumns(sheet); writer.WritePropertyName(format.RowsPropertyName ?? "cells"); writer.WriteStartArray(); if (sheet.FirstRowIndex.HasValue && sheet.LastRowIndex.HasValue) { - var rowOffset = sheet.ColumnsHaveBeenGenerated ? 0 : -1; for (var rowIndex = sheet.FirstRowIndex.Value; rowIndex <= sheet.LastRowIndex.Value; rowIndex++) { sheet.Rows.TryGetValue(rowIndex, out var row); @@ -1769,7 +1815,10 @@ void writeSheet(BookDocumentSheet sheet) { if (row.Cells.TryGetValue(columnIndex, out var cell)) { - writePositionedCell(cell, rowIndex + rowOffset, columnIndex); + if (!options.HasFlag(ExportOptions.JsonNoDefaultCellValues) || (cell != null && !isDefaultJsonValue(cell.Value))) + { + writePositionedCell(cell, rowIndex, columnIndex); + } } } } @@ -1781,7 +1830,7 @@ void writeSheet(BookDocumentSheet sheet) return; } - if (sheet.ColumnsHaveBeenGenerated && !options.HasFlag(ExportOptions.JsonRowsAsObject)) + if (!options.HasFlag(ExportOptions.JsonRowsAsObject)) { writer.WriteStartArray(); if (sheet.FirstRowIndex.HasValue && sheet.LastRowIndex.HasValue) @@ -1878,6 +1927,22 @@ public class BookDocumentCellError(Cell cell) { } + public class BookDocumentColumn + { + public BookDocumentColumn(Column column) + { + ArgumentNullException.ThrowIfNull(column); + Column = column; + } + + public Column Column { get; } + public virtual int Index => Column.Index; + public virtual string? Name => Column.Name; + public virtual bool IsAutoGenerated { get; set; } + + public override string ToString() => Column.Name ?? string.Empty; + } + public class BookDocumentJsonCell : BookDocumentCell, IWithJsonElement { public BookDocumentJsonCell(Book.JsonCell cell) @@ -1898,7 +1963,7 @@ public class BookDocumentJsonCellError(Book.JsonCell cell) public class BookDocumentRow { - private readonly IDictionary _cells; + private IDictionary _cells; public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) { @@ -1911,6 +1976,7 @@ public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) Row = row; RowIndex = row.Index; + SortIndex = RowIndex; IsHidden = !row.IsVisible; foreach (var cell in row.EnumerateCells()) { @@ -1918,7 +1984,10 @@ public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) if (bdCell == null) continue; - _cells[cell.ColumnIndex] = CreateCell(cell); + if (!sheet.EnsureColumn(book, cell.ColumnIndex)) + break; + + _cells[cell.ColumnIndex] = bdCell; if (LastCellIndex == null || row.Index > LastCellIndex) { @@ -1938,13 +2007,14 @@ public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) } public Row Row { get; } - public int RowIndex { get; } + public int RowIndex { get; } // orginal index + public virtual int SortIndex { get; set; } // current sorted index public virtual bool IsHidden { get; } public int? FirstCellIndex { get; } public int? LastCellIndex { get; } public IDictionary Cells => _cells; - protected virtual IDictionary CreateCells() => new Dictionary(); + protected virtual internal IDictionary CreateCells() => new Dictionary(); protected virtual BookDocumentCell CreateCell(Cell cell) { ArgumentNullException.ThrowIfNull(cell); @@ -1954,13 +2024,19 @@ protected virtual BookDocumentCell CreateCell(Cell cell) return cell.IsError ? new BookDocumentCellError(cell) : new BookDocumentCell(cell); } + protected virtual internal void ReplaceCells(IDictionary cells) + { + ArgumentNullException.ThrowIfNull(cells); + _cells = cells; + } + public override string ToString() => RowIndex.ToString(); } public class BookDocumentSheet { - private readonly IDictionary _rows; - private readonly IDictionary _columns; + private IDictionary _rows; + private readonly IDictionary _columns; public BookDocumentSheet(BookDocument book, Sheet sheet) { @@ -2012,7 +2088,11 @@ public virtual void Load(BookDocument book, Sheet sheet) foreach (var col in sheet.EnumerateColumns()) { - _columns[col.Index] = col; + var column = CreateColumn(col); + if (column == null) + continue; + + _columns[col.Index] = column; if (LastColumnIndex == null || col.Index > LastColumnIndex) { LastColumnIndex = col.Index; @@ -2023,48 +2103,184 @@ public virtual void Load(BookDocument book, Sheet sheet) FirstColumnIndex = col.Index; } - e = new StateChangedEventArgs(StateChangedType.ColumnAddded, this, null, col); + e = new StateChangedEventArgs(StateChangedType.ColumnAddded, this, null, column); book.OnStateChanged(this, e); if (e.Cancel) break; } + } + + public virtual string Name { get; } + public virtual bool IsHidden { get; } + public int? FirstColumnIndex { get; protected set; } + public int? LastColumnIndex { get; protected set; } + public int? FirstRowIndex { get; protected set; } + public int? LastRowIndex { get; protected set; } + public IDictionary Rows => _rows; + public IDictionary Columns => _columns; + public ListSortDirection? SortDirection { get; protected set; } + public int? SortColumnIndex { get; protected set; } + + public override string ToString() => Name; + + public virtual bool EnsureColumn(BookDocument book, int columnIndex) + { + ArgumentNullException.ThrowIfNull(book); + if (Columns.ContainsKey(columnIndex)) + return true; + + var column = CreateColumn(new Column { Index = columnIndex, Name = Row.GetExcelColumnName(columnIndex) }); + column.IsAutoGenerated = true; + _columns[columnIndex] = column; + if (!LastColumnIndex.HasValue || columnIndex > LastColumnIndex) + { + LastColumnIndex = columnIndex; + } + + if (!FirstColumnIndex.HasValue || columnIndex < FirstColumnIndex) + { + FirstColumnIndex = columnIndex; + } + + var e = new StateChangedEventArgs(StateChangedType.ColumnAddded, this, null, column); + book.OnStateChanged(this, e); + if (e.Cancel) + return false; + + return true; + } + + public virtual void UnsortRows() + { + if (!FirstRowIndex.HasValue || !LastRowIndex.HasValue) + return; + + if (_rows.Count > 0) + { + var rows = new Dictionary(); + foreach (var kv in _rows) + { + rows[kv.Value.RowIndex] = kv.Value; + } + _rows = rows; + } + + SortColumnIndex = null; + SortDirection = null; + } + + public virtual void SortRows(int columnIndex, ListSortDirection direction, Comparison? comparison = null) + { + if (!FirstRowIndex.HasValue || !LastRowIndex.HasValue) + return; + + // determine sortable rows + var rowCells = new List<(int rowIndex, BookDocumentCell? cell)>(); + var rowsNoCell = new List<(int rowIndex, BookDocumentRow row)>(); + foreach (var kv in _rows) + { + if (kv.Value.Cells.TryGetValue(columnIndex, out var cell) && cell.Value != null) + { + rowCells.Add((kv.Key, cell)); + } + else + { + rowsNoCell.Add((kv.Key, kv.Value)); + } + } - if (_columns.Count == 0 && _rows.Count > 0) + if (rowCells.Count > 0) { - ColumnsHaveBeenGenerated = true; - for (var i = 0; i < _rows[0].Cells.Count; i++) + rowCells.Sort((x, y) => + { + var cmp = Compare(x.cell, y.cell, comparison); + if (direction == ListSortDirection.Descending) + return cmp; + + return -cmp; + }); + + + var index = 0; + var missingRows = Math.Max(0, FirstRowIndex.Value + 1 - rowsNoCell.Count - rowCells.Count); + var rows = new Dictionary(); + var nullsFirst = direction == ListSortDirection.Descending; + if (nullsFirst) { - var col = new Column { Index = i }; - _columns[i] = col; - if (!LastColumnIndex.HasValue || col.Index > LastColumnIndex) + index += missingRows; + foreach (var row in rowsNoCell.OrderBy(r => r.rowIndex)) { - LastColumnIndex = col.Index; + rows[index] = row.row; + row.row.SortIndex = index++; } + } - if (!FirstColumnIndex.HasValue || col.Index < FirstColumnIndex) + for (var i = 0; i < rowCells.Count; i++) + { + var row = _rows[rowCells[i].rowIndex]; + rows[index] = row; + row.SortIndex = index++; + } + + if (!nullsFirst) + { + foreach (var row in rowsNoCell.OrderBy(r => r.rowIndex)) { - FirstColumnIndex = col.Index; + rows[index] = row.row; + row.row.SortIndex = index++; } - - e = new StateChangedEventArgs(StateChangedType.ColumnAddded, this, null, col); - book.OnStateChanged(this, e); - if (e.Cancel) - break; + index += missingRows; } + + _rows = rows; } + + SortColumnIndex = columnIndex; + SortDirection = direction; } - public virtual string Name { get; } - public virtual bool IsHidden { get; } - public bool ColumnsHaveBeenGenerated { get; protected set; } - public int? FirstColumnIndex { get; protected set; } - public int? LastColumnIndex { get; protected set; } - public int? FirstRowIndex { get; protected set; } - public int? LastRowIndex { get; protected set; } - public IDictionary Rows => _rows; - public IDictionary Columns => _columns; + protected virtual int Compare(BookDocumentCell? x, BookDocumentCell? y, Comparison? comparison) + { + if (comparison != null) + return comparison(x, y); - public override string ToString() => Name; + var ix = x?.Value as IComparable; + var iy = y?.Value as IComparable; + if (IsNullForComparison(ix)) + { + if (IsNullForComparison(iy)) + return 0; + + return 1; + } + else if (IsNullForComparison(iy)) + return -1; + + var xt = ix!.GetType(); + var yt = iy!.GetType(); + if (xt.IsAssignableFrom(yt) || yt.IsAssignableFrom(xt)) + return ix.CompareTo(iy); + + try + { + return ix.CompareTo((IComparable)Convert.ChangeType(iy, xt)); + } + catch + { + // continue + } + try + { + return iy.CompareTo((IComparable)Convert.ChangeType(ix, yt)); + } + catch + { + // continue + } + return 0; + } + + protected virtual bool IsNullForComparison(object? o) => o is null || Convert.IsDBNull(o); public BookDocumentCell? GetCell(RowCol? rowCol) { @@ -2083,6 +2299,92 @@ public virtual void Load(BookDocument book, Sheet sheet) return cell; } + public virtual bool SwapColumns(int columnIndex1, int columnIndex2) + { + if (columnIndex1 == columnIndex2) + return false; + + if (!FirstColumnIndex.HasValue || !LastColumnIndex.HasValue) + return false; + + // swap columns + if (!Columns.Remove(columnIndex1, out var col1)) + return false; + + if (!Columns.Remove(columnIndex2, out var col2)) + return false; + + Columns.Add(columnIndex1, col2); + Columns.Add(columnIndex2, col1); + + // recompute columns indices + var indices = new int[LastColumnIndex.Value - FirstColumnIndex.Value + 1]; + var idx = 0; + if (columnIndex1 > columnIndex2) + { + for (var i = FirstColumnIndex.Value; i <= LastColumnIndex.Value; i++) + { + if (i == columnIndex1) + { + // skip + continue; + } + + if (i == columnIndex2) + { + // insert + indices[idx++] = columnIndex1; + if (idx == indices.Length) + break; + } + + indices[idx++] = i; + if (idx == indices.Length) + break; + } + } + else + { + for (var i = FirstColumnIndex.Value; i <= LastColumnIndex.Value; i++) + { + if (i == columnIndex1) + { + // skip + continue; + } + + indices[idx++] = i; + if (idx == indices.Length) + break; + + if (i == columnIndex2) + { + indices[idx++] = columnIndex1; + if (idx == indices.Length) + break; + } + } + } + + // rebuild all rows cells in new order + foreach (var kv in Rows) + { + var cells = kv.Value.CreateCells(); + var idx2 = 0; + foreach (var i in indices) + { + if (kv.Value.Cells.TryGetValue(i, out var cell)) + { + cells[idx2] = cell; + } + idx2++; + } + + kv.Value.ReplaceCells(cells); + } + return true; + } + public virtual string? FormatValue(object? value) { if (value is null) @@ -2139,7 +2441,8 @@ public virtual void Load(BookDocument book, Sheet sheet) } protected virtual BookDocumentRow CreateRow(BookDocument book, Row row) => new(book, this, row); - protected virtual IDictionary CreateColumns() => new Dictionary(); + protected virtual BookDocumentColumn CreateColumn(Column column) => new(column); + protected virtual IDictionary CreateColumns() => new Dictionary(); protected virtual IDictionary CreateRows() => new Dictionary(); } @@ -2707,13 +3010,13 @@ public class StateChangedEventArgs( StateChangedType type, BookDocumentSheet sheet, BookDocumentRow? row = null, - Column? column = null, + BookDocumentColumn? column = null, BookDocumentCell? cell = null) : CancelEventArgs { public StateChangedType Type { get; } = type; public BookDocumentSheet Sheet { get; } = sheet; public BookDocumentRow? Row { get; } = row; - public Column? Column { get; } = column; + public BookDocumentColumn? Column { get; } = column; public BookDocumentCell? Cell { get; } = cell; } diff --git a/SheetReader.Test/x.json b/SheetReader.Test/x.json deleted file mode 100644 index 9b21c85..0000000 --- a/SheetReader.Test/x.json +++ /dev/null @@ -1,2855 +0,0 @@ -{ - "sheets": [ - { - "name": "PdG", - "columns": [ - null, - null, - null, - null, - null, - null, - null, - null - ], - "cells": [ - { - "r": 0, - "c": 0, - "value": "DIRECTION INGENIERIE ET AMENAGEMENT\nDIRECTION MAITRISE D\u0027\u0152UVRE ET ARCHITECTURE\nMa\u00EEtrise d\u0027\u0152uvre Infrastructure CDG\n" - }, - { - "r": 4, - "c": 0, - "value": "AFFAIRES N\u00B0 141 668 \u2013 Paris \u2013Charles de Gaulle" - }, - { - "r": 6, - "c": 0, - "value": "TERRASSEMENT ET RADIER" - }, - { - "r": 8, - "c": 0, - "value": "PHASE PRO - LOT 17T" - }, - { - "r": 12, - "c": 0, - "value": "DPGF" - }, - { - "r": 15, - "c": 0, - "value": "Indice" - }, - { - "r": 15, - "c": 2, - "value": "Date" - }, - { - "r": 15, - "c": 4, - "value": "Nature des modifications" - }, - { - "r": 16, - "c": 0, - "value": "A" - }, - { - "r": 16, - "c": 2, - "value": "11/01/2017 00:00:00" - }, - { - "r": 16, - "c": 4, - "value": "Emission initiale" - }, - { - "r": 17, - "c": 0, - "value": "B" - }, - { - "r": 17, - "c": 2, - "value": "11/15/2017 00:00:00" - }, - { - "r": 17, - "c": 4, - "value": "Revue de dossier MOE" - }, - { - "r": 18, - "c": 0, - "value": "C" - }, - { - "r": 18, - "c": 2, - "value": "01/08/2018 00:00:00" - }, - { - "r": 18, - "c": 4, - "value": "Revue de dossier MOE n\u00B02" - }, - { - "r": 19, - "c": 0, - "value": "D" - }, - { - "r": 19, - "c": 2, - "value": "05/11/2018 00:00:00" - }, - { - "r": 19, - "c": 4, - "value": "Revue AMO \u002B retour Offre 1" - }, - { - "r": 20, - "c": 0, - "value": "E" - }, - { - "r": 20, - "c": 2, - "value": "06/14/2018 00:00:00" - }, - { - "r": 20, - "c": 4, - "value": "Retour Offre 2" - }, - { - "r": 23, - "c": 0, - "value": "Ma\u00EEtre d\u0027Ouvrage (MOA)" - }, - { - "r": 23, - "c": 2, - "value": ":" - }, - { - "r": 23, - "c": 3, - "value": "CDG \u2013 F. GOLDNADEL" - }, - { - "r": 24, - "c": 0, - "value": "Ma\u00EEtrise d\u0027Ouvrage d\u00E9l\u00E9gu\u00E9 (MOD)" - }, - { - "r": 24, - "c": 2, - "value": ":" - }, - { - "r": 24, - "c": 3, - "value": "DIAP1 \u2013 ML. KEPEKLIAN" - }, - { - "r": 25, - "c": 0, - "value": "Ma\u00EEtrise d\u0027\u0152uvre (MOE)" - }, - { - "r": 25, - "c": 2, - "value": ":" - }, - { - "r": 25, - "c": 3, - "value": "DIAMI \u2013 T. CAMPIN" - } - ] - }, - { - "name": "G\u00E9nie civil", - "columns": [ - null, - null, - null, - null, - null, - null - ], - "cells": [ - { - "r": 1, - "c": 1, - "value": "D\u00E9signation des ouvrages" - }, - { - "r": 1, - "c": 2, - "value": "Unit\u00E9" - }, - { - "r": 1, - "c": 3, - "value": "Quantit\u00E9" - }, - { - "r": 1, - "c": 4, - "value": "Prix d\u0027unit\u00E9 (en \u20AC HT)" - }, - { - "r": 1, - "c": 5, - "value": "Prix total (en \u20AC HT)" - }, - { - "r": 2, - "c": 0, - "value": "1" - }, - { - "r": 2, - "c": 1, - "value": "GENIE CIVIL" - }, - { - "r": 3, - "c": 0, - "value": "1.1" - }, - { - "r": 3, - "c": 1, - "value": "INSTALLATION DE CHANTIER / ETUDES" - }, - { - "r": 3, - "c": 5, - "value": "0" - }, - { - "r": 4, - "c": 0, - "value": "1.1.1" - }, - { - "r": 4, - "c": 1, - "value": "Installation de chantier" - }, - { - "r": 4, - "c": 2, - "value": "ens" - }, - { - "r": 4, - "c": 3, - "value": "1" - }, - { - "r": 4, - "c": 5, - "value": "0" - }, - { - "r": 5, - "c": 0, - "value": "1.1.2" - }, - { - "r": 5, - "c": 1, - "value": "Etudes" - }, - { - "r": 6, - "c": 0, - "value": "1.1.2.1" - }, - { - "r": 6, - "c": 1, - "value": "Etudes d\u0027\u00E9x\u00E9cution" - }, - { - "r": 6, - "c": 2, - "value": "ens" - }, - { - "r": 6, - "c": 3, - "value": "1" - }, - { - "r": 6, - "c": 5, - "value": "0" - }, - { - "r": 7, - "c": 0, - "value": "1.1.2.2" - }, - { - "r": 7, - "c": 1, - "value": "tenues au feu" - }, - { - "r": 7, - "c": 2, - "value": "ens" - }, - { - "r": 7, - "c": 3, - "value": "1" - }, - { - "r": 7, - "c": 5, - "value": "0" - }, - { - "r": 8, - "c": 0, - "value": "1.1.3" - }, - { - "r": 8, - "c": 1, - "value": "Mat\u00E9riel de ventilation de chantier" - }, - { - "r": 8, - "c": 2, - "value": "ens" - }, - { - "r": 8, - "c": 3, - "value": "1" - }, - { - "r": 8, - "c": 5, - "value": "0" - }, - { - "r": 9, - "c": 0, - "value": "1.1.4" - }, - { - "r": 9, - "c": 1, - "value": "Mat\u00E9riel d\u0027\u00E9clairage de chantier" - }, - { - "r": 9, - "c": 2, - "value": "ens" - }, - { - "r": 9, - "c": 3, - "value": "1" - }, - { - "r": 9, - "c": 5, - "value": "0" - }, - { - "r": 10, - "c": 0, - "value": "1.1.5" - }, - { - "r": 10, - "c": 1, - "value": "Mat\u00E9riel de lavage de chantier" - }, - { - "r": 10, - "c": 2, - "value": "ens" - }, - { - "r": 10, - "c": 3, - "value": "1" - }, - { - "r": 10, - "c": 5, - "value": "0" - }, - { - "r": 11, - "c": 0, - "value": "1.1.6" - }, - { - "r": 11, - "c": 1, - "value": "Caisson de secours" - }, - { - "r": 11, - "c": 2, - "value": "ens" - }, - { - "r": 11, - "c": 3, - "value": "1" - }, - { - "r": 11, - "c": 5, - "value": "0" - }, - { - "r": 12, - "c": 0, - "value": "1.1.7" - }, - { - "r": 12, - "c": 1, - "value": "Agent de s\u00E9curit\u00E9 en entr\u00E9e de tunnel, y compris gu\u00E9rite\u0022" - }, - { - "r": 12, - "c": 2, - "value": "ens" - }, - { - "r": 12, - "c": 3, - "value": "1" - }, - { - "r": 12, - "c": 5, - "value": "0" - }, - { - "r": 13, - "c": 0, - "value": "1.2" - }, - { - "r": 13, - "c": 1, - "value": "TERRASSEMENT EN SOUS \u0152UVRE" - }, - { - "r": 13, - "c": 5, - "value": "0" - }, - { - "r": 14, - "c": 0, - "value": "1.2.1" - }, - { - "r": 14, - "c": 1, - "value": "D\u00E9blais tron\u00E7on Sud" - }, - { - "r": 14, - "c": 2, - "value": "m3" - }, - { - "r": 14, - "c": 3, - "value": "28310" - }, - { - "r": 14, - "c": 5, - "value": "0" - }, - { - "r": 15, - "c": 0, - "value": "1.2.2" - }, - { - "r": 15, - "c": 1, - "value": "D\u00E9blais s\u00E9quentiels tron\u00E7on Nord" - }, - { - "r": 15, - "c": 2, - "value": "m3" - }, - { - "r": 15, - "c": 3, - "value": "19880" - }, - { - "r": 15, - "c": 5, - "value": "0" - }, - { - "r": 16, - "c": 0, - "value": "1.2.3" - }, - { - "r": 16, - "c": 1, - "value": "B\u00E9ton CM de propret\u00E9 tron\u00E7on Sud" - }, - { - "r": 16, - "c": 2, - "value": "m3" - }, - { - "r": 16, - "c": 3, - "value": "375" - }, - { - "r": 16, - "c": 5, - "value": "0" - }, - { - "r": 17, - "c": 0, - "value": "1.2.4" - }, - { - "r": 17, - "c": 1, - "value": "B\u00E9ton CM de propret\u00E9 s\u00E9quentiel tron\u00E7on Nord" - }, - { - "r": 17, - "c": 2, - "value": "m3" - }, - { - "r": 17, - "c": 3, - "value": "272" - }, - { - "r": 17, - "c": 5, - "value": "0" - }, - { - "r": 18, - "c": 0, - "value": "1.3" - }, - { - "r": 18, - "c": 1, - "value": "TRAVAUX PREPARATOIRES" - }, - { - "r": 18, - "c": 5, - "value": "0" - }, - { - "r": 19, - "c": 0, - "value": "1.3.1" - }, - { - "r": 19, - "c": 1, - "value": "D\u00E9tection scanner et d\u00E9gagement coupleurs Tron\u00E7on Sud ou d\u00E9tection armatures existantes" - }, - { - "r": 19, - "c": 2, - "value": "ml" - }, - { - "r": 19, - "c": 3, - "value": "706" - }, - { - "r": 19, - "c": 5, - "value": "0" - }, - { - "r": 20, - "c": 0, - "value": "1.3.2" - }, - { - "r": 20, - "c": 1, - "value": "Detection scanner et scellement des attentes troncon Nord" - }, - { - "r": 20, - "c": 2, - "value": "ml" - }, - { - "r": 20, - "c": 3, - "value": "524" - }, - { - "r": 20, - "c": 5, - "value": "0" - }, - { - "r": 21, - "c": 0, - "value": "1.3.3" - }, - { - "r": 21, - "c": 1, - "value": "Rabotage paroi moul\u00E9e (parement vu) tron\u00E7on Nord" - }, - { - "r": 21, - "c": 2, - "value": "m2" - }, - { - "r": 21, - "c": 3, - "value": "4020" - }, - { - "r": 21, - "c": 5, - "value": "0" - }, - { - "r": 22, - "c": 0, - "value": "1.3.4" - }, - { - "r": 22, - "c": 1, - "value": "Rabotage paroi moul\u00E9e (parement vu) tron\u00E7on Sud" - }, - { - "r": 22, - "c": 2, - "value": "m2" - }, - { - "r": 22, - "c": 3, - "value": "5400" - }, - { - "r": 22, - "c": 5, - "value": "0" - }, - { - "r": 23, - "c": 0, - "value": "1.3.5" - }, - { - "r": 23, - "c": 1, - "value": "Prospection des parois tron\u00E7on Nord" - }, - { - "r": 23, - "c": 2, - "value": "m2" - }, - { - "r": 23, - "c": 3, - "value": "3500" - }, - { - "r": 23, - "c": 5, - "value": "0" - }, - { - "r": 24, - "c": 0, - "value": "1.3.6" - }, - { - "r": 24, - "c": 1, - "value": "Relev\u00E9 3D du tunnel /ml de tunnel" - }, - { - "r": 24, - "c": 2, - "value": "ml" - }, - { - "r": 24, - "c": 3, - "value": "615" - }, - { - "r": 24, - "c": 5, - "value": "0" - }, - { - "r": 25, - "c": 0, - "value": "1.3.7" - }, - { - "r": 25, - "c": 1, - "value": "Op\u00E9rations de lev\u00E9s topo pour contr\u00F4le de non d\u00E9placement de l\u0027ouvrage" - }, - { - "r": 25, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 25, - "c": 3, - "value": "18000" - }, - { - "r": 25, - "c": 5, - "value": "0" - }, - { - "r": 26, - "c": 0, - "value": "1.4" - }, - { - "r": 26, - "c": 1, - "value": "RADIERS" - }, - { - "r": 26, - "c": 5, - "value": "0" - }, - { - "r": 27, - "c": 0, - "value": "1.4.1" - }, - { - "r": 27, - "c": 1, - "value": "Armatures radier Sud" - }, - { - "r": 27, - "c": 2, - "value": "kg" - }, - { - "r": 27, - "c": 3, - "value": "665560" - }, - { - "r": 27, - "c": 5, - "value": "0" - }, - { - "r": 28, - "c": 0, - "value": "1.4.2" - }, - { - "r": 28, - "c": 1, - "value": "Armatures radier Nord" - }, - { - "r": 28, - "c": 2, - "value": "kg" - }, - { - "r": 28, - "c": 3, - "value": "530650" - }, - { - "r": 28, - "c": 5, - "value": "0" - }, - { - "r": 29, - "c": 0, - "value": "1.4.3" - }, - { - "r": 29, - "c": 1, - "value": "B\u00E9ton EGA (C40/50) radier Sud" - }, - { - "r": 29, - "c": 2, - "value": "m3" - }, - { - "r": 29, - "c": 3, - "value": "3170" - }, - { - "r": 29, - "c": 5, - "value": "0" - }, - { - "r": 30, - "c": 0, - "value": "1.4.4" - }, - { - "r": 30, - "c": 1, - "value": "B\u00E9ton EGA (C60/75) radier Nord" - }, - { - "r": 30, - "c": 2, - "value": "m3" - }, - { - "r": 30, - "c": 3, - "value": "2445" - }, - { - "r": 30, - "c": 5, - "value": "0" - }, - { - "r": 31, - "c": 0, - "value": "1.4.5" - }, - { - "r": 31, - "c": 1, - "value": "Scellement attentes complementaires troncon Sud" - }, - { - "r": 31, - "c": 2, - "value": "ml" - }, - { - "r": 31, - "c": 3, - "value": "706" - }, - { - "r": 31, - "c": 5, - "value": "0" - }, - { - "r": 32, - "c": 0, - "value": "1.4.6" - }, - { - "r": 32, - "c": 1, - "value": "Mise \u00E0 la terre :" - }, - { - "r": 32, - "c": 5, - "value": "0" - }, - { - "r": 33, - "c": 0, - "value": "1.4.6.1" - }, - { - "r": 33, - "c": 1, - "value": "Barre collectrice longitudinale jonction les armatures du radiers " - }, - { - "r": 33, - "c": 2, - "value": "Ft" - }, - { - "r": 33, - "c": 3, - "value": "1" - }, - { - "r": 33, - "c": 5, - "value": "0" - }, - { - "r": 34, - "c": 0, - "value": "1.4.6.2" - }, - { - "r": 34, - "c": 1, - "value": "Troncon 2018 - piochage et raccord des aciers des paroi moul\u00E9es avec aciers du radiers (2 par plot de dalle soit 10u) " - }, - { - "r": 34, - "c": 2, - "value": "Ft" - }, - { - "r": 34, - "c": 3, - "value": "1" - }, - { - "r": 34, - "c": 5, - "value": "0" - }, - { - "r": 35, - "c": 0, - "value": "1.4.6.3" - }, - { - "r": 35, - "c": 1, - "value": "Troncon 2016 et 1991 - piochage et raccord des aciers des paroi moul\u00E9es (Un de chaque cot\u00E9 tout les 10m) " - }, - { - "r": 35, - "c": 2, - "value": "Ft" - }, - { - "r": 35, - "c": 3, - "value": "1" - }, - { - "r": 35, - "c": 5, - "value": "0" - }, - { - "r": 36, - "c": 0, - "value": "1.4.6.4" - }, - { - "r": 36, - "c": 1, - "value": "Troncon 2016 et 1991 - piochage et raccord des aciers de dalle de couverture (Un de chaque cot\u00E9 tout les 30m)" - }, - { - "r": 36, - "c": 2, - "value": "Ft" - }, - { - "r": 36, - "c": 3, - "value": "1" - }, - { - "r": 36, - "c": 5, - "value": "0" - }, - { - "r": 37, - "c": 0, - "value": "1.4.6.5" - }, - { - "r": 37, - "c": 1, - "value": "mise \u00E0 disposition de tresse pour raccordement de la terre SNCF" - }, - { - "r": 37, - "c": 2, - "value": "Ft" - }, - { - "r": 37, - "c": 3, - "value": "1" - }, - { - "r": 37, - "c": 5, - "value": "0" - }, - { - "r": 38, - "c": 0, - "value": "1.4.7" - }, - { - "r": 38, - "c": 1, - "value": "travers\u00E9es de voies dans radier (4\u00D8110)" - }, - { - "r": 38, - "c": 2, - "value": "Ft" - }, - { - "r": 38, - "c": 3, - "value": "1" - }, - { - "r": 38, - "c": 5, - "value": "0" - }, - { - "r": 39, - "c": 0, - "value": "1.5" - }, - { - "r": 39, - "c": 1, - "value": "NICHES POMPIERS" - }, - { - "r": 39, - "c": 5, - "value": "0" - }, - { - "r": 40, - "c": 0, - "value": "1.5.1" - }, - { - "r": 40, - "c": 1, - "value": "Hydro d\u00E9molition" - }, - { - "r": 40, - "c": 2, - "value": "m3" - }, - { - "r": 40, - "c": 3, - "value": "6" - }, - { - "r": 40, - "c": 5, - "value": "0" - }, - { - "r": 41, - "c": 0, - "value": "1.5.2" - }, - { - "r": 41, - "c": 1, - "value": "Scellements" - }, - { - "r": 41, - "c": 2, - "value": "ff" - }, - { - "r": 41, - "c": 3, - "value": "1" - }, - { - "r": 41, - "c": 5, - "value": "0" - }, - { - "r": 42, - "c": 0, - "value": "1.5.3" - }, - { - "r": 42, - "c": 1, - "value": "Armatures" - }, - { - "r": 42, - "c": 2, - "value": "kg" - }, - { - "r": 42, - "c": 3, - "value": "360" - }, - { - "r": 42, - "c": 5, - "value": "0" - }, - { - "r": 43, - "c": 0, - "value": "1.5.4" - }, - { - "r": 43, - "c": 1, - "value": "B\u00E9ton projet\u00E9 QGA (30/40) ep10cm" - }, - { - "r": 43, - "c": 2, - "value": "m3" - }, - { - "r": 43, - "c": 3, - "value": "2" - }, - { - "r": 43, - "c": 5, - "value": "0" - }, - { - "r": 44, - "c": 0, - "value": "1.5.5" - }, - { - "r": 44, - "c": 1, - "value": "Talochage " - }, - { - "r": 44, - "c": 2, - "value": "m2" - }, - { - "r": 44, - "c": 3, - "value": "17" - }, - { - "r": 44, - "c": 5, - "value": "0" - }, - { - "r": 45, - "c": 0, - "value": "1.6" - }, - { - "r": 45, - "c": 1, - "value": "PROTECTION AU FEU-TRONCON NORD" - }, - { - "r": 45, - "c": 5, - "value": "0" - }, - { - "r": 46, - "c": 0, - "value": "1.6.1" - }, - { - "r": 46, - "c": 1, - "value": "Scellements armatures voiles" - }, - { - "r": 46, - "c": 2, - "value": "unit\u00E9" - }, - { - "r": 46, - "c": 3, - "value": "28400" - }, - { - "r": 46, - "c": 5, - "value": "0" - }, - { - "r": 47, - "c": 0, - "value": "1.6.2" - }, - { - "r": 47, - "c": 1, - "value": "Scellements armatures dalle" - }, - { - "r": 47, - "c": 2, - "value": "unit\u00E9" - }, - { - "r": 47, - "c": 3, - "value": "21800" - }, - { - "r": 47, - "c": 5, - "value": "0" - }, - { - "r": 48, - "c": 0, - "value": "1.6.3" - }, - { - "r": 48, - "c": 1, - "value": "B\u00E9ton projet\u00E9 EGA (30/40) ep5cm" - }, - { - "r": 48, - "c": 2, - "value": "m3" - }, - { - "r": 48, - "c": 3, - "value": "314" - }, - { - "r": 48, - "c": 5, - "value": "0" - }, - { - "r": 49, - "c": 0, - "value": "1.6.4" - }, - { - "r": 49, - "c": 1, - "value": "Talochage vertical parois " - }, - { - "r": 49, - "c": 2, - "value": "m2" - }, - { - "r": 49, - "c": 3, - "value": "3550" - }, - { - "r": 49, - "c": 5, - "value": "0" - }, - { - "r": 50, - "c": 0, - "value": "1.6.5" - }, - { - "r": 50, - "c": 1, - "value": "Talochage horizontal dalle" - }, - { - "r": 50, - "c": 2, - "value": "m2" - }, - { - "r": 50, - "c": 3, - "value": "2720" - }, - { - "r": 50, - "c": 5, - "value": "0" - }, - { - "r": 51, - "c": 0, - "value": "1.7" - }, - { - "r": 51, - "c": 1, - "value": "EQUIPEMENTS" - }, - { - "r": 51, - "c": 5, - "value": "0" - }, - { - "r": 52, - "c": 0, - "value": "1.7.1" - }, - { - "r": 52, - "c": 1, - "value": "Caniveaux drainants yc raccord" - }, - { - "r": 52, - "c": 2, - "value": "ml" - }, - { - "r": 52, - "c": 3, - "value": "1230" - }, - { - "r": 52, - "c": 5, - "value": "0" - }, - { - "r": 53, - "c": 0, - "value": "1.7.2" - }, - { - "r": 53, - "c": 1, - "value": "Joint hydrogonflant " - }, - { - "r": 53, - "c": 2, - "value": "ml" - }, - { - "r": 53, - "c": 3, - "value": "1230" - }, - { - "r": 53, - "c": 5, - "value": "0" - }, - { - "r": 54, - "c": 0, - "value": "1.7.3" - }, - { - "r": 54, - "c": 1, - "value": "Joint Waterstop en applique " - }, - { - "r": 54, - "c": 2, - "value": "ml" - }, - { - "r": 54, - "c": 3, - "value": "105" - }, - { - "r": 54, - "c": 5, - "value": "0" - }, - { - "r": 55, - "c": 0, - "value": "1.8" - }, - { - "r": 55, - "c": 1, - "value": "PERRE SOUS PONT R26" - }, - { - "r": 55, - "c": 5, - "value": "0" - }, - { - "r": 56, - "c": 0, - "value": "1.8.1" - }, - { - "r": 56, - "c": 1, - "value": "D\u00E9molition perr\u00E9 existant (yc \u00E9vacuation en d\u00E9charge publique)" - }, - { - "r": 56, - "c": 2, - "value": "m3" - }, - { - "r": 56, - "c": 3, - "value": "120" - }, - { - "r": 56, - "c": 5, - "value": "0" - }, - { - "r": 57, - "c": 0, - "value": "1.8.2" - }, - { - "r": 57, - "c": 1, - "value": "D\u00E9blais (yc \u00E9vacuation en d\u00E9charge publique)" - }, - { - "r": 57, - "c": 2, - "value": "m3" - }, - { - "r": 57, - "c": 3, - "value": "1050" - }, - { - "r": 57, - "c": 5, - "value": "0" - }, - { - "r": 58, - "c": 0, - "value": "1.8.3" - }, - { - "r": 58, - "c": 1, - "value": "Remblais grave naturelle 0/31,5" - }, - { - "r": 58, - "c": 2, - "value": "m3" - }, - { - "r": 58, - "c": 3, - "value": "100" - }, - { - "r": 58, - "c": 5, - "value": "0" - }, - { - "r": 59, - "c": 0, - "value": "1.8.4" - }, - { - "r": 59, - "c": 1, - "value": "Coffrage soign\u00E9 b\u00EAches p\u00E9riph\u00E9riques" - }, - { - "r": 59, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 59, - "c": 3, - "value": "150" - }, - { - "r": 59, - "c": 5, - "value": "0" - }, - { - "r": 60, - "c": 0, - "value": "1.8.5" - }, - { - "r": 60, - "c": 1, - "value": "B\u00E9ton de propret\u00E9 (e=5cm)" - }, - { - "r": 60, - "c": 2, - "value": "m3" - }, - { - "r": 60, - "c": 3, - "value": "37.5" - }, - { - "r": 60, - "c": 5, - "value": "0" - }, - { - "r": 61, - "c": 0, - "value": "1.8.6" - }, - { - "r": 61, - "c": 1, - "value": "B\u00E9ton projet\u00E9 liss\u00E9" - }, - { - "r": 61, - "c": 2, - "value": "m3" - }, - { - "r": 61, - "c": 3, - "value": "91" - }, - { - "r": 61, - "c": 5, - "value": "0" - }, - { - "r": 62, - "c": 0, - "value": "1.8.7" - }, - { - "r": 62, - "c": 1, - "value": "armatures" - }, - { - "r": 62, - "c": 2, - "value": "kg" - }, - { - "r": 62, - "c": 3, - "value": "8000" - }, - { - "r": 62, - "c": 5, - "value": "0" - }, - { - "r": 63, - "c": 0, - "value": "1.8.8" - }, - { - "r": 63, - "c": 1, - "value": "Lissage et engravures / m\u00B2 de perr\u00E9" - }, - { - "r": 63, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 63, - "c": 3, - "value": "360" - }, - { - "r": 63, - "c": 5, - "value": "0" - }, - { - "r": 64, - "c": 1, - "value": "MONTANT TOTAL DES TRAVAUX" - }, - { - "r": 64, - "c": 5, - "value": "0" - }, - { - "r": 66, - "c": 1, - "value": "_" - } - ] - }, - { - "name": "Infra", - "columns": [ - null, - null, - null, - null, - null, - null, - null - ], - "cells": [ - { - "r": 0, - "c": 0, - "value": "ESTIMATION - LOT 17T - Sous Lot 2 - Infrastructures" - }, - { - "r": 1, - "c": 1, - "value": "D\u00E9signation des ouvrages" - }, - { - "r": 1, - "c": 2, - "value": "Unit\u00E9" - }, - { - "r": 1, - "c": 3, - "value": "Quantit\u00E9" - }, - { - "r": 1, - "c": 4, - "value": "Prix d\u0027unit\u00E9 (en \u20AC HT)" - }, - { - "r": 1, - "c": 5, - "value": "Prix total (en \u20AC HT)" - }, - { - "r": 2, - "c": 0, - "value": "2" - }, - { - "r": 2, - "c": 1, - "value": "INFRASTRUCTURES" - }, - { - "r": 3, - "c": 0, - "value": "2.1" - }, - { - "r": 3, - "c": 1, - "value": "INSTALLATIONS GENERALES DE CHANTIER" - }, - { - "r": 4, - "c": 0, - "value": "2.1.1" - }, - { - "r": 4, - "c": 1, - "value": "Installations de chantier" - }, - { - "r": 4, - "c": 2, - "value": "Voir sous-lot 1" - }, - { - "r": 5, - "c": 0, - "value": "2.1.2" - }, - { - "r": 5, - "c": 1, - "value": "Etudes dossiers d\u0027ex\u00E9cution et de r\u00E9colement" - }, - { - "r": 5, - "c": 2, - "value": "Forfait" - }, - { - "r": 5, - "c": 3, - "value": "1" - }, - { - "r": 5, - "c": 5, - "value": "0" - }, - { - "r": 6, - "c": 0, - "value": "2.1.3" - }, - { - "r": 6, - "c": 1, - "value": "Exploitation sous chantier (Cloture, GBA \u2026)" - }, - { - "r": 6, - "c": 2, - "value": "Forfait" - }, - { - "r": 6, - "c": 3, - "value": "1" - }, - { - "r": 6, - "c": 5, - "value": "0" - }, - { - "r": 7, - "c": 0, - "value": "2.1.4" - }, - { - "r": 7, - "c": 1, - "value": "Propret\u00E9 du chantier et des voiries" - }, - { - "r": 7, - "c": 2, - "value": "Forfait" - }, - { - "r": 7, - "c": 3, - "value": "1" - }, - { - "r": 7, - "c": 5, - "value": "0" - }, - { - "r": 8, - "c": 1, - "value": "sous total 1.1" - }, - { - "r": 8, - "c": 5, - "value": "0" - }, - { - "r": 10, - "c": 0, - "value": "2.2" - }, - { - "r": 10, - "c": 1, - "value": "TRAVAUX PREPARATOIRES" - }, - { - "r": 11, - "c": 0, - "value": "2.2.1" - }, - { - "r": 11, - "c": 1, - "value": "Piquetage des r\u00E9seaux" - }, - { - "r": 11, - "c": 2, - "value": "F" - }, - { - "r": 11, - "c": 3, - "value": "1" - }, - { - "r": 11, - "c": 5, - "value": "0" - }, - { - "r": 12, - "c": 0, - "value": "2.2.2" - }, - { - "r": 12, - "c": 1, - "value": "D\u00E9molition de chauss\u00E9e souple" - }, - { - "r": 12, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 12, - "c": 3, - "value": "500" - }, - { - "r": 12, - "c": 5, - "value": "0" - }, - { - "r": 13, - "c": 0, - "value": "2.2.3" - }, - { - "r": 13, - "c": 1, - "value": "D\u00E9molition diverses b\u00E9ton arm\u00E9 ou non" - }, - { - "r": 13, - "c": 2, - "value": "m3" - }, - { - "r": 13, - "c": 3, - "value": "5" - }, - { - "r": 13, - "c": 5, - "value": "0" - }, - { - "r": 14, - "c": 0, - "value": "2.2.4" - }, - { - "r": 14, - "c": 1, - "value": "D\u00E9pose de panneau" - }, - { - "r": 14, - "c": 2, - "value": "U" - }, - { - "r": 14, - "c": 3, - "value": "10" - }, - { - "r": 14, - "c": 5, - "value": "0" - }, - { - "r": 15, - "c": 0, - "value": "2.2.5" - }, - { - "r": 15, - "c": 1, - "value": "d\u00E9pose de glissiere m\u00E9tallique" - }, - { - "r": 15, - "c": 2, - "value": "ml" - }, - { - "r": 15, - "c": 3, - "value": "35" - }, - { - "r": 15, - "c": 5, - "value": "0" - }, - { - "r": 16, - "c": 1, - "value": "sous total 1.2" - }, - { - "r": 16, - "c": 5, - "value": "0" - }, - { - "r": 18, - "c": 0, - "value": "2.3" - }, - { - "r": 18, - "c": 1, - "value": "TERRASSEMENTS" - }, - { - "r": 19, - "c": 0, - "value": "2.3.1" - }, - { - "r": 19, - "c": 1, - "value": "Decapage terre v\u00E9g\u00E9tale, mise en stock et remise en place" - }, - { - "r": 19, - "c": 2, - "value": "m3" - }, - { - "r": 19, - "c": 3, - "value": "900" - }, - { - "r": 19, - "c": 5, - "value": "0" - }, - { - "r": 20, - "c": 0, - "value": "2.3.2" - }, - { - "r": 20, - "c": 1, - "value": "D\u00E9blais mis en d\u00E9charge" - }, - { - "r": 20, - "c": 2, - "value": "m3" - }, - { - "r": 20, - "c": 3, - "value": "18879" - }, - { - "r": 20, - "c": 5, - "value": "0" - }, - { - "r": 21, - "c": 0, - "value": "2.3.3" - }, - { - "r": 21, - "c": 1, - "value": "Terrassements en tranch\u00E9e avec ou sans blindage" - }, - { - "r": 21, - "c": 2, - "value": "m3" - }, - { - "r": 21, - "c": 3, - "value": "200" - }, - { - "r": 21, - "c": 5, - "value": "0" - }, - { - "r": 22, - "c": 0, - "value": "2.3.4" - }, - { - "r": 22, - "c": 1, - "value": "Traitement arase chaux (objectif 30 Mpa)" - }, - { - "r": 22, - "c": 2, - "value": "m2" - }, - { - "r": 22, - "c": 3, - "value": "4000" - }, - { - "r": 22, - "c": 5, - "value": "0" - }, - { - "r": 23, - "c": 0, - "value": "2.3.5" - }, - { - "r": 23, - "c": 1, - "value": "GNT" - }, - { - "r": 23, - "c": 2, - "value": "m3" - }, - { - "r": 23, - "c": 3, - "value": "3800" - }, - { - "r": 23, - "c": 5, - "value": "0" - }, - { - "r": 24, - "c": 0, - "value": "2.3.6" - }, - { - "r": 24, - "c": 1, - "value": "Protection couche de forme et sous couche" - }, - { - "r": 24, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 24, - "c": 3, - "value": "8000" - }, - { - "r": 24, - "c": 5, - "value": "0" - }, - { - "r": 25, - "c": 0, - "value": "2.3.7" - }, - { - "r": 25, - "c": 1, - "value": "Geotextile" - }, - { - "r": 25, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 25, - "c": 3, - "value": "5400" - }, - { - "r": 25, - "c": 5, - "value": "0" - }, - { - "r": 26, - "c": 0, - "value": "2.3.8" - }, - { - "r": 26, - "c": 1, - "value": "Engazonnement" - }, - { - "r": 26, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 26, - "c": 3, - "value": "4400" - }, - { - "r": 26, - "c": 5, - "value": "0" - }, - { - "r": 27, - "c": 0, - "value": "2.3.9" - }, - { - "r": 27, - "c": 1, - "value": "R\u00E9alisation de la transition : Radier/plateforme souple" - }, - { - "r": 27, - "c": 2, - "value": "F" - }, - { - "r": 27, - "c": 3, - "value": "1" - }, - { - "r": 27, - "c": 5, - "value": "0" - }, - { - "r": 28, - "c": 1, - "value": "sous total 1.3" - }, - { - "r": 28, - "c": 5, - "value": "0" - }, - { - "r": 30, - "c": 0, - "value": "2.4" - }, - { - "r": 30, - "c": 1, - "value": "COUCHES DE CHAUSSEES" - }, - { - "r": 31, - "c": 0, - "value": "2.4.1" - }, - { - "r": 31, - "c": 1, - "value": "BBSG \u00E9paisseur 6cm" - }, - { - "r": 31, - "c": 2, - "value": "m\u00B2" - }, - { - "r": 31, - "c": 3, - "value": "400" - }, - { - "r": 31, - "c": 5, - "value": "0" - }, - { - "r": 32, - "c": 1, - "value": "sous total 1.4" - }, - { - "r": 32, - "c": 5, - "value": "0" - }, - { - "r": 34, - "c": 0, - "value": "2.5" - }, - { - "r": 34, - "c": 1, - "value": "GC ELEC" - }, - { - "r": 35, - "c": 0, - "value": "2.5.1" - }, - { - "r": 35, - "c": 1, - "value": "Blocs multitubulaires 4\u00D8150\u002B4\u00D880" - }, - { - "r": 35, - "c": 2, - "value": "ml" - }, - { - "r": 35, - "c": 3, - "value": "100" - }, - { - "r": 35, - "c": 5, - "value": "0" - }, - { - "r": 36, - "c": 0, - "value": "2.5.2" - }, - { - "r": 36, - "c": 1, - "value": "Regard EP120" - }, - { - "r": 36, - "c": 2, - "value": "U" - }, - { - "r": 36, - "c": 3, - "value": "4" - }, - { - "r": 36, - "c": 5, - "value": "0" - }, - { - "r": 37, - "c": 0, - "value": "2.5.3" - }, - { - "r": 37, - "c": 1, - "value": "Drain \u00C6150" - }, - { - "r": 37, - "c": 2, - "value": "ml" - }, - { - "r": 37, - "c": 3, - "value": "100" - }, - { - "r": 37, - "c": 5, - "value": "0" - }, - { - "r": 38, - "c": 1, - "value": "sous total 1.5" - }, - { - "r": 38, - "c": 5, - "value": "0" - }, - { - "r": 39, - "c": 0, - "value": "2.6" - }, - { - "r": 39, - "c": 1, - "value": "ASSAINISSEMENT" - }, - { - "r": 40, - "c": 0, - "value": "2.6.1" - }, - { - "r": 40, - "c": 1, - "value": "Foss\u00E9e pr\u00E9fabriqu\u00E9 en b\u00E9ton (section 20cmx100cm)" - }, - { - "r": 40, - "c": 2, - "value": "ml" - }, - { - "r": 40, - "c": 3, - "value": "200" - }, - { - "r": 40, - "c": 5, - "value": "0" - }, - { - "r": 41, - "c": 0, - "value": "2.6.2" - }, - { - "r": 41, - "c": 1, - "value": "Foss\u00E9e en terre" - }, - { - "r": 41, - "c": 2, - "value": "ml" - }, - { - "r": 41, - "c": 3, - "value": "300" - }, - { - "r": 41, - "c": 5, - "value": "0" - }, - { - "r": 42, - "c": 0, - "value": "2.6.3" - }, - { - "r": 42, - "c": 1, - "value": "Am\u00E9nagement de raccodement des caniveaux du tunnel (sous lot1) au foss\u00E9s " - }, - { - "r": 42, - "c": 2, - "value": "F" - }, - { - "r": 42, - "c": 3, - "value": "1" - }, - { - "r": 42, - "c": 5, - "value": "0" - }, - { - "r": 43, - "c": 0, - "value": "2.6.4" - }, - { - "r": 43, - "c": 1, - "value": "Regard EP diam\u00E8tre 1000" - }, - { - "r": 43, - "c": 2, - "value": "U" - }, - { - "r": 43, - "c": 3, - "value": "12" - }, - { - "r": 43, - "c": 5, - "value": "0" - }, - { - "r": 44, - "c": 0, - "value": "2.6.5" - }, - { - "r": 44, - "c": 1, - "value": "Regard avec r\u00E9gulateur de d\u00E9bit" - }, - { - "r": 44, - "c": 2, - "value": "U" - }, - { - "r": 44, - "c": 3, - "value": "1" - }, - { - "r": 44, - "c": 5, - "value": "0" - }, - { - "r": 45, - "c": 0, - "value": "2.6.6" - }, - { - "r": 45, - "c": 1, - "value": "Tuyau d\u0027assainissement b\u00E9ton " - }, - { - "r": 45, - "c": 5, - "value": "0" - }, - { - "r": 46, - "c": 0, - "value": "2.6.6.1" - }, - { - "r": 46, - "c": 1, - "value": " \u00D8300" - }, - { - "r": 46, - "c": 2, - "value": "ml" - }, - { - "r": 46, - "c": 3, - "value": "60" - }, - { - "r": 46, - "c": 5, - "value": "0" - }, - { - "r": 47, - "c": 0, - "value": "2.6.6.2" - }, - { - "r": 47, - "c": 1, - "value": " \u00D8200" - }, - { - "r": 47, - "c": 2, - "value": "ml" - }, - { - "r": 47, - "c": 3, - "value": "30" - }, - { - "r": 47, - "c": 5, - "value": "0" - }, - { - "r": 48, - "c": 0, - "value": "2.6.7" - }, - { - "r": 48, - "c": 1, - "value": "g\u00E9omembrane sur basin de r\u00E9tention" - }, - { - "r": 48, - "c": 2, - "value": "F" - }, - { - "r": 48, - "c": 3, - "value": "1" - }, - { - "r": 48, - "c": 5, - "value": "0" - }, - { - "r": 49, - "c": 0, - "value": "2.6.8" - }, - { - "r": 49, - "c": 1, - "value": "escalier d\u0027acc\u00E9s en fond de bassin" - }, - { - "r": 49, - "c": 2, - "value": "F" - }, - { - "r": 49, - "c": 3, - "value": "1" - }, - { - "r": 49, - "c": 5, - "value": "0" - }, - { - "r": 50, - "c": 0, - "value": "2.6.9" - }, - { - "r": 50, - "c": 1, - "value": "Filet anti-aviaire" - }, - { - "r": 50, - "c": 2, - "value": "F" - }, - { - "r": 50, - "c": 3, - "value": "1" - }, - { - "r": 50, - "c": 5, - "value": "0" - }, - { - "r": 51, - "c": 1, - "value": "sous total 1.6" - }, - { - "r": 51, - "c": 5, - "value": "0" - }, - { - "r": 52, - "c": 0, - "value": "2.7" - }, - { - "r": 52, - "c": 1, - "value": "DIVERS" - }, - { - "r": 53, - "c": 0, - "value": "2.7.1" - }, - { - "r": 53, - "c": 1, - "value": "Cloture " - }, - { - "r": 53, - "c": 2, - "value": "ml" - }, - { - "r": 54, - "c": 0, - "value": "2.7.1.1" - }, - { - "r": 54, - "c": 1, - "value": " d\u00E9pose cloture domaniale" - }, - { - "r": 54, - "c": 2, - "value": "ml" - }, - { - "r": 54, - "c": 3, - "value": "70" - }, - { - "r": 54, - "c": 5, - "value": "0" - }, - { - "r": 55, - "c": 0, - "value": "2.7.1.2" - }, - { - "r": 55, - "c": 1, - "value": " pose cloture domaniale" - }, - { - "r": 55, - "c": 2, - "value": "ml" - }, - { - "r": 55, - "c": 3, - "value": "60" - }, - { - "r": 55, - "c": 5, - "value": "0" - }, - { - "r": 56, - "c": 0, - "value": "2.7.2" - }, - { - "r": 56, - "c": 1, - "value": "Fourniture et mise en \u0153uvre d\u0027une bache \u00E0 eau (480m3) y compris am\u00E9nagement (cloture, poteau incendie...)" - }, - { - "r": 56, - "c": 2, - "value": "F" - }, - { - "r": 56, - "c": 3, - "value": "1" - }, - { - "r": 56, - "c": 5, - "value": "0" - }, - { - "r": 57, - "c": 0, - "value": "2.7.3" - }, - { - "r": 57, - "c": 1, - "value": "Glissiere GS2 en bordure de bassin " - }, - { - "r": 57, - "c": 2, - "value": "ml" - }, - { - "r": 57, - "c": 3, - "value": "70" - }, - { - "r": 57, - "c": 5, - "value": "0" - }, - { - "r": 58, - "c": 1, - "value": "sous total 1.6" - }, - { - "r": 58, - "c": 5, - "value": "0" - }, - { - "r": 60, - "c": 1, - "value": "TOTAL SOUS-LOT 2: INFRASTRUCTURES" - }, - { - "r": 60, - "c": 5, - "value": "0" - }, - { - "r": 63, - "c": 1, - "value": "Les quantit\u00E9s sont donn\u00E9es \u00E0 titre indicatif" - } - ] - }, - { - "name": "R\u00E9capitulatif", - "columns": [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ], - "cells": [ - { - "r": 2, - "c": 0, - "value": "RECAPITULATIF \n- AFFAIRES 141 668 - CDG - TERRASSEMENT et RADIER\n - LOT 17T- PHASE PRO" - }, - { - "r": 12, - "c": 1, - "value": "MONTANT TOTAL DES TRAVAUX :" - }, - { - "r": 12, - "c": 6, - "value": "R\u00E9capitulatif LOT 17T" - }, - { - "r": 13, - "c": 1, - "value": "MONTANT TOTAL Sous-Lot 1: G\u00E9nie civil" - }, - { - "r": 13, - "c": 6, - "value": "0" - }, - { - "r": 14, - "c": 1, - "value": "MONTANT TOTAL Sous-Lot 2: Infrastrucures" - }, - { - "r": 14, - "c": 6, - "value": "0" - }, - { - "r": 18, - "c": 1, - "value": "MONTANT TOTAL DU LOT 17T (HT)" - }, - { - "r": 18, - "c": 6, - "value": "0" - } - ] - } - ] -} \ No newline at end of file diff --git a/SheetReader.Wpf.Test/MainWindow.xaml b/SheetReader.Wpf.Test/MainWindow.xaml index c891b3b..1933d1c 100644 --- a/SheetReader.Wpf.Test/MainWindow.xaml +++ b/SheetReader.Wpf.Test/MainWindow.xaml @@ -82,6 +82,7 @@ diff --git a/SheetReader.Wpf.Test/MainWindow.xaml.cs b/SheetReader.Wpf.Test/MainWindow.xaml.cs index a0113f2..5c87b41 100644 --- a/SheetReader.Wpf.Test/MainWindow.xaml.cs +++ b/SheetReader.Wpf.Test/MainWindow.xaml.cs @@ -478,32 +478,49 @@ private void Grid_Drop(object sender, DragEventArgs e) } } + private void SheetControl_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + var ctl = (SheetControl)sender; + var result = ctl.HitTest(e); + var cell = result.Cell; + if (cell == null) + return; + + var formatted = ctl.Sheet.FormatValue(cell.Value); + MessageBox.Show(formatted, result.RowCol!.ExcelReference); + } + private void SheetControl_MouseMove(object sender, MouseEventArgs e) { var ctl = (SheetControl)sender; - var result = ctl.HitTest(e.GetPosition(ctl)); + status.Text = GetHitTestText(ctl, e, 100); + } + + private string GetHitTestText(SheetControl ctl, MouseEventArgs e, int max) + { + var result = ctl.HitTest(e); if (result.RowCol != null) { if (result.IsOverRowHeader) - { - status.Text = $"Row: {result.RowCol.RowIndex + 1}"; - return; - } + return $"Row: {result.RowCol.RowIndex + 1}"; if (result.IsOverColumnHeader) - { - status.Text = $"Column: {Row.GetExcelColumnName(result.RowCol.ColumnIndex)} ({result.RowCol.ColumnIndex})"; - return; - } + return $"Column: {Row.GetExcelColumnName(result.RowCol.ColumnIndex)} ({result.RowCol.ColumnIndex})"; var cell = result.Cell; if (cell != null) { - status.Text = $"Cell: {result.RowCol.ExcelReference}: {ctl.Sheet.FormatValue(cell.Value)}"; - return; + var type = cell.Value?.GetType().Name ?? ""; + var formatted = ctl.Sheet.FormatValue(cell.Value)?.Replace('\r', '⇣')?.Replace('\n', '⇣'); + + if (formatted?.Length > max) + { + formatted = formatted[..max] + "…"; + } + return $"Cell: {result.RowCol.ExcelReference}: '{formatted}' of type {type}"; } } - status.Text = string.Empty; + return string.Empty; } private sealed class StyledBookDocument : BookDocument diff --git a/SheetReader.Wpf/SheetControl.cs b/SheetReader.Wpf/SheetControl.cs index fb48f72..dacc616 100644 --- a/SheetReader.Wpf/SheetControl.cs +++ b/SheetReader.Wpf/SheetControl.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Text; @@ -58,6 +59,11 @@ public class SheetControl : Control typeof(SheetControl), new UIPropertyMetadata(Brushes.Green)); + public static readonly DependencyProperty ColumnMovingColorProperty = DependencyProperty.Register(nameof(ColumnMovingColor), + typeof(Color), + typeof(SheetControl), + new UIPropertyMetadata(Colors.Black)); + public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register(nameof(TextAlignment), typeof(TextAlignment), typeof(SheetControl), @@ -83,6 +89,7 @@ public class SheetControl : Control public Brush LineBrush { get => (Brush)GetValue(LineBrushProperty); set => SetValue(LineBrushProperty, value); } public Brush HeaderBrush { get => (Brush)GetValue(HeaderBrushProperty); set => SetValue(HeaderBrushProperty, value); } public Brush SelectionBrush { get => (Brush)GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } + public Color ColumnMovingColor { get => (Color)GetValue(ColumnMovingColorProperty); set => SetValue(ColumnMovingColorProperty, value); } public TextAlignment TextAlignment { get => (TextAlignment)GetValue(TextAlignmentProperty); set => SetValue(TextAlignmentProperty, value); } public TextTrimming TextTrimming { get => (TextTrimming)GetValue(TextTrimmingProperty); set => SetValue(TextTrimmingProperty, value); } public Thickness CellPadding { get => (Thickness)GetValue(CellPaddingProperty); set => SetValue(CellPaddingProperty, value); } @@ -90,7 +97,7 @@ public class SheetControl : Control public event RoutedEventHandler SelectionChanged { add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } private const double _minWidth = 4; - private const double _movingColumnTolerance = 4; + private const double _sizingColumnTolerance = 4; internal double GetRowHeight() => Math.Max(RowHeight, _minWidth); internal double GetRowMargin() => Math.Max(RowMargin, _minWidth); @@ -107,6 +114,7 @@ static SheetControl() private ScrollViewer? _scrollViewer; private SheetGrid? _grid; private MovingColumn? _movingColumn; + private SizingColumn? _sizingColumn; private bool _extendingSelection; private readonly List _columnSettings = []; @@ -119,6 +127,7 @@ public SheetControl() public SheetSelection Selection { get; } public IReadOnlyList ColumnSettings => _columnSettings.AsReadOnly(); + public virtual SheetControlHitTestResult HitTest(MouseEventArgs e) => e != null ? HitTest(e.GetPosition(_scrollViewer)) : new(); public virtual SheetControlHitTestResult HitTest(Point point) => _grid?.HitTest(point) ?? new(); public virtual void SetColumnSize(int columnIndex, double width) { @@ -135,6 +144,8 @@ public virtual bool EnsureVisible(int rowIndex, int columnIndex) if (_scrollViewer == null) return false; + rowIndex = Math.Max(rowIndex, 0); + columnIndex = Math.Max(columnIndex, 0); var selection = SheetSelection.From(this, rowIndex, columnIndex); if (selection == null) return false; @@ -418,12 +429,18 @@ protected override void OnPreviewKeyDown(KeyEventArgs e) private void ReleaseCapture(bool commit) { - if (_movingColumn != null && !commit) + if (_sizingColumn != null) { - _movingColumn.Current = _movingColumn.Start; + _sizingColumn.Release(commit); + _sizingColumn = null; + } + + if (_movingColumn != null) + { + _movingColumn.Release(commit); + _movingColumn = null; } - _movingColumn = null; Cursor = null; ReleaseMouseCapture(); } @@ -436,13 +453,13 @@ protected override void OnPreviewMouseDoubleClick(MouseButtonEventArgs e) if (sheet != null) { var result = HitTest(e.GetPosition(_scrollViewer)); - if (result.MovingColumnIndex.HasValue) + if (result.SizingColumnIndex.HasValue) { // move column size var next = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) && Keyboard.Modifiers.HasFlag(ModifierKeys.Control); - SetColumnsAutoSize(result.MovingColumnIndex.Value, next, sheet.Rows.Values); + SetColumnsAutoSize(result.SizingColumnIndex.Value, next, sheet.Rows.Values); } - else if (result.RowCol != null) + else if (result.RowCol != null && result.IsOverColumnHeader) { // sort by column if (sheet.SortDirection == ListSortDirection.Ascending && sheet.SortColumnIndex == result.RowCol.ColumnIndex) @@ -474,15 +491,23 @@ protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { Focus(); var result = HitTest(e.GetPosition(_scrollViewer)); - if (result.MovingColumnIndex.HasValue) + if (result.IsOverColumnHeader && result.MovingColumnIndex.HasValue) { - Cursor = Cursors.SizeWE; + Cursor = Cursors.ScrollWE; CaptureMouse(); _movingColumn = new MovingColumn(this, _columnSettings[result.MovingColumnIndex.Value], e.GetPosition(this)); return; } - if (!result.IsOverRowHeader && !result.IsOverColumnHeader && result.RowCol != null) + if (result.SizingColumnIndex.HasValue) + { + Cursor = Cursors.SizeWE; + CaptureMouse(); + _sizingColumn = new SizingColumn(this, _columnSettings[result.SizingColumnIndex.Value], e.GetPosition(this)); + return; + } + + if (e.ClickCount == 1 && !result.IsOverRowHeader && !result.IsOverColumnHeader && result.RowCol != null) { Selection.Select(result.RowCol); _extendingSelection = true; @@ -493,13 +518,18 @@ protected override void OnMouseMove(MouseEventArgs e) { if (_movingColumn != null) { - Cursor = Cursors.SizeWE; + Cursor = Cursors.ScrollWE; _movingColumn.Current = e.GetPosition(this); } + else if (_sizingColumn != null) + { + Cursor = Cursors.SizeWE; + _sizingColumn.Current = e.GetPosition(this); + } else { var result = HitTest(e.GetPosition(_scrollViewer)); - if (result.MovingColumnIndex.HasValue) + if (result.SizingColumnIndex.HasValue) { Cursor = Cursors.SizeWE; } @@ -515,14 +545,78 @@ protected override void OnMouseMove(MouseEventArgs e) } } - private sealed class MovingColumn(SheetControl control, SheetControlColumn column, Point start) + private abstract class BaseColumn(SheetControl control, SheetControlColumn column, Point start) { - private Point _current; - + public SheetControl Control { get; } = control; public SheetControlColumn Column { get; } = column; public double Width { get; } = column.Width; public Point Start { get; } = start; - public Point Current + public abstract Point Current { get; set; } + + public virtual void Release(bool commit) + { + if (!commit) + { + Current = Start; + } + } + } + + private sealed class MovingColumn(SheetControl control, SheetControlColumn column, Point start) : BaseColumn(control, column, start) + { + public int SourceColumnIndex => Column.Column.Index; + public int TargetColumnIndex { get; private set; } = column.Column.Index; + + private Point _current; + public override Point Current + { + get => _current; + set + { + if (_current == value) + return; + + _current = value; + if (Control._scrollViewer == null || Control._scrollViewer.ViewportWidth == 0) + return; + + var result = Control.HitTest(_current); + if (result.RowCol != null && result.IsOverColumnHeader) + { + TargetColumnIndex = result.RowCol.ColumnIndex; + var delta = _current.X - Start.X; + var ratio = Control._scrollViewer.ExtentWidth / Control._scrollViewer.ViewportWidth; + Control._scrollViewer.ScrollToHorizontalOffset(Control._scrollViewer.HorizontalOffset + delta * ratio); + Control._grid?.InvalidateVisual(); + } + } + } + + public override void Release(bool commit) + { + if (commit && SourceColumnIndex != TargetColumnIndex) + { + Control.Sheet.SwapColumns(SourceColumnIndex, TargetColumnIndex); + if (Control.Selection.CrossesColumn(SourceColumnIndex)) + { + var tl = Control.Selection.TopLeft; + var br = Control.Selection.BottomRight; + var topRow = tl.RowIndex; + var bottomRow = br.RowIndex; + var leftCol = tl.ColumnIndex; + var rightCol = br.ColumnIndex; + Control.Selection.SelectTo(topRow, Math.Min(TargetColumnIndex, leftCol)); + Control.Selection.SelectTo(bottomRow, Math.Max(TargetColumnIndex, rightCol)); + } + } + Control._grid?.InvalidateVisual(); + } + } + + private sealed class SizingColumn(SheetControl control, SheetControlColumn column, Point start) : BaseColumn(control, column, start) + { + private Point _current; + public override Point Current { get => _current; set @@ -535,7 +629,7 @@ public Point Current var delta = _current.X - Start.X; var newWidth = Math.Max(Width + delta, _minWidth); Column.Width = newWidth; - control._grid?.InvalidateMeasure(); + Control._grid?.InvalidateMeasure(); } } } @@ -559,10 +653,12 @@ public SheetControlHitTestResult HitTest(Point point) if (_lastResult != null && _lastResultPoint != null && _lastResultPoint == point) return _lastResult; - var result = new SheetControlHitTestResult(); - if (IsSheetVisible() && _control._scrollViewer != null && - point.X < _control._scrollViewer.ViewportWidth && - point.Y < _control._scrollViewer.ViewportHeight) + var result = new SheetControlHitTestResult + { + IsInViewport = _control._scrollViewer != null && point.X < _control._scrollViewer.ViewportWidth && point.Y < _control._scrollViewer.ViewportHeight + }; + + if (IsSheetVisible() && _control._scrollViewer != null) { var context = _control.CreateStyleContext(); if (context == null) @@ -602,13 +698,19 @@ public SheetControlHitTestResult HitTest(Point point) var colSeparatorX = context.RowFullMargin.Value; for (var i = 0; i < _control._columnSettings.Count; i++) { - colSeparatorX += _control._columnSettings[i].Width; - if ((x + _movingColumnTolerance) >= colSeparatorX && (x - _movingColumnTolerance) <= (colSeparatorX + context.LineSize.Value)) + if (x >= (colSeparatorX + _sizingColumnTolerance) && x <= (colSeparatorX + _control._columnSettings[i].Width - _sizingColumnTolerance)) { result.MovingColumnIndex = i; break; } + colSeparatorX += _control._columnSettings[i].Width; + if ((x + _sizingColumnTolerance) >= colSeparatorX && (x - _sizingColumnTolerance) <= (colSeparatorX + context.LineSize.Value)) + { + result.SizingColumnIndex = i; + break; + } + colSeparatorX += context.LineSize.Value; } } @@ -643,6 +745,9 @@ protected override Size MeasureCore(Size availableSize) private int? GetColumnIndex(double x, double maxWidth, bool allowReturnNull) { + if (x < 0) + { + } if (allowReturnNull && x < 0) return -1; @@ -693,6 +798,65 @@ protected override void OnRender(DrawingContext drawingContext) if (!firstDrawnColumnIndex.HasValue || !lastDrawnColumnIndex.HasValue) return; + // build a column indices range, rearrange columns if there's a moving column + int[] drawColumnsIndices; + var mc = _control._movingColumn; + if (mc != null && mc.SourceColumnIndex != mc.TargetColumnIndex) + { + drawColumnsIndices = new int[lastDrawnColumnIndex.Value - firstDrawnColumnIndex.Value + 1]; + + var idx = 0; + if (mc.SourceColumnIndex > mc.TargetColumnIndex) + { + for (var i = firstDrawnColumnIndex.Value; i <= lastDrawnColumnIndex.Value; i++) + { + if (i == mc.SourceColumnIndex) + { + // skip + continue; + } + + if (i == mc.TargetColumnIndex) + { + // insert + drawColumnsIndices[idx++] = mc.SourceColumnIndex; + if (idx == drawColumnsIndices.Length) + break; + } + + drawColumnsIndices[idx++] = i; + if (idx == drawColumnsIndices.Length) + break; + } + } + else + { + for (var i = firstDrawnColumnIndex.Value; i <= lastDrawnColumnIndex.Value; i++) + { + if (i == mc.SourceColumnIndex) + { + // skip + continue; + } + + drawColumnsIndices[idx++] = i; + if (idx == drawColumnsIndices.Length) + break; + + if (i == mc.TargetColumnIndex) + { + drawColumnsIndices[idx++] = mc.SourceColumnIndex; + if (idx == drawColumnsIndices.Length) + break; + } + } + } + } + else + { + drawColumnsIndices = Enumerable.Range(firstDrawnColumnIndex.Value, lastDrawnColumnIndex.Value - firstDrawnColumnIndex.Value + 1).ToArray(); + } + var firstDrawnRowIndex = Math.Max((int)((offsetY - context.RowFullHeight!.Value) / context.RowFullHeight!.Value), 0); var lastDrawnRowIndex = Math.Max(Math.Min((int)((offsetY - context.RowFullHeight.Value + viewHeight) / context.RowFullHeight.Value), _control.Sheet.LastRowIndex!.Value), firstDrawnRowIndex); @@ -719,7 +883,7 @@ protected override void OnRender(DrawingContext drawingContext) { context.RowCol.RowIndex = i; var ccx = startCurrentColX; - for (var j = firstDrawnColumnIndex.Value; j <= lastDrawnColumnIndex; j++) + foreach (var j in drawColumnsIndices) { var colWidth = _control._columnSettings[j].Width; var cellWidth = colWidth - (cellPadding.Right + cellPadding.Left); @@ -800,14 +964,39 @@ protected override void OnRender(DrawingContext drawingContext) columnsRect.Height = context.RowHeight.Value; drawingContext.DrawRectangle(_control.HeaderBrush, null, columnsRect); + Brush? movingLeftBrush = null; + Brush? movingRightBrush = null; + if (_control.ColumnMovingColor != Colors.Transparent) + { + movingLeftBrush = new LinearGradientBrush(_control.ColumnMovingColor, Colors.White, 0) { Opacity = 0.2f }; + movingRightBrush = new LinearGradientBrush(Colors.White, _control.ColumnMovingColor, 0) { Opacity = movingLeftBrush.Opacity }; + } + var currentColX = startCurrentColX; - for (var i = firstDrawnColumnIndex.Value; i <= lastDrawnColumnIndex; i++) + foreach (var i in drawColumnsIndices) { - // draw col name var colWidth = _control._columnSettings[i].Width; + var isMovingColumn = _control._movingColumn != null && _control._movingColumn.SourceColumnIndex != _control._movingColumn.TargetColumnIndex && _control._movingColumn.SourceColumnIndex == i; + if (isMovingColumn && movingLeftBrush != null && movingRightBrush != null) + { + var h = offsetY + Math.Min(rowsHeight, viewHeight); + drawingContext.DrawRectangle(movingLeftBrush, null, new Rect(currentColX, offsetY, colWidth / 2, h)); + drawingContext.DrawRectangle(movingRightBrush, null, new Rect(currentColX + colWidth / 2, offsetY, colWidth / 2, h)); + } + + // draw col name _control.Sheet.Columns.TryGetValue(i, out var col); - var name = col?.Name ?? Row.GetExcelColumnName(i); + + string name; + if (col?.Name != null) + { + name = "'" + col.Name + "'"; + } + else + { + name = Row.GetExcelColumnName(i); + } var formattedCol = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, context.Typeface, _control.FontSize, _control.Foreground, context.PixelsPerDip) { MaxTextWidth = colWidth, @@ -856,7 +1045,7 @@ protected override void OnRender(DrawingContext drawingContext) { var focusRc = rc; focusRc.Inflate(focusMargin, focusMargin); - var focusPen = new Pen(selectionBrush, context.LineSize.Value + 1) { DashStyle = new DashStyle([0.0, 3.0], 0) }; + var focusPen = new Pen(selectionBrush, context.LineSize.Value + 1) { DashStyle = new DashStyle([0, 3], 0) }; drawingContext.DrawRectangle(null, focusPen, focusRc); } diff --git a/SheetReader.Wpf/SheetControlHitTestResult.cs b/SheetReader.Wpf/SheetControlHitTestResult.cs index 2006ed5..da50b85 100644 --- a/SheetReader.Wpf/SheetControlHitTestResult.cs +++ b/SheetReader.Wpf/SheetControlHitTestResult.cs @@ -6,6 +6,8 @@ public sealed class SheetControlHitTestResult public BookDocumentCell? Cell { get; internal set; } public bool IsOverRowHeader { get; internal set; } public bool IsOverColumnHeader { get; internal set; } + public bool IsInViewport { get; internal set; } + public int? SizingColumnIndex { get; internal set; } public int? MovingColumnIndex { get; internal set; } public bool IsOverCorner => IsOverColumnHeader && IsOverRowHeader; diff --git a/SheetReader.Wpf/SheetSelection.cs b/SheetReader.Wpf/SheetSelection.cs index 0653f71..e25b4f4 100644 --- a/SheetReader.Wpf/SheetSelection.cs +++ b/SheetReader.Wpf/SheetSelection.cs @@ -81,6 +81,54 @@ public RowCol BottomRight public RowCol TopRight => new(TopLeft.RowIndex, BottomRight.ColumnIndex); public RowCol BottomLeft => new(BottomRight.RowIndex, TopLeft.ColumnIndex); + public bool CrossesRow(int rowIndex) + { + var topLeft = TopLeft; + if (rowIndex < topLeft.RowIndex) + return false; + + var bottomRight = BottomRight; + if (rowIndex > bottomRight.RowIndex) + return false; + + return true; + } + + public bool CrossesColumn(int columnIndex) + { + var topLeft = TopLeft; + if (columnIndex < topLeft.ColumnIndex) + return false; + + var bottomRight = BottomRight; + if (columnIndex > bottomRight.ColumnIndex) + return false; + + return true; + } + + public bool Contains(RowCol? rowCol) + { + if (rowCol == null) + return false; + + var topLeft = TopLeft; + if (rowCol.ColumnIndex < topLeft.ColumnIndex) + return false; + + if (rowCol.RowIndex < topLeft.RowIndex) + return false; + + var bottomRight = BottomRight; + if (rowCol.ColumnIndex > bottomRight.ColumnIndex) + return false; + + if (rowCol.RowIndex > bottomRight.RowIndex) + return false; + + return true; + } + public virtual void SelectRow(int rowIndex) { if (Control.Sheet == null || !Control.Sheet.LastRowIndex.HasValue) diff --git a/SheetReader/BookDocument.cs b/SheetReader/BookDocument.cs index e4925e8..8085600 100644 --- a/SheetReader/BookDocument.cs +++ b/SheetReader/BookDocument.cs @@ -213,6 +213,18 @@ static bool isDefaultJsonValue(object? value) if (value is decimal dec && dec == 0m) return true; + if (value is DateTime dt && dt == DateTime.MinValue) + return true; + + if (value is DateTimeOffset dto && dto == DateTimeOffset.MinValue) + return true; + + if (value is TimeSpan ts && ts == TimeSpan.Zero) + return true; + + if (value is Guid g && g == Guid.Empty) + return true; + if (value is uint ui && ui == 0) return true; @@ -224,15 +236,19 @@ static bool isDefaultJsonValue(object? value) void writePositionedCell(BookDocumentCell? cell, int rowIndex, int columnIndex) { - // don't output null values - if (cell == null || cell.Value == null || Convert.IsDBNull(cell.Value)) - return; - writer.WriteStartObject(); writer.WriteNumber("r", rowIndex); writer.WriteNumber("c", columnIndex); writer.WritePropertyName("value"); + // don't output null values + if (cell == null || cell.Value == null || Convert.IsDBNull(cell.Value)) + { + writer.WriteNullValue(); + writer.WriteEndObject(); + return; + } + if (cell.IsError) { if (cell.Value != null) @@ -308,6 +324,20 @@ void writePositionedCell(BookDocumentCell? cell, int rowIndex, int columnIndex) return; } + if (cell.Value is DateTime dt) + { + writer.WriteStringValue(dt.ToString("u")); + writer.WriteEndObject(); + return; + } + + if (cell.Value is DateTimeOffset dto) + { + writer.WriteStringValue(dto.ToString("u")); + writer.WriteEndObject(); + return; + } + if (cell.Value is byte[] bytes) { writer.WriteBase64StringValue(bytes); @@ -420,6 +450,18 @@ void writeCell(BookDocumentCell? cell) return; } + if (cell.Value is DateTime dt) + { + writer.WriteStringValue(dt.ToString("u")); + return; + } + + if (cell.Value is DateTimeOffset dto) + { + writer.WriteStringValue(dto.ToString("u")); + return; + } + if (cell.Value is byte[] bytes) { writer.WriteBase64StringValue(bytes); @@ -481,7 +523,7 @@ void writeRowAsObjects(BookDocumentSheet sheet, BookDocumentRow? row, Dictionary row?.Cells.TryGetValue(columnIndex, out cell); if (!options.HasFlag(ExportOptions.JsonNoDefaultCellValues) || (cell != null && !isDefaultJsonValue(cell.Value))) { - string colName = colNames[columnIndex]; + var colName = colNames[columnIndex]; writer.WritePropertyName(colName); writeCell(cell); } @@ -547,7 +589,10 @@ void writeSheet(BookDocumentSheet sheet) { if (row.Cells.TryGetValue(columnIndex, out var cell)) { - writePositionedCell(cell, rowIndex, columnIndex); + if (!options.HasFlag(ExportOptions.JsonNoDefaultCellValues) || (cell != null && !isDefaultJsonValue(cell.Value))) + { + writePositionedCell(cell, rowIndex, columnIndex); + } } } } diff --git a/SheetReader/BookDocumentRow.cs b/SheetReader/BookDocumentRow.cs index b101fb5..9004c9f 100644 --- a/SheetReader/BookDocumentRow.cs +++ b/SheetReader/BookDocumentRow.cs @@ -5,7 +5,7 @@ namespace SheetReader { public class BookDocumentRow { - private readonly IDictionary _cells; + private IDictionary _cells; public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) { @@ -56,7 +56,7 @@ public BookDocumentRow(BookDocument book, BookDocumentSheet sheet, Row row) public int? LastCellIndex { get; } public IDictionary Cells => _cells; - protected virtual IDictionary CreateCells() => new Dictionary(); + protected virtual internal IDictionary CreateCells() => new Dictionary(); protected virtual BookDocumentCell CreateCell(Cell cell) { ArgumentNullException.ThrowIfNull(cell); @@ -66,6 +66,12 @@ protected virtual BookDocumentCell CreateCell(Cell cell) return cell.IsError ? new BookDocumentCellError(cell) : new BookDocumentCell(cell); } + protected virtual internal void ReplaceCells(IDictionary cells) + { + ArgumentNullException.ThrowIfNull(cells); + _cells = cells; + } + public override string ToString() => RowIndex.ToString(); } } diff --git a/SheetReader/BookDocumentSheet.cs b/SheetReader/BookDocumentSheet.cs index 7146874..f79a818 100644 --- a/SheetReader/BookDocumentSheet.cs +++ b/SheetReader/BookDocumentSheet.cs @@ -177,10 +177,12 @@ public virtual void SortRows(int columnIndex, ListSortDirection direction, Compa var index = 0; + var missingRows = Math.Max(0, FirstRowIndex.Value + 1 - rowsNoCell.Count - rowCells.Count); var rows = new Dictionary(); var nullsFirst = direction == ListSortDirection.Descending; if (nullsFirst) { + index += missingRows; foreach (var row in rowsNoCell.OrderBy(r => r.rowIndex)) { rows[index] = row.row; @@ -202,11 +204,10 @@ public virtual void SortRows(int columnIndex, ListSortDirection direction, Compa rows[index] = row.row; row.row.SortIndex = index++; } + index += missingRows; } _rows = rows; - FirstRowIndex = 0; - LastRowIndex = index - 1; } SortColumnIndex = columnIndex; @@ -230,7 +231,28 @@ protected virtual int Compare(BookDocumentCell? x, BookDocumentCell? y, Comparis else if (IsNullForComparison(iy)) return -1; - return ix!.CompareTo(iy); + var xt = ix!.GetType(); + var yt = iy!.GetType(); + if (xt.IsAssignableFrom(yt) || yt.IsAssignableFrom(xt)) + return ix.CompareTo(iy); + + try + { + return ix.CompareTo((IComparable)Convert.ChangeType(iy, xt)); + } + catch + { + // continue + } + try + { + return iy.CompareTo((IComparable)Convert.ChangeType(ix, yt)); + } + catch + { + // continue + } + return 0; } protected virtual bool IsNullForComparison(object? o) => o is null || Convert.IsDBNull(o); @@ -252,6 +274,92 @@ protected virtual int Compare(BookDocumentCell? x, BookDocumentCell? y, Comparis return cell; } + public virtual bool SwapColumns(int columnIndex1, int columnIndex2) + { + if (columnIndex1 == columnIndex2) + return false; + + if (!FirstColumnIndex.HasValue || !LastColumnIndex.HasValue) + return false; + + // swap columns + if (!Columns.Remove(columnIndex1, out var col1)) + return false; + + if (!Columns.Remove(columnIndex2, out var col2)) + return false; + + Columns.Add(columnIndex1, col2); + Columns.Add(columnIndex2, col1); + + // recompute columns indices + var indices = new int[LastColumnIndex.Value - FirstColumnIndex.Value + 1]; + var idx = 0; + if (columnIndex1 > columnIndex2) + { + for (var i = FirstColumnIndex.Value; i <= LastColumnIndex.Value; i++) + { + if (i == columnIndex1) + { + // skip + continue; + } + + if (i == columnIndex2) + { + // insert + indices[idx++] = columnIndex1; + if (idx == indices.Length) + break; + } + + indices[idx++] = i; + if (idx == indices.Length) + break; + } + } + else + { + for (var i = FirstColumnIndex.Value; i <= LastColumnIndex.Value; i++) + { + if (i == columnIndex1) + { + // skip + continue; + } + + indices[idx++] = i; + if (idx == indices.Length) + break; + + if (i == columnIndex2) + { + indices[idx++] = columnIndex1; + if (idx == indices.Length) + break; + } + } + } + + // rebuild all rows cells in new order + foreach (var kv in Rows) + { + var cells = kv.Value.CreateCells(); + var idx2 = 0; + foreach (var i in indices) + { + if (kv.Value.Cells.TryGetValue(i, out var cell)) + { + cells[idx2] = cell; + } + idx2++; + } + + kv.Value.ReplaceCells(cells); + } + return true; + } + public virtual string? FormatValue(object? value) { if (value is null) diff --git a/SheetReader/Properties/AssemblyVersionInfo.cs b/SheetReader/Properties/AssemblyVersionInfo.cs index 88568c6..b412cac 100644 --- a/SheetReader/Properties/AssemblyVersionInfo.cs +++ b/SheetReader/Properties/AssemblyVersionInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("3.3.0.0")] -[assembly: AssemblyFileVersion("3.3.0.0")] +[assembly: AssemblyVersion("3.4.0.0")] +[assembly: AssemblyFileVersion("3.4.0.0")]