Repository: wangguanquan/eec
Pull request #87 (2020-01-05 14:09:19)

Fix#33


Patch:
diff --git a/README.md b/README.md
index 959659a6..e0cf586d 100755
--- a/README.md
+++ b/README.md
@@ -347,6 +347,8 @@ Excel如下图
 Excel读取使用`ExcelReader#read`静态方法，内部采用流式操作，当使用某一行数据时才会真正
 读入内存，所以即使是GB级别的excel文件也只占用少量内存。
 
+默认的ExcelReader仅读取单元格的值而忽略单元格的公式，可以使用`ExcelReader#parseFormula`方法使Reader解析单元格的公式。
+
 下面展示一些常规的读取方法
 
 #### 1. 使用iterator迭代每行数据
diff --git a/pom.xml b/pom.xml
index eae7b16a..bc9ae652 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 
     <groupId>org.ttzero</groupId>
     <artifactId>eec</artifactId>
-    <version>0.3.6</version>
+    <version>0.3.7-SNAPSHOT</version>
     <name>Excel Write/Read Framework</name>
     <packaging>jar</packaging>
     <url>https://www.ttzero.org</url>
diff --git a/src/main/java/org/ttzero/excel/entity/CSVSheet.java b/src/main/java/org/ttzero/excel/entity/CSVSheet.java
index 6cab922f..53de554e 100644
--- a/src/main/java/org/ttzero/excel/entity/CSVSheet.java
+++ b/src/main/java/org/ttzero/excel/entity/CSVSheet.java
@@ -68,6 +68,7 @@ public class CSVSheet extends Sheet {
     /**
      * Constructor worksheet
      * @param is the csv source InputStream
+     * @throws IOException if I/O error occur.
      */
     public CSVSheet(InputStream is) throws IOException {
         this(null, is);
@@ -77,6 +78,7 @@ public class CSVSheet extends Sheet {
      * Constructor worksheet
      * @param name the worksheet name
      * @param is the csv source InputStream
+     * @throws IOException if I/O error occur.
      */
     public CSVSheet(String name, InputStream is) throws IOException {
         super(name);
@@ -88,6 +90,7 @@ public class CSVSheet extends Sheet {
     /**
      * Constructor worksheet
      * @param reader the csv source InputStream
+     * @throws IOException if I/O error occur.
      */
     public CSVSheet(Reader reader) throws IOException {
         this(null, reader);
@@ -97,6 +100,7 @@ public class CSVSheet extends Sheet {
      * Constructor worksheet
      * @param name the worksheet name
      * @param reader the csv source InputStream
+     * @throws IOException if I/O error occur.
      */
     public CSVSheet(String name, Reader reader) throws IOException {
         super(name);
@@ -115,6 +119,7 @@ public class CSVSheet extends Sheet {
      * Setting header flag
      *
      * @param hasHeader boolean value
+     * @return {@link CSVSheet}
      */
     public CSVSheet setHasHeader(boolean hasHeader) {
         this.hasHeader = hasHeader;
diff --git a/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java b/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java
index 1dbe1994..951c20c2 100644
--- a/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java
+++ b/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java
@@ -45,6 +45,7 @@ public interface ICellValueAndStyle {
     /**
      * Setting cell value and cell styles
      *
+     * @param row  the row number
      * @param cell the cell
      * @param e    the cell value
      * @param hc   the header column
@@ -61,6 +62,7 @@ public interface ICellValueAndStyle {
     /**
      * Returns the cell style index
      *
+     * @param rows the row number
      * @param hc the header column
      * @param o  the cell value
      * @return the style index in xf
@@ -70,6 +72,7 @@ public interface ICellValueAndStyle {
     /**
      * Setting cell value
      *
+     * @param row the row number
      * @param cell  the cell
      * @param e     the cell value
      * @param hc    the header column
@@ -125,6 +128,7 @@ public interface ICellValueAndStyle {
     /**
      * Setting cell value as null
      *
+     * @param row the row number
      * @param cell  the cell
      * @param hc    the header column
      */
@@ -139,6 +143,7 @@ public interface ICellValueAndStyle {
     /**
      * Int value conversion to others
      *
+     * @param row the row number
      * @param cell the cell
      * @param n    the cell value
      * @param hc   the header column
diff --git a/src/main/java/org/ttzero/excel/entity/Sheet.java b/src/main/java/org/ttzero/excel/entity/Sheet.java
index ec5f2e69..df466023 100644
--- a/src/main/java/org/ttzero/excel/entity/Sheet.java
+++ b/src/main/java/org/ttzero/excel/entity/Sheet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -1293,8 +1293,8 @@ public abstract class Sheet implements Cloneable, Storageable {
     /**
      * Int conversion to column string number
      * The max column on sheet is 16_384
-     * <p>
-     * int  | column number
+     * <blockquote><pre>
+     * int    | column number
      * -------|---------
      * 1      | A
      * 10     | J
@@ -1303,10 +1303,11 @@ public abstract class Sheet implements Cloneable, Storageable {
      * 28     | AB
      * 53     | BA
      * 16_384 | XFD
+     * </pre></blockquote>
+     * @param n the column number
+     * @return column string
      */
-    private ThreadLocal<char[][]> cache = ThreadLocal.withInitial(() -> new char[][]{{65}, {65, 65}, {65, 65, 65}});
-
-    public char[] int2Col(int n) {
+    public static char[] int2Col(int n) {
         char[][] cache_col = cache.get();
         char[] c;
         char A = 'A';
@@ -1340,6 +1341,9 @@ public abstract class Sheet implements Cloneable, Storageable {
         return c;
     }
 
+    private static ThreadLocal<char[][]> cache
+        = ThreadLocal.withInitial(() -> new char[][]{ {65}, {65, 65}, {65, 65, 65} });
+
     /**
      * Check empty header row
      *
@@ -1365,7 +1369,7 @@ public abstract class Sheet implements Cloneable, Storageable {
      * Each row-block is multiplexed and will be called to reset
      * the data when a row-block is completely written.
      * Call the {@link #getRowBlockSize()} method to get
-     * the row-block size, call the {@link ICellValueAndStyle#reset(int, Cell, Object, Column)}
+     * the row-block size, call the {@link ICellValueAndStyle#reset(int, Cell, Object, Sheet.Column)}
      * method to set value and styles.
      */
     protected abstract void resetBlockData();
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
index 4d6451eb..0abfda36 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -53,7 +53,6 @@ import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -156,7 +155,10 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             Files.createDirectory(themeP);
         }
         try {
-            Files.copy(getClass().getClassLoader().getResourceAsStream("template/theme1.xml"), themeP.resolve("theme1.xml"));
+            InputStream theme = getClass().getClassLoader().getResourceAsStream("template/theme1.xml");
+            if (theme != null) {
+                Files.copy(theme, themeP.resolve("theme1.xml"));
+            }
         } catch (IOException e) {
             // Nothing
         }
@@ -243,14 +245,14 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
                         Document document;
                         try {
                             document = reader.read(Files.newInputStream(pomPath));
+                            Element pomRoot = document.getRootElement();
+                            String application = pomRoot.elementText("groupId") + "." + pomRoot.elementText("artifactId");
+                            app.setApplication(application);
+                            String appVersion = pomRoot.elementText("version");
+                            app.setAppVersion(appVersion);
                         } catch (DocumentException | IOException e) {
-                            throw new ExcelReadException(e);
+                            // Nothing
                         }
-                        Element pomRoot = document.getRootElement();
-                        String application = pomRoot.elementText("groupId") + "." + pomRoot.elementText("artifactId");
-                        app.setAppVersion(application);
-                        String appVersion = pomRoot.elementText("version");
-                        app.setAppVersion(appVersion);
                     }
                 }
             }
@@ -260,13 +262,13 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
                 app.setAppVersion(pom.getProperty("version"));
                 // Can't read pom.xml if running as dev on window
             }
-            if (StringUtil.isEmpty(app.getAppVersion())) {
-                app.setApplication("org.ttzero.eec");
-                app.setAppVersion("1.0.0");
-            }
         } catch (IOException e) {
             // Nothing
         }
+        if (StringUtil.isEmpty(app.getAppVersion())) {
+            app.setApplication("org.ttzero.eec");
+            app.setAppVersion("1.0.0");
+        }
 
         int size = workbook.getSize();
 
@@ -394,9 +396,6 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             }
         }
 
-        // Calculation Properties
-        rootElement.addElement("calcPr").addAttribute("calcId", "124519");
-
         Document doc = factory.createDocument(rootElement);
         FileUtil.writeToDiskNoFormat(doc, root.resolve(rootName + Const.Suffix.XML)); // write to desk
     }
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 5d0d990a..058ccb44 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -47,6 +47,7 @@ import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.function.Supplier;
 
+import static org.ttzero.excel.entity.Sheet.int2Col;
 import static org.ttzero.excel.reader.Cell.BLANK;
 import static org.ttzero.excel.reader.Cell.BOOL;
 import static org.ttzero.excel.reader.Cell.CHARACTER;
@@ -319,7 +320,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
         if (size > 0) {
             bw.write(':');
             n--;
-            char[] col = sheet.int2Col(columns.length);
+            char[] col = int2Col(columns.length);
             bw.write(col);
             n -= col.length;
             bw.writeInt(size + 1);
@@ -387,7 +388,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
         int c = 1, defaultStyle = sheet.defaultHeadStyle();
         for (Sheet.Column hc : columns) {
             bw.write("<c r=\"");
-            bw.write(sheet.int2Col(c++));
+            bw.write(int2Col(c++));
             bw.writeInt(row);
             bw.write("\" t=\"s\" s=\"");
             bw.writeInt(defaultStyle);
@@ -587,7 +588,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
         }
         Sheet.Column hc = columns[column];
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         int i;
         if (StringUtil.isEmpty(s)) {
@@ -639,7 +640,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeDouble(double d, int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" s=\"");
         bw.writeInt(xf);
@@ -677,7 +678,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeDecimal(BigDecimal bd, int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" s=\"");
         bw.writeInt(xf);
@@ -715,7 +716,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeChar(char c, int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" t=\"s\" s=\"");
         bw.writeInt(xf);
@@ -735,7 +736,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeNumeric(long l, int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" s=\"");
         bw.writeInt(xf);
@@ -773,7 +774,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeBool(boolean bool, int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" t=\"b\" s=\"");
         bw.writeInt(xf);
@@ -792,7 +793,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     protected void writeNull(int row, int column, int xf) throws IOException {
         bw.write("<c r=\"");
-        bw.write(sheet.int2Col(column + 1));
+        bw.write(int2Col(column + 1));
         bw.writeInt(row);
         bw.write("\" s=\"");
         bw.writeInt(xf);
@@ -878,7 +879,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
                 int fill = 11; // fill 11 space
                 buffer.put((byte) ':');
                 fill--;
-                char[] col = sheet.int2Col(columns.length);
+                char[] col = int2Col(columns.length);
                 buffer.put((new String(col) + (rows + 1)).getBytes(StandardCharsets.US_ASCII));
                 fill -= col.length;
                 fill -= stringSize(rows + 1);
diff --git a/src/main/java/org/ttzero/excel/manager/ExcelType.java b/src/main/java/org/ttzero/excel/manager/ExcelType.java
index a1d3f2c4..4501c939 100644
--- a/src/main/java/org/ttzero/excel/manager/ExcelType.java
+++ b/src/main/java/org/ttzero/excel/manager/ExcelType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -32,7 +32,7 @@ public enum ExcelType {
     XLS,
 
     /**
-     * Excel 12.0~
+     * Excel 12.0(2007) or later
      */
     XLSX,
 
diff --git a/src/main/java/org/ttzero/excel/manager/docProps/App.java b/src/main/java/org/ttzero/excel/manager/docProps/App.java
index 617abc79..40e53cb9 100644
--- a/src/main/java/org/ttzero/excel/manager/docProps/App.java
+++ b/src/main/java/org/ttzero/excel/manager/docProps/App.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -25,7 +25,11 @@ import org.ttzero.excel.entity.NameValue;
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.ttzero.excel.util.StringUtil.isEmpty;
+
 /**
+ * The creation information
+ *
  * Created by guanquan.wang on 2017/9/21.
  */
 @TopNS(prefix = {"vt", ""}, uri = {"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
@@ -102,8 +106,24 @@ public class App extends XmlEntity {
         this.hyperlinksChanged = hyperlinksChanged;
     }
 
+    /**
+     * Setting the app version, it must not be null
+     *
+     * @param appVersion the app version
+     */
     public void setAppVersion(String appVersion) {
-        this.appVersion = appVersion;
+        if (isEmpty(appVersion)) {
+            this.appVersion = "1.0.0";
+        } else {
+            // Filter other character but number and `.`
+            char[] chars = appVersion.toCharArray();
+            int i = 0;
+            for (int j = 0; j < chars.length; j++) {
+                if (chars[j] >= '0' && chars[j] <= '9' || chars[j] == '.' && i > 0 && chars[i - 1] != '.')
+                    chars[i++] = chars[j];
+            }
+            this.appVersion = i > 0 ? new String(chars, 0, chars[i - 1] != '.' ? i : i - 1) : "1.0.0";
+        }
     }
 
     public String getApplication() {
diff --git a/src/main/java/org/ttzero/excel/reader/AppInfo.java b/src/main/java/org/ttzero/excel/reader/AppInfo.java
index 1666e552..f35eefec 100644
--- a/src/main/java/org/ttzero/excel/reader/AppInfo.java
+++ b/src/main/java/org/ttzero/excel/reader/AppInfo.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -24,69 +24,141 @@ import org.ttzero.excel.util.StringUtil;
 import java.util.Date;
 
 /**
+ * The file basic information
+ *
  * Create by guanquan.wang at 2019-04-15 16:00
  */
 public class AppInfo {
-    private App app;
-    private Core core;
+    private final App app;
+    private final Core core;
 
     AppInfo(App app, Core core) {
         this.app = app;
         this.core = core;
     }
 
+    /**
+     * The app name for file creation
+     *
+     * @return the app name
+     */
     public String getApplication() {
         return app.getApplication();
     }
 
+    /**
+     * About company info
+     *
+     * @return company value or null
+     */
     public String getCompany() {
         return app.getCompany();
     }
 
+    /**
+     * The app version for file creation
+     *
+     * @return the app version not null
+     */
     public String getAppVersion() {
         return app.getAppVersion();
     }
 
+    /**
+     * The content's title
+     *
+     * @return title value or null
+     */
     public String getTitle() {
         return core.getTitle();
     }
 
+    /**
+     * The content's subject
+     *
+     * @return subject value or null
+     */
     public String getSubject() {
         return core.getSubject();
     }
 
+    /**
+     * The file creator name
+     *
+     * @return name of creator or null
+     */
     public String getCreator() {
         return core.getCreator();
     }
 
+    /**
+     * The content's short description
+     *
+     * @return the desc value or null
+     */
     public String getDescription() {
         return core.getDescription();
     }
 
+    /**
+     * The content keyword, join with ','
+     *
+     * @return keyword value or null
+     */
     public String getKeywords() {
         return core.getKeywords();
     }
 
+    /**
+     * Returns the last modified user
+     *
+     * @return the modified user or null
+     */
     public String getLastModifiedBy() {
         return core.getLastModifiedBy();
     }
 
+    /**
+     * Returns the App version
+     *
+     * @return the excel app version or the eec version if it used eec
+     */
     public String getVersion() {
         return core.getVersion();
     }
 
+    /**
+     * Returns the file revision
+     *
+     * @return revision or null
+     */
     public String getRevision() {
         return core.getRevision();
     }
 
+    /**
+     * Returns the content's category, the value join with ','
+     *
+     * @return category value or null
+     */
     public String getCategory() {
         return core.getCategory();
     }
 
+    /**
+     * Returns the create time
+     *
+     * @return the file create date or null
+     */
     public Date getCreated() {
         return core.getCreated();
     }
 
+    /**
+     * Returns the last modified
+     *
+     * @return last modified date or null
+     */
     public Date getModified() {
         return core.getModified();
     }
@@ -94,19 +166,19 @@ public class AppInfo {
     @Override
     public String toString() {
         return "Application: " + getApplication()
-            + " Company: " + getCompany()
-            + " AppVersion: " + getAppVersion()
-            + " Title: " + getTitle()
-            + " Subject: " + getSubject()
-            + " Creator: " + getCreator()
-            + " Description: " + getDescription()
-            + " Keywords: " + getKeywords()
-            + " LastModifiedBy: " + getLastModifiedBy()
-            + " Version: " + getVersion()
-            + " Revision: " + getRevision()
-            + " Category: " + getCategory()
-            + " Created: " + (getCreated() != null ? DateUtil.toString(getCreated()) : StringUtil.EMPTY)
-            + " Modified: " + (getModified() != null ? DateUtil.toString(getModified()) : StringUtil.EMPTY)
+            + System.lineSeparator() + "Company: " + getCompany()
+            + System.lineSeparator() + "AppVersion: " + getAppVersion()
+            + System.lineSeparator() + "Title: " + getTitle()
+            + System.lineSeparator() + "Subject: " + getSubject()
+            + System.lineSeparator() + "Creator: " + getCreator()
+            + System.lineSeparator() + "Description: " + getDescription()
+            + System.lineSeparator() + "Keywords: " + getKeywords()
+            + System.lineSeparator() + "LastModifiedBy: " + getLastModifiedBy()
+            + System.lineSeparator() + "Version: " + getVersion()
+            + System.lineSeparator() + "Revision: " + getRevision()
+            + System.lineSeparator() + "Category: " + getCategory()
+            + System.lineSeparator() + "Created: " + (getCreated() != null ? DateUtil.toString(getCreated()) : StringUtil.EMPTY)
+            + System.lineSeparator() + "Modified: " + (getModified() != null ? DateUtil.toString(getModified()) : StringUtil.EMPTY)
             ;
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/Cell.java b/src/main/java/org/ttzero/excel/reader/Cell.java
index 097d59f5..7bc676cb 100644
--- a/src/main/java/org/ttzero/excel/reader/Cell.java
+++ b/src/main/java/org/ttzero/excel/reader/Cell.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -36,28 +36,61 @@ public class Cell {
     public static final char DATETIME    = 'i';
     public static final char DATE        = 'a';
     public static final char TIME        = 't';
-    // n=numeric
-    // s=string
-    // b=boolean
-    // f=function string
-    // r=inlineStr
-    // l=long
-    // d=double
+    /**
+     * Value type
+     * n=numeric
+     * s=string
+     * b=boolean
+     * f=function string
+     * r=inlineStr
+     * l=long
+     * d=double
+     */
     public char t; // type
-    // value
+    /**
+     * String value
+     */
     public String sv;
+    /**
+     * Integer value contain short
+     */
     public int nv;
+    /**
+     * Long value
+     */
     public long lv;
+    /**
+     * Double value contain float
+     */
     public double dv;
+    /**
+     * Boolean value
+     */
     public boolean bv;
+    /**
+     * Char value
+     */
     public char cv;
+    /**
+     * Decimal value
+     */
     public BigDecimal mv;
+    /**
+     * Style index
+     */
     public int xf;
-
     /**
-     * The style index
+     * Formula string
+     */
+    public String fv;
+    /**
+     * Shared calc id
+     */
+    public int si;
+    /**
+     * Has formula
      */
-    public short s;
+    public boolean f;
 
     public void setT(char t) {
         this.t = t;
@@ -127,6 +160,8 @@ public class Cell {
         this.cv = '\0';
         this.mv = null;
         this.xf = 0;
-        this.s = 0;
+        this.fv = null;
+        this.f = false;
+        this.si = -1;
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/Dimension.java b/src/main/java/org/ttzero/excel/reader/Dimension.java
index 28e25e35..36ddac77 100644
--- a/src/main/java/org/ttzero/excel/reader/Dimension.java
+++ b/src/main/java/org/ttzero/excel/reader/Dimension.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019-2020, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -16,41 +16,90 @@
 
 package org.ttzero.excel.reader;
 
+import static org.ttzero.excel.entity.Sheet.int2Col;
+import static org.ttzero.excel.reader.ExcelReader.cellRangeToLong;
+
 /**
+ * Worksheet dimension
+ * <p>
+ * This record contains the range address of the used area in the current sheet.
+ * <p>
  * Create by guanquan.wang at 2019-12-20 10:07
  */
 public class Dimension {
+    // Index to first used row
     public final int firstRow;
+    // Index to last used row, increased by 1
     public final int lastRow;
+    // Index to first used column
     public final short firstColumn;
+    // Index to last used column, increased by 1
     public final short lastColumn;
 
-    Dimension(int firstRow, int lastRow, short firstColumn, short lastColumn) {
+    public Dimension(int firstRow, short firstColumn, int lastRow, short lastColumn) {
         this.firstRow = firstRow;
-        this.lastRow = lastRow;
         this.firstColumn = firstColumn;
+        this.lastRow = lastRow;
         this.lastColumn = lastColumn;
     }
 
+    /**
+     * Create {@link Dimension} from a range string
+     *
+     * @param range range string like {@code A2:B2}
+     * @return the {@link Dimension} entry
+     */
+    public static Dimension from(String range) {
+        int i = range.indexOf(':');
+        if (i < 0 || i == range.length() - 1)
+            throw new IllegalArgumentException(range + " can't convert to Dimension.");
+
+        long f = cellRangeToLong(range.substring(0, i))
+            , t = cellRangeToLong(range.substring(i + 1));
+        return new Dimension((int) (f >> 16), (short) f, (int) (t >> 16), (short) t);
+    }
+
+    /**
+     * Returns the index to first used row, the min value is 1
+     *
+     * @return the first row number
+     */
     public int getFirstRow() {
         return firstRow;
     }
 
+    /**
+     * Returns the index to last used row, the max value
+     * is 1,048,576 in office 2007 or later and 65,536 in office 2003
+     *
+     * @return the last row number
+     */
     public int getLastRow() {
         return lastRow;
     }
 
+    /**
+     * Returns the index to first used column, the min value is 1
+     *
+     * @return the first column number
+     */
     public short getFirstColumn() {
         return firstColumn;
     }
 
+    /**
+     * Returns the index to last used column, the max value
+     * is 16,384 in office 2007 or later and 256 in office 2003
+     *
+     * @return the last column number
+     */
     public short getLastColumn() {
         return lastColumn;
     }
 
     @Override
     public String toString() {
-        return "{\"first-row\": " + firstRow + ", \"last-row\": " + lastRow
-            + ", \"first-column\": " + firstColumn + ", \"last-column\": " + lastColumn + "}";
+        return new String(int2Col(firstColumn)) + this.firstRow
+            + ":" + new String(int2Col(lastColumn)) + this.lastRow;
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index 792a4c4e..9d5613c5 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -47,6 +47,7 @@ import java.nio.file.Path;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.Iterator;
@@ -56,6 +57,9 @@ import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
+import static org.ttzero.excel.reader.SharedStrings.toInt;
+import static org.ttzero.excel.util.StringUtil.isNotEmpty;
+
 /**
  * Excel Reader tools
  *
@@ -74,7 +78,7 @@ import java.util.stream.StreamSupport;
  * Create by guanquan.wang on 2018-09-22
  */
 public class ExcelReader implements AutoCloseable {
-    private Logger logger = LogManager.getLogger(getClass());
+    private Logger LOGGER = LogManager.getLogger(getClass());
 
     protected ExcelReader() { }
 
@@ -83,6 +87,7 @@ public class ExcelReader implements AutoCloseable {
     private Path temp;
     private ExcelType type;
     private AppInfo appInfo;
+    boolean hasFormula;
 
     /**
      * The Shared String Table
@@ -291,29 +296,54 @@ public class ExcelReader implements AutoCloseable {
      * @return the information
      */
     public AppInfo getAppInfo() {
-        if (appInfo == null) {
-            appInfo = getGeneralInfo();
-        }
         return appInfo;
     }
 
+    /**
+     * Check current workbook has formula
+     *
+     * @return boolean
+     */
+    public boolean hasFormula() {
+        return hasFormula;
+    }
+
+    /**
+     * Make the reader parse formula
+     *
+     * @return self
+     */
+    public ExcelReader parseFormula() {
+        for (Sheet sheet : sheets) {
+            sheet.parseFormula();
+        }
+        return this;
+    }
 
     // --- PRIVATE FUNCTIONS
 
 
     private ExcelReader(Path path, int bufferSize, int cacheSize) throws IOException {
         // Store template stream as zip file
-        Path temp = FileUtil.mktmp(Const.EEC_PREFIX);
-        ZipUtil.unzip(Files.newInputStream(path), temp);
+        Path tmp = FileUtil.mktmp(Const.EEC_PREFIX);
+        ZipUtil.unzip(Files.newInputStream(path), tmp);
+
+        // Check the file format and parse general information
+        try {
+            appInfo = getGeneralInfo(tmp);
+        } catch (Exception e) {
+            FileUtil.rm_rf(tmp.toFile(), true);
+            throw e;
+        }
 
         // load workbook.xml
         SAXReader reader = new SAXReader();
         Document document;
         try {
-            document = reader.read(Files.newInputStream(temp.resolve("xl/_rels/workbook.xml.rels")));
+            document = reader.read(Files.newInputStream(tmp.resolve("xl/_rels/workbook.xml.rels")));
         } catch (DocumentException | IOException e) {
-            FileUtil.rm_rf(temp.toFile(), true);
-            throw new ExcelReadException(e);
+            FileUtil.rm_rf(tmp.toFile(), true);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/_rels/workbook.xml.rels]");
         }
         @SuppressWarnings("unchecked")
         List<Element> list = document.getRootElement().elements();
@@ -325,20 +355,29 @@ public class ExcelReader implements AutoCloseable {
         RelManager relManager = RelManager.of(rels);
 
         try {
-            document = reader.read(Files.newInputStream(temp.resolve("xl/workbook.xml")));
+            document = reader.read(Files.newInputStream(tmp.resolve("xl/workbook.xml")));
         } catch (DocumentException | IOException e) {
             // read style file fail.
-            FileUtil.rm_rf(temp.toFile(), true);
-            throw new ExcelReadException(e);
+            FileUtil.rm_rf(tmp.toFile(), true);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/workbook.xml]");
         }
         Element root = document.getRootElement();
         Namespace ns = root.getNamespaceForPrefix("r");
 
         // Load SharedString
-        sst = new SharedStrings(temp.resolve("xl/sharedStrings.xml"), bufferSize, cacheSize).load();
+        Path ss = tmp.resolve("xl/sharedStrings.xml");
+        if (Files.exists(ss)) {
+            sst = new SharedStrings(ss, bufferSize, cacheSize).load();
+        }
 
         // Load Styles
-        styles = Styles.load(temp.resolve("xl/styles.xml"));
+        Path s = tmp.resolve("xl/styles.xml");
+        if (Files.exists(s)) {
+            styles = Styles.load(s);
+        } else {
+            FileUtil.rm_rf(tmp.toFile(), true);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/styles.xml]");
+        }
 
         List<Sheet> sheets = new ArrayList<>();
         @SuppressWarnings("unchecked")
@@ -352,11 +391,11 @@ public class ExcelReader implements AutoCloseable {
             sheet.setHidden("hidden".equals(state));
             Relationship r = relManager.getById(e.attributeValue(QName.get("id", ns)));
             if (r == null) {
-                FileUtil.rm_rf(temp.toFile(), true);
+                FileUtil.rm_rf(tmp.toFile(), true);
                 sheet.close();
-                throw new ExcelReadException("File has be destroyed");
+                throw new ExcelReadException("The file format is incorrect or corrupted.");
             }
-            sheet.setPath(temp.resolve("xl").resolve(r.getTarget()));
+            sheet.setPath(tmp.resolve("xl").resolve(r.getTarget()));
             // put shared string
             sheet.setSst(sst);
             // Setting styles
@@ -364,14 +403,33 @@ public class ExcelReader implements AutoCloseable {
             sheets.add(sheet);
         }
 
+        if (sheets.isEmpty()) {
+            FileUtil.rm_rf(tmp.toFile(), true);
+            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 = temp;
+        self = tmp;
     }
 
     /**
@@ -461,14 +519,14 @@ public class ExcelReader implements AutoCloseable {
         return excelType;
     }
 
-    protected AppInfo getGeneralInfo() {
+    protected AppInfo getGeneralInfo(Path tmp) {
         // load workbook.xml
         SAXReader reader = new SAXReader();
         Document document;
         try {
-            document = reader.read(Files.newInputStream(self.resolve("docProps/app.xml")));
+            document = reader.read(Files.newInputStream(tmp.resolve("docProps/app.xml")));
         } catch (DocumentException | IOException e) {
-            throw new ExcelReadException(e);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [docProps/app.xml]");
         }
         Element root = document.getRootElement();
         App app = new App();
@@ -477,9 +535,9 @@ public class ExcelReader implements AutoCloseable {
         app.setAppVersion(root.elementText("AppVersion"));
 
         try {
-            document = reader.read(Files.newInputStream(self.resolve("docProps/core.xml")));
+            document = reader.read(Files.newInputStream(tmp.resolve("docProps/core.xml")));
         } catch (DocumentException | IOException e) {
-            throw new ExcelReadException(e);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [docProps/core.xml]");
         }
         root = document.getRootElement();
         Core core = new Core();
@@ -494,26 +552,109 @@ public class ExcelReader implements AutoCloseable {
 
             f.setAccessible(true);
             int nsIndex = StringUtil.indexOf(prefixs, ns.value());
-            if (nsIndex > -1) {
-                Namespace namespace = new Namespace(ns.value(), urls[nsIndex]);
-                Class<?> type = f.getType();
-                String v = root.elementText(new QName(f.getName(), namespace));
-                if (type == String.class) {
-                    try {
-                        f.set(core, v);
-                    } catch (IllegalAccessException e) {
-                        logger.warn("Set field (" + f + ") error.");
-                    }
-                } else if (type == Date.class) {
-                    try {
-                        f.set(core, format.parse(v));
-                    } catch (ParseException | IllegalAccessException e) {
-                        logger.warn("Set field (" + f + ") error.");
-                    }
+            if (nsIndex < 0) continue;
+
+            Namespace namespace = new Namespace(ns.value(), urls[nsIndex]);
+            Class<?> type = f.getType();
+            String v = root.elementText(new QName(f.getName(), namespace));
+            if (type == String.class) {
+                try {
+                    f.set(core, v);
+                } catch (IllegalAccessException e) {
+                    LOGGER.warn("Set field ({}) error.", f);
+                }
+            } else if (type == Date.class) {
+                try {
+                    f.set(core, format.parse(v));
+                } catch (ParseException | IllegalAccessException e) {
+                    LOGGER.warn("Set field ({}) error.", f);
                 }
             }
         }
 
         return new AppInfo(app, core);
     }
+
+    /* Parse `calcChain` */
+    private long[][] parseCalcChain(Path path, int n) {
+        Element calcChain;
+        try {
+            SAXReader reader = new SAXReader();
+            calcChain = reader.read(Files.newInputStream(path)).getRootElement();
+        } catch (DocumentException | IOException e) {
+            LOGGER.warn("Part of `calcChain` has be damaged, It will be ignore all formulas.");
+            return null;
+        }
+
+        @SuppressWarnings("unchecked")
+        Iterator<Element> ite = calcChain.elementIterator();
+        int i = 1;
+        long[][] array = new long[n][];
+        int[] indices = new int[n];
+        for (; ite.hasNext(); ) {
+            Element e = ite.next();
+            String si = e.attributeValue("i"), r = e.attributeValue("r");
+            if (isNotEmpty(si)) {
+                i = toInt(si.toCharArray(), 0, si.length());
+            }
+            if (isNotEmpty(r)) {
+                long[] sub = array[i - 1];
+                if (sub == null) {
+                    sub = new long[10];
+                    array[i - 1] = sub;
+                }
+
+                if (++indices[i - 1] > sub.length) {
+                    long[] _sub = new long[sub.length << 1];
+                    System.arraycopy(sub, 0, _sub, 0, sub.length);
+                    array[i - 1] = sub = _sub;
+                }
+                sub[indices[i - 1] - 1] = cellRangeToLong(r);
+            }
+        }
+
+        i = 0;
+        for (; i < n; i++) {
+            if (indices[i] > 0) {
+                long[] a = Arrays.copyOf(array[i], indices[i]);
+                Arrays.sort(a);
+                array[i] = a;
+            } else array[i] = null;
+        }
+        return array;
+    }
+
+    /**
+     * Cell range string convert to long
+     * 0-16: column number
+     * 17-48: row number
+     * <blockquote><pre>
+     * range string| long value
+     * ------------|------------
+     * A1          | 65537
+     * AA10        | 655387
+     * </pre></blockquote>
+     * @param r the range string of cell
+     * @return long value
+     */
+    public static long cellRangeToLong(String r) {
+        char[] values = r.toCharArray();
+        long v = 0L;
+        int n = 0;
+        for (char value : values) {
+            if (value >= 'A' && value <= 'Z') {
+                v = v * 26 + value - 'A' + 1;
+            }
+            else if (value >= 'a' && value <= 'z') {
+                v = v * 26 + value - 'a' + 1;
+            }
+            else if (value >= '0') {
+                n = n * 10 + value - '0';
+            }
+            else
+                throw new ExcelReadException("Column mark out of range: " + r);
+        }
+        return (v & 0x7FFF) | ((long) n) << 16;
+    }
+
 }
diff --git a/src/main/java/org/ttzero/excel/reader/HeaderRow.java b/src/main/java/org/ttzero/excel/reader/HeaderRow.java
index 2863a5a6..3ce13367 100644
--- a/src/main/java/org/ttzero/excel/reader/HeaderRow.java
+++ b/src/main/java/org/ttzero/excel/reader/HeaderRow.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
diff --git a/src/main/java/org/ttzero/excel/reader/RowSetProcessor.java b/src/main/java/org/ttzero/excel/reader/MergeCalc.java
similarity index 64%
rename from src/main/java/org/ttzero/excel/reader/RowSetProcessor.java
rename to src/main/java/org/ttzero/excel/reader/MergeCalc.java
index 0f22c557..b6eeab1c 100644
--- a/src/main/java/org/ttzero/excel/reader/RowSetProcessor.java
+++ b/src/main/java/org/ttzero/excel/reader/MergeCalc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -16,17 +16,20 @@
 
 package org.ttzero.excel.reader;
 
-import java.io.IOException;
-
 /**
- * Created by guanquan.wang on 2018/6/13.
+ * Test and merge formula each rows.
+ *
+ * Create by guanquan.wang at 2019-12-31 15:42
  */
 @FunctionalInterface
-interface RowSetProcessor {
+interface MergeCalc {
+
     /**
-     * The row set
+     * Merge formula in rows
      *
-     * @return the row data
+     * @param row thr row number
+     * @param cells the cells in row
+     * @param n count of cells
      */
-    Row next() throws IOException;
+    void accept(int row, Cell[] cells, int n);
 }
diff --git a/src/main/java/org/ttzero/excel/reader/PreCalc.java b/src/main/java/org/ttzero/excel/reader/PreCalc.java
new file mode 100644
index 00000000..59702519
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/reader/PreCalc.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2019-2021, 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;
+
+import org.ttzero.excel.manager.Const;
+
+import static org.ttzero.excel.entity.Sheet.int2Col;
+import static org.ttzero.excel.reader.Row.toCellIndex;
+import static org.ttzero.excel.reader.SharedStrings.toInt;
+
+/**
+ * Preprocessed calc node
+ *
+ * Create by guanquan.wang at 2020-01-05 18:40
+ */
+class PreCalc {
+    // Reference position
+    private long position;
+    // calc string value
+    private char[] cb;
+    private Node head, tail;
+
+    private static class Node {
+        // From index, to index
+        private int f, t;
+        // Cell coordinate
+        private long coordinate;
+        // The next node
+        private Node next;
+    }
+
+    PreCalc(long position) {
+        this.position = position;
+    }
+
+    /* Pre-processing formula strings for fast getting */
+    void setCalc(char[] cb) {
+        this.cb = cb;
+
+        int len = cb.length;
+        int f = 0, m = f, n;
+        for (; f < len; ) {
+            // FIXME maybe include quote and double quotes
+            for (; f < len && (cb[f] < 'A' || cb[f] > 'Z') && cb[f] != '"'; f++) ;
+            // EOF
+            if (f >= len) break;
+
+            // Find the end quote tag
+            if (cb[f] == '"') {
+                for (; ++f < len && cb[f] != '"'; ) ;
+                // EOF
+                if (f >= len) break;
+                f++;
+                continue;
+            } else n = f;
+
+            // Column
+            for (; f < len && cb[f] >= 'A' && cb[f] <= 'Z'; f++) ;
+
+            int c = toCellIndex(cb, n, f);
+            if (c > Const.Limit.MAX_COLUMNS_ON_SHEET) {
+                continue;
+            }
+
+            int t = f; // tmp
+            // Row
+            for (; f < len && cb[f] >= '0' && cb[f] <= '9'; f++) ;
+            if (f == t) continue;
+            int r = toInt(cb, t, f);
+            if (r < 0 || r > Const.Limit.MAX_ROWS_ON_SHEET) {
+                continue;
+            }
+
+            Node node = new Node();
+            node.f = m;
+            node.t = n;
+
+            if (head == null) {
+                head = tail = node;
+            } else {
+                tail.next = node;
+                tail = node;
+            }
+
+            Node next = new Node();
+            next.coordinate = r << 14 | c;
+
+            tail.next = next;
+            tail = next;
+
+            m = f;
+        }
+
+        if (m > 0 && m < f) {
+            Node node = new Node();
+            node.f = m;
+            node.t = f;
+            tail.next = node;
+            tail = node;
+        }
+    }
+
+    String get(long coordinate) {
+        if (head == null) {
+            return new String(cb, 0, cb.length);
+        } else {
+//            int t = (int) (position & 0x03);
+
+            // Offset from first calc cell
+            int offset_x = (int) ((coordinate & (1 << 14) - 1) - (position >> 28 & (1 << 14) - 1));
+            int offset_y = (int) (((coordinate >> 14) & (1 << 20) - 1) - (position >> 42 & (1 << 20) - 1));
+
+
+            Node node = head;
+            StringBuilder buf = new StringBuilder();
+            for (; node != null; ) {
+                if (node.coordinate > 0) {
+                    buf.append(int2Col((int) (node.coordinate & (1 << 14) - 1) + offset_x));
+                    buf.append((int) (node.coordinate >> 14) + offset_y);
+                } else {
+                    buf.append(cb, node.f, node.t - node.f);
+                }
+                node = node.next;
+            }
+
+            return buf.toString();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return get((position >> 42 & (1 << 20) - 1) << 14 | ((position >> 28) & (1 << 14) - 1));
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/reader/Row.java b/src/main/java/org/ttzero/excel/reader/Row.java
index 77bdfefe..d7ab55ee 100644
--- a/src/main/java/org/ttzero/excel/reader/Row.java
+++ b/src/main/java/org/ttzero/excel/reader/Row.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -19,12 +19,12 @@ package org.ttzero.excel.reader;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.ttzero.excel.entity.style.Styles;
-import org.ttzero.excel.util.DateUtil;
 import org.ttzero.excel.util.StringUtil;
 
 import java.lang.reflect.InvocationTargetException;
 import java.math.BigDecimal;
 import java.sql.Timestamp;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.StringJoiner;
 
@@ -34,14 +34,17 @@ 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.FUNCTION;
 import static org.ttzero.excel.reader.Cell.INLINESTR;
 import static org.ttzero.excel.reader.Cell.LONG;
 import static org.ttzero.excel.reader.Cell.NUMERIC;
 import static org.ttzero.excel.reader.Cell.SST;
 import static org.ttzero.excel.reader.Cell.TIME;
+import static org.ttzero.excel.util.DateUtil.toDate;
 import static org.ttzero.excel.util.DateUtil.toLocalDate;
+import static org.ttzero.excel.util.DateUtil.toTime;
 import static org.ttzero.excel.util.DateUtil.toTimestamp;
+import static org.ttzero.excel.util.StringUtil.EMPTY;
+import static org.ttzero.excel.util.StringUtil.isNotEmpty;
 
 /**
  * Create by guanquan.wang at 2019-04-17 11:08
@@ -64,13 +67,16 @@ public abstract class Row {
     protected HeaderRow hr;
     boolean unknownLength;
 
+    // Cache formulas
+    private PreCalc[] sharedCalc;
+
     /**
      * The global styles
      */
     Styles styles;
 
     /**
-     * The number of row. (zero base)
+     * The number of row. (one base)
      *
      * @return int value
      */
@@ -195,10 +201,10 @@ public abstract class Row {
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                v = StringUtil.isNotEmpty(c.sv);
+                v = isNotEmpty(c.sv);
                 break;
             case INLINESTR:
-                v = StringUtil.isNotEmpty(c.sv);
+                v = isNotEmpty(c.sv);
                 break;
 
             default: v = false;
@@ -289,15 +295,13 @@ public abstract class Row {
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                String s = c.sv;
-                if (StringUtil.isNotEmpty(s)) {
-                    cc |= s.charAt(0);
+                if (isNotEmpty(c.sv)) {
+                    cc |= c.sv.charAt(0);
                 }
                 break;
             case INLINESTR:
-                s = c.sv;
-                if (StringUtil.isNotEmpty(s)) {
-                    cc |= s.charAt(0);
+                if (isNotEmpty(c.sv)) {
+                    cc |= c.sv.charAt(0);
                 }
                 break;
             case NUMERIC:
@@ -412,18 +416,10 @@ public abstract class Row {
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                try {
-                    n = Integer.parseInt(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to int");
-                }
+                n = Integer.parseInt(c.sv);
                 break;
             case INLINESTR:
-                try {
-                    n = Integer.parseInt(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to int");
-                }
+                n = Integer.parseInt(c.sv);
                 break;
 
             default: throw new UncheckedTypeException("unknown type");
@@ -475,18 +471,10 @@ public abstract class Row {
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                try {
-                    l = Long.parseLong(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to long");
-                }
+                l = Long.parseLong(c.sv);
                 break;
             case INLINESTR:
-                try {
-                    l = Long.parseLong(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to long");
-                }
+                l = Long.parseLong(c.sv);
                 break;
             case BOOL:
                 l = c.bv ? 1L : 0L;
@@ -537,7 +525,7 @@ public abstract class Row {
                 s = c.sv;
                 break;
             case BLANK:
-                s = StringUtil.EMPTY;
+                s = EMPTY;
                 break;
             case LONG:
                 s = String.valueOf(c.lv);
@@ -614,18 +602,13 @@ public abstract class Row {
                 d = c.nv;
                 break;
             case SST:
-                try {
-                    d = Double.valueOf(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to double");
+                if (c.sv == null) {
+                    c.setSv(sst.get(c.nv));
                 }
+                d = Double.valueOf(c.sv);
                 break;
             case INLINESTR:
-                try {
-                    d = Double.valueOf(c.sv);
-                } catch (NumberFormatException e) {
-                    throw new UncheckedTypeException("String value " + c.sv + " can't convert to double");
-                }
+                d = Double.valueOf(c.sv);
                 break;
 
             default: throw new UncheckedTypeException("unknown type");
@@ -708,19 +691,19 @@ public abstract class Row {
         Date date;
         switch (c.t) {
             case NUMERIC:
-                date = DateUtil.toDate(c.nv);
+                date = toDate(c.nv);
                 break;
             case DOUBLE:
-                date = DateUtil.toDate(c.dv);
+                date = toDate(c.dv);
                 break;
             case SST:
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                date = DateUtil.toDate(c.sv);
+                date = toDate(c.sv);
                 break;
             case INLINESTR:
-                date = DateUtil.toDate(c.sv);
+                date = toDate(c.sv);
                 break;
             default: throw new UncheckedTypeException("");
         }
@@ -759,19 +742,19 @@ public abstract class Row {
         Timestamp ts;
         switch (c.t) {
             case NUMERIC:
-                ts = DateUtil.toTimestamp(c.nv);
+                ts = toTimestamp(c.nv);
                 break;
             case DOUBLE:
-                ts = DateUtil.toTimestamp(c.dv);
+                ts = toTimestamp(c.dv);
                 break;
             case SST:
                 if (c.sv == null) {
                     c.setSv(sst.get(c.nv));
                 }
-                ts = DateUtil.toTimestamp(c.sv);
+                ts = toTimestamp(c.sv);
                 break;
             case INLINESTR:
-                ts = DateUtil.toTimestamp(c.sv);
+                ts = toTimestamp(c.sv);
                 break;
             default: throw new UncheckedTypeException("");
         }
@@ -787,7 +770,7 @@ public abstract class Row {
     public java.sql.Time getTime(int columnIndex) {
         Cell c = getCell(columnIndex);
         if (c.t == DOUBLE) {
-            return DateUtil.toTime(c.dv);
+            return toTime(c.dv);
         }
         throw new UncheckedTypeException("can't convert to java.sql.Time");
     }
@@ -801,11 +784,53 @@ public abstract class Row {
     public java.sql.Time getTime(String columnName) {
         Cell c = getCell(columnName);
         if (c.t == DOUBLE) {
-            return DateUtil.toTime(c.dv);
+            return toTime(c.dv);
         }
         throw new UncheckedTypeException("can't convert to java.sql.Time");
     }
 
+    /**
+     * Returns formula if exists
+     *
+     * @param columnIndex the cell index
+     * @return the formula string if exists, otherwise return null
+     */
+    public String getFormula(int columnIndex) {
+        Cell c = getCell(columnIndex);
+        return c.fv;
+    }
+
+    /**
+     * Returns formula if exists
+     *
+     * @param columnName the cell name
+     * @return the formula string if exists, otherwise return null
+     */
+    public String getFormula(String columnName) {
+        Cell c = getCell(columnName);
+        return c.fv;
+    }
+
+    /**
+     * Check cell has formula
+     *
+     * @param columnIndex the cell index
+     * @return the formula string if exists, otherwise return null
+     */
+    public boolean hasFormula(int columnIndex) {
+        return getCell(columnIndex).f;
+    }
+
+    /**
+     * Check cell has formula
+     *
+     * @param columnName the cell name
+     * @return the formula string if exists, otherwise return null
+     */
+    public boolean hasFormula(String columnName) {
+        return getCell(columnName).f;
+    }
+
     /**
      * Returns the type of cell
      *
@@ -843,13 +868,13 @@ public abstract class Row {
                 break;
             case NUMERIC:
             case CHARACTER:
-                type = !styles.fastTestDateFmt(c.s) ? CellType.INTEGER : CellType.DATE;
+                type = !styles.fastTestDateFmt(c.xf) ? CellType.INTEGER : CellType.DATE;
                 break;
             case LONG:
                 type = CellType.LONG;
                 break;
             case DOUBLE:
-                type = !styles.fastTestDateFmt(c.s) ? CellType.DOUBLE : CellType.DATE;
+                type = !styles.fastTestDateFmt(c.xf) ? CellType.DOUBLE : CellType.DATE;
                 break;
             case BOOL:
                 type = CellType.BOOLEAN;
@@ -983,26 +1008,116 @@ public abstract class Row {
                 case BOOL:
                     joiner.add(String.valueOf(c.bv));
                     break;
-                case FUNCTION:
-                    joiner.add("<function>");
-                    break;
+//                case FUNCTION: // convert to inner string
+//                    joiner.add("<function>");
+//                    break;
                 case NUMERIC:
-                    if (!styles.fastTestDateFmt(c.s)) joiner.add(String.valueOf(c.nv));
+                    if (!styles.fastTestDateFmt(c.xf)) joiner.add(String.valueOf(c.nv));
                     else joiner.add(toLocalDate(c.nv).toString());
                     break;
                 case LONG:
                     joiner.add(String.valueOf(c.lv));
                     break;
                 case DOUBLE:
-                    if (!styles.fastTestDateFmt(c.s)) joiner.add(String.valueOf(c.dv));
+                    if (!styles.fastTestDateFmt(c.xf)) joiner.add(String.valueOf(c.dv));
                     else joiner.add(toTimestamp(c.dv).toString());
                     break;
                 case BLANK:
-                    joiner.add(StringUtil.EMPTY);
+                    joiner.add(EMPTY);
                 default:
                     joiner.add(null);
             }
         }
         return joiner.toString();
     }
+
+    /**
+     * Add function shared ref
+     * <blockquote><pre>
+     * 63   : Not used
+     * 42-62: First row number
+     * 28-41: First column number
+     * 8-27/14-27: Size, if axis is zero the size used 20 bits, otherwise used 14 bits
+     * 2-7/2-13: Not used
+     * 0-1    : Axis, 00: range 01: y-axis 10: x-axis
+     * </pre></blockquote>
+     *
+     * @param i the ref id
+     * @param ref ref value, a range dimension string
+     */
+    void addRef(int i, String ref) {
+        if (StringUtil.isEmpty(ref) || ref.indexOf(':') < 0)
+            return;
+
+        if (sharedCalc == null) {
+            sharedCalc = new PreCalc[Math.max(10, i + 1)];
+        } else if (i >= sharedCalc.length) {
+            sharedCalc = Arrays.copyOf(sharedCalc, i + 10);
+        }
+        Dimension dim = Dimension.from(ref);
+
+        long l = 0;
+        l |= (long) (dim.firstRow & (1 << 20) - 1) << 42;
+        l |= (long) (dim.firstColumn & (1 << 14) - 1) << 28;
+
+        if (dim.firstColumn == dim.lastColumn) {
+            l |= ((dim.lastRow - dim.firstRow) & (1 << 20) - 1) << 8;
+            l |= (1 << 1);
+        }
+        else if (dim.firstRow == dim.lastRow) {
+            l |= ((dim.lastColumn - dim.firstColumn) & (1 << 14) - 1) << 14;
+            l |= 1;
+        }
+        sharedCalc[i] = new PreCalc(l);
+    }
+
+    /**
+     * Setting calc string
+     *
+     * @param i the ref id
+     * @param calc the calc string
+     */
+    void setCalc(int i, String calc) {
+        if (sharedCalc == null || sharedCalc.length <= i
+            || sharedCalc[i] == null || StringUtil.isEmpty(calc))
+            return;
+
+        sharedCalc[i].setCalc(calc.toCharArray());
+    }
+
+    /**
+     * Get calc string by ref id and coordinate
+     *
+     * @param i the ref id
+     * @param coordinate the cell coordinate
+     * @return calc string
+     */
+    String getCalc(int i, long coordinate) {
+        // Index out of range
+        if (sharedCalc == null || sharedCalc.length <= i
+            || sharedCalc[i] == null)
+            return EMPTY;
+
+        return sharedCalc[i].get(coordinate);
+    }
+
+    /**
+     * Convert to column index
+     *
+     * @param cb character buffer
+     * @param a the start index
+     * @param b the end index
+     * @return the cell index
+     */
+    public static int toCellIndex(char[] cb, int a, int b) {
+        int n = 0;
+        for (; a <= b; a++) {
+            if (cb[a] <= 'Z' && cb[a] >= 'A') {
+                n = n * 26 + cb[a] - '@';
+            } else if (cb[a] <= 'z' && cb[a] >= 'a') {
+                n = n * 26 + cb[a] - '、';
+            } else break;
+        }
+        return n;
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/RowSetIterator.java b/src/main/java/org/ttzero/excel/reader/RowSetIterator.java
index 904922fb..c3a5c1cc 100644
--- a/src/main/java/org/ttzero/excel/reader/RowSetIterator.java
+++ b/src/main/java/org/ttzero/excel/reader/RowSetIterator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -16,21 +16,20 @@
 
 package org.ttzero.excel.reader;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Supplier;
 
 /**
  * Create by guanquan.wang at 2019-04-17 19:03
  */
 class RowSetIterator implements Iterator<Row> {
     private boolean onlyDataRow;
-    private RowSetProcessor processor;
+    private Supplier<Row> supplier;
     private Row nextRow = null;
 
-    public RowSetIterator(RowSetProcessor processor, boolean onlyDataRow) {
-        this.processor = processor;
+    public RowSetIterator(Supplier<Row> supplier, boolean onlyDataRow) {
+        this.supplier = supplier;
         this.onlyDataRow = onlyDataRow;
     }
 
@@ -39,17 +38,13 @@ class RowSetIterator implements Iterator<Row> {
         if (nextRow != null) {
             return true;
         } else {
-            try {
-                if (onlyDataRow) {
-                    // Skip empty rows
-                    for (; (nextRow = processor.next()) != null && nextRow.isEmpty(); ) ;
-                } else {
-                    nextRow = processor.next();
-                }
-                return (nextRow != null);
-            } catch (IOException e) {
-                throw new UncheckedIOException(e);
+            if (onlyDataRow) {
+                // Skip empty rows
+                for (; (nextRow = supplier.get()) != null && nextRow.isEmpty(); ) ;
+            } else {
+                nextRow = supplier.get();
             }
+            return (nextRow != null);
         }
     }
 
diff --git a/src/main/java/org/ttzero/excel/reader/SharedStrings.java b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
index 1b5f8a69..a6db64eb 100644
--- a/src/main/java/org/ttzero/excel/reader/SharedStrings.java
+++ b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
@@ -27,6 +27,8 @@ import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.ttzero.excel.util.StringUtil.EMPTY;
+
 /**
  * Read sharedString data
  * <p>
@@ -476,6 +478,7 @@ public class SharedStrings implements AutoCloseable {
      * unescape
      */
     static String unescape(StringBuilder escapeBuf, char[] cb, int from, int to) {
+        if (from == to) return EMPTY;
         int idx_38 = indexOf(cb, '&', from)
             , idx_59 = idx_38 > -1 && idx_38 < to ? indexOf(cb, ';', idx_38 + 1) : -1;
 
diff --git a/src/main/java/org/ttzero/excel/reader/Sheet.java b/src/main/java/org/ttzero/excel/reader/Sheet.java
index fef5c875..e14337a3 100644
--- a/src/main/java/org/ttzero/excel/reader/Sheet.java
+++ b/src/main/java/org/ttzero/excel/reader/Sheet.java
@@ -137,9 +137,10 @@ public interface Sheet extends AutoCloseable {
      * Reset the {@link Sheet}'s row index to begging
      *
      * @return the unread {@link Sheet}
-     * @throws IOException if I/O error occur
+     * @throws ExcelReadException if I/O error occur.
+     * @throws UnsupportedOperationException if sub-class un-implement this function.
      */
-    default Sheet reset() throws IOException {
+    default Sheet reset() {
         throw new UnsupportedOperationException();
     }
 
@@ -196,6 +197,7 @@ public interface Sheet extends AutoCloseable {
      * Save file as Comma-Separated Values. Each worksheet corresponds to
      * a csv file. Default charset is 'UTF8' and separator character is ','.
      * @param path the output storage path
+     * @throws IOException if I/O error occur.
      */
     default void saveAsCSV(Path path) throws IOException {
         // Create path if not exists
@@ -213,6 +215,7 @@ public interface Sheet extends AutoCloseable {
      * Save file as Comma-Separated Values. Each worksheet corresponds to
      * a csv file. Default charset is 'UTF8' and separator character is ','.
      * @param os the output
+     * @throws IOException if I/O error occur.
      */
     default void saveAsCSV(OutputStream os) throws IOException {
         try (CSVUtil.Writer writer = CSVUtil.newWriter(os)) {
@@ -229,23 +232,21 @@ public interface Sheet extends AutoCloseable {
                             writer.write(c.sv);
                             break;
                         case INLINESTR:
+                        case FUNCTION: // Formula string value
                             writer.write(c.sv);
                             break;
                         case BOOL:
                             writer.write(c.bv);
                             break;
-                        case FUNCTION:
-                            writer.write("<function>");
-                            break;
                         case NUMERIC:
-                            if (!row.styles.fastTestDateFmt(c.s)) writer.write(c.nv);
+                            if (!row.styles.fastTestDateFmt(c.xf)) writer.write(c.nv);
                             else writer.write(toLocalDate(c.nv).toString());
                             break;
                         case LONG:
                             writer.write(c.lv);
                             break;
                         case DOUBLE:
-                            if (!row.styles.fastTestDateFmt(c.s)) writer.write(c.dv);
+                            if (!row.styles.fastTestDateFmt(c.xf)) writer.write(c.dv);
                             else writer.write(toTimestamp(c.dv).toString());
                             break;
                         default:
@@ -256,4 +257,9 @@ public interface Sheet extends AutoCloseable {
             }
         }
     }
+
+    /**
+     * Make reader parse the formula
+     */
+    void parseFormula();
 }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLRow.java b/src/main/java/org/ttzero/excel/reader/XMLRow.java
index fbcbedd0..c58c8687 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLRow.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLRow.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -26,6 +26,7 @@ import static org.ttzero.excel.reader.Cell.INLINESTR;
 import static org.ttzero.excel.reader.Cell.BLANK;
 import static org.ttzero.excel.reader.SharedStrings.toInt;
 import static org.ttzero.excel.reader.SharedStrings.unescape;
+import static org.ttzero.excel.util.StringUtil.swap;
 
 /**
  * Row data, shared by the Row object in the same Sheet page.
@@ -42,9 +43,11 @@ import static org.ttzero.excel.reader.SharedStrings.unescape;
 class XMLRow extends Row {
     private int startRow;
     private StringBuilder buf;
+    private MergeCalc calcFun;
+    private boolean hasCalc;
 
     /**
-     * The number of row. (zero base)
+     * The number of row. (one base)
      *
      * @return int value
      */
@@ -53,17 +56,19 @@ class XMLRow extends Row {
         if (index == -1)
             searchRowNumber();
         // The first row index is one
-        return index - 1;
+        return index;
     }
 
     @SuppressWarnings("unused")
-	private XMLRow() { }
+    private XMLRow() { }
 
-    XMLRow(SharedStrings sst, Styles styles, int startRow) {
+    XMLRow(SharedStrings sst, Styles styles, int startRow, MergeCalc calcFun) {
         this.sst = sst;
         this.styles = styles;
         this.startRow = startRow;
         buf = new StringBuilder();
+        this.calcFun = calcFun;
+        this.hasCalc = calcFun != null;
     }
 
     /////////////////////////unsafe////////////////////////
@@ -96,7 +101,7 @@ class XMLRow extends Row {
     }
 
     private int searchRowNumber() {
-        int _f = from + 4, a; // skip '<row '
+        int _f = from + 4, a; // skip '<row'
         for (; cb[_f] != '>' && _f < to; _f++) {
             if (cb[_f] <= ' ' && cb[_f + 1] == 'r' && cb[_f + 2] == '=') {
                 a = _f += 4;
@@ -111,7 +116,7 @@ class XMLRow extends Row {
     }
 
     private int searchSpan() {
-        int i = from, _lc = lc;
+        int i = from + 4, _lc = lc;
         for (; cb[i] != '>'; i++) {
             if (cb[i] <= ' ' && cb[i + 1] == 's' && cb[i + 2] == 'p'
                 && cb[i + 3] == 'a' && cb[i + 4] == 'n' && cb[i + 5] == 's'
@@ -150,6 +155,13 @@ class XMLRow extends Row {
         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);
+        }
+
+        // Parse cell value
         if (unknownLength) {
             while (nextCell() != null) index++;
         } else {
@@ -176,14 +188,14 @@ class XMLRow extends Row {
         // find type
         // n=numeric (default), s=string, b=boolean, str=function string
         char t = NUMERIC; // default
-        // The style index
-        short s = 0;
+        int xf = 0, i = 1;
         for (; cb[cursor] != '>'; cursor++) {
             // Cell index
             if (cb[cursor] <= ' ' && cb[cursor + 1] == 'r' && cb[cursor + 2] == '=') {
                 int a = cursor += 4;
                 for (; cb[cursor] != '"'; cursor++) ;
-                cell = cells[unknownLength ? (lc = toCellIndex(a, cursor)) - 1 : toCellIndex(a, cursor) - 1];
+                i = unknownLength ? (lc = toCellIndex(cb, a, cursor)) : toCellIndex(cb, a, cursor);
+                cell = cells[i - 1];
             }
             // Cell type
             if (cb[cursor] <= ' ' && cb[cursor + 1] == 't' && cb[cursor + 2] == '=') {
@@ -203,16 +215,30 @@ class XMLRow extends Row {
             if (cb[cursor] <= ' ' && cb[cursor + 1] == 's' && cb[cursor + 2] == '=') {
                 int a = cursor += 4;
                 for (; cb[cursor] != '"'; cursor++) ;
-                s = (short) (toInt(cb, a, cursor) & 0xFFFF);
+                xf = toInt(cb, a, cursor);
             }
         }
 
         if (cell == null) return null;
 
         // The style index
-        cell.s = s;
+        cell.xf = xf;
 
-        // get value
+        // 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
@@ -235,6 +261,12 @@ class XMLRow extends Row {
                 }
                 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);
@@ -300,69 +332,167 @@ class XMLRow extends Row {
         return true;
     }
 
+    /* Found specify target  */
+    private int get(int e, char c) {
+        // Ignore all attributes
+        return get(null, e, c, null);
+    }
+
     /**
-     * inner string
-     * <is><t>cell value</t></is>
+     * Parses the value and attributes of the specified tag
      *
-     * @param e the last index in char buffer
-     * @return the end index of string value
+     * @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 getT(int e) {
-        for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != 't'
-            || cb[cursor + 2] != '>'); cursor++) ;
+    private int get(Cell cell, int e, 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;
-        int a = cursor += 3;
+
+        int a;
+        if (cb[cursor + 2] == '>') {
+            a = cursor += 3;
+        }
+        // Some other attributes
+        else if (cb[cursor + 2] == ' ') {
+            int i = cursor + 3;
+            for (; cursor < e && cb[cursor] != '>'; cursor++) ;
+            // If parse attributes
+            if (attrConsumer != null)
+                attrConsumer.accept(cell, cb, i, cb[cursor - 1] != '/' ? cursor : cursor - 1);
+
+            cursor++;
+            if (cb[cursor - 2] == '/' || cursor == e) {
+                return cursor;
+            }
+            a = cursor;
+        }
+        // Empty tag
+        else if (cb[cursor + 2] == '/') {
+            cursor += 3;
+            return cursor;
+        }
+        else {
+            a = cursor += 3;
+        }
+
+        // Found end tag
         for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != '/'
-            || cb[cursor + 2] != 't' || cb[cursor + 3] != '>'); cursor++) ;
+            || cb[cursor + 2] != c || cb[cursor + 3] != '>'); cursor++) ;
+
         return a;
     }
 
     /**
-     * The string index in shared string table
+     * Found text tag range
+     *
+     * Code like this {@code <is><t>cell value</t></is>}
      *
      * @param e the last index in char buffer
-     * @return the end index of int value
+     * @return the end index of string value
      */
-    private int getV(int e) {
-        for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != 'v'
-            || cb[cursor + 2] != '>'); cursor++) ;
-        if (cursor == e) return cursor;
-        int a = cursor += 3;
-        for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != '/'
-            || cb[cursor + 2] != 'v' || cb[cursor + 3] != '>'); cursor++) ;
-        return a;
+    private int getT(int e) {
+        return get(e, 't');
     }
 
     /**
-     * function string
+     * Found value tag range
+     *
+     * Code like this {@code <v>0</v>
      *
      * @param e the last index in char buffer
-     * @return the end index of function value
+     * @return the end index of int value
      */
-    @SuppressWarnings("unused")
-    private int getF(int e) {
-        // undo
-        // return end index of row
-        return e;
+    private int getV(int e) {
+        return get(e, 'v');
     }
 
     /**
-     * Convert to column index
+     * Found the Function tag range
+     * Code like this {@code <f t="shared" ref="B1:B10" si="0">SUM(A1:A10)</f>
      *
-     * @param a the start index
-     * @param b the end index
-     * @return the cell index
+     * @param e the last index in char buffer
+     * @return the end index of function value
      */
-    private int toCellIndex(int a, int b) {
-        int n = 0;
-        for (; a <= b; a++) {
-            if (cb[a] <= 'Z' && cb[a] >= 'A') {
-                n = n * 26 + cb[a] - '@';
-            } else if (cb[a] <= 'z' && cb[a] >= 'a') {
-                n = n * 26 + cb[a] - '、';
+    private int getF(Cell cell, int e) {
+        return get(cell, e, 'f', this::parseFunAttr);
+    }
+
+    /* Parse function tag's attribute */
+    private void parseFunAttr(Cell cell, char[] cb, int a, int b) {
+        // t="shared" ref="B2:B3" si="0"
+        String[] values = new String[10];
+        int index = 0;
+        boolean sv = false; // is string value
+        for (int i = a ; ; ) {
+            for (; a < b && cb[a] > ' ' && cb[a] != '='; a++) ;
+            values[index++] = new String(cb, i, sv ? a - i - 1 : a - i);
+            sv = false;
+
+            if (a + 1 < b) {
+                // String value
+                if (cb[a + 1] == '"') {
+                    a += 2;
+                    sv = true;
+                }
+                // Boolean value
+                else if (cb[a + 1] == ' ') {
+                    values[index++] = "1";
+                    a++;
+                }
+                else a++;
+                i = a;
             } else break;
         }
-        return n;
+
+        if (index < 2 || (index & 1) == 1) {
+            logger.warn("The function format error.[{}]", new String(cb, a, b - a));
+            return;
+        }
+
+        // Sort like t, si, ref
+        for (int i = 0, len = index >> 1; i < len; i++) {
+            int _i = i << 1, vl = values[_i].length();
+            if (vl - 1 == i) {
+                continue;
+            }
+            // Will be sort
+            int _n = vl - 1;
+            if (_n > index - 1) {
+                logger.warn("Unknown attribute on function tag.[{}]", values[_i]);
+                return;
+            }
+            swap(values, _n << 1, _i);
+            swap(values, (_n << 1) + 1, _i + 1);
+        }
+
+        int si = Integer.parseInt(values[3]);
+
+        // Append and share href
+        if (index > 4) {
+            addRef(si, values[5]);
+        }
+
+        // Storage formula shared id
+        cell.si = si;
     }
 
+    /**
+     * Attribute consumer
+     */
+    @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);
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLSheet.java b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
index 7bcfe90f..84575989 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLSheet.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -71,6 +72,9 @@ class XMLSheet implements Sheet {
      */
     private long dimension;
     private int rc; // Range of column
+    private long[] calc; // Array of formula
+    private boolean hasCalc;
+    private boolean parseFormula;
 
     /**
      * Setting the worksheet name
@@ -108,6 +112,16 @@ 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
      *
@@ -136,8 +150,10 @@ class XMLSheet implements Sheet {
      * size of rows.
      *
      * @return size of rows
-     * -1: unknown size
+     *      -1: unknown size
+     * @deprecated use {@link #getDimension()} to getting full range address
      */
+    @Deprecated
     @Override
     public int getSize() {
         if (dimension == 0) return -1;
@@ -159,8 +175,8 @@ class XMLSheet implements Sheet {
             parseDimension();
         }
         return new Dimension((int)(dimension & 0x7FFFFFFF)
-            , (int) (dimension >>> 32)
             , (short) (rc & 0x7FFF)
+            , (int) (dimension >>> 32)
             , (short) (rc >> 16));
     }
 
@@ -306,7 +322,8 @@ class XMLSheet implements Sheet {
             eof = true;
         } else {
             eof = false;
-            sRow = new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1); // share row space
+            sRow = new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1
+                , parseFormula && hasCalc ? this::findCalc : null); // share row space
         }
 
         mark = nChar;
@@ -317,9 +334,8 @@ class XMLSheet implements Sheet {
      * iterator rows
      *
      * @return Row
-     * @throws IOException if io error occur
      */
-    private XMLRow nextRow() throws IOException {
+    private XMLRow nextRow() {
         if (eof) return null;
         boolean endTag = false;
         int start = nChar;
@@ -349,14 +365,18 @@ class XMLSheet implements Sheet {
             } else {
                 System.arraycopy(cb, start, cb, 0, n = length - start);
             }
-            length = reader.read(cb, n, cb.length - n);
-            // end of file
-            if (length < 0) {
-                eof = true;
-                reader.close(); // close reader
-                reader = null; // wait GC
-                logger.debug("end of file.");
-                return null;
+            try {
+                length = reader.read(cb, n, cb.length - n);
+                // end of file
+                if (length < 0) {
+                    eof = true;
+                    reader.close(); // close reader
+                    reader = null; // wait GC
+                    logger.debug("end of file.");
+                    return null;
+                }
+            } catch (IOException e) {
+                throw new ExcelReadException("Parse row data error", e);
             }
             nChar = 0;
             length += n;
@@ -422,7 +442,8 @@ class XMLSheet implements Sheet {
         }
 
         // row
-        return new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1).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);
     }
 
     /**
@@ -465,23 +486,35 @@ 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
      *
      * @return the unread {@link XMLSheet}
      */
     @Override
-    public XMLSheet reset() throws IOException {
-        // Close the opening reader
-        if (reader != null) {
-            reader.close();
+    public XMLSheet reset() {
+        try {
+            // Close the opening reader
+            if (reader != null) {
+                reader.close();
+            }
+            // Reload
+            reader = Files.newBufferedReader(path);
+            reader.skip(mark);
+            length = reader.read(cb);
+            nChar = 0;
+            eof = length <= 0;
+        } catch (IOException e) {
+            throw new ExcelReadException("Reset worksheet[" + getName() + "] error occur.", e);
         }
-        // Reload
-        reader = Files.newBufferedReader(path);
-        reader.skip(mark);
-        length = reader.read(cb);
-        nChar = 0;
-        eof = length <= 0;
 
         return this;
     }
@@ -593,4 +626,26 @@ class XMLSheet implements Sheet {
             ls = ls * 10 + (charBuffer.get(i) - '0');
         return new int[] {row, cs, ls};
     }
+
+    /* Found calc */
+    private void findCalc(int row, Cell[] cells, int n) {
+        long r = ((long) row) << 16;
+        int i = Arrays.binarySearch(calc, r);
+        if (i < 0) {
+            i = ~i;
+            if (i >= calc.length) return;
+        }
+        long a = calc[i];
+        if ((int) (a >> 16) != row) return;
+
+        cells[(((int) a) & 0x7FFF) - 1].f = true;
+        int j = 1;
+        if (n == -1) n = cells.length;
+        n = Math.min(n, calc.length - i);
+        for (; j < n; j++) {
+            if ((calc[i + j] >> 16) == row)
+                cells[(((int) calc[i + j]) & 0x7FFF) - 1].f = true;
+            else break;
+        }
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/util/StringUtil.java b/src/main/java/org/ttzero/excel/util/StringUtil.java
index fa116d20..1ddc3d31 100644
--- a/src/main/java/org/ttzero/excel/util/StringUtil.java
+++ b/src/main/java/org/ttzero/excel/util/StringUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -23,16 +23,45 @@ package org.ttzero.excel.util;
  */
 public class StringUtil {
     private StringUtil() { }
+
+    /**
+     * Const `""` string
+     */
     public final static String EMPTY = "";
 
+    /**
+     * Returns {@code true} if it is null or {@link String#length()} is {@code 0}.
+     *
+     * @param s string value to check
+     * @return {@code true} if null or {@link String#length()} is {@code 0}, otherwise
+     * {@code false}
+     */
     public static boolean isEmpty(String s) {
         return s == null || s.isEmpty();
     }
 
+    /**
+     * Returns {@code true} if, and only if, {@link String#length()} greater than {@code 0}.
+     *
+     * @param s string value to check
+     * @return {@code true} if {@link String#length()} greater than {@code 0}, otherwise
+     * {@code false}
+     */
     public static boolean isNotEmpty(String s) {
         return s != null && s.length() > 0;
     }
 
+    /**
+     * Returns the index within this string array of the first occurrence of
+     * the specified string.
+     * <p>
+     * If no such string occurs in this array, then {@code -1} is returned.
+     *
+     * @param array array to search
+     * @param v the specify string
+     * @return the index of the first occurrence of the string in array, or
+     *      {@code -1} if the string does not occur.
+     */
     public static int indexOf(String[] array, String v) {
         if (v != null) {
             for (int i = 0; i < array.length; i++) {
@@ -50,7 +79,13 @@ public class StringUtil {
         return -1;
     }
 
-
+    /**
+     * Turn the first character into an upper case. It means if the first
+     * character is between 97 and 122, it will be minus {@code 32}.
+     *
+     * @param key a string to processor
+     * @return a string witch the first character is upper case
+     */
     public static String uppFirstKey(String key) {
         char first = key.charAt(0);
         if (first >= 97 && first <= 122) {
@@ -61,6 +96,13 @@ public class StringUtil {
         return key;
     }
 
+    /**
+     * Turn the first character into an lower case. It means if the first
+     * character is between 65 and 90, it will be plus {@code 32}.
+     *
+     * @param key a string to processor
+     * @return a string witch the first character is lower case
+     */
     public static String lowFirstKey(String key) {
         char first = key.charAt(0);
         if (first >= 65 && first <= 90) {
@@ -71,6 +113,12 @@ public class StringUtil {
         return key;
     }
 
+    /**
+     * Convert to camel case string.
+     *
+     * @param name a string to processor
+     * @return a camel case string
+     */
     public static String toPascalCase(String name) {
         if (name.indexOf('_') < 0) return name;
         char[] oldValues = name.toLowerCase().toCharArray();
@@ -90,21 +138,34 @@ public class StringUtil {
         return new String(oldValues, 0, idx);
     }
 
-    public static String append(String src, String a, int n, int origin) {
-        StringBuilder buf = new StringBuilder();
-        // Insert header
-        if (origin == -1) {
-            for (; n-- > 0; ) {
-                buf.append(a);
-            }
-            buf.append(src);
-        } else {
-            buf.append(src);
-            for (; n-- > 0; ) {
-                buf.append(a);
-            }
-        }
+//    public static String append(String src, String a, int n, int origin) {
+//        StringBuilder buf = new StringBuilder();
+//        // Insert header
+//        if (origin == -1) {
+//            for (; n-- > 0; ) {
+//                buf.append(a);
+//            }
+//            buf.append(src);
+//        } else {
+//            buf.append(src);
+//            for (; n-- > 0; ) {
+//                buf.append(a);
+//            }
+//        }
+//
+//        return buf.toString();
+//    }
 
-        return buf.toString();
+    /**
+     * Wrap value in string array
+     *
+     * @param values the array to warp
+     * @param a from index
+     * @param b to index
+     */
+    public static void swap(String[] values, int a, int b) {
+        String t = values[a];
+        values[a] = values[b];
+        values[b] = t;
     }
 }


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 40af33ce..9487b396 100644
--- a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
+++ b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, guanquan.wang@yandex.com All Rights Reserved.
+ * Copyright (c) 2019-2021, 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.
@@ -36,7 +36,10 @@ import java.util.stream.Collectors;
 
 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.cellRangeToLong;
+import static org.ttzero.excel.util.StringUtil.swap;
 
 /**
  * Create by guanquan.wang at 2019-04-26 17:42
@@ -203,12 +206,140 @@ public class ExcelReaderTest {
 
     @Test public void testDimension() {
         try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("#81.xlsx"))) {
-            System.out.println(reader.sheet(0).getDimension());
+            Dimension dimension = reader.sheet(0).getDimension();
+            System.out.println(dimension);
+
+            assert dimension.firstRow == 1;
+            assert dimension.lastRow == 6;
+            assert dimension.firstColumn == 1;
+            assert dimension.lastColumn == 2;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testDimensionConstructor() {
+        Dimension dimension = Dimension.from("A1:C5");
+        assert "A1:C5".equals(dimension.toString());
+
+        assert dimension.firstRow == 1;
+        assert dimension.firstColumn == 1;
+        assert dimension.lastRow == 5;
+        assert dimension.lastColumn == 3;
+    }
+
+    @Test public void testFormula() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("formula.xlsx"))) {
+            // 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('|');
+                    }
+                }
+            });
+
+            // Reset
+            reader.sheets().forEach(Sheet::reset);
+
+            // Read value
+            reader.sheets().flatMap(Sheet::rows).forEach(Print::println);
+
+            if (reader.hasFormula()) {
+                // Reset and parse formula
+                reader.parseFormula().sheets().flatMap(sheet -> {
+                    println("----------------" + sheet.getName() + "----------------");
+                    return sheet.dataRows();
+                }).forEach(row -> {
+                    for (int i = row.fc; i < row.lc; i++) {
+                        if (row.hasFormula(i)) {
+                            print(int2Col(i + 1));
+                            print(row.getRowNumber());
+                            print("=");
+                            print(row.getFormula(i));
+                            println();
+                        }
+                    }
+                });
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testSearch() {
+        long[] array = { 131075L, 327683L };
+        int column = 2, row = 3;
+        boolean h = Arrays.binarySearch(array, ((column + 1) & 0x7FFF) | ((long) row) << 16) >= 0;
+        println(h);
+
+        print(cellRangeToLong("AA10"));
+    }
+
+    @Test public void testClassBind() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("1.xlsx"))) {
+            reader.sheet(0).bind(Entry.class).dataRows().forEach(row -> {
+                // Use bind...get...
+                // Getting and convert to specify Entry
+                Entry entry = row.get();
+                System.out.println(entry.toString());
+            });
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testClassSharedBind() {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("1.xlsx"))) {
+            reader.sheet(0).bind(Entry.class).dataRows().forEach(row -> {
+                // Use bind...geet...
+                // Getting and convert to specify Entry, the entry is shared in memory
+                Entry entry = row.geet();
+                System.out.println(entry.toString());
+            });
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
 
+    @Test public void testVersionFilter() {
+        char[] chars = "..0...3...7.SNAPSHOT.".toCharArray();
+        int i = 0;
+        for (int j = 0; j < chars.length; j++) {
+            if (chars[j] >= '0' && chars[j] <= '9' || chars[j] == '.' && i > 0 && chars[i - 1] != '.')
+                chars[i++] = chars[j];
+        }
+        String version = i > 0 ? new String(chars, 0, chars[i - 1] != '.' ? i : i - 1) : "1.0.0";
+        assert "0.3.7".equals(version);
+    }
+
+    @Test public void testSort() {
+        int index = 6;
+        String[] values = {"ref", "B2:B8", "t", "shared","si", "0"};
+        // Sort like t, si, ref
+        for (int i = 0, len = index >> 1; i < len; i++) {
+            int _i = i << 1;
+            int vl = values[_i].length();
+            if (vl - 1 == i) {
+                continue;
+            }
+            // Will be sort
+            int _n = vl - 1;
+            swap(values, _n << 1, _i);
+            swap(values, (_n << 1) + 1, _i + 1);
+        }
+
+        assert "t".equals(values[0]);
+        assert "shared".equals(values[1]);
+        assert "si".equals(values[2]);
+        assert "0".equals(values[3]);
+        assert "ref".equals(values[4]);
+        assert "B2:B8".equals(values[5]);
+    }
+
+
+
     public static class Customer {
         @ExcelColumn("客户编码")
         private String code;
diff --git a/src/test/java/org/ttzero/excel/reader/PreCalcTest.java b/src/test/java/org/ttzero/excel/reader/PreCalcTest.java
new file mode 100644
index 00000000..cfe66de6
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/reader/PreCalcTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2019-2021, 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;
+
+import org.junit.Test;
+
+import static org.ttzero.excel.Print.println;
+
+/**
+ * Create by guanquan.wang at 2020-01-05 20:35
+ */
+public class PreCalcTest {
+    @Test public void testSimple() {
+        Row row = new MyRow();
+        row.addRef(0, "B2:B8");
+        row.setCalc(0, "A89");
+        String calc = row.getCalc(0, 5 << 14 | 2);
+        assert "A92".equals(calc);
+    }
+
+    @Test public void testPlus() {
+        Row row = new MyRow();
+        row.addRef(0, "B2:B8");
+        row.setCalc(0, "(A2+A3)+1");
+        String calc = row.getCalc(0, 4 << 14 | 2);
+        println(calc);
+        assert "(A4+A5)+1".equals(calc);
+    }
+
+    @Test public void testRange() {
+        Row row = new MyRow();
+        row.addRef(0, "B2:D8");
+        row.setCalc(0, "(A2+B3)+1");
+        String calc = row.getCalc(0, 4 << 14 | 3);
+        println(calc);
+        assert "(B4+C5)+1".equals(calc);
+    }
+
+    @Test public void testDoubleQuotes() {
+        Row row = new MyRow();
+        row.addRef(0, "B2:D8");
+        row.setCalc(0, "\"AB12\".substring(3)");
+        String calc = row.getCalc(0, 4 << 14 | 3);
+        println(calc);
+        assert "\"AB12\".substring(3)".equals(calc);
+    }
+
+    @Test public void testDoubleQuotes2() {
+        Row row = new MyRow();
+        row.addRef(0, "B2:D8");
+        row.setCalc(0, "\"AB12.substring(3)");
+        String calc = row.getCalc(0, 4 << 14 | 3);
+        println(calc);
+        assert "\"AB12.substring(3)".equals(calc);
+    }
+
+    @Test public void testRows() {
+        Row row = new MyRow();
+        row.addRef(1, "B9:B65");
+        row.setCalc(1, "A9+1");
+        String calc = row.getCalc(1, 10 << 14 | 2);
+        println(calc);
+        assert "A10+1".equals(calc);
+    }
+
+    private static class MyRow extends Row { }
+}
diff --git a/src/test/java/org/ttzero/excel/util/CSVUtilTest.java b/src/test/java/org/ttzero/excel/util/CSVUtilTest.java
index 80e4f901..09883f10 100644
--- a/src/test/java/org/ttzero/excel/util/CSVUtilTest.java
+++ b/src/test/java/org/ttzero/excel/util/CSVUtilTest.java
@@ -411,8 +411,8 @@ public class CSVUtilTest {
     }
 
     @Test public void testGBKCharset() {
-        String s1 = "打造中国千亿级MRO供应链平台";
-        String s2 = "让工业用品采购更简单";
+        String s1 = "双引号或单引号中间的一切都是字符串";
+        String s2 = "中文测试使用GBK";
 
         Charset GBK = Charset.forName("GBK");