Repository: wangguanquan/eec
Pull request #283 (2022-08-11 11:34:01)

Fix#too many merged


Patch:
diff --git a/src/main/java/org/ttzero/excel/entity/Column.java b/src/main/java/org/ttzero/excel/entity/Column.java
index 5cc290e0..6d254a16 100644
--- a/src/main/java/org/ttzero/excel/entity/Column.java
+++ b/src/main/java/org/ttzero/excel/entity/Column.java
@@ -817,6 +817,7 @@ public class Column {
      * Returns an array containing all of the sub-column
      *
      * @param dist the array into which the elements of the column are to be stored
+     * @return header columns
      * @throws NullPointerException if the specified array is null
      */
     public Column[] toArray(Column[] dist) {
@@ -832,6 +833,8 @@ public class Column {
 
     /**
      * Returns the real col-index(one base)
+     *
+     * @return real col-index(one base)
      */
     public int getRealColIndex() {
         return realColIndex;
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
index 35abad51..d9f07f26 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
@@ -327,6 +327,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
     /**
      * Write the header row
      *
+     * @return row number
      * @throws IOException if I/O error occur
      */
     protected int writeHeaderRow() throws IOException {
diff --git a/src/main/java/org/ttzero/excel/reader/Dimension.java b/src/main/java/org/ttzero/excel/reader/Dimension.java
index 4693e59e..49e69df1 100644
--- a/src/main/java/org/ttzero/excel/reader/Dimension.java
+++ b/src/main/java/org/ttzero/excel/reader/Dimension.java
@@ -35,12 +35,17 @@ public class Dimension {
     public final short firstColumn;
     // Index to last used column (one base)
     public final short lastColumn;
+    // Width and Height
+    public final int width, height;
 
     public Dimension(int firstRow, short firstColumn) {
         this.firstRow = firstRow;
         this.firstColumn = firstColumn;
         this.lastRow = 0;
         this.lastColumn = 0;
+
+        this.width = 1;
+        this.height = 1;
     }
 
     public Dimension(int firstRow, short firstColumn, int lastRow, short lastColumn) {
@@ -48,6 +53,9 @@ public class Dimension {
         this.firstColumn = firstColumn;
         this.lastRow = lastRow;
         this.lastColumn = lastColumn;
+
+        this.width = lastColumn > 0 && firstColumn > 0 ? lastColumn - firstColumn + 1 : lastColumn > 0 ? lastColumn : 1;
+        this.height = lastRow > 0 && firstRow > 0 ? lastRow - firstRow + 1 : lastRow > 0 ? lastRow : 1;
     }
 
     /**
@@ -109,6 +117,24 @@ public class Dimension {
         return lastColumn;
     }
 
+    /**
+     * Returns the width in dimension
+     *
+     * @return width
+     */
+    public int getWidth() {
+        return width;
+    }
+
+    /**
+     * Returns the height in dimension
+     *
+     * @return height
+     */
+    public int getHeight() {
+        return height;
+    }
+
     @Override
     public String toString() {
         return new String(int2Col(firstColumn)) + this.firstRow
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index 9949064f..3c2d9cb4 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -394,7 +394,7 @@ public class ExcelReader implements Closeable {
     public ExcelReader parseFormula() {
         if (hasFormula) {
             // Formula string if exists
-            long[][] calcArray = parseCalcChain();
+            long[][] calcArray = parseCalcChain(self);
 
             if (calcArray == null) return this;
             int i = 0;
@@ -803,8 +803,8 @@ public class ExcelReader implements Closeable {
     }
 
     /* Parse `calcChain` */
-    private long[][] parseCalcChain() {
-        Path calcPath = self.resolve("xl/calcChain.xml");
+    static long[][] parseCalcChain(Path root) {
+        Path calcPath = root.resolve("xl/calcChain.xml");
         if (!FileUtil.exists(calcPath)) return null;
         Element calcChain;
         try {
@@ -816,7 +816,7 @@ public class ExcelReader implements Closeable {
         }
 
         Iterator<Element> ite = calcChain.elementIterator();
-        int i = 1, n = sheets().map(Sheet::getId).max(Integer::compareTo).orElse(0);
+        int i = 1, n = 10;
         long[][] array = new long[n][];
         int[] indices = new int[n];
         for (; ite.hasNext(); ) {
@@ -826,6 +826,13 @@ public class ExcelReader implements Closeable {
                 i = toInt(si.toCharArray(), 0, si.length());
             }
             if (isNotEmpty(r)) {
+                if (n < i) {
+                    n <<= 1;
+                    indices = Arrays.copyOf(indices, n);
+                    long[][] _array = new long[n][];
+                    for (int j = 0; j < n; j++) _array[j] = array[j]; // Do not copy hear.
+                    array = _array;
+                }
                 long[] sub = array[i - 1];
                 if (sub == null) {
                     sub = new long[10];
diff --git a/src/main/java/org/ttzero/excel/reader/Grid.java b/src/main/java/org/ttzero/excel/reader/Grid.java
index c4378266..c173b21d 100644
--- a/src/main/java/org/ttzero/excel/reader/Grid.java
+++ b/src/main/java/org/ttzero/excel/reader/Grid.java
@@ -25,6 +25,7 @@ import java.util.StringJoiner;
 import static java.lang.Integer.numberOfTrailingZeros;
 import static org.ttzero.excel.reader.Cell.BLANK;
 import static org.ttzero.excel.reader.Cell.EMPTY_TAG;
+import static org.ttzero.excel.util.StringUtil.formatBinarySize;
 
 /**
  * @author guanquan.wang at 2020-01-09 16:54
@@ -79,6 +80,13 @@ public interface Grid {
      */
     void merge(int r, Cell cell);
 
+    /**
+     * Returns count of merged cells
+     *
+     * @return count of merged cells
+     */
+    int size();
+
 
     /**
      * Use binary to mark whether the cells are `merged` and set
@@ -157,6 +165,11 @@ public interface Grid {
             }
         }
 
+        @Override
+        public int size() {
+            return scanner.size();
+        }
+
         boolean range(int r, int c) {
             return r >= fr && r <= lr && c >= fc && c <= lc;
         }
@@ -168,7 +181,7 @@ public interface Grid {
         @Override
         public String toString() {
             StringJoiner joiner = new StringJoiner("\n");
-            joiner.add(getClass().getSimpleName());
+            joiner.add(getClass().getSimpleName() + " Size: " + formatBinarySize(g.length << 3));
             int last = lr - fr + 1, j = 0;
             A: for (long l : g) {
                 String s = append(Long.toBinaryString(l));
@@ -201,6 +214,7 @@ public interface Grid {
     final class IndexGrid implements Grid {
         private final int fr, fc, lr, lc; // Start index of Row and Column(One base)
         private final Map<Long, Cell> index;
+        private int size;
         IndexGrid(Dimension dim, int n) {
             fr = dim.firstRow;
             lr = dim.lastRow;
@@ -218,6 +232,7 @@ public interface Grid {
                     index.put(((long) i) << 16 | j, cell);
                 }
             }
+            size++;
         }
 
         @Override
@@ -229,6 +244,7 @@ public interface Grid {
         public void merge(int r, Cell cell) {
             if (!range(r, cell.i)) return;
             Cell c = index.get(((long) r) << 16 | cell.i);
+            if (c == null) return;
 
             if (cell.t == EMPTY_TAG || cell.t == BLANK) {
                 // Copy value from the first merged cell
@@ -240,6 +256,11 @@ public interface Grid {
             }
         }
 
+        @Override
+        public int size() {
+            return size;
+        }
+
         boolean range(int r, int c) {
             return r >= fr && r <= lr && c >= fc && c <= lc;
         }
@@ -289,6 +310,11 @@ public interface Grid {
             }
         }
 
+        @Override
+        public int size() {
+            return scanner.size;
+        }
+
         boolean range(int r, int c) {
             return r >= fr && r <= lr && c >= fc && c <= lc;
         }
@@ -361,8 +387,9 @@ public interface Grid {
 
             if (head != null) {
                 Node f = head, bf = null;
-                for (; f != null; f = f.next) {
-                    if (f.entry.getDim().firstRow > entry.getDim().firstRow) {
+                for (int p; f != null; f = f.next) {
+                    p = f.entry.getDim().firstRow - entry.getDim().firstRow;
+                    if (p > 0 || p == 0 && f.entry.getDim().firstColumn > entry.getDim().firstColumn) {
                         Node newNode = new Node(e, f);
                         if (f == head) head = newNode;
                         else bf.next = newNode;
@@ -396,7 +423,7 @@ public interface Grid {
             if (val != null) {
                 int n = --val.entry.n;
                 // Insert entry ahead
-                if (n > 0 && val != head) {
+                if (n > 0 && val != head && val.entry.dim.width > 1) {
                     bf.next = val.next;
                     val.next = head;
                     head = val;
diff --git a/src/main/java/org/ttzero/excel/reader/GridFactory.java b/src/main/java/org/ttzero/excel/reader/GridFactory.java
index caa4b27b..40f5c8fb 100644
--- a/src/main/java/org/ttzero/excel/reader/GridFactory.java
+++ b/src/main/java/org/ttzero/excel/reader/GridFactory.java
@@ -17,16 +17,12 @@
 
 package org.ttzero.excel.reader;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.util.List;
 
 /**
  * @author guanquan.wang at 2022-07-04 11:56
  */
 public class GridFactory {
-    private static final Logger LOGGER = LoggerFactory.getLogger(GridFactory.class);
     private GridFactory() { }
     public static Grid create(List<Dimension> mergeCells) {
         Dimension dim = mergeCells.get(0);
@@ -48,13 +44,10 @@ public class GridFactory {
 
         n = r * c;
 
-        Grid grid = c <= 64 && r < 1 << 14 ? new Grid.FastGrid(range)
-            : n > 1 << 10 ? new Grid.FractureGrid(range) : new Grid.IndexGrid(range, n);
+        Grid grid = c <= 64 && r < 1 << 15 ? new Grid.FastGrid(range)
+            : n > 1 << 17 ? new Grid.FractureGrid(range) : new Grid.IndexGrid(range, n);
 
-        for (Dimension d : mergeCells) {
-            grid.mark(d);
-            LOGGER.debug("merged cells range {}", d);
-        }
+        for (Dimension d : mergeCells) grid.mark(d);
         return grid;
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/SharedStrings.java b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
index c8b0ce6a..5f97c55c 100644
--- a/src/main/java/org/ttzero/excel/reader/SharedStrings.java
+++ b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
@@ -110,6 +110,7 @@ public class SharedStrings implements Closeable {
      * @param sst {@link IndexSharedStringTable}
      * @param cacheSize the number of word per load
      * @param hotSize   the number of high frequency word
+     * @throws IOException if I/O error occur.
      */
     public SharedStrings(IndexSharedStringTable sst, int cacheSize, int hotSize) throws IOException {
         this.sst = sst;
diff --git a/src/main/java/org/ttzero/excel/reader/Sheet.java b/src/main/java/org/ttzero/excel/reader/Sheet.java
index 859496c0..0e164b83 100644
--- a/src/main/java/org/ttzero/excel/reader/Sheet.java
+++ b/src/main/java/org/ttzero/excel/reader/Sheet.java
@@ -285,6 +285,13 @@ public interface Sheet extends Closeable {
         }
     }
 
+    /**
+     * Make worksheets parse value only
+     *
+     * @return a empty {@link Sheet}
+     */
+    Sheet asSheet();
+
     /**
      * Make worksheets parse formulas
      *
@@ -304,6 +311,11 @@ public interface Sheet extends Closeable {
 
 interface CalcSheet extends Sheet { }
 
-interface MergeSheet extends Sheet { }
+interface MergeSheet extends Sheet {
+    /**
+     * Returns CellMerged info
+     */
+    Grid getMergeGrid();
+}
 
 //interface FullSheet extends CalcSheet, MergeSheet { }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLSheet.java b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
index bf2c4f25..8bd63ef0 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLSheet.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
@@ -23,7 +23,6 @@ import org.ttzero.excel.entity.style.Styles;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -46,6 +45,29 @@ public class XMLSheet implements Sheet {
 
     public XMLSheet() { }
 
+    public XMLSheet(XMLSheet sheet) {
+        this.name = sheet.name;
+        this.index = sheet.index;
+        this.path = sheet.path;
+        this.sst = sheet.sst;
+        this.styles = sheet.styles;
+        this.id = sheet.id;
+        this.startRow = sheet.startRow;
+        this.header = sheet.header;
+        this.hidden = sheet.hidden;
+        this.dimension = sheet.dimension;
+        this.drawings = sheet.drawings;
+        this.reader = sheet.reader;
+        this.cb = sheet.cb;
+        this.nChar = sheet.nChar;
+        this.length = sheet.length;
+        this.eof = sheet.eof;
+        this.heof = sheet.heof;
+        this.mark = sheet.mark;
+        this.sRow = sheet.sRow;
+        this.lastRowMark = sheet.lastRowMark;
+    }
+
     protected String name;
     protected int index; // per sheet index of workbook
 //        , size = -1; // size of rows per sheet
@@ -253,13 +275,14 @@ public class XMLSheet implements Sheet {
     }
 
     /////////////////////////////////Read sheet file/////////////////////////////////
-    private BufferedReader reader;
-    private char[] cb; // buffer
-    private int nChar, length;
+    protected BufferedReader reader;
+    protected char[] cb; // buffer
+    protected int nChar, length;
     protected boolean eof = false, heof = false; // OPTIONS = false
     protected long mark;
 
     protected XMLRow sRow;
+    protected long lastRowMark;
 
     /**
      * Load sheet.xml as BufferedReader
@@ -269,11 +292,12 @@ public class XMLSheet implements Sheet {
      */
     @Override
     public XMLSheet load() throws IOException {
-        LOGGER.debug("load {}", path.toString());
+        // Prevent multiple parsing
         if (sRow != null) {
             reset();
             return this;
         }
+        LOGGER.debug("Load {}", path.toString());
         reader = Files.newBufferedReader(path);
         cb = new char[8192];
         nChar = 0;
@@ -288,7 +312,6 @@ public class XMLSheet implements Sheet {
                 int index = line.indexOf(size), end = index > 0 ? line.indexOf('"', index += size.length()) : -1;
                 if (end > 0) {
                     String l_ = line.substring(index, end);
-                    LOGGER.debug("Dimension-Range: {}", l_);
                     Pattern pat = Pattern.compile("([A-Z]+)(\\d+):([A-Z]+)(\\d+)");
                     Matcher mat = pat.matcher(l_);
                     if (mat.matches()) {
@@ -341,6 +364,7 @@ public class XMLSheet implements Sheet {
         if (dimension == null) {
             dimension = new Dimension(1, (short) 1);
         }
+        LOGGER.debug("Dimension-Range: {}", dimension);
 
         return this;
     }
@@ -529,6 +553,7 @@ public class XMLSheet implements Sheet {
      */
     @Override
     public XMLSheet reset() {
+        LOGGER.debug("Reset {}", path.toString());
         try {
             // Close the opening reader
             if (reader != null) {
@@ -556,126 +581,111 @@ public class XMLSheet implements Sheet {
     }
 
     /*
-    If the Dimension information is missing from the header,
-    you can skip to the end of the file and look at the line
-    number of the last line to confirm the scope of the entire worksheet.
+    If the Dimension information is not write in header,
+    Read from tail and look at the line number of the last line
+    to confirm the scope of the entire worksheet.
      */
     void parseDimension() {
         try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
             long position = Files.size(path);
-            int block = (int) Math.min(1 << 11, position);
+            final int block = (int) Math.min(1 << 11, position), c = 7;
             ByteBuffer buffer = ByteBuffer.allocate(block);
             byte[] left = null;
             int left_size = 0, i;
 
-            CharBuffer charBuffer;
-            boolean eof;
+            boolean eof, getit = false;
             for (; ;) {
                 position -= block + left_size;
                 channel.position(Math.max(0, position));
                 channel.read(buffer);
-                eof = buffer.limit() < block;
                 if (left_size > 0) {
                     buffer.limit(block);
                     buffer.put(left, 0, left_size);
-                    left_size = 0;
                 }
                 buffer.flip();
+                eof = buffer.limit() < block;
 
-                charBuffer = StandardCharsets.UTF_8.decode(buffer);
-                int limit = charBuffer.limit(), c;
+                int limit = buffer.limit();
                 i = limit - 1;
 
-                if(dimension == null) {
-                    c = 7;
-                    // <row r="
-                    for (; i >= c && (charBuffer.get(i) != '"' || charBuffer.get(i - 1) != '='
-                        || charBuffer.get(i - 2) != 'r' || charBuffer.get(i - 3) > ' '
-                        || charBuffer.get(i - 4) != 'w' || charBuffer.get(i - 5) != 'o'
-                        || charBuffer.get(i - 6) != 'r' || charBuffer.get(i - 7) != '<'); i--)
-                        ;
-                } else {
-                    // <mergeCells
-                    c = 12;
-                    for (; i >= c && (charBuffer.get(i) > ' ' || charBuffer.get(i - 1) != 's'
-                        || charBuffer.get(i - 2) != 'l' || charBuffer.get(i - 3) != 'l'
-                        || charBuffer.get(i - 4) != 'e' || charBuffer.get(i - 5) != 'C'
-                        || charBuffer.get(i - 6) != 'e' || charBuffer.get(i - 7) != 'g'
-                        || charBuffer.get(i - 8) != 'r' || charBuffer.get(i - 9) != 'e'
-                        || charBuffer.get(i - 10) != 'm' || charBuffer.get(i - 11) != '<'); i--)
-                        ;
-                }
+                // <row r="
+                for (; i >= c && (buffer.get(i) != '"' || buffer.get(i - 1) != '='
+                    || buffer.get(i - 2) != 'r' || buffer.get(i - 3) > ' '
+                    || buffer.get(i - 4) != 'w' || buffer.get(i - 5) != 'o'
+                    || buffer.get(i - 6) != 'r' || buffer.get(i - 7) != '<'); i--)
+                    ;
+
                 // Not Found
                 if (i < c) {
                     if (eof || (eof = position <= 0)) break;
-                    for (; i < limit && charBuffer.get(i) != '>'; i++) ;
+                    for (; i < limit && buffer.get(i) != '>'; i++) ;
                     i++;
                     if (i < limit - 1) {
-                        charBuffer.position(i);
-                        int newLimit = StandardCharsets.UTF_8.encode(charBuffer).limit();
-                        int last_size = buffer.limit() - newLimit;
-                        if (left == null || last_size > left.length) {
-                            left = new byte[last_size];
+                        buffer.position(i);
+                        left_size = i;
+                        if (left == null || left_size > left.length) {
+                            left = new byte[left_size];
                         }
                         buffer.position(0);
-                        buffer.get(left, 0, last_size);
-                        left_size = last_size;
-                    }
+                        buffer.get(left, 0, left_size);
+                    } else left_size = 0;
                 }
                 // Found the last row or empty worksheet
                 else {
+                    getit = true;
+                    buffer.position(i);
+                    // Align channel and buffer position
+                    if (left_size > 0) channel.position(channel.position() + left_size);
+                    lastRowMark = channel.position() - buffer.remaining();
                     break;
                 }
                 buffer.position(0);
                 buffer.limit(buffer.capacity() - left_size);
             }
 
-            if (eof) return;
-
-            charBuffer.position(i);
-            // Dimension
-            if (dimension == null) {
-                parseDim(channel, charBuffer, buffer);
-                charBuffer.position(i);
+            // Empty worksheet
+            if (!getit) {
+                dimension = Dimension.of("A1");
+                return;
             }
 
-            // Find mergeCells tag
-            parseMerge(channel, charBuffer, buffer, block);
+            // Dimension
+            parseDim(channel, buffer);
         } catch (IOException e) {
             // Ignore error
             LOGGER.debug("", e);
         }
     }
 
-    private int[] innerParse(CharBuffer charBuffer, int i) {
+    private int[] innerParse(ByteBuffer buffer, int i) {
         int row = 0;
-        for (; charBuffer.get(i) != '"'; i++)
-            row = row * 10 + (charBuffer.get(i) - '0');
+        for (; buffer.get(i) != '"'; i++)
+            row = row * 10 + (buffer.get(i) - '0');
         // spans="
         i++;
-        for (; charBuffer.limit() - i >= 7 && (charBuffer.get(i) != 's' || charBuffer.get(i + 1) != 'p'
-            || charBuffer.get(i + 2) != 'a' || charBuffer.get(i + 3) != 'n'
-            || charBuffer.get(i + 4) != 's' || charBuffer.get(i + 5) != '='
-            || charBuffer.get(i + 6) != '"'); i++) ;
+        for (; buffer.limit() - i >= 7 && (buffer.get(i) != 's' || buffer.get(i + 1) != 'p'
+            || buffer.get(i + 2) != 'a' || buffer.get(i + 3) != 'n'
+            || buffer.get(i + 4) != 's' || buffer.get(i + 5) != '='
+            || buffer.get(i + 6) != '"'); i++) ;
         i += 7;
         int cs = 0, ls = 0;
-        if (charBuffer.limit() <= i) {
+        if (buffer.limit() <= i) {
             eof = true; // EOF
             return new int[] { row, 1, 1 };
         }
-        for (; charBuffer.get(i) != ':'; i++)
-            cs = cs * 10 + (charBuffer.get(i) - '0');
+        for (; buffer.get(i) != ':'; i++)
+            cs = cs * 10 + (buffer.get(i) - '0');
         i++;
-        for (; charBuffer.get(i) != '"'; i++)
-            ls = ls * 10 + (charBuffer.get(i) - '0');
+        for (; buffer.get(i) != '"'; i++)
+            ls = ls * 10 + (buffer.get(i) - '0');
         return new int[] { row, cs, ls };
     }
 
-    private void parseDim(SeekableByteChannel channel, CharBuffer charBuffer, ByteBuffer buffer) throws IOException {
+    private void parseDim(SeekableByteChannel channel, ByteBuffer buffer) throws IOException {
         long rr = 0L;
-        int rc, i = charBuffer.position();
+        int rc, i = buffer.position();
 
-        int[] info = innerParse(charBuffer, ++i);
+        int[] info = innerParse(buffer, ++i);
         rc = (info[2] << 16) | (info[1] & 0x7FFF);
         rr |= ((long) info[0]) << 32;
 
@@ -686,15 +696,14 @@ public class XMLSheet implements Sheet {
             channel.read(buffer);
             buffer.flip();
 
-            charBuffer = StandardCharsets.UTF_8.decode(buffer);
             i = 0;
-            for (; charBuffer.get(i) != '<' || charBuffer.get(i + 1) != 'r'
-                || charBuffer.get(i + 2) != 'o' || charBuffer.get(i + 3) != 'w'
-                || charBuffer.get(i + 4) > ' ' || charBuffer.get(i + 5) != 'r'
-                || charBuffer.get(i + 6) != '=' || charBuffer.get(i + 7) != '"'; i++) ;
+            for (; buffer.get(i) != '<' || buffer.get(i + 1) != 'r'
+                || buffer.get(i + 2) != 'o' || buffer.get(i + 3) != 'w'
+                || buffer.get(i + 4) > ' ' || buffer.get(i + 5) != 'r'
+                || buffer.get(i + 6) != '=' || buffer.get(i + 7) != '"'; i++) ;
             i += 8;
 
-            info = innerParse(charBuffer, i);
+            info = innerParse(buffer, i);
             rr |= info[0];
             if (info[1] > (rc & 0x7FFF)) {
                 rc |= info[1] & 0x7FFF;
@@ -707,13 +716,15 @@ public class XMLSheet implements Sheet {
         dimension = new Dimension((int) rr, (short) rc, (int) (rr >>> 32), (short) (rc >>> 16));
     }
 
-    void parseMerge(SeekableByteChannel channel, CharBuffer charBuffer, ByteBuffer buffer, int block)
-        throws IOException { }
-
     Row createHeader(char[] cb, int start, int n) {
         return createRow().init(sst, styles, startRow > 0 ? startRow : 1).with(cb, start, n);
     }
 
+    @Override
+    public XMLSheet asSheet() {
+        return (this instanceof XMLCalcSheet || this instanceof XMLMergeSheet) ? new XMLSheet(this) : this;
+    }
+
     @Override
     public XMLCalcSheet asCalcSheet() {
         return !(this instanceof XMLCalcSheet) ? new XMLCalcSheet(this) : (XMLCalcSheet) this;
@@ -732,6 +743,15 @@ public class XMLSheet implements Sheet {
     interface HeaderRowFunc {
         Row accept(char[] cb, int start, int n);
     }
+
+    // For debug
+    static void watch(ByteBuffer buffer) {
+        int p = buffer.position();
+        byte[] bytes = new byte[Math.min(1 << 7, buffer.remaining())];
+        buffer.get(bytes, 0, bytes.length);
+        System.out.println(new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII));
+        buffer.position(p);
+    }
 }
 
 /**
@@ -739,6 +759,7 @@ public class XMLSheet implements Sheet {
  */
 class XMLCalcSheet extends XMLSheet implements CalcSheet {
     private long[] calc; // Array of formula
+    boolean ready;
 
     XMLCalcSheet(XMLSheet sheet) {
         this.name = sheet.name;
@@ -746,6 +767,26 @@ class XMLCalcSheet extends XMLSheet implements CalcSheet {
         this.path = sheet.path;
         this.sst = sheet.sst;
         this.styles = sheet.styles;
+        this.id = sheet.id;
+        this.startRow = sheet.startRow;
+        this.header = sheet.header;
+        this.hidden = sheet.hidden;
+        this.dimension = sheet.dimension;
+        this.drawings = sheet.drawings;
+        this.reader = sheet.reader;
+        this.cb = sheet.cb;
+        this.nChar = sheet.nChar;
+        this.length = sheet.length;
+        this.eof = sheet.eof;
+        this.heof = sheet.heof;
+        this.mark = sheet.mark;
+        this.sRow = sheet.sRow;
+        this.lastRowMark = sheet.lastRowMark;
+
+        if (this.path != null) {
+
+            if (reader != null && !ready) this.load0();
+        }
     }
 
     /**
@@ -758,12 +799,23 @@ class XMLCalcSheet extends XMLSheet implements CalcSheet {
     public XMLCalcSheet load() throws IOException {
         super.load();
 
+        load0();
+
+        return this;
+    }
+
+    void load0() {
+        if (ready) return;
+
+        // Parse calc.xml
+        long[][] calcArray = ExcelReader.parseCalcChain(path.getParent());
+        if (calcArray != null && calcArray.length >= id) setCalc(calcArray[id - 1]);
+
         if (!eof && !(sRow instanceof XMLCalcRow)) {
             sRow = sRow.asCalcRow();
             if (calc != null) ((XMLCalcRow) sRow).setCalcFun(this::findCalc);
         }
-
-        return this;
+        ready = true;
     }
 
     /**
@@ -812,6 +864,7 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
 
     // A merge cells grid
     private Grid mergeCells;
+    boolean ready;
 
     XMLMergeSheet(XMLSheet sheet) {
         this.name = sheet.name;
@@ -819,6 +872,25 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
         this.path = sheet.path;
         this.sst = sheet.sst;
         this.styles = sheet.styles;
+        this.id = sheet.id;
+        this.startRow = sheet.startRow;
+        this.header = sheet.header;
+        this.hidden = sheet.hidden;
+        this.dimension = sheet.dimension;
+        this.drawings = sheet.drawings;
+        this.reader = sheet.reader;
+        this.cb = sheet.cb;
+        this.nChar = sheet.nChar;
+        this.length = sheet.length;
+        this.eof = sheet.eof;
+        this.heof = sheet.heof;
+        this.mark = sheet.mark;
+        this.sRow = sheet.sRow;
+        this.lastRowMark = sheet.lastRowMark;
+
+        if (path != null) {
+            if (reader != null && !ready) this.load0();
+        }
     }
 
     /**
@@ -831,74 +903,160 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
     public XMLMergeSheet load() throws IOException {
         super.load();
 
+        load0();
+
+        return this;
+    }
+
+    // Parse merge tag
+    void load0() {
+        if (ready) return;
         if (mergeCells == null && !eof) {
-            parseDimension();
+            parseMerge();
         }
 
         if (!eof && !(sRow instanceof XMLMergeRow) && mergeCells != null) {
             sRow = sRow.asMergeRow().setCopyValueFunc(this::mergeCell);
         }
-
-        return this;
+        ready = true;
     }
 
     /*
     Parse `mergeCells` tag
      */
-    @Override
-    void parseMerge(SeekableByteChannel channel, CharBuffer charBuffer, ByteBuffer buffer, int block) throws IOException {
-        // Find mergeCells tag
-        int limit = charBuffer.limit();
-        List<Dimension> mergeCells = null;
+    void parseMerge() {
+        try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
+            long position = Files.size(path);
+            final int block = (int) Math.min(1 << 11, position), c = 12;
+            ByteBuffer buffer = ByteBuffer.allocate(block);
+            byte[] left;
+            int left_size = 0, i, limit;
+            boolean eof, getit = false;
+            // Skip if marked
+            if (lastRowMark > 0L) {
+                channel.position(lastRowMark);
+                channel.read(buffer);
+                buffer.flip();
+                getit = true; // Unknown
+            } else {
+                left = new byte[12];
+                for (; ; ) {
+                    channel.position(position -= block - left_size);
+                    channel.read(buffer);
+                    if (left_size > 0) {
+                        buffer.limit(block);
+                        buffer.put(left, 0, left_size);
+                    }
+                    buffer.flip();
+                    eof = buffer.limit() < block;
+
+                    limit = buffer.limit();
+                    i = limit - 1;
+
+                    // </sheetData>
+                    for (; i >= c && (buffer.get(i) != '>' || buffer.get(i - 1) != 'a'
+                        || buffer.get(i - 2) != 't' || buffer.get(i - 3) != 'a'
+                        || buffer.get(i - 4) != 'D' || buffer.get(i - 5) != 't'
+                        || buffer.get(i - 6) != 'e' || buffer.get(i - 7) != 'e'
+                        || buffer.get(i - 8) != 'h' || buffer.get(i - 9) != 's'
+                        || buffer.get(i - 10) != '/' || buffer.get(i - 11) != '<'); i--)
+                        ;
+
+                    // Not Found
+                    if (i < c) {
+                        if (eof) break;
+                        buffer.position(0);
+                        left_size = 12;
+                        buffer.get(left, 0, left_size);
+                        buffer.clear();
+                        buffer.limit(block - left_size);
+                    }
+                    // Found the last row or empty worksheet
+                    else {
+                        buffer.position(i + 1);
+                        getit = true;
+                        // Align channel and buffer position
+                        channel.position(channel.position() + left_size);
+                        break;
+                    }
+                }
+            }
+
+            if (getit) {
+                // Find mergeCells tag
+                parseMerge(channel, buffer, block);
+            }
+        } catch (IOException e) {
+            // Ignore error
+            LOGGER.warn("", e);
+        }
+    }
+
+    // Find mergeCell tags
+    void parseMerge(SeekableByteChannel channel, ByteBuffer buffer, int block) throws IOException {
+        List<Dimension> mergeCells = new ArrayList<>();
         boolean eof = false;
-        int i = charBuffer.position();
+        int limit = buffer.limit(), i = buffer.position(), f, n;
+        byte[] bytes = new byte[32];
         for (; ;) {
-            for (; i < limit - 11 && (charBuffer.get(i) != '<' || charBuffer.get(i + 1) != 'm'
-                || charBuffer.get(i + 2) != 'e' || charBuffer.get(i + 3) != 'r'
-                || charBuffer.get(i + 4) != 'g' || charBuffer.get(i + 5) != 'e'
-                || charBuffer.get(i + 6) != 'C' || charBuffer.get(i + 7) != 'e'
-                || charBuffer.get(i + 8) != 'l' || charBuffer.get(i + 9) != 'l'
-                || charBuffer.get(i + 10) > ' '); i++) ;
-
-            if (i >= limit - 11) {
+            for (; i < limit - 11 && (buffer.get(i) != '<' || buffer.get(i + 1) != 'm'
+                || buffer.get(i + 2) != 'e' || buffer.get(i + 3) != 'r'
+                || buffer.get(i + 4) != 'g' || buffer.get(i + 5) != 'e'
+                || buffer.get(i + 6) != 'C' || buffer.get(i + 7) != 'e'
+                || buffer.get(i + 8) != 'l' || buffer.get(i + 9) != 'l'
+                || buffer.get(i + 10) > ' '); i++) ;
+
+            if (i >= limit - 22) {
                 if (eof) break;
-                for (; i >= 0 && charBuffer.get(i) != '<'; i--) ;
-                if (i > 0) channel.position(channel.position() + i);
-
-                buffer.clear();
+                buffer.position(limit - 22);
+                for (; i < buffer.limit() && buffer.get() != '<'; i++) ;
+                if (buffer.get(buffer.position() - 1) == '<') buffer.position(buffer.position() - 1);
+                buffer.compact();
                 channel.read(buffer);
                 buffer.flip();
-                if (buffer.limit() <= 0) break;
-
-                charBuffer = StandardCharsets.UTF_8.decode(buffer);
-                limit = charBuffer.limit();
+                if ((limit = buffer.limit()) <= 0) break;
                 i = 0;
                 eof = limit < block;
             } else {
                 i += 11;
-                for (; i < limit - 5 && (charBuffer.get(i) != 'r' || charBuffer.get(i + 1) != 'e'
-                    || charBuffer.get(i + 2) != 'f' || charBuffer.get(i + 3) != '='
-                    || charBuffer.get(i + 4) != '"'); i++) ;
-                if (i > limit - 5) continue;
-                int f = i += 5;
-                for (; charBuffer.get(i) != '"'; i++) ;
-                if (i > limit) continue;
-                char[] chars = new char[i - f];
-                charBuffer.position(f);
-                charBuffer.get(chars, 0, i - f);
-                if (mergeCells == null) mergeCells = new ArrayList<>();
-                mergeCells.add(Dimension.of(new String(chars)));
-                charBuffer.position(++i);
+                for (; i < limit - 5 && (buffer.get(i) != 'r' || buffer.get(i + 1) != 'e'
+                    || buffer.get(i + 2) != 'f' || buffer.get(i + 3) != '='
+                    || buffer.get(i + 4) != '"'); i++) ;
+                f = i += 5;
+                for (; i < limit && buffer.get(i) != '"'; i++) ;
+                if (i >= limit) {
+                    buffer.compact();
+                    channel.read(buffer);
+                    buffer.flip();
+                    if ((limit = buffer.limit()) <= 0) break;
+
+                    i = 0;
+                    eof = limit < block;
+                    continue;
+                }
+                n = i - f;
+                if (n > bytes.length) bytes = new byte[n];
+
+                buffer.position(f);
+                buffer.get(bytes, 0, n);
+
+                mergeCells.add(Dimension.of(new String(bytes, 0, n, StandardCharsets.US_ASCII)));
+                buffer.get(); // Skip '"'
             }
         }
 
-        if (mergeCells != null) {
+        if (!mergeCells.isEmpty()) {
             this.mergeCells = GridFactory.create(mergeCells);
-            LOGGER.debug("Grid: {}", this.mergeCells);
+            LOGGER.debug("Grid: Size: {} ==> {}", this.mergeCells.size(), this.mergeCells);
         }
     }
 
     private void mergeCell(int row, Cell cell) {
         mergeCells.merge(row, cell);
     }
+
+    @Override
+    public Grid getMergeGrid() {
+        return mergeCells;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/ttzero/excel/util/StringUtil.java b/src/main/java/org/ttzero/excel/util/StringUtil.java
index 3a2b79cb..adc351c1 100644
--- a/src/main/java/org/ttzero/excel/util/StringUtil.java
+++ b/src/main/java/org/ttzero/excel/util/StringUtil.java
@@ -180,4 +180,35 @@ public class StringUtil {
     public static boolean isNotBlank(final CharSequence cs) {
         return !isBlank(cs);
     }
+
+    /**
+     * long size to string
+     *
+     * @param size file size in bytes
+     * @return String size
+     */
+    public static String formatBinarySize(long size) {
+        long kb = 1 << 10, mb = kb << 10, gb = mb << 10;
+        String s;
+        if (size >= gb) s = String.format("%.2fGB", (double) size / gb);
+        else if (size >= mb) s = String.format("%.2fMB", (double) size / mb);
+        else if (size >= kb) s = String.format("%.2fKB", (double) size / kb);
+        else s = String.format("%dB", size);
+        return s.replace(".00", "");
+    }
+
+
+    /**
+     * Time millis to String, like 1h:3s or 4m:1s
+     *
+     * @param t time millis
+     * @return string
+     */
+    public static String timeToString(long t) {
+        int n = (int) t / 1000;
+        int h = n / 3600, m = (n - h * 3600) / 60, s = n - h * 3600 - m * 60;
+        return "" + (h > 0 ? h + "h" : "")
+            + (m > 0 ? (h > 0 ? ":" : "") + m + "m" : "")
+            + ((h + m > 0 ? ":" : "") + s + "s");
+    }
 }


Test patch:
diff --git a/src/test/java/org/ttzero/excel/entity/ValidationTest.java b/src/test/java/org/ttzero/excel/entity/ValidationTest.java
new file mode 100644
index 00000000..ef0fa9f6
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/ValidationTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2017-2019, guanquan.wang@yandex.com All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.ttzero.excel.entity;
+
+import org.junit.Test;
+import org.ttzero.excel.entity.e7.XMLWorksheetWriter;
+import org.ttzero.excel.reader.Dimension;
+import org.ttzero.excel.util.DateUtil;
+import org.ttzero.excel.util.StringUtil;
+
+import java.io.IOException;
+import java.sql.Time;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangguanquan3 at 2022-08-17 ‏‎20:05:42
+ */
+public class ValidationTest extends WorkbookTest {
+    @Test public void test() throws IOException {
+        List<Validation> validations = new ArrayList<>();
+        // 下拉框选择“男”，“女”
+        validations.add(new ListValidation<>().in("男", "女").dimension(Dimension.of("A1")));
+        // B1:E1 单元格只能输入大于1的数
+        validations.add(new WholeValidation().greaterThan(1).dimension(Dimension.of("B1:E1")));
+        // 限制日期在2022年
+        validations.add(new DateValidation().between("2022-01-01", "2022-12-31").dimension(Dimension.of("A2")));
+        // 限制时间小于下午6点（因为此时下班...）
+        validations.add(new TimeValidation().lessThan(DateUtil.toTimeValue(Time.valueOf("18:00:00"))).dimension(Dimension.of("B2")));
+        new Workbook().addSheet(new EmptySheet().putExtProp("dataValidations", validations).setSheetWriter(new ValidationWorksheetWriter())).writeTo(defaultTestPath);
+    }
+
+    public static abstract class Validation {
+        /**
+         * 允许为空
+         */
+        boolean allowBlank = true;
+        /**
+         * 显示下拉框
+         */
+        boolean showInputMessage = true;
+        /**
+         * 显示提示信息
+         */
+        boolean showErrorMessage = true;
+        /**
+         * 作用范围
+         */
+        Dimension sqref;
+        /**
+         * 操作符，不指定时默认between
+         */
+        Operator operator;
+
+        /**
+         * 数据校验类型
+         */
+        public abstract String getType();
+
+        /**
+         * 校验内容
+         */
+        public abstract String validationFormula();
+
+        @Override
+        public String toString() {
+            return "<dataValidation type=\"" + getType()
+                    + (operator == null || operator == Operator.between ? "" : "\" operator=\"" + operator)
+                    + "\" allowBlank=\"" + (allowBlank ? 1 : 0)
+                    + "\" showInputMessage=\"" + (showInputMessage ? 1 : 0)
+                    + "\" showErrorMessage=\"" + (showErrorMessage ? 1 : 0)
+                    + "\" sqref=\"" + sqref + "\">"
+                    + validationFormula()
+                    + "</dataValidation>";
+        }
+
+        public enum Operator {
+            between, notBetween, equal, notEqual, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual
+        }
+
+    }
+
+    public static class ListValidation<T> extends Validation {
+        List<T> options;
+        Dimension referer;
+
+        public ListValidation<T> in(List<T> options) {
+            this.options = options;
+            return this;
+        }
+
+        @SafeVarargs
+        public final ListValidation<T> in(T... options) {
+            this.options = Arrays.asList(options);
+            return this;
+        }
+
+        public ListValidation<T> in(Dimension referer) {
+            this.referer = referer;
+            return this;
+        }
+
+        @Override
+        public String getType() {
+            return "list";
+        }
+
+        public ListValidation<T> dimension(Dimension sqref) {
+            this.sqref = sqref;
+            return this;
+        }
+
+        @Override
+        public String validationFormula() {
+            return "<formula1>\"" + (options != null ? options.stream().map(String::valueOf).collect(Collectors.joining(",")) : referer) + "\"</formula1>";
+        }
+    }
+
+    public static abstract class Tuple2Validation<V1, V2> extends Validation {
+        V1 v1;
+        V2 v2;
+
+        public Tuple2Validation<V1, V2> equal(V1 v1) {
+            this.operator = Operator.equal;
+            this.v1 = v1;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> nowEqual(V1 v1) {
+            this.operator = Operator.notEqual;
+            this.v1 = v1;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> greaterThan(V1 v1) {
+            this.operator = Operator.greaterThan;
+            this.v1 = v1;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> greaterThanOrEqual(V1 v1) {
+            this.operator = Operator.greaterThanOrEqual;
+            this.v1 = v1;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> lessThan(V2 v2) {
+            this.operator = Operator.lessThan;
+            this.v2 = v2;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> lessThanOrEqual(V2 v2) {
+            this.operator = Operator.lessThanOrEqual;
+            this.v2 = v2;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> between(V1 v1, V2 v2) {
+            this.operator = Operator.between;
+            this.v1 = v1;
+            this.v2 = v2;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> notBetween(V1 v1, V2 v2) {
+            this.operator = Operator.notBetween;
+            this.v1 = v1;
+            this.v2 = v2;
+            return this;
+        }
+
+        public Tuple2Validation<V1, V2> dimension(Dimension sqref) {
+            this.sqref = sqref;
+            return this;
+        }
+
+        @Override
+        public String validationFormula() {
+            String v;
+            if (operator == null) operator = Operator.between;
+            switch (operator) {
+                case equal:
+                case notEqual:
+                case greaterThan:
+                case greaterThanOrEqual:
+                    v = "<formula1>" + v1 + "</formula1>";
+                    break;
+                case lessThan:
+                case lessThanOrEqual:
+                    v = "<formula1>" + v2 + "</formula1>";
+                    break;
+                default:
+                    v = "<formula1>" + v1 + "</formula1><formula2>" + v2 + "</formula2>";
+                    break;
+            }
+            return v;
+        }
+    }
+
+    public static class WholeValidation extends Tuple2Validation<Integer, Integer> {
+        @Override
+        public String getType() {
+            return "whole";
+        }
+    }
+
+    public static class TextLengthValidation extends Tuple2Validation<Integer, Integer> {
+        @Override
+        public String getType() {
+            return "textLength";
+        }
+    }
+
+    public static class DateValidation extends Tuple2Validation<Integer, Integer> {
+        public DateValidation between(Date from, Date to) {
+            if (from != null) v1 = DateUtil.toDateValue(from);
+            if (to != null) v2 = DateUtil.toDateValue(to);
+            return this;
+        }
+
+        /**
+         * @param from time in format "yyyy-MM-dd"
+         * @param to   time in format "yyyy-MM-dd"
+         */
+        public DateValidation between(String from, String to) {
+            if (StringUtil.isNotEmpty(from))
+                v1 = DateUtil.toDateValue(DateUtil.dateFormat.get().parse(from.substring(0, Math.min(from.length(), 10)), new ParsePosition(0)));
+            if (StringUtil.isNotEmpty(to))
+                v2 = DateUtil.toDateValue(DateUtil.dateFormat.get().parse(to.substring(0, Math.min(to.length(), 10)), new ParsePosition(0)));
+            return this;
+        }
+
+        @Override
+        public String getType() {
+            return "date";
+        }
+
+    }
+
+    public static class TimeValidation extends Tuple2Validation<Double, Double> {
+        public TimeValidation between(Date from, Date to) {
+            if (from != null) v1 = DateUtil.toTimeValue(from);
+            if (to != null) v2 = DateUtil.toTimeValue(to);
+            return this;
+        }
+
+        public TimeValidation between(Time from, Time to) {
+            if (from != null) v1 = DateUtil.toTimeValue(from);
+            if (to != null) v2 = DateUtil.toTimeValue(to);
+            return this;
+        }
+
+        /**
+         * @param from time in format "hh:mm:ss"
+         * @param to   time in format "hh:mm:ss"
+         */
+        public TimeValidation between(String from, String to) {
+            if (StringUtil.isNotEmpty(from)) v1 = DateUtil.toTimeValue(Time.valueOf(from));
+            if (StringUtil.isNotEmpty(to)) v2 = DateUtil.toTimeValue(Time.valueOf(to));
+            return this;
+        }
+
+        @Override
+        public String getType() {
+            return "time";
+        }
+    }
+
+
+    public static class ValidationWorksheetWriter extends XMLWorksheetWriter {
+        @Override
+        protected void afterSheetData() throws IOException {
+            super.afterSheetData();
+
+            @SuppressWarnings("unchecked")
+            List<Validation> validations = (List<Validation>) sheet.getExtPropValue("dataValidations");
+            if (validations != null && !validations.isEmpty()) {
+                bw.write("<dataValidations count=\"");
+                bw.writeInt(validations.size());
+                bw.write("\">");
+                for (Validation e : validations) {
+                    bw.write(e.toString());
+                }
+                bw.write("</dataValidations>");
+            }
+        }
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/reader/DimensionTest.java b/src/test/java/org/ttzero/excel/reader/DimensionTest.java
index c02b1d8d..d2193827 100644
--- a/src/test/java/org/ttzero/excel/reader/DimensionTest.java
+++ b/src/test/java/org/ttzero/excel/reader/DimensionTest.java
@@ -29,6 +29,8 @@ public class DimensionTest {
         assert d.firstRow == 1;
         assert d.lastColumn == 0;
         assert d.lastRow == 0;
+        assert d.width == 1;
+        assert d.height == 1;
     }
 
     @Test public void testFirstDim2() {
@@ -37,6 +39,8 @@ public class DimensionTest {
         assert d.firstRow == 3;
         assert d.lastColumn == 0;
         assert d.lastRow == 0;
+        assert d.width == 1;
+        assert d.height == 1;
     }
 
     @Test public void testLastDim() {
@@ -45,6 +49,8 @@ public class DimensionTest {
         assert d.firstRow == 0;
         assert d.lastColumn == 3;
         assert d.lastRow == 2;
+        assert d.width == 3;
+        assert d.height == 2;
     }
 
     @Test public void testFullDim() {
@@ -53,5 +59,7 @@ public class DimensionTest {
         assert d.firstRow == 1;
         assert d.lastColumn == 3;
         assert d.lastRow == 2;
+        assert d.width == 3;
+        assert d.height == 2;
     }
 }
diff --git a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
index 0d37c147..1d86590e 100644
--- a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
+++ b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
@@ -552,6 +552,113 @@ public class ExcelReaderTest {
         }
     }
 
+    @Test public void testLargeMerge() throws IOException {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("largeMerged.xlsx"))) {
+            MergeSheet mergeSheet = reader.sheet(0).asMergeSheet();
+            Grid grid = mergeSheet.getMergeGrid();
+            assert grid.size() == 2608;
+            assert grid.test(3, 1);
+            assert grid.test(382, 1);
+            assert grid.test(722, 2);
+            assert grid.test(1374, 2);
+            assert grid.test(2101, 10);
+            assert grid.test(2201, 6);
+            assert !grid.test(2113, 5);
+            long count = mergeSheet.rows().count();
+
+            Sheet sheet = mergeSheet.asCalcSheet();
+            assert sheet.getClass() == XMLCalcSheet.class;
+            assert sheet.reset().rows().count() == count;
+
+            sheet = sheet.asSheet();
+            assert sheet.getClass() == XMLSheet.class;
+            assert sheet.reset().rows().count() == count;
+
+            sheet = sheet.asMergeSheet();
+            assert sheet.getClass() == XMLMergeSheet.class;
+            assert sheet.reset().rows().count() == count;
+
+        }
+    }
+
+    @Test public void testSheetConvert() throws IOException {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("formula.xlsx"))) {
+            Sheet sheet = reader.sheet(0);
+            // Read two rows as value only
+            Iterator<Row> ite = sheet.iterator();
+            for (Iterator<Row> it = sheet.iterator(); it.hasNext(); ) {
+                Row row = it.next();
+                for (int i = row.fc; i < row.lc; i++) {
+                    print(int2Col(i + 1));
+                    print(row.getRowNum());
+                    print(": v=");
+                    print(row.getString(i));
+                    print(" | ");
+
+                    assert !row.hasFormula(i);
+                }
+                if (row.getRowNum() == 1) assert "D".equals(row.getString(3));
+                if (row.getRowNum() == 2) assert StringUtil.isEmpty(row.getString(3));
+
+                println();
+                if (row.getRowNum() == 2) break;
+            }
+
+            // Read next 48 rows as calc sheet
+            sheet = sheet.asCalcSheet();
+            for (Iterator<Row> it = sheet.iterator(); it.hasNext(); ) {
+                Row row = it.next();
+                for (int i = row.fc; i < row.lc; i++) {
+                    print(int2Col(i + 1));
+                    print(row.getRowNum());
+                    print(": v=");
+                    print(row.getString(i));
+                    if (row.hasFormula(i)) {
+                        print(", f=");
+                        print(row.getFormula(i));
+                    }
+                    print(" | ");
+                }
+                if (row.getRowNum() == 3) assert "SUM(A1:A10)".equals(row.getFormula(2));
+                if (row.getRowNum() == 11) assert "G11+1".equals(row.getFormula(7));
+                if (row.getRowNum() == 66) assert "A66+1".equals(row.getFormula(1));
+                if (row.getRowNum() == 11) assert row.getInt(4) == 15;
+                if (row.getRowNum() == 16) assert StringUtil.isEmpty(row.getString(4));
+
+                println();
+                if (row.getRowNum() == 50) break;
+            }
+
+            // Read last rows as merged sheet
+            sheet = sheet.asMergeSheet();
+            for (Iterator<Row> it = sheet.iterator(); it.hasNext(); ) {
+                Row row = it.next();
+                for (int i = row.fc; i < row.lc; i++) {
+                    print(int2Col(i + 1));
+                    print(row.getRowNum());
+                    print(": v=");
+                    print(row.getString(i));
+                    print(" | ");
+
+                    assert !row.hasFormula(i);
+                }
+                println();
+
+                // Copy on merged
+                if (row.getRowNum() == 56) {
+                    assert row.getInt(1) == 57;
+                    assert row.getInt(2) == 57;
+                    assert row.getInt(3) == 57;
+                }
+                if (row.getRowNum() >= 59 && row.getRowNum() <= 64) {
+                    assert row.getInt(0) == 59;
+                    if (row.getRowNum() > 59) assert row.getInt(1) == 1; // formula=A60+1
+                    else assert row.getInt(1) == 60; // formula=A59+1
+                }
+            }
+        }
+    }
+
     public static class O {
         @ExcelColumn("亚马逊FBA子单号/箱唛号")
         private String fbaNo;
diff --git a/src/test/java/org/ttzero/excel/reader/GridTest.java b/src/test/java/org/ttzero/excel/reader/GridTest.java
index 1a64afc0..2983e763 100644
--- a/src/test/java/org/ttzero/excel/reader/GridTest.java
+++ b/src/test/java/org/ttzero/excel/reader/GridTest.java
@@ -39,7 +39,7 @@ public class GridTest {
         assert grid instanceof Grid.FastGrid;
 
         grid = GridFactory.create(Collections.singletonList(Dimension.of("A1:B16384")));
-        assert grid instanceof Grid.FractureGrid;
+        assert grid instanceof Grid.FastGrid;
 
     }
 
@@ -100,7 +100,7 @@ public class GridTest {
 
         Grid grid = GridFactory.create(list);
 
-        assert grid.toString().equals("FastGrid\n" +
+        assert grid.toString().equals("FastGrid Size: 72B\n" +
             "00011001\n00000001\n00000001\n00000001\n" +
             "00001101\n00001101\n00001101\n00001101\n" +
             "00001101\n00001101\n00001101\n00001101\n" +
@@ -165,8 +165,9 @@ public class GridTest {
     @Test public void testLinkedScanner() {
         Grid.Scanner scanner = new Grid.LinkedScanner();
         scanner.put(new Grid.LinkedScanner.E(Dimension.of("E5:F8"), null));
-        scanner.put(new Grid.LinkedScanner.E(Dimension.of("B2:C2"), null));
+        scanner.put(new Grid.LinkedScanner.E(Dimension.of("D2:F2"), null));
         scanner.put(new Grid.LinkedScanner.E(Dimension.of("B16:E17"), null));
+        scanner.put(new Grid.LinkedScanner.E(Dimension.of("B2:C2"), null));
         scanner.put(new Grid.LinkedScanner.E(Dimension.of("A13:A20"), null));
 
         // Test iterator
@@ -174,10 +175,10 @@ public class GridTest {
             println(entry.getDim());
         }
 
-        assert "B2:C2->E5:F8->A13:A20->B16:E17".equals(scanner.toString());
+        assert "B2:C2->D2:F2->E5:F8->A13:A20->B16:E17".equals(scanner.toString());
 
         scanner.get(5, 5);
-        assert "E5:F8->B2:C2->A13:A20->B16:E17".equals(scanner.toString());
+        assert "E5:F8->B2:C2->D2:F2->A13:A20->B16:E17".equals(scanner.toString());
 
         scanner.get(5, 6);
         scanner.get(6, 5);
@@ -187,7 +188,7 @@ public class GridTest {
         scanner.get(8, 5);
         scanner.get(8, 6);
 
-        assert "B2:C2->A13:A20->B16:E17->E5:F8".equals(scanner.toString());
+        assert "B2:C2->D2:F2->A13:A20->B16:E17->E5:F8".equals(scanner.toString());
     }
 
     @Test public void testIndexGrid() {