Summary
This question is the follow-up of a a desire to architect a simple spreadsheet API while keeping it user-friend
The used architecture has gone through an object class that I named CellCollection
. Here's what it does:
Based on these hypothesis:
Given that an Excel worksheet has 256 columns and 65536 lines;
Given that 16,777,216 (256 * 65536) cells needed to be instantiated at a time;
Given that the most common use of a worksheet takes less then 1,000 lines and less than 100 columns;
Given that I needed it to be able to refer to the cells with their addresses ("A1"); and
Given that it is benchmarked that accessing all the values at once and load them into a
object[,]
in memory as being the fastest way to work with an underlying Excel worksheet,*
I have considered not to instantiate any of the cells, letting my CellCollection
property within my IWorksheet
interface initialized and empty upon instantiation, except for an existing workbook. So, when opening a workbook, I verify that NativeSheet.UsedRange
is empty or return null (Nothing in Visual Basic), otherwise, I have already gotten the used "native cells" in memory so that only remains to add them in my internal CellCollection
dictionary while indexing them with their respective address.
Finally, Lazy Initialization Design Pattern to the rescue! =)
public class Sheet : ISheet {
public Worksheet(Microsoft.Office.Interop.Excel.Worksheet nativeSheet) {
NativeSheet = nativeSheet;
Cells = new CellCollection(this);
}
public Microsoft.Office.Interop.Excel.Worksheet NativeSheet { get; private set; }
public CellCollection Cells { get; private set; }
}
public sealed class CellCollection {
private IDictionary _cells;
private ReadOnlyDictionary _readonlyCells;
public CellCollection(ISheet sheet) {
_cells = new Dictionary();
_readonlyCells = new ReadonlyDictionary(_cells);
Sheet = sheet;
}
public readonly ReadOnlyDictionary Cells(string addresses) {
get {
if (string.IsNullOrEmpty(addresses) || 0 = address.Trim().Length)
throw new ArgumentNullException("addresses");
if (!Regex.IsMatch(addresses, "(([A-Za-z]{1,2,3}[0-9]*)[:,]*)"))
throw new FormatException("addresses");
foreach(string address in addresses.Split(",") {
Microsoft.Office.Interop.Excel.Range range = Sheet.NativeSheet.Range(address)
foreach(Microsoft.Office.Interop.Excel.Range cell in range) {
ICell c = null;
if (!_cells.TryGetValue(cell.Address(false, false), c)) {
c = new Cell(cell);
_cells.Add(c.Name, c);
}
}
}
return _readonlyCells;
}
}
public readonly ISheet Sheet { get; private set; }
}
Obviously, this is a first try shot, and it works just fine so far, with more than acceptable performance. Humbly though, I feel like it could use some optimizations, though I will use it this way for now, and optimize it later if needed.
After having written this collection, I was able to come to the expected behaviour. Now, I shall try to implement some of the .NET interfaces to make it useable against some IEnumerable
, IEnumerable
, ICollection
, ICollection
, etc. so that it may respectively be considered as a true .NET collection.
Feel free to comment and bring constructive alternatives and/or changes to this code so that it may become even greater than it currently is.
I DO hope this will serve one's purpose someday.
Thanks for reading! =)