Repository: wangguanquan/eec
Pull request #92 (2020-02-24 02:44:58)

Fix#51
1. Worksheet增加`getDimension`方法返回单元格范围，替换原`getSize`方法
2. 支持获取单元格公式
3. 修复SNAPSHOT版导出excel文件无法正常打开的错误
4. ExcelReader增加打开模式，可以指定读取单元格公式或者`合并单元格`的值。


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index b49830c0..7577718c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,13 @@
+Version 0.3.7 ()
+-------------
+1. Worksheet增加`getDimension`方法返回单元格范围，替换原`getSize`方法
+2. 支持获取单元格公式
+3. 修复SNAPSHOT版导出excel文件无法正常打开的错误
+4. ExcelReader增加打开模式，可以指定读取单元格公式或者`合并单元格`的值。
+
 Version 0.3.6 (2019-11-21)
 -------------
-1. Hotfix读取空单元格下可能抛NPE异常
+1. Hotfix读取空单元格时可能抛NPE异常
 
 Version 0.3.5 (2019-11-20)
 -------------
diff --git a/src/main/java/org/ttzero/excel/reader/Cell.java b/src/main/java/org/ttzero/excel/reader/Cell.java
index 144cdab7..29ff46a9 100644
--- a/src/main/java/org/ttzero/excel/reader/Cell.java
+++ b/src/main/java/org/ttzero/excel/reader/Cell.java
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
  */
 public class Cell {
     public Cell() { }
+    public Cell(short i) { this.i = i; }
     public static final char SST         = 's';
     public static final char BOOL        = 'b';
     public static final char FUNCTION    = 'f';
@@ -36,6 +37,7 @@ public class Cell {
     public static final char DATETIME    = 'i';
     public static final char DATE        = 'a';
     public static final char TIME        = 't';
+    public static final char EMPTY_TAG   = 'e';
     /**
      * Value type
      * n=numeric
@@ -91,6 +93,10 @@ public class Cell {
      * Has formula
      */
     public boolean f;
+    /**
+     * y-axis of cell in row
+     */
+    public transient short i;
 
     public void setT(char t) {
         this.t = t;
@@ -151,7 +157,7 @@ public class Cell {
     }
 
     public void clear() {
-        this.t = '\0';
+        this.t  = '\0';
         this.sv = null;
         this.nv = 0;
         this.dv = 0.0;
@@ -161,7 +167,24 @@ public class Cell {
         this.mv = null;
         this.xf = 0;
         this.fv = null;
-        this.f = false;
+        this.f  = false;
         this.si = -1;
     }
+
+    public Cell from(Cell cell) {
+        this.t  = cell.t;
+        this.sv = cell.sv;
+        this.nv = cell.nv;
+        this.dv = cell.dv;
+        this.bv = cell.bv;
+        this.lv = cell.lv;
+        this.cv = cell.cv;
+        this.mv = cell.mv;
+        this.xf = cell.xf;
+        this.fv = cell.fv;
+        this.f  = cell.f;
+        this.si = cell.si;
+
+        return this;
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/Dimension.java b/src/main/java/org/ttzero/excel/reader/Dimension.java
index 2c9cd6b2..8934a308 100644
--- a/src/main/java/org/ttzero/excel/reader/Dimension.java
+++ b/src/main/java/org/ttzero/excel/reader/Dimension.java
@@ -49,7 +49,7 @@ public class Dimension {
      * @param range range string like {@code A2:B2}
      * @return the {@link Dimension} entry
      */
-    public static Dimension from(String range) {
+    public static Dimension of(String range) {
         int i = range.indexOf(':');
         if (i < 0 || i == range.length() - 1)
             throw new IllegalArgumentException(range + " can't convert to Dimension.");
@@ -102,4 +102,15 @@ public class Dimension {
         return new String(int2Col(firstColumn)) + this.firstRow
             + ":" + new String(int2Col(lastColumn)) + this.lastRow;
     }
+
+    /**
+     * Determine whether the specified coordinates are within the {@link Dimension}
+     *
+     * @param r row number
+     * @param c column number
+     * @return true if axis is in {@link Dimension}
+     */
+    public boolean checkRange(int r, int c) {
+        return r >= firstRow && r <= lastRow && c >= firstColumn && c <= lastColumn;
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index e3f94bb0..e8d2af44 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -81,6 +81,19 @@ import static org.ttzero.excel.util.StringUtil.isNotEmpty;
 public class ExcelReader implements AutoCloseable {
     private Logger LOGGER = LogManager.getLogger(getClass());
 
+    /**
+     * Specify {@link ExcelReader} only parse cell value (Default)
+     */
+    public static final int VALUE_ONLY = 0;
+    /**
+     * Parse cell value and calc
+     */
+    public static final int VALUE_AND_CALC = 1 << 1;
+    /**
+     * Copy value on merge cells
+     */
+    public static final int COPY_ON_MERGED = 1 << 2;
+
     protected ExcelReader() { }
 
     protected Path self;
@@ -88,7 +101,6 @@ public class ExcelReader implements AutoCloseable {
     private Path temp;
     private ExcelType type;
     private AppInfo appInfo;
-    boolean hasFormula;
 
     /**
      * The Shared String Table
@@ -96,9 +108,20 @@ public class ExcelReader implements AutoCloseable {
     private SharedStrings sst;
 
     /**
-     * The {@link Styles}
+     * Reader Option
+     * 0: only parse cell value (default)
+     * 2: parse cell value and calc
+     * 4: copy value on merge cells
+     *
+     * These attributes can be combined via `|`,
+     * like: VALUE_ONLY|COPY_ON_MERGED
      */
-    private Styles styles;
+    int option;
+
+    /**
+     * A formula flag
+     */
+    boolean hasFormula;
 
     /**
      * Constructor Excel Reader
@@ -108,7 +131,7 @@ public class ExcelReader implements AutoCloseable {
      * @throws IOException if path not exists or I/O error occur
      */
     public static ExcelReader read(Path path) throws IOException {
-        return read(path, 0, 0);
+        return read(path, 0, 0, VALUE_ONLY);
     }
 
     /**
@@ -119,7 +142,31 @@ public class ExcelReader implements AutoCloseable {
      * @throws IOException if I/O error occur
      */
     public static ExcelReader read(InputStream stream) throws IOException {
-        return read(stream, 0, 0);
+        return read(stream, 0, 0, VALUE_ONLY);
+    }
+
+    /**
+     * Constructor Excel Reader
+     *
+     * @param path the excel path
+     * @param option the reader option.
+     * @return the {@link ExcelReader}
+     * @throws IOException if path not exists or I/O error occur
+     */
+    public static ExcelReader read(Path path, int option) throws IOException {
+        return read(path, 0, 0, option);
+    }
+
+    /**
+     * Constructor Excel Reader
+     *
+     * @param stream the {@link InputStream} of excel
+     * @param option the reader option.
+     * @return the {@link ExcelReader}
+     * @throws IOException if I/O error occur
+     */
+    public static ExcelReader read(InputStream stream, int option) throws IOException {
+        return read(stream, 0, 0, option);
     }
 
     /**
@@ -128,11 +175,12 @@ public class ExcelReader implements AutoCloseable {
      * @param path       the excel path
      * @param bufferSize the {@link SharedStrings} buffer size. default is 512
      *                   This parameter affects the number of read times.
+     * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws IOException if path not exists or I/O error occur
      */
-    public static ExcelReader read(Path path, int bufferSize) throws IOException {
-        return read(path, bufferSize, 0);
+    public static ExcelReader read(Path path, int bufferSize, int option) throws IOException {
+        return read(path, bufferSize, 0, option);
     }
 
     /**
@@ -141,11 +189,12 @@ public class ExcelReader implements AutoCloseable {
      * @param stream     the {@link InputStream} of excel
      * @param bufferSize the {@link SharedStrings} buffer size. default is 512
      *                   This parameter affects the number of read times.
+     * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws IOException if I/O error occur
      */
-    public static ExcelReader read(InputStream stream, int bufferSize) throws IOException {
-        return read(stream, bufferSize, 0);
+    public static ExcelReader read(InputStream stream, int bufferSize, int option) throws IOException {
+        return read(stream, bufferSize, 0, option);
     }
 
     /**
@@ -155,11 +204,12 @@ public class ExcelReader implements AutoCloseable {
      * @param bufferSize the {@link SharedStrings} buffer size. default is 512
      *                   This parameter affects the number of read times.
      * @param cacheSize  the {@link Cache} size, default is 512
+     * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws IOException if path not exists or I/O error occur
      */
-    public static ExcelReader read(Path path, int bufferSize, int cacheSize) throws IOException {
-        return read(path, bufferSize, cacheSize, false);
+    public static ExcelReader read(Path path, int bufferSize, int cacheSize, int option) throws IOException {
+        return read(path, bufferSize, cacheSize, false, option);
     }
 
     /**
@@ -169,16 +219,17 @@ public class ExcelReader implements AutoCloseable {
      * @param bufferSize the {@link SharedStrings} buffer size. default is 512
      *                   This parameter affects the number of read times.
      * @param cacheSize  the {@link Cache} size, default is 512
+     * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws IOException if I/O error occur
      */
-    public static ExcelReader read(InputStream stream, int bufferSize, int cacheSize) throws IOException {
+    public static ExcelReader read(InputStream stream, int bufferSize, int cacheSize, int option) throws IOException {
         Path temp = FileUtil.mktmp(Const.EEC_PREFIX);
         if (temp == null) {
             throw new IOException("Create temp directory error. Please check your permission");
         }
         FileUtil.cp(stream, temp);
-        return read(temp, bufferSize, cacheSize, true);
+        return read(temp, bufferSize, cacheSize, true, option);
     }
 
     /**
@@ -306,17 +357,59 @@ public class ExcelReader implements AutoCloseable {
      * @return boolean
      */
     public boolean hasFormula() {
-        return hasFormula;
+        return this.hasFormula;
     }
 
     /**
      * Make the reader parse formula
      *
-     * @return self
+     * @return {@link ExcelReader}
      */
     public ExcelReader parseFormula() {
-        for (Sheet sheet : sheets) {
-            sheet.parseFormula();
+        if (!hasFormula()) return this;
+        // Formula string if exists
+        Path calcPath = self.resolve("xl/calcChain.xml");
+        long[][] calcArray = parseCalcChain(calcPath, sheets[sheets.length - 1].getIndex());
+
+        if (calcArray == null) return this;
+        int i = 0;
+        for (int n; i < sheets.length; i++) {
+            n = sheets[i].getIndex();
+            if (calcArray[n - 1] == null) continue;
+            if (!(sheets[i] instanceof  CalcSheet)) {
+                sheets[i] = sheets[i].asCalcSheet();
+            }
+            if (sheets[i] instanceof XMLCalcSheet) {
+                ((XMLCalcSheet) sheets[i]).setCalc(calcArray[n - 1]);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Copy values when reading merged cells.
+     * <p>
+     * By default, the values of the merged cells are only
+     * stored in the first Cell, and other cells have no values.
+     * Call this method to copy the value to other cells in the merge.
+     * <blockquote><pre>
+     * |---------|     |---------|     |---------|
+     * |         |     |  1 |    |     |  1 |  1 |
+     * |    1    |  =&gt; |----|----|  =&gt; |----|----|
+     * |         |     |    |    |     |  1 |  1 |
+     * |---------|     |---------|     |---------|
+     * Merged(A1:B2)     Default           Copy
+     *                  Value in A1
+     *                  others are
+     *                  `null`
+     * </pre></blockquote>
+     *
+     * @return {@link ExcelReader}
+     */
+    public ExcelReader copyOnMergeCells() {
+        for (int i = 0; i < sheets.length; i++) {
+            if (sheets[i] instanceof MergeSheet) continue;
+            sheets[i] = sheets[i].asMergeSheet();
         }
         return this;
     }
@@ -324,7 +417,7 @@ public class ExcelReader implements AutoCloseable {
     // --- PRIVATE FUNCTIONS
 
 
-    private ExcelReader(Path path, int bufferSize, int cacheSize) throws IOException {
+    private ExcelReader(Path path, int bufferSize, int cacheSize, int option) throws IOException {
         // Store template stream as zip file
         Path tmp = FileUtil.mktmp(Const.EEC_PREFIX);
         ZipUtil.unzip(Files.newInputStream(path), tmp);
@@ -373,6 +466,8 @@ public class ExcelReader implements AutoCloseable {
 
         // Load Styles
         Path s = tmp.resolve("xl/styles.xml");
+
+        Styles styles;
         if (Files.exists(s)) {
             styles = Styles.load(s);
         } else {
@@ -380,12 +475,15 @@ public class ExcelReader implements AutoCloseable {
             throw new ExcelReadException("The file format is incorrect or corrupted. [xl/styles.xml]");
         }
 
+        this.option = option;
+        hasFormula = Files.exists(tmp.resolve("xl/calcChain.xml"));
+
         List<Sheet> sheets = new ArrayList<>();
         @SuppressWarnings("unchecked")
         Iterator<Element> sheetIter = root.element("sheets").elementIterator();
         for (; sheetIter.hasNext(); ) {
             Element e = sheetIter.next();
-            XMLSheet sheet = new XMLSheet();
+            XMLSheet sheet = sheetFactory(option);
             sheet.setName(e.attributeValue("name"));
             sheet.setIndex(Integer.parseInt(e.attributeValue("sheetId")));
             String state = e.attributeValue("state");
@@ -409,28 +507,18 @@ public class ExcelReader implements AutoCloseable {
             throw new ExcelReadException("The file format is incorrect or corrupted. [There has no worksheet]");
         }
 
-        // Formula string if exists
-        Path calcPath = tmp.resolve("xl/calcChain.xml");
-        long[][] calcArray = null;
-        if (Files.exists(calcPath)) {
-            calcArray = parseCalcChain(calcPath, sheets.size());
-        }
-
         // sort by sheet index
         sheets.sort(Comparator.comparingInt(Sheet::getIndex));
 
         Sheet[] sheets1 = new Sheet[sheets.size()];
         sheets.toArray(sheets1);
-        if (calcArray != null) {
-            i = 0;
-            for (; i < sheets1.length; i++) {
-                ((XMLSheet) sheets1[i]).setCalc(calcArray[i]);
-            }
-            hasFormula = true;
-        }
 
         this.sheets = sheets1;
         self = tmp;
+
+        if ((option >> 1 & 1) == 1) {
+            parseFormula();
+        }
     }
 
     /**
@@ -441,16 +529,17 @@ public class ExcelReader implements AutoCloseable {
      *                   This parameter affects the number of read times.
      * @param cacheSize  the {@link Cache} size, default is 512
      * @param rmSource   remove the source files
+     * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws IOException if path not exists or I/O error occur
      */
-    private static ExcelReader read(Path path, int bufferSize, int cacheSize, boolean rmSource) throws IOException {
+    private static ExcelReader read(Path path, int bufferSize, int cacheSize, boolean rmSource, int option) throws IOException {
         // Check document type
         ExcelType type = getType(path);
         ExcelReader er;
         switch (type) {
             case XLSX:
-                er = new ExcelReader(path, bufferSize, cacheSize);
+                er = new ExcelReader(path, bufferSize, cacheSize, option);
                 break;
             case XLS:
                 try {
@@ -474,6 +563,21 @@ public class ExcelReader implements AutoCloseable {
         return er;
     }
 
+    private XMLSheet sheetFactory(int option) {
+        XMLSheet sheet;
+        switch (option) {
+            // Value only
+            case VALUE_ONLY: sheet = new XMLSheet(); break;
+            case VALUE_AND_CALC:
+                sheet = hasFormula() ? new XMLCalcSheet() : new XMLSheet(); break;
+            case COPY_ON_MERGED: sheet = new XMLMergeSheet(); break;
+            // TODO full reader
+//            case VALUE_AND_CALC|COPY_ON_MERGED: break;
+            default: sheet = new XMLSheet();
+        }
+        return sheet;
+    }
+
     /**
      * Check the documents type
      *
@@ -649,7 +753,7 @@ public class ExcelReader implements AutoCloseable {
             else if (value >= 'a' && value <= 'z') {
                 v = v * 26 + value - 'a' + 1;
             }
-            else if (value >= '0') {
+            else if (value >= '0' && value <= '9') {
                 n = n * 10 + value - '0';
             }
             else
diff --git a/src/main/java/org/ttzero/excel/reader/Grid.java b/src/main/java/org/ttzero/excel/reader/Grid.java
index 44fa0a70..1b84f083 100644
--- a/src/main/java/org/ttzero/excel/reader/Grid.java
+++ b/src/main/java/org/ttzero/excel/reader/Grid.java
@@ -16,6 +16,15 @@
 
 package org.ttzero.excel.reader;
 
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.StringJoiner;
+
+import static java.lang.Integer.numberOfTrailingZeros;
+import static org.ttzero.excel.reader.Cell.EMPTY_TAG;
+
 /**
  * @author guanquan.wang at 2020-01-09 16:54
  */
@@ -25,56 +34,438 @@ interface Grid {
      * Mark `1` at the specified coordinates
      *
      * @param coordinate the excel coordinate string,
-     *                   it's a coordinate or range coordinates like `A1` or `A1:C4`
+     *                   it's a coordinate or range coordinates
+     *                   like `A1` or `A1:C4`
      */
     default void mark(String coordinate) {
-        mark(coordinate.toCharArray(), 0, coordinate.length());
+        mark(Dimension.of(coordinate));
     }
 
     /**
      * Mark `1` at the specified coordinates
      *
      * @param chars the excel coordinate buffer,
-     *              it's a coordinate or range coordinates like `A1` or `A1:C4`
-     * @param from the begin index
-     * @param to the end index
+     *              it's a coordinate or range coordinates
+     *              like `A1` or `A1:C4`
+     * @param from  the begin index
+     * @param to    the end index
      */
-    void mark(char[] chars, int from, int to);
-}
+    default void mark(char[] chars, int from, int to) {
+        mark(Dimension.of(new String(chars, from, to - from + 1)));
+    }
 
-abstract class AbstractGrid implements Grid {
-    int r, c; // Start index of Row and Column(One base)
+    /**
+     * Mark `1` at the specified {@link Dimension}
+     *
+     * @param dimension range {@link Dimension}
+     */
+    void mark(Dimension dimension);
 
-}
+    /**
+     * Test the specified {@link Dimension} has be marked.
+     *
+     * @param r row number (from one)
+     * @param c column number (from one)
+     * @return true if the specified dimension has be marked
+     */
+    boolean test(int r, int c);
+
+    /**
+     * Merge cell in range cells
+     *
+     * @param r row number (from one)
+     * @param cell column number (from one)
+     */
+    void merge(int r, Cell cell);
+
+
+    /**
+     * Use binary to mark whether the cells are `merged` and set
+     * them accordingly if they are merged, so that you can quickly
+     * mark and check the cell status and save space.
+     */
+    final class FastGrid implements Grid {
+        private final int fr, fc, lr, lc; // Start index of Row and Column(One base)
+        private final long[] g;
+
+        private final int c;
+        private final Scanner scanner;
 
-class FractureGrid implements Grid {
+        FastGrid(Dimension dim) {
+            fr = dim.firstRow;
+            lr = dim.lastRow;
+            fc = dim.firstColumn;
+            lc = dim.lastColumn;
 
-    @Override
-    public void mark(char[] chars, int from, int to) {
+            // Power of two minus 1
+            int nc = lc - fc + 1, nr = lr - fr + 1, b = powerOneBit(nc);
+            c = numberOfTrailingZeros(b + 1) + (isPowerOfTwo(nc) ? -1 : 0);
+            int n = 6 - c, len = nr >> n;
+            g = new long[len > 0 ? nr > (len << n) ? len + 1 : len : 1];
 
+            scanner = new FastLinkedScanner();
+        }
+
+        static int powerOneBit(int i) {
+            i |= (i >>  1);
+            i |= (i >>  2);
+            i |= (i >>  4);
+            i |= (i >>  8);
+            i |= (i >> 16);
+            return i;
+        }
+
+        static boolean isPowerOfTwo(int n) {
+            return (n & -n) == n; // OR (n & n - 1) == 0;
+        }
+
+        @Override
+        public void mark(Dimension dimension) {
+            int n = dimension.lastColumn - dimension.firstColumn + 1
+                , p = 1 << (6 - c);
+            long l = ~(~0L >> n << n) << (dimension.firstColumn - fc);
+            for (int i = dimension.firstRow; i <= dimension.lastRow; i++)
+                g[getRow(i)] |= l << ((p - ((i - fr + 1) & (p - 1))) << c);
+
+            // Create index on the first axis
+            scanner.put(new LinkedScanner.E(dimension, new Cell()));
+        }
+
+        @Override
+        public boolean test(int r, int c) {
+            if (!range(r, c)) return false;
+            long l = g[getRow(r)];
+            int p = 1 << (6 - this.c);
+            l >>= ((p - ((r - fr + 1) & (p - 1))) << this.c);
+            l >>= (c - fc);
+            return (l & 1) == 1;
+        }
+
+        @Override
+        public void merge(int r, Cell cell) {
+            if (!test(r, cell.i)) return;
+
+            Scanner.Entry e = scanner.get(r, cell.i);
+            if (cell.t == EMPTY_TAG) {
+                // Copy value from the first merged cell
+                cell.from(e.getCell());
+            }
+            // Current cell has value
+            else {
+                e.getCell().from(cell);
+            }
+        }
+
+        boolean range(int r, int c) {
+            return r >= fr && r <= lr && c >= fc && c <= lc;
+        }
+
+        int getRow(int i) {
+            return (i - fr) >> (6 - c);
+        }
+
+        @Override
+        public String toString() {
+            StringJoiner joiner = new StringJoiner("\n");
+            joiner.add(getClass().getSimpleName());
+            int last = lr - fr + 1, j = 0;
+            A: for (long l : g) {
+                String s = append(Long.toBinaryString(l));
+                for (int i = 0, n = 1 << 6 - c; i < n; i++) {
+                    joiner.add(s.substring(i << c, (i + 1) << c));
+                    if (++j >= last) break A;
+                }
+            }
+            return joiner.toString();
+        }
+
+        private char[] chars = new char[64];
+
+        private String append(String s) {
+            int n = s.length();
+            s.getChars(0, n, chars, chars.length - n);
+            Arrays.fill(chars, 0, chars.length - n, '0');
+            return new String(chars);
+        }
+
+        private static class FastLinkedScanner extends LinkedScanner {
+            public Entry get(int r, int c) {
+                if (size() == 1) return head.entry;
+                return super.get(r, c);
+            }
+        }
     }
-}
 
-class LongGrid extends AbstractGrid {
+    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;
+        IndexGrid(Dimension dim, int n) {
+            fr = dim.firstRow;
+            lr = dim.lastRow;
+            fc = dim.firstColumn;
+            lc = dim.lastColumn;
+
+            index = new HashMap<>(n);
+        }
 
-    @Override
-    public void mark(char[] chars, int from, int to) {
+        @Override
+        public void mark(Dimension dim) {
+            Cell cell = new Cell();
+            for (int i = dim.firstRow; i <= dim.lastRow; i++) {
+                for (int j = dim.firstColumn; j <= dim.lastColumn; j++) {
+                    index.put(((long) i) << 16 | j, cell);
+                }
+            }
+        }
 
+        @Override
+        public boolean test(int r, int c) {
+            return range(r, c) && index.containsKey(((long) r) << 16 | c);
+        }
+
+        @Override
+        public void merge(int r, Cell cell) {
+            if (!range(r, cell.i)) return;
+            Cell c = index.get(((long) r) << 16 | cell.i);
+
+            if (cell.t == EMPTY_TAG) {
+                // Copy value from the first merged cell
+                cell.from(c);
+            }
+            // Current cell has value
+            else {
+                c.from(cell);
+            }
+        }
+
+        boolean range(int r, int c) {
+            return r >= fr && r <= lr && c >= fc && c <= lc;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + " has " + index.size() +" keys";
+        }
     }
-}
 
-class IntegerGrid extends AbstractGrid {
+    final class FractureGrid implements Grid {
+        private final int fr, fc, lr, lc; // Start index of Row and Column(One base)
+        private final LinkedScanner scanner;
 
-    @Override
-    public void mark(char[] chars, int from, int to) {
+        FractureGrid(Dimension dim) {
+            fr = dim.firstRow;
+            lr = dim.lastRow;
+            fc = dim.firstColumn;
+            lc = dim.lastColumn;
 
+            scanner = new LinkedScanner();
+        }
+
+        @Override
+        public void mark(Dimension dim) {
+            scanner.put(new LinkedScanner.E(dim, new Cell()));
+        }
+
+        @Override
+        public boolean test(int r, int c) {
+            return range(r, c) && scanner.get(r, c) != null;
+        }
+
+        @Override
+        public void merge(int r, Cell cell) {
+            if (!range(r, cell.i)) return;
+            Scanner.Entry e = scanner.get(r, cell.i);
+            if (e == null) return;
+
+            if (cell.t == EMPTY_TAG) {
+                // Copy value from the first merged cell
+                cell.from(e.getCell());
+            }
+            // Current cell has value
+            else {
+                e.getCell().from(cell);
+            }
+        }
+
+        boolean range(int r, int c) {
+            return r >= fr && r <= lr && c >= fc && c <= lc;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + " has " + scanner.size() +" dimensions";
+        }
     }
-}
 
-class ShortGrid extends AbstractGrid {
 
-    @Override
-    public void mark(char[] chars, int from, int to) {
+    interface Scanner extends Iterable<Scanner.Entry> {
+
+        void put(Entry entry);
+
+        Entry get(int r, int c);
+
+        int size();
+
+        interface Entry {
+            Dimension getDim();
+
+            Cell getCell();
+        }
+    }
+
+
+    class LinkedScanner implements Scanner {
+
+        final static class E implements Entry {
+            private Dimension dim;
+            private Cell cell;
+            private int n;
+
+            E(Dimension dim, Cell cell) {
+                this.dim = dim;
+                this.cell = cell;
+                n = (dim.lastRow - dim.firstRow + 1) * (dim.lastColumn - dim.firstColumn + 1);
+            }
+
+            @Override
+            public Dimension getDim() {
+                return dim;
+            }
+
+            @Override
+            public Cell getCell() {
+                return cell;
+            }
+        }
+
+        private static class Node {
+            private Node next;
+            private E entry;
 
+            Node(E entry, Node next) {
+                this.entry = entry;
+                this.next = next;
+            }
+        }
+
+        Node head, tail;
+        private int size;
+
+        @Override
+        public void put(Entry entry) {
+            E e;
+            if (entry instanceof E) e = (E) entry;
+            else e = new E(entry.getDim(), entry.getCell());
+
+            if (head != null) {
+                Node f = head, bf = null;
+                for (; f != null; f = f.next) {
+                    if (f.entry.getDim().firstRow > entry.getDim().firstRow) {
+                        Node newNode = new Node(e, f);
+                        if (f == head) head = newNode;
+                        else bf.next = newNode;
+                        break;
+                    }
+                    bf = f;
+                }
+
+                if (f == null) {
+                    tail = bf.next = new Node(e, null);
+                }
+            } else {
+                head = tail = new Node(e, null);
+            }
+            size++;
+        }
+
+        public Entry get(int r, int c) {
+            Node val = null;
+            if (head == null) return null;
+            Node f = head, bf = null;
+            for (; f != null; f = f.next) {
+                if (f.entry.getDim().checkRange(r, c)) {
+                    val = f;
+                    break;
+                }
+                bf = f;
+            }
+
+            if (val != null) {
+                int n = --val.entry.n;
+                // Insert entry ahead
+                if (n > 0 && val != head) {
+                    bf.next = val.next;
+                    val.next = head;
+                    head = val;
+                    // Move entry back
+                } else if (n == 0) {
+                    head = val.next;
+                    val.next = null;
+                    tail.next = val;
+                    tail = val;
+                }
+            }
+            // Not Found, it never occur
+            else return null;
+
+            return val.entry;
+        }
+
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public Iterator<Entry> iterator() {
+            return new ForwardIterator(head);
+        }
+
+        private static class ForwardIterator implements Iterator<LinkedScanner.Entry> {
+            private Node first;
+            ForwardIterator(Node first) {
+                this.first = first;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return first != null;
+            }
+            @Override
+            public Entry next() {
+                Entry e = first.entry;
+                first = first.next;
+                return e;
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringJoiner joiner = new StringJoiner("->");
+
+            for (Entry entry : this) {
+                joiner.add(entry.getDim().toString());
+            }
+
+            return joiner.toString();
+        }
     }
 }
+
+final class GridFactory {
+    private GridFactory() { }
+    static Grid create(Dimension dim) {
+        int r = dim.lastRow - dim.firstRow + 1
+            , c = dim.lastColumn - dim.firstColumn + 1;
+
+        return create(dim, r * c);
+    }
+
+    static Grid create(Dimension dim, int n) {
+        int r = dim.lastRow - dim.firstRow + 1
+            , c = dim.lastColumn - dim.firstColumn + 1;
+
+        return c <= 64 && r < 1 << 14 ? new Grid.FastGrid(dim)
+            : n > 1 << 10 ? new Grid.FractureGrid(dim) : new Grid.IndexGrid(dim, n);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java b/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
index ee4c5669..7a3de366 100644
--- a/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
+++ b/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
@@ -210,9 +210,9 @@ public class IndexSharedStringTable extends SharedStringTable {
      * Batch getting
      *
      * @param fromIndex the index of the first element, inclusive, to be sorted
-     * @param array Destination array
+     * @param array     Destination array
      * @return The number of string read, or -1 if the end of the
-     *              stream has been reached
+     * stream has been reached
      * @throws IOException if I/O error occur
      */
     public int get(int fromIndex, String[] array) throws IOException {
@@ -234,7 +234,7 @@ public class IndexSharedStringTable extends SharedStringTable {
             readBuffer.flip();
         }
         int i = 0;
-        A: for (; ;) {
+        A: for ( ; ; ) {
 
             if (i == 0 && fromIndex != this.index) {
                 skipTo(fromIndex);
@@ -316,6 +316,7 @@ public class IndexSharedStringTable extends SharedStringTable {
     /**
      * Calculate the position according to the subscript recorded in
      * the SharedStringTable.
+     * <p>
      * The index position is {@code keyIndex / kSplit * sizeOf(long)},
      * get the position of the string record through the index position
      *
diff --git a/src/main/java/org/ttzero/excel/reader/MergeCalc.java b/src/main/java/org/ttzero/excel/reader/MergeCalc.java
deleted file mode 100644
index 866ce6a3..00000000
--- a/src/main/java/org/ttzero/excel/reader/MergeCalc.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.reader;
-
-/**
- * Test and merge formula each rows.
- *
- * @author guanquan.wang at 2019-12-31 15:42
- */
-@FunctionalInterface
-interface MergeCalc {
-
-    /**
-     * Merge formula in rows
-     *
-     * @param row thr row number
-     * @param cells the cells in row
-     * @param n count of cells
-     */
-    void accept(int row, Cell[] cells, int n);
-}
diff --git a/src/main/java/org/ttzero/excel/reader/Row.java b/src/main/java/org/ttzero/excel/reader/Row.java
index 921c0bb1..2000415e 100644
--- a/src/main/java/org/ttzero/excel/reader/Row.java
+++ b/src/main/java/org/ttzero/excel/reader/Row.java
@@ -34,6 +34,7 @@ import static org.ttzero.excel.reader.Cell.CHARACTER;
 import static org.ttzero.excel.reader.Cell.DATE;
 import static org.ttzero.excel.reader.Cell.DATETIME;
 import static org.ttzero.excel.reader.Cell.DOUBLE;
+import static org.ttzero.excel.reader.Cell.EMPTY_TAG;
 import static org.ttzero.excel.reader.Cell.INLINESTR;
 import static org.ttzero.excel.reader.Cell.LONG;
 import static org.ttzero.excel.reader.Cell.NUMERIC;
@@ -148,7 +149,7 @@ public abstract class Row {
      *
      * @return header Row
      */
-    public HeaderRow asHeader() {
+    HeaderRow asHeader() {
         HeaderRow hr = HeaderRow.with(this);
         this.hr = hr;
         return hr;
@@ -1023,7 +1024,9 @@ public abstract class Row {
                     else joiner.add(toTimestamp(c.dv).toString());
                     break;
                 case BLANK:
+                case EMPTY_TAG:
                     joiner.add(EMPTY);
+                    break;
                 default:
                     joiner.add(null);
             }
@@ -1054,7 +1057,7 @@ public abstract class Row {
         } else if (i >= sharedCalc.length) {
             sharedCalc = Arrays.copyOf(sharedCalc, i + 10);
         }
-        Dimension dim = Dimension.from(ref);
+        Dimension dim = Dimension.of(ref);
 
         long l = 0;
         l |= (long) (dim.firstRow & (1 << 20) - 1) << 42;
@@ -1121,3 +1124,38 @@ public abstract class Row {
         return n;
     }
 }
+
+/**
+ * Test and merge formula each rows.
+ *
+ * @author guanquan.wang at 2019-12-31 15:42
+ */
+@FunctionalInterface
+interface MergeCalcFunc {
+
+    /**
+     * Merge formula in rows
+     *
+     * @param row thr row number
+     * @param cells the cells in row
+     * @param n count of cells
+     */
+    void accept(int row, Cell[] cells, int n);
+}
+
+/**
+ * Test and copy value on merged cells
+ *
+ * @author guanquan.wang at 2020-01-17 11:36
+ */
+@FunctionalInterface
+interface MergeValueFunc {
+
+    /**
+     * Copy merged values
+     *
+     * @param row thr row number
+     * @param cell all cell in row
+     */
+    void accept(int row, Cell cell);
+}
\ No newline at end of file
diff --git a/src/main/java/org/ttzero/excel/reader/Sheet.java b/src/main/java/org/ttzero/excel/reader/Sheet.java
index cf15da1d..228093a7 100644
--- a/src/main/java/org/ttzero/excel/reader/Sheet.java
+++ b/src/main/java/org/ttzero/excel/reader/Sheet.java
@@ -259,7 +259,24 @@ public interface Sheet extends AutoCloseable {
     }
 
     /**
-     * Make reader parse the formula
+     * Make worksheets parse formulas
+     *
+     * @return a empty {@link CalcSheet}
+     */
+    CalcSheet asCalcSheet();
+
+    /**
+     * Make worksheets copy value on merge cells
+     *
+     * @return a empty {@link MergeSheet}
      */
-    void parseFormula();
+    MergeSheet asMergeSheet();
+
+//    FullSheet asFullSheet();
 }
+
+interface CalcSheet extends Sheet { }
+
+interface MergeSheet extends Sheet { }
+
+//interface FullSheet extends CalcSheet, MergeSheet { }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLRow.java b/src/main/java/org/ttzero/excel/reader/XMLRow.java
index 77188294..4459e476 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLRow.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLRow.java
@@ -19,6 +19,7 @@ package org.ttzero.excel.reader;
 import org.ttzero.excel.entity.style.Styles;
 
 import static org.ttzero.excel.reader.Cell.BOOL;
+import static org.ttzero.excel.reader.Cell.EMPTY_TAG;
 import static org.ttzero.excel.reader.Cell.NUMERIC;
 import static org.ttzero.excel.reader.Cell.FUNCTION;
 import static org.ttzero.excel.reader.Cell.SST;
@@ -41,10 +42,8 @@ import static org.ttzero.excel.util.StringUtil.swap;
  * @author guanquan.wang on 2018-09-22
  */
 class XMLRow extends Row {
-    private int startRow;
-    private StringBuilder buf;
-    private MergeCalc calcFun;
-    private boolean hasCalc;
+    int startRow;
+    StringBuilder buf;
 
     /**
      * The number of row. (one base)
@@ -60,21 +59,19 @@ class XMLRow extends Row {
     }
 
     @SuppressWarnings("unused")
-    private XMLRow() { }
+    XMLRow() { }
 
-    XMLRow(SharedStrings sst, Styles styles, int startRow, MergeCalc calcFun) {
+    XMLRow(SharedStrings sst, Styles styles, int startRow) {
         this.sst = sst;
         this.styles = styles;
         this.startRow = startRow;
         buf = new StringBuilder();
-        this.calcFun = calcFun;
-        this.hasCalc = calcFun != null;
     }
 
     /////////////////////////unsafe////////////////////////
-    private char[] cb;
-    private int from, to;
-    private int cursor;
+    char[] cb;
+    int from, to;
+    int cursor, e;
 
     ///////////////////////////////////////////////////////
     XMLRow with(char[] cb, int from, int size) {
@@ -100,7 +97,7 @@ class XMLRow extends Row {
         return this;
     }
 
-    private int searchRowNumber() {
+    private void searchRowNumber() {
         int _f = from + 4, a; // skip '<row'
         for (; cb[_f] != '>' && _f < to; _f++) {
             if (cb[_f] <= ' ' && cb[_f + 1] == 'r' && cb[_f + 2] == '=') {
@@ -112,10 +109,9 @@ class XMLRow extends Row {
                 break;
             }
         }
-        return _f;
     }
 
-    private int searchSpan() {
+    int searchSpan() {
         int i = from + 4, _lc = lc;
         for (; cb[i] != '>'; i++) {
             if (cb[i] <= ' ' && cb[i + 1] == 's' && cb[i + 2] == 'p'
@@ -142,7 +138,7 @@ class XMLRow extends Row {
         // clear and share
         for (int n = 0, len = lc > 0 ? Math.max(lc, _lc) : cells.length; n < len; n++) {
             if (cells[n] != null) cells[n].clear();
-            else cells[n] = new Cell();
+            else cells[n] = new Cell((short) (n + 1));
         }
         return i;
     }
@@ -150,22 +146,18 @@ class XMLRow extends Row {
     /**
      * Loop parse cell
      */
-    private void parseCells() {
-        int index = 0;
+    void parseCells() {
         cursor = searchSpan();
         for (; cb[cursor++] != '>'; ) ;
         unknownLength = lc < 0;
 
-        // Parse formula if exists and can parse
-        if (hasCalc) {
-            calcFun.accept(getRowNumber(), cells, !unknownLength ? lc - fc : -1);
-        }
-
+        Cell cell;
+        int index = 0;
         // Parse cell value
         if (unknownLength) {
-            while (nextCell() != null) index++;
+            for(; (cell = nextCell()) != null; index++, parseCellValue(cell));
         } else {
-            while (index < lc && nextCell() != null) ;
+            for(; index < lc && (cell = nextCell()) != null; parseCellValue(cell)) ;
         }
     }
 
@@ -181,14 +173,14 @@ class XMLRow extends Row {
         if (cursor >= to) return null;
         cursor += 2;
         // find end of cell
-        int e = cursor;
+        e = cursor;
         for (; e < to && (cb[e] != '<' || cb[e + 1] != 'c' || cb[e + 2] > ' '); e++) ;
 
         Cell cell = null;
         // find type
         // n=numeric (default), s=string, b=boolean, str=function string
         char t = NUMERIC; // default
-        int xf = 0, i = 1;
+        int xf = 0, i;
         for (; cb[cursor] != '>'; cursor++) {
             // Cell index
             if (cb[cursor] <= ' ' && cb[cursor + 1] == 'r' && cb[cursor + 2] == '=') {
@@ -223,71 +215,7 @@ class XMLRow extends Row {
 
         // The style index
         cell.xf = xf;
-
-        // Ignore Formula string default
-        if (hasCalc) {
-            int a = getF(cell, e);
-            // Inner text
-            if (a < cursor) {
-                cell.fv = unescape(buf, cb, a, cursor);
-                if (cell.si > -1) setCalc(cell.si, cell.fv);
-            }
-            // Function string is shared
-            else if (cell.si > -1) {
-                // Get from ref
-                cell.fv = getCalc(cell.si, (getRowNumber() << 14) | i);
-            }
-        }
-        // Get value
-        int a;
-        switch (t) {
-            case INLINESTR: // inner string
-                a = getT(e);
-                if (a == cursor) { // null value
-                    cell.setT(BLANK); // Reset type to BLANK if null value
-                } else {
-                    cell.setSv(unescape(buf, cb, a, cursor));
-                }
-                break;
-            case SST: // shared string lazy get
-                a = getV(e);
-                cell.setNv(toInt(cb, a, cursor));
-                cell.setT(SST);
-                break;
-            case BOOL: // boolean value
-                a = getV(e);
-                if (cursor - a == 1) {
-                    cell.setBv(toInt(cb, a, cursor) == 1);
-                }
-                break;
-            case FUNCTION: // function string
-                a = getV(e);
-                if (a == cursor) { // null value
-                    cell.setT(BLANK); // Reset type to BLANK if null value
-                } else {
-                    cell.setSv(unescape(buf, cb, a, cursor));
-                }
-                break;
-            default:
-                a = getV(e);
-                if (a < cursor) {
-                    if (isNumber(a, cursor)) {
-                        long l = toLong(a, cursor);
-                        if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) {
-                            cell.setNv((int) l);
-                        } else {
-                            cell.setLv(l);
-                        }
-                    } else if (isDouble(a, cursor)) {
-                        cell.setDv(toDouble(a, cursor));
-                    } else {
-                        cell.setSv(toString(a, cursor));
-                    }
-                }
-        }
-
-        // end of cell
-        cursor = e;
+        cell.t = t;
 
         return cell;
     }
@@ -334,21 +262,20 @@ class XMLRow extends Row {
     }
 
     /* Found specify target  */
-    private int get(int e, char c) {
+    private int get(char c) {
         // Ignore all attributes
-        return get(null, e, c, null);
+        return get(null, c, null);
     }
 
     /**
      * Parses the value and attributes of the specified tag
      *
      * @param cell current cell
-     * @param e the last character index
      * @param c the specified tag
      * @param attrConsumer an attribute consumer
      * @return the start index of value
      */
-    private int get(Cell cell, int e, char c, Attribute attrConsumer) {
+    int get(Cell cell, char c, Attribute attrConsumer) {
         for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != c
             || cb[cursor + 2] != '>' && cb[cursor + 2] > ' ' && cb[cursor + 2] != '/'); cursor++) ;
         if (cursor == e) return cursor;
@@ -392,11 +319,10 @@ class XMLRow extends Row {
      *
      * Code like this {@code <is><t>cell value</t></is>}
      *
-     * @param e the last index in char buffer
      * @return the end index of string value
      */
-    private int getT(int e) {
-        return get(e, 't');
+    private int getT() {
+        return get('t');
     }
 
     /**
@@ -404,22 +330,189 @@ class XMLRow extends Row {
      *
      * Code like this {@code <v>0</v>
      *
-     * @param e the last index in char buffer
      * @return the end index of int value
      */
-    private int getV(int e) {
-        return get(e, 'v');
+    private int getV() {
+        return get('v');
+    }
+
+    /**
+     * Parse cell value
+     *
+     * @param cell current {@link Cell}
+     */
+    void parseCellValue(Cell cell) {
+        // @Mark: Ignore Formula string default
+
+        // Get value
+        int a;
+        switch (cell.t) {
+            case INLINESTR: // inner string
+                a = getT();
+                if (a > cursor) {
+                    cell.setSv(unescape(buf, cb, a, cursor));
+                } else { // null value
+                    cell.setT(BLANK); // Reset type to BLANK if null value
+                }
+                break;
+            case SST: // shared string lazy get
+                a = getV();
+                cell.setNv(toInt(cb, a, cursor));
+                cell.setT(SST);
+                break;
+            case BOOL: // boolean value
+                a = getV();
+                if (cursor - a == 1) {
+                    cell.setBv(toInt(cb, a, cursor) == 1);
+                } else cell.setBv(false);
+                break;
+            case FUNCTION: // function string
+                a = getV();
+                if (a > cursor) {
+                    cell.setSv(unescape(buf, cb, a, cursor));
+                } else { // null value
+                    cell.setT(BLANK); // Reset type to BLANK if null value
+                }
+                break;
+            default:
+                a = getV();
+                if (a < cursor) {
+                    if (isNumber(a, cursor)) {
+                        long l = toLong(a, cursor);
+                        if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) {
+                            cell.setNv((int) l);
+                        } else {
+                            cell.setLv(l);
+                        }
+                    } else if (isDouble(a, cursor)) {
+                        cell.setDv(toDouble(a, cursor));
+                    } else {
+                        cell.setSv(toString(a, cursor));
+                    }
+                }
+                // Maybe the cell should be merged
+                else cell.setT(EMPTY_TAG);
+        }
+
+        // end of cell
+        cursor = e;
+    }
+
+    XMLCalcRow asCalcRow() {
+        return !(this instanceof XMLCalcRow) ? new XMLCalcRow(this) : (XMLCalcRow) this;
+    }
+
+    XMLMergeRow asMergeRow() {
+        return !(this instanceof XMLMergeRow) ? new XMLMergeRow(this) : (XMLMergeRow) this;
+    }
+
+    /**
+     * Attribute consumer
+     */
+    @FunctionalInterface
+    interface Attribute {
+        /**
+         * Performs this operation on the given argument.
+         *
+         * @param cell current cell
+         * @param cb characters for the entire attribute
+         * @param a start index
+         * @param b end index
+         */
+        void accept(Cell cell, char[] cb, int a, int b);
+    }
+}
+
+/**
+ * Cell with Calc
+ */
+class XMLCalcRow extends XMLRow {
+    private MergeCalcFunc calcFun;
+
+    XMLCalcRow(SharedStrings sst, Styles styles, int startRow, MergeCalcFunc calcFun) {
+        this.sst = sst;
+        this.styles = styles;
+        this.startRow = startRow;
+        this.buf = new StringBuilder();
+        this.calcFun = calcFun;
+    }
+
+    XMLCalcRow(XMLRow row) {
+        this.sst = row.sst;
+        this.styles = row.styles;
+        this.startRow = row.startRow;
+        this.buf = row.buf;
+    }
+
+    XMLCalcRow setCalcFun(MergeCalcFunc calcFun) {
+        this.calcFun = calcFun;
+        return this;
+    }
+
+    /**
+     * Loop parse cell
+     */
+    void parseCells() {
+        int index = 0;
+        cursor = searchSpan();
+        for (; cb[cursor++] != '>'; ) ;
+        unknownLength = lc < 0;
+
+        // Parse formula if exists and can parse
+        calcFun.accept(getRowNumber(), cells, !unknownLength ? lc - fc : -1);
+
+        Cell cell;
+        // Parse cell value
+        if (unknownLength) {
+            for(; (cell = nextCell()) != null; index++, parseCellValue(cell));
+        } else {
+            for(; index < lc && (cell = nextCell()) != null; parseCellValue(cell)) ;
+        }
+    }
+
+    /**
+     * Parse cell value
+     *
+     * @param cell current {@link Cell}
+     */
+    @Override
+    void parseCellValue(Cell cell) {
+        // Parse calc
+        parseCalcFunc(cell);
+
+        // Parse value
+        super.parseCellValue(cell);
     }
 
+    /**
+     * Parse calc on reader cells
+     *
+     * @param cell current {@link Cell}
+     */
+    private void parseCalcFunc(Cell cell) {
+        int a = getF(cell);
+        // Inner text
+        if (a < cursor) {
+            cell.fv = unescape(buf, cb, a, cursor);
+            if (cell.si > -1) setCalc(cell.si, cell.fv);
+        }
+        // Function string is shared
+        else if (cell.si > -1) {
+            // Get from ref
+            cell.fv = getCalc(cell.si, (getRowNumber() << 14) | cell.i);
+        }
+    }
+
+
     /**
      * Found the Function tag range
      * Code like this {@code <f t="shared" ref="B1:B10" si="0">SUM(A1:A10)</f>
      *
-     * @param e the last index in char buffer
+     * @param cell current {@link Cell}
      * @return the end index of function value
      */
-    private int getF(Cell cell, int e) {
-        return get(cell, e, 'f', this::parseFunAttr);
+    private int getF(Cell cell) {
+        return get(cell, 'f', this::parseFunAttr);
     }
 
     /* Parse function tag's attribute */
@@ -480,20 +573,39 @@ class XMLRow extends Row {
         // Storage formula shared id
         cell.si = si;
     }
+}
+
+/**
+ * Copy value on merge cells
+ */
+class XMLMergeRow extends XMLRow {
+    // InterfaceFunction
+    private MergeValueFunc func;
+
+    XMLMergeRow(XMLRow row) {
+        this.sst = row.sst;
+        this.styles = row.styles;
+        this.startRow = row.startRow;
+        this.buf = row.buf;
+    }
+
+    XMLMergeRow setCopyValueFunc(MergeValueFunc func) {
+        this.func = func;
+        return this;
+    }
 
     /**
-     * Attribute consumer
+     * Parse cell value
+     *
+     * @param cell current {@link Cell}
      */
-    @FunctionalInterface
-    private interface Attribute {
-        /**
-         * Performs this operation on the given argument.
-         *
-         * @param cell current cell
-         * @param cb characters for the entire attribute
-         * @param a start index
-         * @param b end index
-         */
-        void accept(Cell cell, char[] cb, int a, int b);
+    @Override
+    void parseCellValue(Cell cell) {
+
+        // Parse value
+        super.parseCellValue(cell);
+
+        // Setting/copy value if merged
+        func.accept(getRowNumber(), cell);
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLSheet.java b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
index 01a609cc..324e69d4 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLSheet.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
@@ -42,30 +42,29 @@ import java.util.regex.Pattern;
  * @author guanquan.wang on 2018-09-22
  */
 class XMLSheet implements Sheet {
-    private Logger logger = LogManager.getLogger(getClass());
+    Logger logger = LogManager.getLogger(getClass());
 
     XMLSheet() { }
 
-    private String name;
-    private int index; // per sheet index of workbook
+    String name;
+    int index; // per sheet index of workbook
 //        , size = -1; // size of rows per sheet
-    private Path path;
+    Path path;
     /**
      * The Shared String Table
      */
-    private SharedStrings sst;
+    SharedStrings sst;
     /**
      * The {@link Styles}
      */
-    private Styles styles;
-    private int startRow = -1; // row index of data
-    private HeaderRow header;
-    private boolean hidden; // state hidden
-    private long[] calc; // Array of formula
-    private boolean hasCalc;
-    private boolean parseFormula;
+    Styles styles;
+    int startRow = -1; // row index of data
+    HeaderRow header;
+    boolean hidden; // state hidden
+
     // Range address of the used area in the current sheet
-    private Dimension dimension;
+    Dimension dimension;
+
 
     /**
      * Setting the worksheet name
@@ -103,15 +102,6 @@ class XMLSheet implements Sheet {
         this.styles = styles;
     }
 
-    /**
-     * Setting formula array
-     *
-     * @param calc array of formula
-     */
-    void setCalc(long[] calc) {
-        this.calc = calc;
-        this.hasCalc = calc != null && calc.length > 0;
-    }
 
     /**
      * The worksheet name
@@ -197,7 +187,7 @@ class XMLSheet implements Sheet {
     @Override
     public Row getHeader() {
         if (header == null && !heof) {
-            XMLRow row = findRow0();
+            Row row = findRow0(this::createHeader);
             if (row != null) {
                 header = row.asHeader();
                 sRow.setHr(header);
@@ -233,10 +223,10 @@ class XMLSheet implements Sheet {
     private BufferedReader reader;
     private char[] cb; // buffer
     private int nChar, length;
-    private boolean eof = false, heof = false; // OPTIONS = false
-    private long mark;
+    boolean eof = false, heof = false; // OPTIONS = false
+    long mark;
 
-    private XMLRow sRow;
+    XMLRow sRow;
 
     /**
      * Load sheet.xml as BufferedReader
@@ -244,6 +234,7 @@ class XMLSheet implements Sheet {
      * @return Sheet
      * @throws IOException if io error occur
      */
+    @Override
     public XMLSheet load() throws IOException {
         logger.debug("load {}", path.toString());
         reader = Files.newBufferedReader(path);
@@ -263,7 +254,7 @@ class XMLSheet implements Sheet {
                     Pattern pat = Pattern.compile("([A-Z]+)(\\d+):([A-Z]+)(\\d+)");
                     Matcher mat = pat.matcher(l_);
                     if (mat.matches()) {
-                        dimension = Dimension.from(l_);
+                        dimension = Dimension.of(l_);
                         this.startRow = dimension.firstRow;
                     } else {
                         pat = Pattern.compile("([A-Z]+)(\\d+)");
@@ -292,12 +283,12 @@ class XMLSheet implements Sheet {
             eof = true;
         } else {
             eof = false;
-            sRow = new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1
-                , parseFormula && hasCalc ? this::findCalc : null); // share row space
+            sRow = new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1);
         }
 
         mark = nChar;
-        if (!eof) {
+        // Deep read if dimension information not write in header
+        if (!eof && dimension == null) {
             parseDimension();
         }
 
@@ -361,7 +352,7 @@ class XMLSheet implements Sheet {
         return sRow.with(cb, start, nChar - start);
     }
 
-    protected XMLRow findRow0() {
+    protected Row findRow0(HeaderRowFunc func) {
         char[] cb = new char[8192];
         int nChar = 0, length;
         // reload file
@@ -415,9 +406,11 @@ class XMLSheet implements Sheet {
             return null;
         }
 
+        return func.accept(cb, start, nChar - start);
         // row
-        return new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1
-            , parseFormula && hasCalc ? this::findCalc : null).with(cb, start, nChar - start);
+//        return new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1
+//            , parseFormula && hasCalc ? this::findCalc : null).with(cb, start, nChar - start);
+
     }
 
     /**
@@ -460,13 +453,6 @@ class XMLSheet implements Sheet {
         }
     }
 
-    /**
-     * Make reader parse the formula
-     */
-    @Override
-    public void parseFormula() {
-        this.parseFormula = true;
-    }
 
     /**
      * Reset the {@link XMLSheet}'s row index to begging
@@ -498,7 +484,7 @@ class XMLSheet implements Sheet {
     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.
      */
-    private void parseDimension() {
+    void parseDimension() {
         try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
             long fileSize = Files.size(path);
             int block = (int) Math.min(1 << 11, fileSize);
@@ -521,17 +507,30 @@ class XMLSheet implements Sheet {
                 buffer.flip();
 
                 charBuffer = StandardCharsets.UTF_8.decode(buffer);
-                int limit = charBuffer.limit();
+                int limit = charBuffer.limit(), c;
                 i = limit - 1;
 
-                // <row r="
-                for (; i >= 7 && (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--) ;
-
+                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--)
+                        ;
+                }
                 // Not Found
-                if (i < 7) {
+                if (i < c) {
                     for (; i < limit && charBuffer.get(i) != '>'; i++) ;
                     i++;
                     if (i < limit - 1) {
@@ -556,91 +555,15 @@ class XMLSheet implements Sheet {
 
             if (eof) return;
 
+            charBuffer.position(i);
             // Dimension
             if (dimension == null) {
-                long rr = 0L;
-                int rc;
-
-                int[] info = innerParse(charBuffer, ++i);
-                rc = (info[2] << 16) | (info[1] & 0x7FFF);
-                rr |= ((long) info[0]) << 32;
-
-                // Skip if the first row is known
-                if (mark > 0) {
-                    channel.position(mark);
-                    buffer.clear();
-                    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++) ;
-                    i += 8;
-
-                    info = innerParse(charBuffer, i);
-                    rr |= info[0];
-                    if (info[1] > (rc & 0x7FFF)) {
-                        rc |= info[1] & 0x7FFF;
-                    }
-                    if (info[2] < (rc >>> 16)) {
-                        rc |= info[2] << 16;
-                    }
-                } else rr |= 1;
-
-                dimension = new Dimension((int) rr, (short) rc, (int) (rr >>> 32), (short) (rc >>> 16));
+                parseDim(channel, charBuffer, buffer);
+                charBuffer.position(i);
             }
 
             // Find mergeCells tag
-            int limit = charBuffer.limit();
-            List<Dimension> mergeCells = null;
-            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) {
-                    if (eof) break;
-                    for (; i >= 0 && charBuffer.get(i) != '<'; i--) ;
-                    if (i > 0) channel.position(channel.position() + i);
-
-                    buffer.clear();
-                    channel.read(buffer);
-                    buffer.flip();
-                    if (buffer.limit() <= 0) break;
-
-                    charBuffer = StandardCharsets.UTF_8.decode(buffer);
-                    limit = charBuffer.limit();
-                    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.from(new String(chars)));
-                    charBuffer.position(++i);
-                }
-            }
-            // TODO merge
-            if (mergeCells != null) {
-                for (Dimension dimension : mergeCells) {
-                    logger.info(dimension);
-                }
-            }
+            parseMerge(channel, charBuffer, buffer, block);
         } catch (IOException e) {
             // Ignore error
             logger.debug("", e);
@@ -667,6 +590,111 @@ class XMLSheet implements Sheet {
         return new int[] {row, cs, ls};
     }
 
+    private void parseDim(SeekableByteChannel channel, CharBuffer charBuffer, ByteBuffer buffer) throws IOException {
+        long rr = 0L;
+        int rc, i = charBuffer.position();
+
+        int[] info = innerParse(charBuffer, ++i);
+        rc = (info[2] << 16) | (info[1] & 0x7FFF);
+        rr |= ((long) info[0]) << 32;
+
+        // Skip if the first row is known
+        if (mark > 0) {
+            channel.position(mark);
+            buffer.clear();
+            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++) ;
+            i += 8;
+
+            info = innerParse(charBuffer, i);
+            rr |= info[0];
+            if (info[1] > (rc & 0x7FFF)) {
+                rc |= info[1] & 0x7FFF;
+            }
+            if (info[2] < (rc >>> 16)) {
+                rc |= info[2] << 16;
+            }
+        } else rr |= 1;
+
+        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 new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1).with(cb, start, n);
+    }
+
+    public XMLCalcSheet asCalcSheet() {
+        return !(this instanceof XMLCalcSheet) ? new XMLCalcSheet(this) : (XMLCalcSheet) this;
+    }
+
+    public XMLMergeSheet asMergeSheet() {
+        return !(this instanceof XMLMergeSheet) ? new XMLMergeSheet(this) : (XMLMergeSheet) this;
+    }
+
+//    public FullSheet asFullSheet() {
+//        throw new IllegalArgumentException();
+//    }
+
+
+    interface HeaderRowFunc {
+        Row accept(char[] cb, int start, int n);
+    }
+}
+
+/**
+ * A sub {@link XMLSheet} to parse cell calc
+ */
+class XMLCalcSheet extends XMLSheet implements CalcSheet {
+    private long[] calc; // Array of formula
+    XMLCalcSheet() { }
+    XMLCalcSheet(XMLSheet sheet) {
+        this.name = sheet.name;
+        this.index = sheet.index;
+        this.path = sheet.path;
+        this.sst = sheet.sst;
+        this.styles = sheet.styles;
+    }
+
+    /**
+     * Load sheet.xml as BufferedReader
+     *
+     * @return Sheet
+     * @throws IOException if io error occur
+     */
+    @Override
+    public XMLCalcSheet load() throws IOException {
+        super.load();
+
+        if (!eof && !(sRow instanceof XMLCalcRow) && calc != null) {
+            sRow = sRow.asCalcRow().setCalcFun(this::findCalc);
+        }
+        return this;
+    }
+
+    /**
+     * Setting formula array
+     *
+     * @param calc array of formula
+     */
+    XMLCalcSheet setCalc(long[] calc) {
+        this.calc = calc;
+        return this;
+    }
+
+    Row createHeader(char[] cb, int start, int n) {
+        return new XMLCalcRow(sst, styles, this.startRow > 0 ? this.startRow : 1, this::findCalc).with(cb, start, n);
+    }
+
     /* Found calc */
     private void findCalc(int row, Cell[] cells, int n) {
         long r = ((long) row) << 16;
@@ -688,4 +716,121 @@ class XMLSheet implements Sheet {
             else break;
         }
     }
+
 }
+
+/**
+ * A sub {@link XMLSheet} to copy value on merge cells
+ */
+class XMLMergeSheet extends XMLSheet implements MergeSheet {
+
+    // A merge cells grid
+    private Grid mergeCells;
+
+    XMLMergeSheet() { }
+    XMLMergeSheet(XMLSheet sheet) {
+        this.name = sheet.name;
+        this.index = sheet.index;
+        this.path = sheet.path;
+        this.sst = sheet.sst;
+        this.styles = sheet.styles;
+    }
+
+    /**
+     * Load sheet.xml as BufferedReader
+     *
+     * @return Sheet
+     * @throws IOException if io error occur
+     */
+    public XMLMergeSheet load() throws IOException {
+        super.load();
+
+        if (mergeCells == null && !eof) {
+            parseDimension();
+        }
+
+        if (!eof && !(sRow instanceof XMLMergeRow) && mergeCells != null) {
+            sRow = sRow.asMergeRow().setCopyValueFunc(this::mergeCell);
+        }
+
+        return this;
+    }
+
+    /*
+    Parse `mergeCells` tag
+     */
+    void parseMerge(SeekableByteChannel channel, CharBuffer charBuffer, ByteBuffer buffer, int block) throws IOException {
+        // Find mergeCells tag
+        int limit = charBuffer.limit();
+        List<Dimension> mergeCells = null;
+        boolean eof = false;
+        int i = charBuffer.position();
+        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) {
+                if (eof) break;
+                for (; i >= 0 && charBuffer.get(i) != '<'; i--) ;
+                if (i > 0) channel.position(channel.position() + i);
+
+                buffer.clear();
+                channel.read(buffer);
+                buffer.flip();
+                if (buffer.limit() <= 0) break;
+
+                charBuffer = StandardCharsets.UTF_8.decode(buffer);
+                limit = charBuffer.limit();
+                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);
+            }
+        }
+
+        if (mergeCells != null) {
+            Dimension dim = mergeCells.get(0);
+            int fr = dim.firstRow, lr = dim.lastRow;
+            short fc = dim.firstColumn, lc = dim.lastColumn;
+            int n = (lr - fr + 1) * (lc - fc + 1);
+            for (int j = 1, len = mergeCells.size(); j < len; j++) {
+                dim = mergeCells.get(j);
+                n += (dim.lastRow - dim.firstRow + 1) * (dim.lastColumn - dim.firstColumn + 1);
+                if (fr > dim.firstRow)    fr = dim.firstRow;
+                if (lr < dim.lastRow)     lr = dim.lastRow;
+                if (fc > dim.firstColumn) fc = dim.firstColumn;
+                if (lc < dim.lastColumn)  lc = dim.lastColumn;
+            }
+
+            Grid grid = GridFactory.create(new Dimension(fr, fc, lr, lc), n);
+            for (Dimension d : mergeCells) {
+                grid.mark(d);
+                logger.debug("merged cells range {}", d);
+            }
+
+            logger.debug("Grid: {}", grid);
+            this.mergeCells = grid;
+        }
+    }
+
+    private void mergeCell(int row, Cell cell) {
+        mergeCells.merge(row, cell);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/ttzero/excel/util/CSVUtil.java b/src/main/java/org/ttzero/excel/util/CSVUtil.java
index 78da19c3..9dd53214 100644
--- a/src/main/java/org/ttzero/excel/util/CSVUtil.java
+++ b/src/main/java/org/ttzero/excel/util/CSVUtil.java
@@ -315,7 +315,7 @@ public class CSVUtil {
      * @return a CSV format writer
      */
     public static Writer newWriter(OutputStream os) {
-        return new Writer(new BufferedWriter(new OutputStreamWriter(os)));
+        return new Writer(new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)));
     }
 
     private static void testOrCreate(Path path) throws IOException {
@@ -428,7 +428,7 @@ public class CSVUtil {
             O o = init(path, separator, charset);
             // Empty file
             if (o == null) {
-                return RowsIterator.creatEmptyIterator();
+                return RowsIterator.createEmptyIterator();
             }
 
             // Use iterator
@@ -447,7 +447,7 @@ public class CSVUtil {
             O o = init(path, separator, charset);
             // Empty file
             if (o == null) {
-                return SharedRowsIterator.creatEmptyIterator();
+                return SharedRowsIterator.createEmptyIterator();
             }
 
             // Use iterator
@@ -622,7 +622,7 @@ public class CSVUtil {
             nextRow = null;
         }
 
-        static RowsIterator creatEmptyIterator() {
+        static RowsIterator createEmptyIterator() {
             RowsIterator iterator = new RowsIterator();
             iterator.EOF = true;
             iterator.nextRow = new String[0];
@@ -957,7 +957,7 @@ public class CSVUtil {
         private int i;
         private char[] cb;
         private int offset;
-        private final int length = 8192;
+        private final static int length = 8192;
 
         /**
          * Line separator string.  This is the value of the line.separator
diff --git a/src/main/java/org/ttzero/excel/util/ReflectUtil.java b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
index f0937f51..fc2040d8 100644
--- a/src/main/java/org/ttzero/excel/util/ReflectUtil.java
+++ b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
@@ -199,7 +199,7 @@ public class ReflectUtil {
         Method[] methods = listDeclaredMethods(beanClass, stopClass);
 
         int n = 0;
-        for ( int i = 0; i < methods.length; i++) {
+        for (int i = 0; i < methods.length; i++) {
             Method method = methods[i];
             if (method.getParameterCount() == 0) {
                 Class<?> returnType = method.getReturnType();
@@ -275,7 +275,7 @@ public class ReflectUtil {
             tmp.add("set" + field.getName().toLowerCase());
 
         int n = 0;
-        for ( int i = 0; i < methods.length; i++) {
+        for (int i = 0; i < methods.length; i++) {
             Method method = methods[i];
             Class<?> returnType = method.getReturnType();
             if (method.getParameterCount() == 1 && (returnType == void.class || returnType == Void.class)
@@ -325,7 +325,7 @@ public class ReflectUtil {
      * equals it or the has a same method name and return-type and same parameters
      *
      * @param methods the array methods to be found
-     * @param source the source method
+     * @param source  the source method
      * @return the index in method array
      */
     public static int indexOf(Method[] methods, Method source) {
@@ -411,7 +411,7 @@ public class ReflectUtil {
             for (int i = 0; i < aClass.length; i++) {
                 if (!(equals = aClass[i].equals(bClass[i])
                     || aClass[i].getType() == bClass[i].getType())
-                    ) {
+                ) {
                     break;
                 }
             }


Test patch:
diff --git a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
index b434e3f6..7abedc53 100644
--- a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
+++ b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
@@ -38,6 +38,8 @@ import static org.ttzero.excel.Print.println;
 import static org.ttzero.excel.Print.print;
 import static org.ttzero.excel.entity.Sheet.int2Col;
 import static org.ttzero.excel.entity.WorkbookTest.getOutputTestPath;
+import static org.ttzero.excel.reader.ExcelReader.COPY_ON_MERGED;
+import static org.ttzero.excel.reader.ExcelReader.VALUE_AND_CALC;
 import static org.ttzero.excel.reader.ExcelReader.cellRangeToLong;
 import static org.ttzero.excel.util.StringUtil.swap;
 
@@ -219,7 +221,7 @@ public class ExcelReaderTest {
     }
 
     @Test public void testDimensionConstructor() {
-        Dimension dimension = Dimension.from("A1:C5");
+        Dimension dimension = Dimension.of("A1:C5");
         assert "A1:C5".equals(dimension.toString());
 
         assert dimension.firstRow == 1;
@@ -268,6 +270,22 @@ public class ExcelReaderTest {
         }
     }
 
+    @Test public void testFormulaOption() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("formula.xlsx"), VALUE_AND_CALC)) {
+            // Read formula
+            reader.sheets().flatMap(Sheet::rows).forEach(row -> {
+                for (int i = row.fc; i < row.lc; i++) {
+                    if (row.hasFormula(i)) {
+                        print(row.getFormula(i));
+                        println('|');
+                    }
+                }
+            });
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
     @Test public void testSearch() {
         long[] array = { 131075L, 327683L };
         int column = 2, row = 3;
@@ -363,8 +381,8 @@ public class ExcelReaderTest {
         assert "B2:B8".equals(values[5]);
     }
 
-    @Test public void testMerge() {
-        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("merge.xlsx"))) {
+    @Test public void testMergeOption() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("merge.xlsx"), COPY_ON_MERGED)) {
             reader.sheets().flatMap(s -> {
                 println("----------------" + s.getName() + "----------------");
                 println("dimension: " + s.getDimension());
@@ -375,6 +393,30 @@ public class ExcelReaderTest {
         }
     }
 
+    @Test public void testMergeFunc() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("merge.xlsx"))) {
+            reader.copyOnMergeCells().sheets().flatMap(s -> {
+                println("----------------" + s.getName() + "----------------");
+                println("dimension: " + s.getDimension());
+                return s.rows();
+            }).forEach(Print::println);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    @Test public void testMergeExcel() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("merge.xlsx"))) {
+            reader.sheets().flatMap(s -> {
+                println("----------------" + s.getName() + "----------------");
+                println("dimension: " + s.getDimension());
+                return s.rows();
+            }).forEach(Print::println);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
 
     public static class Customer {
         @ExcelColumn("客户编码")
diff --git a/src/test/java/org/ttzero/excel/reader/GridTest.java b/src/test/java/org/ttzero/excel/reader/GridTest.java
index dacc28e3..8ea5770f 100644
--- a/src/test/java/org/ttzero/excel/reader/GridTest.java
+++ b/src/test/java/org/ttzero/excel/reader/GridTest.java
@@ -18,11 +18,219 @@ package org.ttzero.excel.reader;
 
 import org.junit.Test;
 
+import static org.ttzero.excel.Print.println;
+import static org.ttzero.excel.reader.Grid.FastGrid.isPowerOfTwo;
+
 /**
  * @author guanquan.wang at 2020-01-09 17:19
  */
 public class GridTest {
-    @Test public void testLongGrid() {
-        LongGrid grid = new LongGrid();
+    @Test public void testGridType() {
+        Grid grid = GridFactory.create(Dimension.of("A1:BM10"));
+
+        assert grid instanceof Grid.IndexGrid;
+
+        grid = GridFactory.create(Dimension.of("A1:B16383"));
+        assert grid instanceof Grid.FastGrid;
+
+        grid = GridFactory.create(Dimension.of("A1:B16384"));
+        assert grid instanceof Grid.FractureGrid;
+
+    }
+
+    @Test public void testGrid1() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 1));
+
+        grid.mark(new Dimension(3, (short) 1, 7, (short) 1));
+
+        println(grid);
+
+        assert !grid.test(2, 1);
+        assert grid.test(3, 1);
+        assert grid.test(7, 1);
+        assert !grid.test(8, 1);
+    }
+
+    @Test public void testGrid2() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 2));
+
+        grid.mark(new Dimension(3, (short) 1, 7, (short) 1));
+
+        println(grid);
+
+        assert !grid.test(2, 1);
+        assert grid.test(3, 1);
+        assert grid.test(7, 1);
+        assert !grid.test(8, 1);
+    }
+
+    @Test public void testGrid4() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 3));
+
+        grid.mark(new Dimension(3, (short) 1, 7, (short) 3));
+
+        println(grid);
+
+        assert !grid.test(2, 2);
+        assert grid.test(3, 2);
+        assert grid.test(3, 3);
+        assert grid.test(7, 1);
+        assert grid.test(7, 2);
+        assert !grid.test(8, 2);
+    }
+
+    @Test public void testGrid8() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 8));
+
+        grid.mark(new Dimension(3, (short) 4, 7, (short) 4));
+
+        println(grid);
+    }
+
+    @Test public void testGrid8_2() {
+        Grid grid = GridFactory.create(Dimension.of("A1:H71"));
+
+        grid.mark(Dimension.of("C10:D10"));
+        grid.mark(Dimension.of("C5:D5"));
+        grid.mark(Dimension.of("C6:D6"));
+        grid.mark(Dimension.of("C7:D7"));
+        grid.mark(Dimension.of("C8:D8"));
+        grid.mark(Dimension.of("C9:D9"));
+        grid.mark(Dimension.of("A39:A71"));
+        grid.mark(Dimension.of("D1:E1"));
+        grid.mark(Dimension.of("A1:A26"));
+        grid.mark(Dimension.of("A27:A38"));
+        grid.mark(Dimension.of("E20:H20"));
+        grid.mark(Dimension.of("E21:H21"));
+        grid.mark(Dimension.of("E22:H22"));
+        grid.mark(Dimension.of("E23:H23"));
+        grid.mark(Dimension.of("E24:H24"));
+        grid.mark(Dimension.of("E25:H25"));
+        grid.mark(Dimension.of("C11:D11"));
+        grid.mark(Dimension.of("C12:D12"));
+        grid.mark(Dimension.of("C13:D13"));
+        grid.mark(Dimension.of("C14:D14"));
+        grid.mark(Dimension.of("C15:D15"));
+        grid.mark(Dimension.of("C16:D16"));
+
+        assert grid.toString().equals("FastGrid\n" +
+            "00011001\n00000001\n00000001\n00000001\n" +
+            "00001101\n00001101\n00001101\n00001101\n" +
+            "00001101\n00001101\n00001101\n00001101\n" +
+            "00001101\n00001101\n00001101\n00001101\n" +
+            "00000001\n00000001\n00000001\n11110001\n" +
+            "11110001\n11110001\n11110001\n11110001\n" +
+            "11110001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001\n00000001\n" +
+            "00000001\n00000001\n00000001");
+    }
+
+    @Test public void testGrid16() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 10));
+
+        grid.mark(new Dimension(4, (short) 5, 9, (short) 7));
+
+        println(grid);
+    }
+
+    @Test public void testGrid162() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 10));
+
+        grid.mark(new Dimension(2, (short) 2, 4, (short) 6));
+        grid.mark(new Dimension(3, (short) 7, 5, (short) 9));
+        grid.mark(new Dimension(7, (short) 10, 10, (short) 10));
+
+        println(grid);
+
+        assert grid.test(7,10);
+        assert !grid.test(6, 10);
+        assert !grid.test(7, 9);
+        assert grid.test(5, 8);
+    }
+
+    @Test public void testGrid32() {
+        Grid grid = GridFactory.create(Dimension.of("A1:AF10"));
+
+        grid.mark(Dimension.of("G3:AA9"));
+
+        println(grid);
+    }
+
+    @Test public void testGrid64() {
+        Grid grid = GridFactory.create(new Dimension(1, (short) 1, 10, (short) 54));
+
+        grid.mark(new Dimension(2, (short) 2, 4, (short) 6));
+
+        println(grid);
+    }
+
+    @Test public void testPowerOfTwo() {
+        assert isPowerOfTwo(1);
+        assert isPowerOfTwo(2);
+        assert isPowerOfTwo(1024);
+        assert !isPowerOfTwo(3);
+        assert !isPowerOfTwo(6);
+    }
+
+    @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("B16:E17"), null));
+        scanner.put(new Grid.LinkedScanner.E(Dimension.of("A13:A20"), null));
+
+        // Test iterator
+        for (Grid.Scanner.Entry entry : scanner) {
+            println(entry.getDim());
+        }
+
+        assert "B2:C2->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());
+
+        scanner.get(5, 6);
+        scanner.get(6, 5);
+        scanner.get(6, 6);
+        scanner.get(7, 5);
+        scanner.get(7, 6);
+        scanner.get(8, 5);
+        scanner.get(8, 6);
+
+        assert "B2:C2->A13:A20->B16:E17->E5:F8".equals(scanner.toString());
+    }
+
+    @Test public void testIndexGrid() {
+        Grid grid = GridFactory.create(Dimension.of("A1:BM10"));
+
+        grid.mark(Dimension.of("B2:E2"));
+
+        assert !grid.test(1, 1);
+        assert grid.test(2, 2);
+        assert grid.test(2, 5);
+        assert !grid.test(3, 3);
+        assert !grid.test(6, 2);
+    }
+
+    @Test public void testFractureGrid() {
+        Grid grid = GridFactory.create(Dimension.of("A1:E16384"));
+
+        grid.mark(Dimension.of("B1:C3"));
+
+        assert !grid.test(1, 1);
+        assert grid.test(1, 2);
+        assert grid.test(2, 2);
+        assert grid.test(3, 3);
+        assert !grid.test(4, 2);
+        assert !grid.test(3, 4);
     }
 }
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
new file mode 100644
index 00000000..29d80ae0
--- /dev/null
+++ b/src/test/resources/log4j2.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" monitorInterval="30">
+    <appenders>
+        <!-- Console -->
+        <Console name="Console" target="SYSTEM_OUT" follow="true">
+            <PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n" />
+        </Console>
+    </appenders>
+    <loggers>
+        <!-- Root Logger -->
+        <root level="debug">
+            <appender-ref ref="Console" />
+        </root>
+    </loggers>
+</Configuration>
\ No newline at end of file