diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..5c400f8ba3
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,77 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others’ private information, such as a physical or email address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [xuri.me](https://xuri.me). All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+
+Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+Community Impact: A violation through a single incident or series of actions.
+
+Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+
+### 3. Temporary Ban
+
+Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
+
+Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+
+Consequence: A permanent ban from any sort of public interaction within the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
+
+Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder.
+
+For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md
similarity index 94%
rename from CONTRIBUTING.md
rename to .github/CONTRIBUTING.md
index 53c650e55f..0c966e4940 100644
--- a/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -21,7 +21,7 @@ issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[xuri.me](https://xuri.me).
-Security reports are greatly appreciated and we will publicly thank you for it.
+Security reports are greatly appreciated and we will publicly thank you for them.
We currently do not offer a paid security bounty program, but are not
ruling it out in the future.
@@ -31,7 +31,7 @@ A great way to contribute to the project is to send a detailed report when you
encounter an issue. We always appreciate a well-written, thorough bug report,
and will thank you for it!
-Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues)
+Check that [our issue database](https://github.com/xuri/excelize/issues)
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, you can use the "subscribe" button to get notified on
updates. Do *not* leave random "+1" or "I have this too" comments, as they
@@ -55,7 +55,7 @@ This section gives the experienced contributor some tips and guidelines.
Not sure if that typo is worth a pull request? Found a bug and know how to fix
it? Do it! We will appreciate it. Any significant improvement should be
-documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before
+documented as [a GitHub issue](https://github.com/xuri/excelize/issues) before
anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them
@@ -103,14 +103,14 @@ Before contributing large or high impact changes, make the effort to coordinate
with the maintainers of the project before submitting a pull request. This
prevents you from doing extra work that may or may not be merged.
-Large PRs that are just submitted without any prior communication are unlikely
+Large PRs that are just submitted without any prior communication is unlikely
to be successful.
While pull requests are the methodology for submitting changes to code, changes
are much more likely to be accepted if they are accompanied by additional
engineering work. While we don't define this explicitly, most of these goals
-are accomplished through communication of the design goals and subsequent
-solutions. Often times, it helps to first state the problem before presenting
+are accomplished through the communication of the design goals and subsequent
+solutions. Oftentimes, it helps to first state the problem before presenting
solutions.
Typically, the best methods of accomplishing this are to submit an issue,
@@ -130,7 +130,7 @@ written in the imperative, followed by an optional, more detailed explanatory
text which is separated from the summary by an empty line.
Commit messages should follow best practices, including explaining the context
-of the problem and how it was solved, including in caveats or follow up changes
+of the problem and how it was solved, including in caveats or follow-up changes
required. They should tell the story of the change and provide readers
understanding of what led to it.
@@ -191,20 +191,18 @@ indicate acceptance.
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
-the below (from [developercertificate.org](http://developercertificate.org/)):
+the below (from [developercertificate.org](https://developercertificate.org)):
```text
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
-1 Letterman Drive
-Suite D4700
-San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
+
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
@@ -262,7 +260,7 @@ Don't forget: being a maintainer is a time investment. Make sure you
will have time to make yourself available. You don't have to be a
maintainer to make a difference on the project!
-If you want to become a meintainer, contact [xuri.me](https://xuri.me) and given a introduction of you.
+If you want to become a maintainer, contact [xuri.me](https://xuri.me) and given an introduction of you.
## Community guidelines
@@ -347,9 +345,9 @@ The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
- [`golint`](https://github.com/golang/lint).
+ [`go vet`](https://pkg.go.dev/cmd/vet).
3. All code should follow the guidelines covered in [Effective
- Go](http://golang.org/doc/effective_go.html) and [Go Code Review
+ Go](https://go.dev/doc/effective_go) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
@@ -372,13 +370,13 @@ The rules:
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
-reading through [Effective Go](https://golang.org/doc/effective_go.html). The
-[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
+reading through [Effective Go](https://go.dev/doc/effective_go). The
+[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.
## Code Review Comments and Effective Go Guidelines
-[CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
+[CodeLingo](https://www.codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://go.dev/doc/effective_go) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
### Package Comment
@@ -416,7 +414,7 @@ The first sentence should be a one-sentence summary that starts with the name be
It's helpful if everyone using the package can use the same name
to refer to its contents, which implies that the package name should
-be good: short, concise, evocative. By convention, packages are
+be good: short, concise, and evocative. By convention, packages are
given lower case, single-word names; there should be no need for
underscores or mixedCaps. Err on the side of brevity, since everyone
using your package will be typing that name. And don't worry about
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000000..ab9fc53ec3
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,7 @@
+github: xuri
+open_collective: excelize
+patreon: xuri
+ko_fi: xurime
+liberapay: xuri
+issuehunt: xuri
+custom: https://www.paypal.com/paypalme/xuri
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index a8c31a04b3..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-
----
-
-
-
-**Description**
-
-
-
-**Steps to reproduce the issue:**
-1.
-2.
-3.
-
-**Describe the results you received:**
-
-**Describe the results you expected:**
-
-**Output of `go version`:**
-
-```text
-(paste your output here)
-```
-
-**Excelize version or commit ID:**
-
-```text
-(paste here)
-```
-
-**Environment details (OS, Microsoft Excel™ version, physical, etc.):**
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..7005cd6c35
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,81 @@
+name: Bug report
+description: Create a report to help us improve
+body:
+ - type: markdown
+ attributes:
+ value: |
+ If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Briefly describe the problem you are having in a few paragraphs.
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduction-steps
+ attributes:
+ label: Steps to reproduce the issue
+ description: Explain how to cause the issue in the provided reproduction.
+ placeholder: |
+ 1.
+ 2.
+ 3.
+ validations:
+ required: true
+
+ - type: textarea
+ id: received
+ attributes:
+ label: Describe the results you received
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Describe the results you expected
+ validations:
+ required: true
+
+ - type: input
+ id: go-version
+ attributes:
+ label: Go version
+ description: |
+ Output of `go version`:
+ placeholder: e.g. 1.23.4
+ validations:
+ required: true
+
+ - type: input
+ id: excelize-version
+ attributes:
+ label: Excelize version or commit ID
+ description: |
+ Which version of Excelize are you using?
+ placeholder: e.g. 2.9.0
+ validations:
+ required: true
+
+ - type: textarea
+ id: env
+ attributes:
+ label: Environment
+ description: Environment details (OS, Microsoft Excel™ version, physical, etc.)
+ render: shell
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: checkboxes
+ attributes:
+ label: Validations
+ description: Before submitting the issue, please make sure you do the following
+ options:
+ - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
+ required: true
+ - label: The provided reproduction is a minimal reproducible example of the bug.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..3ba13e0cec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 1737cd4421..0000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-
----
-
-
-
-**Description**
-
-
-
-**Steps to reproduce the issue:**
-1.
-2.
-3.
-
-**Describe the results you received:**
-
-**Describe the results you expected:**
-
-**Output of `go version`:**
-
-```text
-(paste your output here)
-```
-
-**Excelize version or commit ID:**
-
-```text
-(paste here)
-```
-
-**Environment details (OS, Microsoft Excel™ version, physical, etc.):**
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000000..af626b8e9a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,30 @@
+name: Feature request
+description: Suggest an idea for this project
+body:
+ - type: markdown
+ attributes:
+ value: |
+ If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Describe the feature that you would like added
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Any other context or screenshots about the feature request here?
+
+ - type: checkboxes
+ id: checkboxes
+ attributes:
+ label: Validations
+ description: Before submitting the issue, please make sure you do the following
+ options:
+ - label: Check that there isn't already an issue that requests the same feature to avoid creating a duplicate.
+ required: true
diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
similarity index 96%
rename from PULL_REQUEST_TEMPLATE.md
rename to .github/PULL_REQUEST_TEMPLATE.md
index d2ac755e96..8479a73206 100644
--- a/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -21,7 +21,7 @@
-
+
## Types of changes
diff --git a/SECURITY.md b/.github/SECURITY.md
similarity index 100%
rename from SECURITY.md
rename to .github/SECURITY.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..e49472e656
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000000..7dc52a200f
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,35 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+ schedule:
+ - cron: '0 6 * * 3'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-24.04
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['go']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000000..5e16cfccae
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,41 @@
+on: [push, pull_request]
+name: build
+jobs:
+
+ test:
+ strategy:
+ matrix:
+ go-version: [1.23.x, 1.24.x]
+ os: [ubuntu-24.04, macos-13, windows-latest]
+ targetplatform: [x86, x64]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go-version }}
+ cache: false
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Get dependencies
+ run: |
+ env GO111MODULE=on go vet ./...
+ - name: Build
+ run: go build -v .
+
+ - name: Test
+ run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic
+
+ - name: Codecov
+ uses: codecov/codecov-action@v5
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ files: coverage.txt
+ flags: unittests
+ name: codecov-umbrella
diff --git a/.gitignore b/.gitignore
index a3fcff22aa..8bf9e7f5b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,15 @@
-~$*.xlsx
-test/Test*.xlsx
+.DS_Store
+.idea
+*.json
*.out
*.test
-.idea
+~$*.xlsx
+test/*.png
+test/BadWorkbook.SaveAsEmptyStruct.xlsx
+test/Encryption*.xlsx
+test/excelize-*
+test/Test*.xlam
+test/Test*.xlsm
+test/Test*.xlsx
+test/Test*.xltm
+test/Test*.xltx
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 03825a8089..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-language: go
-
-install:
- - go get -d -t -v ./... && go build -v ./...
-
-go:
- - 1.11.x
- - 1.12.x
- - 1.13.x
- - 1.14.x
- - 1.15.x
-
-os:
- - linux
- - osx
-
-env:
- jobs:
- - GOARCH=amd64
- - GOARCH=386
-
-script:
- - env GO111MODULE=on go vet ./...
- - env GO111MODULE=on go test ./... -v -coverprofile=coverage.txt -covermode=atomic
-
-after_success:
- - bash <(curl -s https://codecov.io/bash)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 572b5612e0..0000000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [xuri.me](https://xuri.me). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct][version]
-
-[homepage]: https://www.contributor-covenant.org
-[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct
diff --git a/LICENSE b/LICENSE
index e0f34bbc6a..a22e6975ff 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
BSD 3-Clause License
-Copyright (c) 2016-2020 The excelize Authors.
+Copyright (c) 2016-2025 The excelize Authors.
+Copyright (c) 2011-2017 Geoffrey J. Teale
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -26,4 +27,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 97db54a62e..9f57747ba7 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,32 @@

-
-
-
-
+
+
+
+
-
+
# Excelize
## Introduction
-Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX / XLSM / XLTM files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.10 or later. The full API docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/).
+Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.23 or later. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/).
## Basic Usage
### Installation
```bash
-go get github.com/360EntSecGroup-Skylar/excelize
+go get github.com/xuri/excelize
```
-- If your package management with [Go Modules](https://blog.golang.org/using-go-modules), please install with following command.
+- If your packages are managed using [Go Modules](https://go.dev/blog/using-go-modules), please install with following command.
```bash
-go get github.com/360EntSecGroup-Skylar/excelize/v2
+go get github.com/xuri/excelize/v2
```
### Create spreadsheet
@@ -39,19 +39,28 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
f := excelize.NewFile()
+ defer func() {
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
// Create a new sheet.
- index := f.NewSheet("Sheet2")
+ index, err := f.NewSheet("Sheet2")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
// Set value of a cell.
f.SetCellValue("Sheet2", "A2", "Hello world.")
f.SetCellValue("Sheet1", "B2", 100)
// Set active sheet of the workbook.
f.SetActiveSheet(index)
- // Save xlsx file by the given path.
+ // Save spreadsheet by the given path.
if err := f.SaveAs("Book1.xlsx"); err != nil {
fmt.Println(err)
}
@@ -68,7 +77,7 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
@@ -77,7 +86,13 @@ func main() {
fmt.Println(err)
return
}
- // Get value from cell by given worksheet name and axis.
+ defer func() {
+ // Close the spreadsheet.
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
+ // Get value from cell by given worksheet name and cell reference.
cell, err := f.GetCellValue("Sheet1", "B2")
if err != nil {
fmt.Println(err)
@@ -86,6 +101,10 @@ func main() {
fmt.Println(cell)
// Get all the rows in the Sheet1.
rows, err := f.GetRows("Sheet1")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
for _, row := range rows {
for _, colCell := range row {
fmt.Print(colCell, "\t")
@@ -97,7 +116,7 @@ func main() {
### Add chart to spreadsheet file
-With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all.
+With Excelize chart generation and management is as easy as a few lines of code. You can build charts based on data in your worksheet or generate charts without any data in your worksheet at all.

@@ -107,24 +126,55 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
- categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
- values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
f := excelize.NewFile()
- for k, v := range categories {
- f.SetCellValue("Sheet1", k, v)
- }
- for k, v := range values {
- f.SetCellValue("Sheet1", k, v)
+ defer func() {
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
+ for idx, row := range [][]interface{}{
+ {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3},
+ {"Normal", 5, 2, 4}, {"Large", 6, 7, 8},
+ } {
+ cell, err := excelize.CoordinatesToCellName(1, idx+1)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ f.SetSheetRow("Sheet1", cell, &row)
}
- if err := f.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`); err != nil {
+ if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
+ Type: excelize.Col3DClustered,
+ Series: []excelize.ChartSeries{
+ {
+ Name: "Sheet1!$A$2",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$2:$D$2",
+ },
+ {
+ Name: "Sheet1!$A$3",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$3:$D$3",
+ },
+ {
+ Name: "Sheet1!$A$4",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$4:$D$4",
+ }},
+ Title: []excelize.RichTextRun{
+ {
+ Text: "Fruit 3D Clustered Column Chart",
+ },
+ },
+ }); err != nil {
fmt.Println(err)
return
}
- // Save xlsx file by the given path.
+ // Save spreadsheet by the given path.
if err := f.SaveAs("Book1.xlsx"); err != nil {
fmt.Println(err)
}
@@ -142,7 +192,7 @@ import (
_ "image/jpeg"
_ "image/png"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
@@ -151,19 +201,34 @@ func main() {
fmt.Println(err)
return
}
+ defer func() {
+ // Close the spreadsheet.
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
// Insert a picture.
- if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil {
+ if err := f.AddPicture("Sheet1", "A2", "image.png", nil); err != nil {
fmt.Println(err)
}
// Insert a picture to worksheet with scaling.
- if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil {
+ if err := f.AddPicture("Sheet1", "D2", "image.jpg",
+ &excelize.GraphicOptions{ScaleX: 0.5, ScaleY: 0.5}); err != nil {
fmt.Println(err)
}
// Insert a picture offset in the cell with printing support.
- if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil {
+ enable, disable := true, false
+ if err := f.AddPicture("Sheet1", "H2", "image.gif",
+ &excelize.GraphicOptions{
+ PrintObject: &enable,
+ LockAspectRatio: false,
+ OffsetX: 15,
+ OffsetY: 10,
+ Locked: &disable,
+ }); err != nil {
fmt.Println(err)
}
- // Save the xlsx file with the origin path.
+ // Save the spreadsheet with the origin path.
if err = f.Save(); err != nil {
fmt.Println(err)
}
@@ -172,7 +237,7 @@ func main() {
## Contributing
-Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm).
+Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/).
## Licenses
diff --git a/README_zh.md b/README_zh.md
index deba22ac68..fab0bdd7bc 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,32 +1,32 @@

-
-
-
-
+
+
+
+
-
+
# Excelize
## 简介
-Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。
+Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.23 或更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。
## 快速上手
### 安装
```bash
-go get github.com/360EntSecGroup-Skylar/excelize
+go get github.com/xuri/excelize
```
-- 如果您使用 [Go Modules](https://blog.golang.org/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。
+- 如果您使用 [Go Modules](https://go.dev/blog/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。
```bash
-go get github.com/360EntSecGroup-Skylar/excelize/v2
+go get github.com/xuri/excelize/v2
```
### 创建 Excel 文档
@@ -39,13 +39,22 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
f := excelize.NewFile()
+ defer func() {
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
// 创建一个工作表
- index := f.NewSheet("Sheet2")
+ index, err := f.NewSheet("Sheet2")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
// 设置单元格的值
f.SetCellValue("Sheet2", "A2", "Hello world.")
f.SetCellValue("Sheet1", "B2", 100)
@@ -68,7 +77,7 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
@@ -77,6 +86,12 @@ func main() {
fmt.Println(err)
return
}
+ defer func() {
+ // 关闭工作簿
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
// 获取工作表中指定单元格的值
cell, err := f.GetCellValue("Sheet1", "B2")
if err != nil {
@@ -86,6 +101,10 @@ func main() {
fmt.Println(cell)
// 获取 Sheet1 上所有单元格
rows, err := f.GetRows("Sheet1")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
for _, row := range rows {
for _, colCell := range row {
fmt.Print(colCell, "\t")
@@ -99,7 +118,7 @@ func main() {
使用 Excelize 生成图表十分简单,仅需几行代码。您可以根据工作表中的已有数据构建图表,或向工作表中添加数据并创建图表。
-
+
```go
package main
@@ -107,20 +126,51 @@ package main
import (
"fmt"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
- categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
- values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
f := excelize.NewFile()
- for k, v := range categories {
- f.SetCellValue("Sheet1", k, v)
- }
- for k, v := range values {
- f.SetCellValue("Sheet1", k, v)
+ defer func() {
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
+ for idx, row := range [][]interface{}{
+ {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3},
+ {"Normal", 5, 2, 4}, {"Large", 6, 7, 8},
+ } {
+ cell, err := excelize.CoordinatesToCellName(1, idx+1)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ f.SetSheetRow("Sheet1", cell, &row)
}
- if err := f.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`); err != nil {
+ if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
+ Type: excelize.Col3DClustered,
+ Series: []excelize.ChartSeries{
+ {
+ Name: "Sheet1!$A$2",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$2:$D$2",
+ },
+ {
+ Name: "Sheet1!$A$3",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$3:$D$3",
+ },
+ {
+ Name: "Sheet1!$A$4",
+ Categories: "Sheet1!$B$1:$D$1",
+ Values: "Sheet1!$B$4:$D$4",
+ }},
+ Title: []excelize.RichTextRun{
+ {
+ Text: "Fruit 3D Clustered Column Chart",
+ },
+ },
+ }); err != nil {
fmt.Println(err)
return
}
@@ -142,7 +192,7 @@ import (
_ "image/jpeg"
_ "image/png"
- "github.com/360EntSecGroup-Skylar/excelize"
+ "github.com/xuri/excelize/v2"
)
func main() {
@@ -151,19 +201,34 @@ func main() {
fmt.Println(err)
return
}
+ defer func() {
+ // 关闭工作簿
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
// 插入图片
- if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil {
+ if err := f.AddPicture("Sheet1", "A2", "image.png", nil); err != nil {
fmt.Println(err)
}
// 在工作表中插入图片,并设置图片的缩放比例
- if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil {
+ if err := f.AddPicture("Sheet1", "D2", "image.jpg",
+ &excelize.GraphicOptions{ScaleX: 0.5, ScaleY: 0.5}); err != nil {
fmt.Println(err)
}
// 在工作表中插入图片,并设置图片的打印属性
- if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil {
+ enable, disable := true, false
+ if err := f.AddPicture("Sheet1", "H2", "image.gif",
+ &excelize.GraphicOptions{
+ PrintObject: &enable,
+ LockAspectRatio: false,
+ OffsetX: 15,
+ OffsetY: 10,
+ Locked: &disable,
+ }); err != nil {
fmt.Println(err)
}
- // 保存文件
+ // 保存工作簿
if err = f.Save(); err != nil {
fmt.Println(err)
}
@@ -172,7 +237,7 @@ func main() {
## 社区合作
-欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm)。
+欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/)。
## 开源许可
diff --git a/adjust.go b/adjust.go
index 40898d9a69..28e54e5809 100644
--- a/adjust.go
+++ b/adjust.go
@@ -1,19 +1,25 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
- "errors"
+ "bytes"
+ "encoding/xml"
+ "io"
+ "strconv"
"strings"
+ "unicode"
+
+ "github.com/xuri/efp"
)
type adjustDirection bool
@@ -23,6 +29,37 @@ const (
rows adjustDirection = true
)
+// adjustHelperFunc defines functions to adjust helper.
+var adjustHelperFunc = [9]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustConditionalFormats(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustDataValidations(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustDrawings(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustMergeCells(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustAutoFilter(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustCalcChain(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustTable(ws, sheet, dir, num, offset, sheetID)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ return f.adjustVolatileDeps(ws, sheet, dir, num, offset, sheetID)
+ },
+}
+
// adjustHelper provides a function to adjust rows and columns dimensions,
// hyperlinks, merged cells and auto filter when inserting or deleting rows or
// columns.
@@ -32,155 +69,660 @@ const (
// row: Index number of the row we're inserting/deleting before
// offset: Number of rows/column to insert/delete negative values indicate deletion
//
-// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells
-//
+// TODO: adjustComments, adjustPageBreaks, adjustProtectedCells
func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error {
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
+ sheetID := f.getSheetID(sheet)
if dir == rows {
- f.adjustRowDimensions(xlsx, num, offset)
+ err = f.adjustRowDimensions(sheet, ws, num, offset)
} else {
- f.adjustColDimensions(xlsx, num, offset)
+ err = f.adjustColDimensions(sheet, ws, num, offset)
}
- f.adjustHyperlinks(xlsx, sheet, dir, num, offset)
- if err = f.adjustMergeCells(xlsx, dir, num, offset); err != nil {
+ if err != nil {
return err
}
- if err = f.adjustAutoFilter(xlsx, dir, num, offset); err != nil {
- return err
+ f.adjustHyperlinks(ws, sheet, dir, num, offset)
+ ws.checkSheet()
+ _ = ws.checkRow()
+ for _, fn := range adjustHelperFunc {
+ if err := fn(f, ws, sheet, dir, num, offset, sheetID); err != nil {
+ return err
+ }
}
- if err = f.adjustCalcChain(dir, num, offset); err != nil {
- return err
+ if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 {
+ ws.MergeCells = nil
}
- checkSheet(xlsx)
- _ = checkRow(xlsx)
+ return nil
+}
- if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 {
- xlsx.MergeCells = nil
+// adjustCols provides a function to update column style when inserting or
+// deleting columns.
+func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error {
+ if ws.Cols == nil {
+ return nil
+ }
+ for i := 0; i < len(ws.Cols.Col); i++ {
+ if offset > 0 {
+ if ws.Cols.Col[i].Min >= col {
+ if ws.Cols.Col[i].Min += offset; ws.Cols.Col[i].Min > MaxColumns {
+ ws.Cols.Col = append(ws.Cols.Col[:i], ws.Cols.Col[i+1:]...)
+ i--
+ continue
+ }
+ }
+ if ws.Cols.Col[i].Max >= col || ws.Cols.Col[i].Max+1 == col {
+ if ws.Cols.Col[i].Max += offset; ws.Cols.Col[i].Max > MaxColumns {
+ ws.Cols.Col[i].Max = MaxColumns
+ }
+ }
+ continue
+ }
+ if ws.Cols.Col[i].Min == col && ws.Cols.Col[i].Max == col {
+ ws.Cols.Col = append(ws.Cols.Col[:i], ws.Cols.Col[i+1:]...)
+ i--
+ continue
+ }
+ if ws.Cols.Col[i].Min > col {
+ ws.Cols.Col[i].Min += offset
+ }
+ if ws.Cols.Col[i].Max >= col {
+ ws.Cols.Col[i].Max += offset
+ }
+ }
+ if len(ws.Cols.Col) == 0 {
+ ws.Cols = nil
}
-
return nil
}
// adjustColDimensions provides a function to update column dimensions when
// inserting or deleting rows or columns.
-func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) {
- for rowIdx := range xlsx.SheetData.Row {
- for colIdx, v := range xlsx.SheetData.Row[rowIdx].C {
- cellCol, cellRow, _ := CellNameToCoordinates(v.R)
- if col <= cellCol {
- if newCol := cellCol + offset; newCol > 0 {
- xlsx.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
+func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset int) error {
+ for rowIdx := range ws.SheetData.Row {
+ for _, v := range ws.SheetData.Row[rowIdx].C {
+ if cellCol, _, _ := CellNameToCoordinates(v.R); col <= cellCol {
+ if newCol := cellCol + offset; newCol > 0 && newCol > MaxColumns {
+ return ErrColumnNumber
+ }
+ }
+ }
+ }
+ for _, sheetN := range f.GetSheetList() {
+ worksheet, err := f.workSheetReader(sheetN)
+ if err != nil {
+ if err.Error() == newNotWorksheetError(sheetN).Error() {
+ continue
+ }
+ return err
+ }
+ for rowIdx := range worksheet.SheetData.Row {
+ for colIdx, v := range worksheet.SheetData.Row[rowIdx].C {
+ if cellCol, cellRow, _ := CellNameToCoordinates(v.R); sheetN == sheet && col <= cellCol {
+ if newCol := cellCol + offset; newCol > 0 {
+ worksheet.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
+ }
+ }
+ if err := f.adjustFormula(sheet, sheetN, &worksheet.SheetData.Row[rowIdx].C[colIdx], columns, col, offset, false); err != nil {
+ return err
}
}
}
}
+ return f.adjustCols(ws, col, offset)
}
// adjustRowDimensions provides a function to update row dimensions when
// inserting or deleting rows or columns.
-func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, row, offset int) {
- for i := range xlsx.SheetData.Row {
- r := &xlsx.SheetData.Row[i]
+func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error {
+ for _, sheetN := range f.GetSheetList() {
+ if sheetN == sheet {
+ continue
+ }
+ worksheet, err := f.workSheetReader(sheetN)
+ if err != nil {
+ if err.Error() == newNotWorksheetError(sheetN).Error() {
+ continue
+ }
+ return err
+ }
+ numOfRows := len(worksheet.SheetData.Row)
+ for i := 0; i < numOfRows; i++ {
+ r := &worksheet.SheetData.Row[i]
+ if err = f.adjustSingleRowFormulas(sheet, sheetN, r, row, offset, false); err != nil {
+ return err
+ }
+ }
+ }
+ totalRows := len(ws.SheetData.Row)
+ if totalRows == 0 {
+ return nil
+ }
+ lastRow := &ws.SheetData.Row[totalRows-1]
+ if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows {
+ return ErrMaxRows
+ }
+ numOfRows := len(ws.SheetData.Row)
+ for i := 0; i < numOfRows; i++ {
+ r := &ws.SheetData.Row[i]
if newRow := r.R + offset; r.R >= row && newRow > 0 {
- f.ajustSingleRowDimensions(r, newRow)
+ r.adjustSingleRowDimensions(offset)
+ }
+ if err := f.adjustSingleRowFormulas(sheet, sheet, r, row, offset, false); err != nil {
+ return err
}
}
+ return nil
}
-// ajustSingleRowDimensions provides a function to ajust single row dimensions.
-func (f *File) ajustSingleRowDimensions(r *xlsxRow, num int) {
- r.R = num
+// adjustSingleRowDimensions provides a function to adjust single row dimensions.
+func (r *xlsxRow) adjustSingleRowDimensions(offset int) {
+ r.R += offset
for i, col := range r.C {
colName, _, _ := SplitCellName(col.R)
- r.C[i].R, _ = JoinCellName(colName, num)
+ r.C[i].R, _ = JoinCellName(colName, r.R)
+ }
+}
+
+// adjustSingleRowFormulas provides a function to adjust single row formulas.
+func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, offset int, si bool) error {
+ for i := 0; i < len(r.C); i++ {
+ if err := f.adjustFormula(sheet, sheetN, &r.C[i], rows, num, offset, si); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// adjustCellRef provides a function to adjust cell reference.
+func (f *File) adjustCellRef(cellRef string, dir adjustDirection, num, offset int) (string, error) {
+ var SQRef []string
+ applyOffset := func(coordinates []int, idx1, idx2, maxVal int) []int {
+ if coordinates[idx1] >= num {
+ coordinates[idx1] += offset
+ }
+ if coordinates[idx2] >= num {
+ if coordinates[idx2] += offset; coordinates[idx2] > maxVal {
+ coordinates[idx2] = maxVal
+ }
+ }
+ return coordinates
+ }
+ for _, ref := range strings.Split(cellRef, " ") {
+ if !strings.Contains(ref, ":") {
+ ref += ":" + ref
+ }
+ coordinates, err := rangeRefToCoordinates(ref)
+ if err != nil {
+ return "", err
+ }
+ if dir == columns {
+ if offset < 0 && coordinates[0] == coordinates[2] && num == coordinates[0] {
+ continue
+ }
+ coordinates = applyOffset(coordinates, 0, 2, MaxColumns)
+ } else {
+ if offset < 0 && coordinates[1] == coordinates[3] && num == coordinates[1] {
+ continue
+ }
+ coordinates = applyOffset(coordinates, 1, 3, TotalRows)
+ }
+ if ref, err = coordinatesToRangeRef(coordinates); err != nil {
+ return "", err
+ }
+ SQRef = append(SQRef, ref)
+ }
+ return strings.Join(SQRef, " "), nil
+}
+
+// adjustFormula provides a function to adjust formula reference and shared
+// formula reference.
+func (f *File) adjustFormula(sheet, sheetN string, cell *xlsxC, dir adjustDirection, num, offset int, si bool) error {
+ var err error
+ if cell.f != "" {
+ if cell.f, err = f.adjustFormulaRef(sheet, sheetN, cell.f, false, dir, num, offset); err != nil {
+ return err
+ }
+ }
+ if cell.F == nil {
+ return nil
+ }
+ if cell.F.Ref != "" && sheet == sheetN {
+ if cell.F.Ref, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil {
+ return err
+ }
+ if si && cell.F.Si != nil {
+ cell.F.Si = intPtr(*cell.F.Si + 1)
+ }
+ }
+ if cell.F.Content != "" {
+ if cell.F.Content, err = f.adjustFormulaRef(sheet, sheetN, cell.F.Content, false, dir, num, offset); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// escapeSheetName enclose sheet name in single quotation marks if the giving
+// worksheet name includes spaces or non-alphabetical characters.
+func escapeSheetName(name string) string {
+ if strings.IndexFunc(name, func(r rune) bool {
+ return !unicode.IsLetter(r) && !unicode.IsNumber(r)
+ }) != -1 {
+ return "'" + strings.ReplaceAll(name, "'", "''") + "'"
+ }
+ return name
+}
+
+// adjustFormulaColumnName adjust column name in the formula reference.
+func adjustFormulaColumnName(name, operand string, abs, keepRelative bool, dir adjustDirection, num, offset int) (string, string, bool, error) {
+ if name == "" || (!abs && keepRelative) {
+ return "", operand + name, abs, nil
+ }
+ col, err := ColumnNameToNumber(name)
+ if err != nil {
+ return "", operand, false, err
+ }
+ if dir == columns && col >= num {
+ if col += offset; col < 1 {
+ col = 1
+ }
+ colName, err := ColumnNumberToName(col)
+ return "", operand + colName, false, err
+ }
+ return "", operand + name, false, nil
+}
+
+// adjustFormulaRowNumber adjust row number in the formula reference.
+func adjustFormulaRowNumber(name, operand string, abs, keepRelative bool, dir adjustDirection, num, offset int) (string, string, bool, error) {
+ if name == "" || (!abs && keepRelative) {
+ return "", operand + name, abs, nil
+ }
+ row, _ := strconv.Atoi(name)
+ if dir == rows && row >= num {
+ if row += offset; row < 1 {
+ row = 1
+ }
+ if row > TotalRows {
+ return "", operand + name, false, ErrMaxRows
+ }
+ return "", operand + strconv.Itoa(row), false, nil
+ }
+ return "", operand + name, false, nil
+}
+
+// adjustFormulaOperandRef adjust cell reference in the operand tokens for the formula.
+func adjustFormulaOperandRef(row, col, operand string, abs, keepRelative bool, dir adjustDirection, num int, offset int) (string, string, string, bool, error) {
+ var err error
+ col, operand, abs, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset)
+ if err != nil {
+ return row, col, operand, abs, err
+ }
+ row, operand, abs, err = adjustFormulaRowNumber(row, operand, abs, keepRelative, dir, num, offset)
+ return row, col, operand, abs, err
+}
+
+// adjustFormulaOperand adjust range operand tokens for the formula.
+func (f *File) adjustFormulaOperand(sheet, sheetN string, keepRelative bool, token efp.Token, dir adjustDirection, num int, offset int) (string, error) {
+ var (
+ err error
+ abs bool
+ sheetName, col, row, operand string
+ cell = token.TValue
+ tokens = strings.Split(token.TValue, "!")
+ )
+ if len(tokens) == 2 { // have a worksheet
+ sheetName, cell = tokens[0], tokens[1]
+ operand = escapeSheetName(sheetName) + "!"
+ }
+ if sheetName == "" {
+ sheetName = sheetN
+ }
+ if sheet != sheetName {
+ return operand + cell, err
+ }
+ for _, r := range cell {
+ if r == '$' {
+ if col, operand, _, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset); err != nil {
+ return operand, err
+ }
+ abs = true
+ operand += string(r)
+ continue
+ }
+ if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
+ col += string(r)
+ continue
+ }
+ if '0' <= r && r <= '9' {
+ row += string(r)
+ col, operand, abs, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset)
+ if err != nil {
+ return operand, err
+ }
+ continue
+ }
+ if row, col, operand, abs, err = adjustFormulaOperandRef(row, col, operand, abs, keepRelative, dir, num, offset); err != nil {
+ return operand, err
+ }
+ operand += string(r)
+ }
+ _, _, operand, _, err = adjustFormulaOperandRef(row, col, operand, abs, keepRelative, dir, num, offset)
+ return operand, err
+}
+
+// adjustFormulaRef returns adjusted formula by giving adjusting direction and
+// the base number of column or row, and offset.
+func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool, dir adjustDirection, num, offset int) (string, error) {
+ var (
+ val string
+ definedNames []string
+ ps = efp.ExcelParser()
+ )
+ for _, definedName := range f.GetDefinedName() {
+ if definedName.Scope == "Workbook" || definedName.Scope == sheet {
+ definedNames = append(definedNames, definedName.Name)
+ }
+ }
+ for _, token := range ps.Parse(formula) {
+ if token.TType == efp.TokenTypeUnknown {
+ val = formula
+ break
+ }
+ if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
+ if inStrSlice(definedNames, token.TValue, true) != -1 {
+ val += token.TValue
+ continue
+ }
+ if strings.ContainsAny(token.TValue, "[]") {
+ val += token.TValue
+ continue
+ }
+ operand, err := f.adjustFormulaOperand(sheet, sheetN, keepRelative, token, dir, num, offset)
+ if err != nil {
+ return val, err
+ }
+ val += operand
+ continue
+ }
+ if paren := transformParenthesesToken(token); paren != "" {
+ val += transformParenthesesToken(token)
+ continue
+ }
+ if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
+ val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble)
+ continue
+ }
+ val += token.TValue
+ }
+ return val, nil
+}
+
+// transformParenthesesToken returns formula part with parentheses by given
+// token.
+func transformParenthesesToken(token efp.Token) string {
+ if isFunctionStartToken(token) || isBeginParenthesesToken(token) {
+ return token.TValue + string(efp.ParenOpen)
+ }
+ if isFunctionStopToken(token) || isEndParenthesesToken(token) {
+ return token.TValue + string(efp.ParenClose)
+ }
+ return ""
+}
+
+// adjustRangeSheetName returns replaced range reference by given source and
+// target sheet name.
+func adjustRangeSheetName(rng, source, target string) string {
+ cellRefs := strings.Split(rng, ",")
+ for i, cellRef := range cellRefs {
+ rangeRefs := strings.Split(cellRef, ":")
+ for j, rangeRef := range rangeRefs {
+ parts := strings.Split(rangeRef, "!")
+ for k, part := range parts {
+ singleQuote := strings.HasPrefix(part, "'") && strings.HasSuffix(part, "'")
+ if singleQuote {
+ part = strings.TrimPrefix(strings.TrimSuffix(part, "'"), "'")
+ }
+ if part == source {
+ if part = target; singleQuote {
+ part = "'" + part + "'"
+ }
+ }
+ parts[k] = part
+ }
+ rangeRefs[j] = strings.Join(parts, "!")
+ }
+ cellRefs[i] = strings.Join(rangeRefs, ":")
+ }
+ return strings.Join(cellRefs, ",")
+}
+
+// arrayFormulaOperandToken defines meta fields for transforming the array
+// formula to the normal formula.
+type arrayFormulaOperandToken struct {
+ operandTokenIndex, topLeftCol, topLeftRow, bottomRightCol, bottomRightRow int
+ sheetName, sourceCellRef, targetCellRef string
+}
+
+// setCoordinates convert each corner cell reference in the array formula cell
+// range to the coordinate number.
+func (af *arrayFormulaOperandToken) setCoordinates() error {
+ for i, ref := range strings.Split(af.sourceCellRef, ":") {
+ cellRef, col, row, err := parseRef(ref)
+ if err != nil {
+ return err
+ }
+ var c, r int
+ if col {
+ if cellRef.Row = TotalRows; i == 0 {
+ cellRef.Row = 1
+ }
+ }
+ if row {
+ if cellRef.Col = MaxColumns; i == 0 {
+ cellRef.Col = 1
+ }
+ }
+ if c, r = cellRef.Col, cellRef.Row; cellRef.Sheet != "" {
+ af.sheetName = cellRef.Sheet + "!"
+ }
+ if af.topLeftCol == 0 || c < af.topLeftCol {
+ af.topLeftCol = c
+ }
+ if af.topLeftRow == 0 || r < af.topLeftRow {
+ af.topLeftRow = r
+ }
+ if c > af.bottomRightCol {
+ af.bottomRightCol = c
+ }
+ if r > af.bottomRightRow {
+ af.bottomRightRow = r
+ }
+ }
+ return nil
+}
+
+// transformArrayFormula transforms an array formula to the normal formula by
+// giving a formula tokens list and formula operand tokens list.
+func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) string {
+ var val string
+ for i, token := range tokens {
+ var skip bool
+ for _, af := range afs {
+ if af.operandTokenIndex == i {
+ val += af.sheetName + af.targetCellRef
+ skip = true
+ break
+ }
+ }
+ if skip {
+ continue
+ }
+ if paren := transformParenthesesToken(token); paren != "" {
+ val += transformParenthesesToken(token)
+ continue
+ }
+ if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
+ val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble)
+ continue
+ }
+ val += token.TValue
+ }
+ return val
+}
+
+// getArrayFormulaTokens returns parsed formula token and operand related token
+// list for in array formula.
+func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) {
+ var (
+ ps = efp.ExcelParser()
+ tokens = ps.Parse(formula)
+ arrayFormulaOperandTokens []arrayFormulaOperandToken
+ )
+ for i, token := range tokens {
+ if token.TSubType == efp.TokenSubTypeRange && token.TType == efp.TokenTypeOperand {
+ tokenVal := token.TValue
+ for _, definedName := range definedNames {
+ if (definedName.Scope == "Workbook" || definedName.Scope == sheet) && definedName.Name == tokenVal {
+ tokenVal = definedName.RefersTo
+ }
+ }
+ if len(strings.Split(tokenVal, ":")) > 1 {
+ arrayFormulaOperandToken := arrayFormulaOperandToken{
+ operandTokenIndex: i,
+ sourceCellRef: tokenVal,
+ }
+ if err := arrayFormulaOperandToken.setCoordinates(); err != nil {
+ return tokens, arrayFormulaOperandTokens, err
+ }
+ arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken)
+ }
+ }
}
+ return tokens, arrayFormulaOperandTokens, nil
}
// adjustHyperlinks provides a function to update hyperlinks when inserting or
// deleting rows or columns.
-func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {
+func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {
// short path
- if xlsx.Hyperlinks == nil || len(xlsx.Hyperlinks.Hyperlink) == 0 {
+ if ws.Hyperlinks == nil || len(ws.Hyperlinks.Hyperlink) == 0 {
return
}
// order is important
if offset < 0 {
- for i := len(xlsx.Hyperlinks.Hyperlink) - 1; i >= 0; i-- {
- linkData := xlsx.Hyperlinks.Hyperlink[i]
+ for i := len(ws.Hyperlinks.Hyperlink) - 1; i >= 0; i-- {
+ linkData := ws.Hyperlinks.Hyperlink[i]
colNum, rowNum, _ := CellNameToCoordinates(linkData.Ref)
if (dir == rows && num == rowNum) || (dir == columns && num == colNum) {
f.deleteSheetRelationships(sheet, linkData.RID)
- if len(xlsx.Hyperlinks.Hyperlink) > 1 {
- xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i],
- xlsx.Hyperlinks.Hyperlink[i+1:]...)
+ if len(ws.Hyperlinks.Hyperlink) > 1 {
+ ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:i],
+ ws.Hyperlinks.Hyperlink[i+1:]...)
} else {
- xlsx.Hyperlinks = nil
+ ws.Hyperlinks = nil
}
}
}
}
-
- if xlsx.Hyperlinks == nil {
+ if ws.Hyperlinks == nil {
return
}
+ for i := range ws.Hyperlinks.Hyperlink {
+ link := &ws.Hyperlinks.Hyperlink[i] // get reference
+ link.Ref, _ = f.adjustFormulaRef(sheet, sheet, link.Ref, false, dir, num, offset)
+ }
+}
- for i := range xlsx.Hyperlinks.Hyperlink {
- link := &xlsx.Hyperlinks.Hyperlink[i] // get reference
- colNum, rowNum, _ := CellNameToCoordinates(link.Ref)
-
- if dir == rows {
- if rowNum >= num {
- link.Ref, _ = CoordinatesToCellName(colNum, rowNum+offset)
- }
- } else {
- if colNum >= num {
- link.Ref, _ = CoordinatesToCellName(colNum+offset, rowNum)
- }
+// adjustTable provides a function to update the table when inserting or
+// deleting rows or columns.
+func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ if ws.TableParts == nil || len(ws.TableParts.TableParts) == 0 {
+ return nil
+ }
+ for idx := 0; idx < len(ws.TableParts.TableParts); idx++ {
+ tbl := ws.TableParts.TableParts[idx]
+ target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID)
+ tableXML := strings.ReplaceAll(target, "..", "xl")
+ content, ok := f.Pkg.Load(tableXML)
+ if !ok {
+ continue
+ }
+ t := xlsxTable{}
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(&t); err != nil && err != io.EOF {
+ return err
+ }
+ coordinates, err := rangeRefToCoordinates(t.Ref)
+ if err != nil {
+ return err
}
+ // Remove the table when deleting the header row of the table
+ if dir == rows && num == coordinates[0] && offset == -1 {
+ ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
+ ws.TableParts.Count = len(ws.TableParts.TableParts)
+ idx--
+ continue
+ }
+ coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset)
+ x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
+ if y2-y1 < 1 || x2-x1 < 0 {
+ ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
+ ws.TableParts.Count = len(ws.TableParts.TableParts)
+ idx--
+ continue
+ }
+ t.Ref, _ = coordinatesToRangeRef([]int{x1, y1, x2, y2})
+ if t.AutoFilter != nil {
+ t.AutoFilter.Ref = t.Ref
+ }
+ _ = f.setTableColumns(sheet, true, x1, y1, x2, &t)
+ // Currently doesn't support query table
+ t.TableType, t.TotalsRowCount, t.ConnectionID = "", 0, 0
+ table, _ := xml.Marshal(t)
+ f.saveFileList(tableXML, table)
}
+ return nil
}
// adjustAutoFilter provides a function to update the auto filter when
// inserting or deleting rows or columns.
-func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error {
- if xlsx.AutoFilter == nil {
+func (f *File) adjustAutoFilter(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ if ws.AutoFilter == nil {
return nil
}
- coordinates, err := f.areaRefToCoordinates(xlsx.AutoFilter.Ref)
+ coordinates, err := rangeRefToCoordinates(ws.AutoFilter.Ref)
if err != nil {
return err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if (dir == rows && y1 == num && offset < 0) || (dir == columns && x1 == num && x2 == num) {
- xlsx.AutoFilter = nil
- for rowIdx := range xlsx.SheetData.Row {
- rowData := &xlsx.SheetData.Row[rowIdx]
+ ws.AutoFilter = nil
+ for rowIdx := range ws.SheetData.Row {
+ rowData := &ws.SheetData.Row[rowIdx]
if rowData.R > y1 && rowData.R <= y2 {
rowData.Hidden = false
}
}
- return nil
+ return err
}
coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset)
x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3]
- if xlsx.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil {
- return err
- }
- return nil
+ ws.AutoFilter.Ref, err = coordinatesToRangeRef([]int{x1, y1, x2, y2})
+ return err
}
// adjustAutoFilterHelper provides a function for adjusting auto filter to
-// compare and calculate cell axis by the given adjust direction, operation
-// axis and offset.
+// compare and calculate cell reference by the giving adjusting direction,
+// operation reference and offset.
func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int {
if dir == rows {
if coordinates[1] >= num {
@@ -189,100 +731,59 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu
if coordinates[3] >= num {
coordinates[3] += offset
}
- } else {
- if coordinates[2] >= num {
- coordinates[2] += offset
- }
- }
- return coordinates
-}
-
-// areaRefToCoordinates provides a function to convert area reference to a
-// pair of coordinates.
-func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
- rng := strings.Split(ref, ":")
- return areaRangeToCoordinates(rng[0], rng[1])
-}
-
-// areaRangeToCoordinates provides a function to convert cell range to a
-// pair of coordinates.
-func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) {
- coordinates := make([]int, 4)
- var err error
- coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell)
- if err != nil {
- return coordinates, err
+ return coordinates
}
- coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell)
- return coordinates, err
-}
-
-// sortCoordinates provides a function to correct the coordinate area, such
-// correct C1:B3 to B1:C3.
-func sortCoordinates(coordinates []int) error {
- if len(coordinates) != 4 {
- return errors.New("coordinates length must be 4")
+ if coordinates[0] >= num {
+ coordinates[0] += offset
}
- if coordinates[2] < coordinates[0] {
- coordinates[2], coordinates[0] = coordinates[0], coordinates[2]
- }
- if coordinates[3] < coordinates[1] {
- coordinates[3], coordinates[1] = coordinates[1], coordinates[3]
- }
- return nil
-}
-
-// coordinatesToAreaRef provides a function to convert a pair of coordinates
-// to area reference.
-func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) {
- if len(coordinates) != 4 {
- return "", errors.New("coordinates length must be 4")
+ if coordinates[2] >= num {
+ coordinates[2] += offset
}
- firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1])
- if err != nil {
- return "", err
- }
- lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3])
- if err != nil {
- return "", err
- }
- return firstCell + ":" + lastCell, err
+ return coordinates
}
// adjustMergeCells provides a function to update merged cells when inserting
// or deleting rows or columns.
-func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error {
- if xlsx.MergeCells == nil {
+func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ if ws.MergeCells == nil {
return nil
}
- for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
- areaData := xlsx.MergeCells.Cells[i]
- coordinates, err := f.areaRefToCoordinates(areaData.Ref)
+ for i := 0; i < len(ws.MergeCells.Cells); i++ {
+ mergedCells := ws.MergeCells.Cells[i]
+ mergedCellsRef := mergedCells.Ref
+ if !strings.Contains(mergedCellsRef, ":") {
+ mergedCellsRef += ":" + mergedCellsRef
+ }
+ coordinates, err := rangeRefToCoordinates(mergedCellsRef)
if err != nil {
return err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if dir == rows {
if y1 == num && y2 == num && offset < 0 {
- f.deleteMergeCell(xlsx, i)
+ f.deleteMergeCell(ws, i)
i--
+ continue
}
- y1 = f.adjustMergeCellsHelper(y1, num, offset)
- y2 = f.adjustMergeCellsHelper(y2, num, offset)
+
+ y1, y2 = f.adjustMergeCellsHelper(y1, y2, num, offset)
} else {
if x1 == num && x2 == num && offset < 0 {
- f.deleteMergeCell(xlsx, i)
+ f.deleteMergeCell(ws, i)
i--
+ continue
}
- x1 = f.adjustMergeCellsHelper(x1, num, offset)
- x2 = f.adjustMergeCellsHelper(x2, num, offset)
+
+ x1, x2 = f.adjustMergeCellsHelper(x1, x2, num, offset)
}
if x1 == x2 && y1 == y2 {
- f.deleteMergeCell(xlsx, i)
+ f.deleteMergeCell(ws, i)
i--
+ continue
}
- if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil {
+ mergedCells.rect = []int{x1, y1, x2, y2}
+ if mergedCells.Ref, err = coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil {
return err
}
}
@@ -290,46 +791,358 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o
}
// adjustMergeCellsHelper provides a function for adjusting merge cells to
-// compare and calculate cell axis by the given pivot, operation axis and
-// offset.
-func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int {
- if pivot >= num {
- pivot += offset
- if pivot < 1 {
- return 1
+// compare and calculate cell reference by the given pivot, operation reference
+// and offset.
+func (f *File) adjustMergeCellsHelper(p1, p2, num, offset int) (int, int) {
+ if p2 < p1 {
+ p1, p2 = p2, p1
+ }
+
+ if offset >= 0 {
+ if num <= p1 {
+ p1 += offset
+ p2 += offset
+ } else if num <= p2 {
+ p2 += offset
}
- return pivot
+ return p1, p2
+ }
+ if num < p1 || (num == p1 && num == p2) {
+ p1 += offset
+ p2 += offset
+ } else if num <= p2 {
+ p2 += offset
}
- return pivot
+ return p1, p2
}
// deleteMergeCell provides a function to delete merged cell by given index.
-func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) {
- if len(sheet.MergeCells.Cells) > idx {
- sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...)
- sheet.MergeCells.Count = len(sheet.MergeCells.Cells)
+func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) {
+ if idx < 0 {
+ return
+ }
+ if len(ws.MergeCells.Cells) > idx {
+ ws.MergeCells.Cells = append(ws.MergeCells.Cells[:idx], ws.MergeCells.Cells[idx+1:]...)
+ ws.MergeCells.Count = len(ws.MergeCells.Cells)
}
}
+// adjustCellName returns updated cell name by giving column/row number and
+// offset on inserting or deleting rows or columns.
+func adjustCellName(cell string, dir adjustDirection, c, r, offset int) (string, error) {
+ if dir == rows {
+ if rn := r + offset; rn > 0 {
+ return CoordinatesToCellName(c, rn)
+ }
+ }
+ return CoordinatesToCellName(c+offset, r)
+}
+
// adjustCalcChain provides a function to update the calculation chain when
// inserting or deleting rows or columns.
-func (f *File) adjustCalcChain(dir adjustDirection, num, offset int) error {
+func (f *File) adjustCalcChain(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
if f.CalcChain == nil {
return nil
}
- for index, c := range f.CalcChain.C {
+ // If sheet ID is omitted, it is assumed to be the same as the i value of
+ // the previous cell.
+ var prevSheetID int
+ for i := 0; f.CalcChain != nil && i < len(f.CalcChain.C); i++ {
+ c := f.CalcChain.C[i]
+ if c.I == 0 {
+ c.I = prevSheetID
+ }
+ prevSheetID = c.I
+ if c.I != sheetID {
+ continue
+ }
colNum, rowNum, err := CellNameToCoordinates(c.R)
if err != nil {
return err
}
if dir == rows && num <= rowNum {
- if newRow := rowNum + offset; newRow > 0 {
- f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow)
+ if num == rowNum && offset == -1 {
+ _ = f.deleteCalcChain(c.I, c.R)
+ i--
+ continue
}
+ f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset)
}
if dir == columns && num <= colNum {
- if newCol := colNum + offset; newCol > 0 {
- f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum)
+ if num == colNum && offset == -1 {
+ _ = f.deleteCalcChain(c.I, c.R)
+ i--
+ continue
+ }
+ f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset)
+ }
+ }
+ return nil
+}
+
+// adjustVolatileDepsTopic updates the volatile dependencies topic when
+// inserting or deleting rows or columns.
+func (vt *xlsxVolTypes) adjustVolatileDepsTopic(cell string, dir adjustDirection, indexes []int) (int, error) {
+ num, offset, i1, i2, i3, i4 := indexes[0], indexes[1], indexes[2], indexes[3], indexes[4], indexes[5]
+ colNum, rowNum, err := CellNameToCoordinates(cell)
+ if err != nil {
+ return i4, err
+ }
+ if dir == rows && num <= rowNum {
+ if num == rowNum && offset == -1 {
+ vt.deleteVolTopicRef(i1, i2, i3, i4)
+ i4--
+ return i4, err
+ }
+ vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset)
+ }
+ if dir == columns && num <= colNum {
+ if num == colNum && offset == -1 {
+ vt.deleteVolTopicRef(i1, i2, i3, i4)
+ i4--
+ return i4, err
+ }
+ if name, _ := adjustCellName(cell, dir, colNum, rowNum, offset); name != "" {
+ vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset)
+ }
+ }
+ return i4, err
+}
+
+// adjustVolatileDeps updates the volatile dependencies when inserting or
+// deleting rows or columns.
+func (f *File) adjustVolatileDeps(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ volTypes, err := f.volatileDepsReader()
+ if err != nil || volTypes == nil {
+ return err
+ }
+ for i1 := 0; i1 < len(volTypes.VolType); i1++ {
+ for i2 := 0; i2 < len(volTypes.VolType[i1].Main); i2++ {
+ for i3 := 0; i3 < len(volTypes.VolType[i1].Main[i2].Tp); i3++ {
+ for i4 := 0; i4 < len(volTypes.VolType[i1].Main[i2].Tp[i3].Tr); i4++ {
+ ref := volTypes.VolType[i1].Main[i2].Tp[i3].Tr[i4]
+ if ref.S != sheetID {
+ continue
+ }
+ if i4, err = volTypes.adjustVolatileDepsTopic(ref.R, dir, []int{num, offset, i1, i2, i3, i4}); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// adjustConditionalFormats updates the cell reference of the worksheet
+// conditional formatting when inserting or deleting rows or columns.
+func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ for i := 0; i < len(ws.ConditionalFormatting); i++ {
+ cf := ws.ConditionalFormatting[i]
+ if cf == nil {
+ continue
+ }
+ ref, err := f.adjustCellRef(cf.SQRef, dir, num, offset)
+ if err != nil {
+ return err
+ }
+ if ref == "" {
+ ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i],
+ ws.ConditionalFormatting[i+1:]...)
+ i--
+ continue
+ }
+ ws.ConditionalFormatting[i].SQRef = ref
+ }
+ return nil
+}
+
+// adjustDataValidations updates the range of data validations for the worksheet
+// when inserting or deleting rows or columns.
+func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ for _, sheetN := range f.GetSheetList() {
+ worksheet, err := f.workSheetReader(sheetN)
+ if err != nil {
+ if err.Error() == newNotWorksheetError(sheetN).Error() {
+ continue
+ }
+ return err
+ }
+ if worksheet.DataValidations == nil {
+ continue
+ }
+ for i := 0; i < len(worksheet.DataValidations.DataValidation); i++ {
+ dv := worksheet.DataValidations.DataValidation[i]
+ if dv == nil {
+ continue
+ }
+ if sheet == sheetN {
+ ref, err := f.adjustCellRef(dv.Sqref, dir, num, offset)
+ if err != nil {
+ return err
+ }
+ if ref == "" {
+ worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation[:i],
+ worksheet.DataValidations.DataValidation[i+1:]...)
+ i--
+ continue
+ }
+ worksheet.DataValidations.DataValidation[i].Sqref = ref
+ }
+ if worksheet.DataValidations.DataValidation[i].Formula1.isFormula() {
+ formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula1.Content)
+ if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil {
+ return err
+ }
+ worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)}
+ }
+ if worksheet.DataValidations.DataValidation[i].Formula2.isFormula() {
+ formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula2.Content)
+ if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil {
+ return err
+ }
+ worksheet.DataValidations.DataValidation[i].Formula2 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)}
+ }
+ }
+ if worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation); worksheet.DataValidations.Count == 0 {
+ worksheet.DataValidations = nil
+ }
+ }
+ return nil
+}
+
+// adjustDrawings updates the starting anchor of the two cell anchor pictures
+// and charts object when inserting or deleting rows or columns.
+func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) {
+ var ok bool
+ if dir == columns && from.Col+1 >= num && from.Col+offset >= 0 {
+ if from.Col+offset >= MaxColumns {
+ return false, ErrColumnNumber
+ }
+ from.Col += offset
+ ok = editAs == "oneCell"
+ }
+ if dir == rows && from.Row+1 >= num && from.Row+offset >= 0 {
+ if from.Row+offset >= TotalRows {
+ return false, ErrMaxRows
+ }
+ from.Row += offset
+ ok = editAs == "oneCell"
+ }
+ return ok, nil
+}
+
+// adjustDrawings updates the ending anchor of the two cell anchor pictures
+// and charts object when inserting or deleting rows or columns.
+func (to *xlsxTo) adjustDrawings(dir adjustDirection, num, offset int, editAs string, ok bool) error {
+ if dir == columns && to.Col+1 >= num && to.Col+offset >= 0 && ok {
+ if to.Col+offset >= MaxColumns {
+ return ErrColumnNumber
+ }
+ to.Col += offset
+ }
+ if dir == rows && to.Row+1 >= num && to.Row+offset >= 0 && ok {
+ if to.Row+offset >= TotalRows {
+ return ErrMaxRows
+ }
+ to.Row += offset
+ }
+ return nil
+}
+
+// adjustDrawings updates the two cell anchor pictures and charts object when
+// inserting or deleting rows or columns.
+func (a *xdrCellAnchor) adjustDrawings(dir adjustDirection, num, offset int) error {
+ editAs := a.EditAs
+ if a.From == nil || a.To == nil || editAs == "absolute" {
+ return nil
+ }
+ ok, err := a.From.adjustDrawings(dir, num, offset, editAs)
+ if err != nil {
+ return err
+ }
+ return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "")
+}
+
+// adjustDrawings updates the existing two cell anchor pictures and charts
+// object when inserting or deleting rows or columns.
+func (a *xlsxCellAnchorPos) adjustDrawings(dir adjustDirection, num, offset int, editAs string) error {
+ if a.From == nil || a.To == nil || editAs == "absolute" {
+ return nil
+ }
+ ok, err := a.From.adjustDrawings(dir, num, offset, editAs)
+ if err != nil {
+ return err
+ }
+ return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "")
+}
+
+// adjustDrawings updates the pictures and charts object when inserting or
+// deleting rows or columns.
+func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ if ws.Drawing == nil {
+ return nil
+ }
+ target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
+ drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
+ var (
+ err error
+ wsDr *xlsxWsDr
+ )
+ if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
+ return err
+ }
+ anchorCb := func(a *xdrCellAnchor) error {
+ if a.GraphicFrame == "" {
+ return a.adjustDrawings(dir, num, offset)
+ }
+ deCellAnchor := decodeCellAnchor{}
+ deCellAnchorPos := decodeCellAnchorPos{}
+ _ = f.xmlNewDecoder(strings.NewReader("" + a.GraphicFrame + "")).Decode(&deCellAnchor)
+ _ = f.xmlNewDecoder(strings.NewReader("" + a.GraphicFrame + "")).Decode(&deCellAnchorPos)
+ xlsxCellAnchorPos := xlsxCellAnchorPos(deCellAnchorPos)
+ for i := 0; i < len(xlsxCellAnchorPos.AlternateContent); i++ {
+ xlsxCellAnchorPos.AlternateContent[i].XMLNSMC = SourceRelationshipCompatibility.Value
+ }
+ if deCellAnchor.From != nil {
+ xlsxCellAnchorPos.From = &xlsxFrom{
+ Col: deCellAnchor.From.Col, ColOff: deCellAnchor.From.ColOff,
+ Row: deCellAnchor.From.Row, RowOff: deCellAnchor.From.RowOff,
+ }
+ }
+ if deCellAnchor.To != nil {
+ xlsxCellAnchorPos.To = &xlsxTo{
+ Col: deCellAnchor.To.Col, ColOff: deCellAnchor.To.ColOff,
+ Row: deCellAnchor.To.Row, RowOff: deCellAnchor.To.RowOff,
+ }
+ }
+ if err = xlsxCellAnchorPos.adjustDrawings(dir, num, offset, a.EditAs); err != nil {
+ return err
+ }
+ cellAnchor, _ := xml.Marshal(xlsxCellAnchorPos)
+ a.GraphicFrame = strings.TrimSuffix(strings.TrimPrefix(string(cellAnchor), ""), "")
+ return err
+ }
+ for _, anchor := range wsDr.TwoCellAnchor {
+ if err = anchorCb(anchor); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// adjustDefinedNames updates the cell reference of the defined names when
+// inserting or deleting rows or columns.
+func (f *File) adjustDefinedNames(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb.DefinedNames != nil {
+ for i := 0; i < len(wb.DefinedNames.DefinedName); i++ {
+ data := wb.DefinedNames.DefinedName[i].Data
+ if data, err = f.adjustFormulaRef(sheet, "", data, true, dir, num, offset); err == nil {
+ wb.DefinedNames.DefinedName[i].Data = data
}
}
}
diff --git a/adjust_test.go b/adjust_test.go
index 13e47ffeab..0acc8bf2eb 100644
--- a/adjust_test.go
+++ b/adjust_test.go
@@ -1,15 +1,21 @@
package excelize
import (
+ "encoding/xml"
+ "fmt"
+ "path/filepath"
+ "strings"
"testing"
+ _ "image/jpeg"
+
"github.com/stretchr/testify/assert"
)
func TestAdjustMergeCells(t *testing.T) {
f := NewFile()
- // testing adjustAutoFilter with illegal cell coordinates.
- assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{
+ // Test adjustAutoFilter with illegal cell reference
+ assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
{
@@ -17,8 +23,8 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
- }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{
+ }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
+ assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
{
@@ -26,7 +32,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
- }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
+ }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@@ -35,7 +41,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
- }, rows, 1, -1))
+ }, "Sheet1", rows, 1, -1, 1))
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@@ -44,77 +50,1285 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
- }, columns, 1, -1))
+ }, "Sheet1", columns, 1, -1, 1))
+ assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2",
+ },
+ },
+ },
+ }, "Sheet1", columns, 1, -1, 1))
+
+ // Test adjust merge cells
+ var cases []struct {
+ label string
+ ws *xlsxWorksheet
+ dir adjustDirection
+ num int
+ offset int
+ expect string
+ expectRect []int
+ }
+
+ // Test adjust merged cell when insert rows and columns
+ cases = []struct {
+ label string
+ ws *xlsxWorksheet
+ dir adjustDirection
+ num int
+ offset int
+ expect string
+ expectRect []int
+ }{
+ {
+ label: "insert row on ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: rows,
+ num: 2,
+ offset: 1,
+ expect: "A3:B4",
+ expectRect: []int{1, 3, 2, 4},
+ },
+ {
+ label: "insert row on bottom of ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: rows,
+ num: 3,
+ offset: 1,
+ expect: "A2:B4",
+ expectRect: []int{1, 2, 2, 4},
+ },
+ {
+ label: "insert column on the left",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: columns,
+ num: 1,
+ offset: 1,
+ expect: "B2:C3",
+ expectRect: []int{2, 2, 3, 3},
+ },
+ }
+ for _, c := range cases {
+ assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, 1, 1))
+ assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
+ assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label)
+ }
+
+ // Test adjust merged cells when delete rows and columns
+ cases = []struct {
+ label string
+ ws *xlsxWorksheet
+ dir adjustDirection
+ num int
+ offset int
+ expect string
+ expectRect []int
+ }{
+ {
+ label: "delete row on top of ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: rows,
+ num: 2,
+ offset: -1,
+ expect: "A2:B2",
+ expectRect: []int{1, 2, 2, 2},
+ },
+ {
+ label: "delete row on bottom of ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: rows,
+ num: 3,
+ offset: -1,
+ expect: "A2:B2",
+ expectRect: []int{1, 2, 2, 2},
+ },
+ {
+ label: "delete column on the ref left",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: columns,
+ num: 1,
+ offset: -1,
+ expect: "A2:A3",
+ expectRect: []int{1, 2, 1, 3},
+ },
+ {
+ label: "delete column on the ref right",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A2:B3",
+ rect: []int{1, 2, 2, 3},
+ },
+ },
+ },
+ },
+ dir: columns,
+ num: 2,
+ offset: -1,
+ expect: "A2:A3",
+ expectRect: []int{1, 2, 1, 3},
+ },
+ }
+ for _, c := range cases {
+ assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
+ assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
+ }
+
+ // Test delete one row or column
+ cases = []struct {
+ label string
+ ws *xlsxWorksheet
+ dir adjustDirection
+ num int
+ offset int
+ expect string
+ expectRect []int
+ }{
+ {
+ label: "delete one row ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A1:B1",
+ rect: []int{1, 1, 2, 1},
+ },
+ },
+ },
+ },
+ dir: rows,
+ num: 1,
+ offset: -1,
+ },
+ {
+ label: "delete one column ref",
+ ws: &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{
+ Cells: []*xlsxMergeCell{
+ {
+ Ref: "A1:A2",
+ rect: []int{1, 1, 1, 2},
+ },
+ },
+ },
+ },
+ dir: columns,
+ num: 1,
+ offset: -1,
+ },
+ }
+ for _, c := range cases {
+ assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
+ assert.Len(t, c.ws.MergeCells.Cells, 0, c.label)
+ }
+
+ f = NewFile()
+ p1, p2 := f.adjustMergeCellsHelper(2, 1, 0, 0)
+ assert.Equal(t, 1, p1)
+ assert.Equal(t, 2, p2)
+ f.deleteMergeCell(nil, -1)
}
func TestAdjustAutoFilter(t *testing.T) {
f := NewFile()
- // testing adjustAutoFilter with illegal cell coordinates.
- assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{
+ assert.NoError(t, f.adjustAutoFilter(&xlsxWorksheet{
+ SheetData: xlsxSheetData{
+ Row: []xlsxRow{{Hidden: true, R: 2}},
+ },
+ AutoFilter: &xlsxAutoFilter{
+ Ref: "A1:A3",
+ },
+ }, "Sheet1", rows, 1, -1, 1))
+ // Test adjustAutoFilter with illegal cell reference
+ assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{
Ref: "A:B1",
},
- }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{
+ }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
+ assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{
Ref: "A1:B",
},
- }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
+ }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
+}
+
+func TestAdjustTable(t *testing.T) {
+ f, sheetName := NewFile(), "Sheet1"
+ for idx, reference := range []string{"B2:C3", "E3:F5", "H5:H8", "J5:K9"} {
+ assert.NoError(t, f.AddTable(sheetName, &Table{
+ Range: reference,
+ Name: fmt.Sprintf("table%d", idx),
+ StyleName: "TableStyleMedium2",
+ ShowFirstColumn: true,
+ ShowLastColumn: true,
+ ShowRowStripes: boolPtr(false),
+ ShowColumnStripes: true,
+ }))
+ }
+ assert.NoError(t, f.RemoveRow(sheetName, 2))
+ assert.NoError(t, f.RemoveRow(sheetName, 3))
+ assert.NoError(t, f.RemoveRow(sheetName, 3))
+ assert.NoError(t, f.RemoveCol(sheetName, "H"))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx")))
+
+ f = NewFile()
+ assert.NoError(t, f.AddTable(sheetName, &Table{Range: "A1:D5"}))
+ // Test adjust table with non-table part
+ f.Pkg.Delete("xl/tables/table1.xml")
+ assert.NoError(t, f.RemoveRow(sheetName, 1))
+ // Test adjust table with unsupported charset
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.RemoveRow(sheetName, 1), "XML syntax error on line 1: invalid UTF-8")
+ // Test adjust table with invalid table range reference
+ f.Pkg.Store("xl/tables/table1.xml", []byte(``))
+ assert.Equal(t, ErrParameterInvalid, f.RemoveRow(sheetName, 1))
}
func TestAdjustHelper(t *testing.T) {
f := NewFile()
- f.NewSheet("Sheet2")
- f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
- MergeCells: &xlsxMergeCells{
- Cells: []*xlsxMergeCell{
- {
- Ref: "A:B1",
- },
- },
- },
- }
- f.Sheet["xl/worksheets/sheet2.xml"] = &xlsxWorksheet{
- AutoFilter: &xlsxAutoFilter{
- Ref: "A1:B",
- },
- }
- // testing adjustHelper with illegal cell coordinates.
- assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
- // testing adjustHelper on not exists worksheet.
- assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist")
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}},
+ })
+ f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{
+ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"},
+ })
+ // Test adjustHelper with illegal cell reference
+ assert.Equal(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
+ assert.Equal(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
+ // Test adjustHelper on not exists worksheet
+ assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN does not exist")
}
func TestAdjustCalcChain(t *testing.T) {
f := NewFile()
f.CalcChain = &xlsxCalcChain{
- C: []xlsxCalcChainC{
- {R: "B2"},
- },
+ C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}},
}
- assert.NoError(t, f.InsertCol("Sheet1", "A"))
- assert.NoError(t, f.InsertRow("Sheet1", 1))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
- f.CalcChain.C[0].R = "invalid coordinates"
- assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`)
+ f.CalcChain = &xlsxCalcChain{
+ C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}},
+ }
+ assert.NoError(t, f.RemoveRow("Sheet1", 3))
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+
+ f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}}
+ f.CalcChain.C[1].R = "invalid coordinates"
+ assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")))
f.CalcChain = nil
- assert.NoError(t, f.InsertCol("Sheet1", "A"))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+}
+
+func TestAdjustCols(t *testing.T) {
+ sheetName := "Sheet1"
+ preset := func() (*File, error) {
+ f := NewFile()
+ if err := f.SetColWidth(sheetName, "J", "T", 5); err != nil {
+ return f, err
+ }
+ if err := f.SetSheetRow(sheetName, "J1", &[]string{"J1", "K1", "L1", "M1", "N1", "O1", "P1", "Q1", "R1", "S1", "T1"}); err != nil {
+ return f, err
+ }
+ return f, nil
+ }
+ baseTbl := []string{"B", "J", "O", "O", "O", "U", "V"}
+ insertTbl := []int{2, 2, 2, 5, 6, 2, 2}
+ expectedTbl := []map[string]float64{
+ {"J": defaultColWidth, "K": defaultColWidth, "U": 5, "V": 5, "W": defaultColWidth},
+ {"J": defaultColWidth, "K": defaultColWidth, "U": 5, "V": 5, "W": defaultColWidth},
+ {"O": 5, "P": 5, "U": 5, "V": 5, "W": defaultColWidth},
+ {"O": 5, "S": 5, "X": 5, "Y": 5, "Z": defaultColWidth},
+ {"O": 5, "S": 5, "Y": 5, "X": 5, "AA": defaultColWidth},
+ {"U": 5, "V": 5, "W": defaultColWidth},
+ {"U": defaultColWidth, "V": defaultColWidth, "W": defaultColWidth},
+ }
+ for idx, columnName := range baseTbl {
+ f, err := preset()
+ assert.NoError(t, err)
+ assert.NoError(t, f.InsertCols(sheetName, columnName, insertTbl[idx]))
+ for column, expected := range expectedTbl[idx] {
+ width, err := f.GetColWidth(sheetName, column)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, width, column)
+ }
+ assert.NoError(t, f.Close())
+ }
+
+ baseTbl = []string{"B", "J", "O", "T"}
+ expectedTbl = []map[string]float64{
+ {"H": defaultColWidth, "I": 5, "S": 5, "T": defaultColWidth},
+ {"I": defaultColWidth, "J": 5, "S": 5, "T": defaultColWidth},
+ {"I": defaultColWidth, "O": 5, "S": 5, "T": defaultColWidth},
+ {"R": 5, "S": 5, "T": defaultColWidth, "U": defaultColWidth},
+ }
+ for idx, columnName := range baseTbl {
+ f, err := preset()
+ assert.NoError(t, err)
+ assert.NoError(t, f.RemoveCol(sheetName, columnName))
+ for column, expected := range expectedTbl[idx] {
+ width, err := f.GetColWidth(sheetName, column)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, width, column)
+ }
+ assert.NoError(t, f.Close())
+ }
+
+ f, err := preset()
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetColWidth(sheetName, "I", "I", 8))
+ for i := 0; i <= 12; i++ {
+ assert.NoError(t, f.RemoveCol(sheetName, "I"))
+ }
+ for c := 9; c <= 21; c++ {
+ columnName, err := ColumnNumberToName(c)
+ assert.NoError(t, err)
+ width, err := f.GetColWidth(sheetName, columnName)
+ assert.NoError(t, err)
+ assert.Equal(t, defaultColWidth, width, columnName)
+ }
+
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).Cols = nil
+ assert.NoError(t, f.RemoveCol(sheetName, "A"))
+
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ assert.NoError(t, f.SetColWidth("Sheet1", "XFB", "XFC", 12))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Min)
+ assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Max)
+
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
+ assert.Nil(t, ws.(*xlsxWorksheet).Cols)
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "(1-0.5)/2"))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ formula, err := f.GetCellFormula("Sheet1", "B2")
+ assert.NoError(t, err)
+ assert.Equal(t, "(1-0.5)/2", formula)
+ assert.NoError(t, f.Close())
+}
+
+func TestAdjustColDimensions(t *testing.T) {
+ f := NewFile()
+ ws, err := f.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
+ assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns))
+
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ f.Sheet.Delete("xl/worksheets/sheet2.xml")
+ f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.adjustColDimensions("Sheet2", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestAdjustRowDimensions(t *testing.T) {
+ f := NewFile()
+ ws, err := f.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
+ assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows))
+
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ f.Sheet.Delete("xl/worksheets/sheet2.xml")
+ f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.adjustRowDimensions("Sheet1", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
+
+ f = NewFile()
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ ws, err = f.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("Sheet2!A%d", TotalRows)))
+ assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet2", ws, 1, TotalRows))
+}
+
+func TestAdjustHyperlinks(t *testing.T) {
+ f := NewFile()
+ ws, err := f.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
+ f.adjustHyperlinks(ws, "Sheet1", rows, 3, -1)
+
+ // Test adjust hyperlinks location with positive offset
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ link, target, err := f.GetCellHyperLink("Sheet1", "F6")
+ assert.NoError(t, err)
+ assert.True(t, link)
+ assert.Equal(t, target, "Sheet1!A1")
+
+ // Test adjust hyperlinks location with negative offset
+ assert.NoError(t, f.RemoveRow("Sheet1", 1))
+ link, target, err = f.GetCellHyperLink("Sheet1", "F5")
+ assert.NoError(t, err)
+ assert.True(t, link)
+ assert.Equal(t, target, "Sheet1!A1")
+
+ // Test adjust hyperlinks location on remove row
+ assert.NoError(t, f.RemoveRow("Sheet1", 5))
+ link, target, err = f.GetCellHyperLink("Sheet1", "F5")
+ assert.NoError(t, err)
+ assert.False(t, link)
+ assert.Empty(t, target)
+
+ // Test adjust hyperlinks location on remove column
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
+ assert.NoError(t, f.RemoveCol("Sheet1", "F"))
+ link, target, err = f.GetCellHyperLink("Sheet1", "F5")
+ assert.NoError(t, err)
+ assert.False(t, link)
+ assert.Empty(t, target)
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustHyperlinks.xlsx")))
+ assert.NoError(t, f.Close())
+}
+
+func TestAdjustFormula(t *testing.T) {
+ f := NewFile()
+ formulaType, ref := STCellFormulaTypeShared, "C1:C5"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
+ assert.NoError(t, f.InsertCols("Sheet1", "B", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} {
+ formula, err := f.GetCellFormula("Sheet1", cell)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, formula)
+ }
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
+ assert.NoError(t, f.Close())
+
+ assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{}, rows, 0, 0, false))
+ assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "-"}}, rows, 0, 0, false))
+ assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "XFD1:XFD1"}}, columns, 0, 1, false))
+
+ _, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1)
+ assert.Equal(t, ErrColumnNumber, err)
+ _, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", false, columns, 0, 1)
+ assert.Equal(t, ErrColumnNumber, err)
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1"))
+ assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
+ assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(1048576:1:2)"))
+ assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(XFD:A:B)"))
+ assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(A:B:XFD)"))
+ assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
+
+ // Test adjust formula with defined name in formula text
+ f = NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Amount",
+ RefersTo: "Sheet1!$B$2",
+ }))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3"))
+ assert.NoError(t, f.RemoveRow("Sheet1", 1))
+ formula, err := f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Amount+B2", formula)
+
+ // Test adjust formula with array formula
+ f = NewFile()
+ formulaType, reference := STCellFormulaTypeArray, "A3:A3"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ formula, err = f.GetCellFormula("Sheet1", "A4")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2:A3", formula)
+
+ // Test adjust formula on duplicate row with array formula
+ f = NewFile()
+ formulaType, reference = STCellFormulaTypeArray, "A3"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ formula, err = f.GetCellFormula("Sheet1", "A4")
+ assert.NoError(t, err)
+ assert.Equal(t, "A2:A3", formula)
+
+ // Test adjust formula on duplicate row with relative and absolute cell references
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11&\" \""))
+ assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2))
+ formula, err = f.GetCellFormula("Sheet1", "B2")
+ assert.NoError(t, err)
+ assert.Equal(t, "A$2+$A3&\" \"", formula)
+
+ t.Run("for_cells_affected_directly", func(t *testing.T) {
+ // Test insert row in middle of range with relative and absolute cell references
+ f := NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "$A1+A$2"))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err := f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "$A1+A$3", formula)
+ assert.NoError(t, f.RemoveRow("Sheet1", 2))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "$A1+A$2", formula)
+
+ // Test insert column in middle of range
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+C1"))
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "B1+D1", formula)
+ assert.NoError(t, f.RemoveCol("Sheet1", "C"))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "B1+C1", formula)
+
+ // Test insert row and column in a rectangular range
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4+D5+E4+E5"))
+ assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "D4+D6+F4+F6", formula)
+
+ // Test insert row in middle of range
+ f = NewFile()
+ formulaType, reference := STCellFormulaTypeArray, "B1:B1"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A1:A3", formula)
+ assert.NoError(t, f.RemoveRow("Sheet1", 2))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A1:A2", formula)
+
+ // Test insert column in middle of range
+ f = NewFile()
+ formulaType, reference = STCellFormulaTypeArray, "A1:A1"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1:C1", FormulaOpts{Ref: &reference, Type: &formulaType}))
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "B1:D1", formula)
+ assert.NoError(t, f.RemoveCol("Sheet1", "C"))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "B1:C1", formula)
+
+ // Test insert row and column in a rectangular range
+ f = NewFile()
+ formulaType, reference = STCellFormulaTypeArray, "A1:A1"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4:E5", FormulaOpts{Ref: &reference, Type: &formulaType}))
+ assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
+ formula, err = f.GetCellFormula("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "D4:F6", formula)
+ })
+ t.Run("for_cells_affected_indirectly", func(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A3+A4"))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err := f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A4+A5", formula)
+ assert.NoError(t, f.RemoveRow("Sheet1", 2))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A3+A4", formula)
+
+ f = NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "D3+D4"))
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "E3+E4", formula)
+ assert.NoError(t, f.RemoveCol("Sheet1", "C"))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "D3+D4", formula)
+ })
+ t.Run("for_entire_cols_rows_reference", func(t *testing.T) {
+ f := NewFile()
+ // Test adjust formula on insert row in the middle of the range
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2:A3:A4,,Table1[])"))
+ assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
+ formula, err := f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(A2:A4:A5,,Table1[])", formula)
+
+ // Test adjust formula on insert at the top of the range
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(A3:A5:A6,,Table1[])", formula)
+
+ f = NewFile()
+ // Test adjust formula on insert row in the middle of the range
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet 1'!A2,A3)"))
+ assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM('Sheet 1'!A2,A4)", formula)
+
+ // Test adjust formula on insert row at the top of the range
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula)
+
+ f = NewFile()
+ // Test adjust formula on insert col in the middle of the range
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3:D3)"))
+ assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(C3:E3)", formula)
+
+ // Test adjust formula on insert at the top of the range
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(D3:F3)", formula)
+
+ f = NewFile()
+ // Test adjust formula on insert column in the middle of the range
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3,D3)"))
+ assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(C3,E3)", formula)
+
+ // Test adjust formula on insert column at the top of the range
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(D3,F3)", formula)
+
+ f = NewFile()
+ // Test adjust formula on insert row in the middle of the range (range of whole row)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(2:3)"))
+ assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(2:4)", formula)
+
+ // Test adjust formula on insert row at the top of the range (range of whole row)
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(3:5)", formula)
+
+ f = NewFile()
+ // Test adjust formula on insert row in the middle of the range (range of whole column)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C:D)"))
+ assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(C:E)", formula)
+
+ // Test adjust formula on insert row at the top of the range (range of whole column)
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ formula, err = f.GetCellFormula("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SUM(D:F)", formula)
+ })
+ t.Run("for_all_worksheet_cells_with_rows_insert", func(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ // Tests formulas referencing Sheet2 should update but those referencing the original sheet should not
+ tbl := [][]string{
+ {"B1", "Sheet2!A1+Sheet2!A2", "Sheet2!A1+Sheet2!A3", "Sheet2!A2+Sheet2!A4"},
+ {"C1", "A1+A2", "A1+A2", "A1+A2"},
+ {"D1", "Sheet2!B1:B2", "Sheet2!B1:B3", "Sheet2!B2:B4"},
+ {"E1", "B1:B2", "B1:B2", "B1:B2"},
+ {"F1", "SUM(Sheet2!C1:C2)", "SUM(Sheet2!C1:C3)", "SUM(Sheet2!C2:C4)"},
+ {"G1", "SUM(C1:C2)", "SUM(C1:C2)", "SUM(C1:C2)"},
+ {"H1", "SUM(Sheet2!D1,Sheet2!D2)", "SUM(Sheet2!D1,Sheet2!D3)", "SUM(Sheet2!D2,Sheet2!D4)"},
+ {"I1", "SUM(D1,D2)", "SUM(D1,D2)", "SUM(D1,D2)"},
+ }
+ for _, preset := range tbl {
+ assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
+ }
+ // Test adjust formula on insert row in the middle of the range
+ assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[2], formula)
+ }
+
+ // Test adjust formula on insert row in the top of the range
+ assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[3], formula)
+ }
+ })
+ t.Run("for_all_worksheet_cells_with_cols_insert", func(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ tbl := [][]string{
+ {"A1", "Sheet2!A1+Sheet2!B1", "Sheet2!A1+Sheet2!C1", "Sheet2!B1+Sheet2!D1"},
+ {"A2", "A1+B1", "A1+B1", "A1+B1"},
+ {"A3", "Sheet2!A2:B2", "Sheet2!A2:C2", "Sheet2!B2:D2"},
+ {"A4", "A2:B2", "A2:B2", "A2:B2"},
+ {"A5", "SUM(Sheet2!A3:B3)", "SUM(Sheet2!A3:C3)", "SUM(Sheet2!B3:D3)"},
+ {"A6", "SUM(A3:B3)", "SUM(A3:B3)", "SUM(A3:B3)"},
+ {"A7", "SUM(Sheet2!A4,Sheet2!B4)", "SUM(Sheet2!A4,Sheet2!C4)", "SUM(Sheet2!B4,Sheet2!D4)"},
+ {"A8", "SUM(A4,B4)", "SUM(A4,B4)", "SUM(A4,B4)"},
+ }
+ for _, preset := range tbl {
+ assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
+ }
+ // Test adjust formula on insert column in the middle of the range
+ assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[2], formula)
+ }
+ // Test adjust formula on insert column in the top of the range
+ assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[3], formula)
+ }
+ })
+ t.Run("for_cross_sheet_ref_with_rows_insert)", func(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ // Tests formulas referencing Sheet2 should update but those referencing
+ // the original sheet or Sheet 3 should not update
+ tbl := [][]string{
+ {"B1", "Sheet2!A1+Sheet2!A2+Sheet1!A3+Sheet1!A4", "Sheet2!A1+Sheet2!A3+Sheet1!A3+Sheet1!A4", "Sheet2!A2+Sheet2!A4+Sheet1!A3+Sheet1!A4"},
+ {"C1", "Sheet2!B1+Sheet2!B2+B3+B4", "Sheet2!B1+Sheet2!B3+B3+B4", "Sheet2!B2+Sheet2!B4+B3+B4"},
+ {"D1", "Sheet2!C1+Sheet2!C2+Sheet3!A3+Sheet3!A4", "Sheet2!C1+Sheet2!C3+Sheet3!A3+Sheet3!A4", "Sheet2!C2+Sheet2!C4+Sheet3!A3+Sheet3!A4"},
+ {"E1", "SUM(Sheet2!D1:D2,Sheet1!A3:A4)", "SUM(Sheet2!D1:D3,Sheet1!A3:A4)", "SUM(Sheet2!D2:D4,Sheet1!A3:A4)"},
+ {"F1", "SUM(Sheet2!E1:E2,A3:A4)", "SUM(Sheet2!E1:E3,A3:A4)", "SUM(Sheet2!E2:E4,A3:A4)"},
+ {"G1", "SUM(Sheet2!F1:F2,Sheet3!A3:A4)", "SUM(Sheet2!F1:F3,Sheet3!A3:A4)", "SUM(Sheet2!F2:F4,Sheet3!A3:A4)"},
+ }
+ for _, preset := range tbl {
+ assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
+ }
+ // Test adjust formula on insert row in the middle of the range
+ assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[2], formula)
+ }
+ // Test adjust formula on insert row in the top of the range
+ assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[3], formula)
+ }
+ })
+ t.Run("for_cross_sheet_ref_with_cols_insert)", func(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ // Tests formulas referencing Sheet2 should update but those referencing
+ // the original sheet or Sheet 3 should not update
+ tbl := [][]string{
+ {"A1", "Sheet2!A1+Sheet2!B1+Sheet1!C1+Sheet1!D1", "Sheet2!A1+Sheet2!C1+Sheet1!C1+Sheet1!D1", "Sheet2!B1+Sheet2!D1+Sheet1!C1+Sheet1!D1"},
+ {"A2", "Sheet2!A2+Sheet2!B2+C2+D2", "Sheet2!A2+Sheet2!C2+C2+D2", "Sheet2!B2+Sheet2!D2+C2+D2"},
+ {"A3", "Sheet2!A3+Sheet2!B3+Sheet3!C3+Sheet3!D3", "Sheet2!A3+Sheet2!C3+Sheet3!C3+Sheet3!D3", "Sheet2!B3+Sheet2!D3+Sheet3!C3+Sheet3!D3"},
+ {"A4", "SUM(Sheet2!A4:B4,Sheet1!C4:D4)", "SUM(Sheet2!A4:C4,Sheet1!C4:D4)", "SUM(Sheet2!B4:D4,Sheet1!C4:D4)"},
+ {"A5", "SUM(Sheet2!A5:B5,C5:D5)", "SUM(Sheet2!A5:C5,C5:D5)", "SUM(Sheet2!B5:D5,C5:D5)"},
+ {"A6", "SUM(Sheet2!A6:B6,Sheet3!C6:D6)", "SUM(Sheet2!A6:C6,Sheet3!C6:D6)", "SUM(Sheet2!B6:D6,Sheet3!C6:D6)"},
+ }
+ for _, preset := range tbl {
+ assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
+ }
+ // Test adjust formula on insert row in the middle of the range
+ assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[2], formula)
+ }
+ // Test adjust formula on insert row in the top of the range
+ assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
+ for _, preset := range tbl {
+ formula, err := f.GetCellFormula("Sheet1", preset[0])
+ assert.NoError(t, err)
+ assert.Equal(t, preset[3], formula)
+ }
+ })
+ t.Run("for_cross_sheet_ref_with_chart_sheet)", func(t *testing.T) {
+ assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ })
+ t.Run("for_array_formula_cell", func(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
+ formulaType, ref := STCellFormulaTypeArray, "C1:C2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ result, err := f.CalcCellValue("Sheet1", "D2")
+ assert.NoError(t, err)
+ assert.Equal(t, "2", result)
+ result, err = f.CalcCellValue("Sheet1", "D3")
+ assert.NoError(t, err)
+ assert.Equal(t, "12", result)
+
+ // Test adjust array formula with invalid range reference
+ formulaType, ref = STCellFormulaTypeArray, "E1:E2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "the column number must be greater than or equal to 1 and less than or equal to 16384")
+ })
+}
+
+func TestAdjustVolatileDeps(t *testing.T) {
+ f := NewFile()
+ f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`|
|
`, NameSpaceSpreadSheet.Value)))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ assert.Equal(t, "D3", f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr[1].R)
+ assert.NoError(t, f.RemoveCol("Sheet1", "D"))
+ assert.NoError(t, f.RemoveRow("Sheet1", 4))
+ assert.Len(t, f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr, 1)
+
+ f = NewFile()
+ f.Pkg.Store(defaultXMLPathVolatileDeps, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.InsertRows("Sheet1", 2, 1), "XML syntax error on line 1: invalid UTF-8")
+
+ f = NewFile()
+ f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`
`, NameSpaceSpreadSheet.Value)))
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1))
+ f.volatileDepsWriter()
+}
+
+func TestAdjustConditionalFormats(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "B1", &[]interface{}{1, nil, 1, 1}))
+ formatID, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}})
+ assert.NoError(t, err)
+ format := []ConditionalFormatOptions{
+ {
+ Type: "cell",
+ Criteria: "greater than",
+ Format: &formatID,
+ Value: "0",
+ },
+ }
+ for _, ref := range []string{"B1", "D1:E1"} {
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, format))
+ }
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, format, 1)
+ assert.Equal(t, format, opts["C1:D1"])
+
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "-"
+ assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
+
+ ws.(*xlsxWorksheet).ConditionalFormatting[0] = nil
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+
+ t.Run("for_remove_conditional_formats_column", func(t *testing.T) {
+ f := NewFile()
+ format := []ConditionalFormatOptions{{
+ Type: "data_bar",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ }}
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
+ assert.NoError(t, f.RemoveCol("Sheet1", "D"))
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, opts, 0)
+ })
+ t.Run("for_remove_conditional_formats_row", func(t *testing.T) {
+ f := NewFile()
+ format := []ConditionalFormatOptions{{
+ Type: "data_bar",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ }}
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:E2", format))
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "F2", format))
+ assert.NoError(t, f.RemoveRow("Sheet1", 2))
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, opts, 0)
+ })
+ t.Run("for_adjust_conditional_formats_row", func(t *testing.T) {
+ f := NewFile()
+ format := []ConditionalFormatOptions{{
+ Type: "data_bar",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ }}
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
+ assert.NoError(t, f.RemoveRow("Sheet1", 1))
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, opts, 2)
+ assert.Equal(t, format, opts["D1:D2"])
+ assert.Equal(t, format, opts["D4:D4"])
+ })
+}
+
+func TestAdjustDataValidations(t *testing.T) {
+ f := NewFile()
+ dv := NewDataValidation(true)
+ dv.Sqref = "B1"
+ assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+ dvs, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dvs, 0)
+
+ assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1))
+ assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2))
+ dv = NewDataValidation(true)
+ dv.Sqref = "C2:D3"
+ dv.SetSqrefDropList("$F$2:$F$3")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+
+ assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10}))
+ dv = NewDataValidation(true)
+ dv.Sqref = "C5:D6"
+ assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+ assert.NoError(t, f.RemoveCol("Sheet2", "B"))
+ dvs, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "B2:C3", dvs[0].Sqref)
+ assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1)
+ assert.Equal(t, "B5:C6", dvs[1].Sqref)
+ assert.Equal(t, "Sheet2!B1", dvs[1].Formula1)
+ assert.Equal(t, "Sheet2!C1", dvs[1].Formula2)
+
+ dv = NewDataValidation(true)
+ dv.Sqref = "C8:D10"
+ assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`}))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+ dvs, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1)
+
+ // Test adjust data validation with multiple cell range
+ dv = NewDataValidation(true)
+ dv.Sqref = "G1:G3 H1:H3 A3:A1048576"
+ assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
+ dvs, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "G1:G4 H1:H4 A4:A1048576", dvs[3].Sqref)
+
+ dv = NewDataValidation(true)
+ dv.Sqref = "C5:D6"
+ assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
+ assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
+
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-"
+ assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
+
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+
+ ws.(*xlsxWorksheet).DataValidations = nil
+ assert.NoError(t, f.RemoveCol("Sheet1", "B"))
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
+
+ t.Run("for_escaped_data_validation_rules_formula", func(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ dv := NewDataValidation(true)
+ dv.Sqref = "A1"
+ assert.NoError(t, dv.SetDropList([]string{"option1", strings.Repeat("\"", 4)}))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ // The double quote symbol in none formula data validation rules will be escaped in the Kingsoft WPS Office
+ formula := strings.ReplaceAll(fmt.Sprintf("\"option1, %s", strings.Repeat("\"", 9)), "\"", """)
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula
+ assert.NoError(t, f.RemoveCol("Sheet2", "A"))
+ dvs, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, formula, dvs[0].Formula1)
+ })
+
+ t.Run("no_data_validations_on_first_sheet", func(t *testing.T) {
+ f := NewFile()
+
+ // Add Sheet2 and set a data validation
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ dv := NewDataValidation(true)
+ dv.Sqref = "C5:D6"
+ assert.NoError(t, f.AddDataValidation("Sheet2", dv))
+
+ // Adjust Sheet2 by removing a column
+ assert.NoError(t, f.RemoveCol("Sheet2", "A"))
+
+ // Verify that data validations on Sheet2 are adjusted correctly
+ dvs, err = f.GetDataValidations("Sheet2")
+ assert.NoError(t, err)
+ assert.Equal(t, "B5:C6", dvs[0].Sqref) // Adjusted range
+ })
}
-func TestCoordinatesToAreaRef(t *testing.T) {
+func TestAdjustDrawings(t *testing.T) {
f := NewFile()
- _, err := f.coordinatesToAreaRef([]int{})
- assert.EqualError(t, err, "coordinates length must be 4")
- _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1})
- assert.EqualError(t, err, "invalid cell coordinates [1, -1]")
- _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1})
- assert.EqualError(t, err, "invalid cell coordinates [1, -1]")
- ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1})
- assert.NoError(t, err)
- assert.EqualValues(t, ref, "A1:A1")
+ // Test add pictures to sheet with positioning
+ assert.NoError(t, f.AddPicture("Sheet1", "B2", filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.NoError(t, f.AddPicture("Sheet1", "B11", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "oneCell"}))
+ assert.NoError(t, f.AddPicture("Sheet1", "B21", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "absolute"}))
+
+ // Test adjust pictures on inserting columns and rows
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 15, 1))
+ cells, err := f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"D3", "D13", "B21"}, cells)
+ wb := filepath.Join("test", "TestAdjustDrawings.xlsx")
+ assert.NoError(t, f.SaveAs(wb))
+
+ // Test adjust pictures on deleting columns and rows
+ assert.NoError(t, f.RemoveCol("Sheet1", "A"))
+ assert.NoError(t, f.RemoveRow("Sheet1", 1))
+ cells, err = f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"C2", "C12", "B21"}, cells)
+
+ // Test adjust existing pictures on inserting columns and rows
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 16, 1))
+ cells, err = f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"F4", "F15", "B21"}, cells)
+
+ // Test adjust drawings with unsupported charset
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "XML syntax error on line 1: invalid UTF-8")
+
+ errors := []error{ErrColumnNumber, ErrColumnNumber, ErrMaxRows, ErrMaxRows}
+ cells = []string{"XFD1", "XFB1"}
+ for i, cell := range cells {
+ f = NewFile()
+ assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.SaveAs(wb))
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
+ }
+ errors = []error{ErrMaxRows, ErrMaxRows}
+ cells = []string{"A1048576", "A1048570"}
+ for i, cell := range cells {
+ f = NewFile()
+ assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
+ assert.NoError(t, f.SaveAs(wb))
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
+ }
+
+ a := xdrCellAnchor{}
+ assert.NoError(t, a.adjustDrawings(columns, 0, 0))
+ p := xlsxCellAnchorPos{}
+ assert.NoError(t, p.adjustDrawings(columns, 0, 0, ""))
+
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`000
0101
0`))
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
}
-func TestSortCoordinates(t *testing.T) {
- assert.EqualError(t, sortCoordinates(make([]int, 3)), "coordinates length must be 4")
+func TestAdjustDefinedNames(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ for _, dn := range []*DefinedName{
+ {Name: "Name1", RefersTo: "Sheet1!$XFD$1"},
+ {Name: "Name2", RefersTo: "Sheet2!$C$1", Scope: "Sheet1"},
+ {Name: "Name3", RefersTo: "Sheet2!$C$1:$D$2", Scope: "Sheet1"},
+ {Name: "Name4", RefersTo: "Sheet2!$C1:D$2"},
+ {Name: "Name5", RefersTo: "Sheet2!C$1:$D2"},
+ {Name: "Name6", RefersTo: "Sheet2!C:$D"},
+ {Name: "Name7", RefersTo: "Sheet2!$C:D"},
+ {Name: "Name8", RefersTo: "Sheet2!C:D"},
+ {Name: "Name9", RefersTo: "Sheet2!$C:$D"},
+ {Name: "Name10", RefersTo: "Sheet2!1:2"},
+ } {
+ assert.NoError(t, f.SetDefinedName(dn))
+ }
+ assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
+ assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
+ assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
+ definedNames := f.GetDefinedName()
+ for i, expected := range []string{
+ "Sheet1!$XFD$2",
+ "Sheet2!$D$2",
+ "Sheet2!$D$2:$E$3",
+ "Sheet2!$D1:D$3",
+ "Sheet2!C$2:$E2",
+ "Sheet2!C:$E",
+ "Sheet2!$D:D",
+ "Sheet2!C:D",
+ "Sheet2!$D:$E",
+ "Sheet2!1:2",
+ } {
+ assert.Equal(t, expected, definedNames[i].RefersTo)
+ }
+
+ f = NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name1",
+ RefersTo: "Sheet1!$A$1",
+ Scope: "Sheet1",
+ }))
+ assert.NoError(t, f.RemoveCol("Sheet1", "A"))
+ definedNames = f.GetDefinedName()
+ assert.Equal(t, "Sheet1!$A$1", definedNames[0].RefersTo)
+
+ f = NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name1",
+ RefersTo: "'1.A & B C'!#REF!",
+ Scope: "Sheet1",
+ }))
+ assert.NoError(t, f.RemoveCol("Sheet1", "A"))
+ definedNames = f.GetDefinedName()
+ assert.Equal(t, "'1.A & B C'!#REF!", definedNames[0].RefersTo)
+
+ f = NewFile()
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.adjustDefinedNames(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/calc.go b/calc.go
index 54683a82c6..44dc4e44af 100644
--- a/calc.go
+++ b/calc.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -17,31 +17,211 @@ import (
"errors"
"fmt"
"math"
+ "math/big"
+ "math/cmplx"
"math/rand"
+ "net/url"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
+ "sync"
"time"
+ "unicode"
+ "unicode/utf8"
+ "unsafe"
"github.com/xuri/efp"
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
)
-// Excel formula errors
const (
+ // Excel formula errors
formulaErrorDIV = "#DIV/0!"
formulaErrorNAME = "#NAME?"
formulaErrorNA = "#N/A"
formulaErrorNUM = "#NUM!"
formulaErrorVALUE = "#VALUE!"
formulaErrorREF = "#REF!"
- formulaErrorNULL = "#NULL"
+ formulaErrorNULL = "#NULL!"
formulaErrorSPILL = "#SPILL!"
formulaErrorCALC = "#CALC!"
formulaErrorGETTINGDATA = "#GETTING_DATA"
+ // Formula criteria condition enumeration
+ _ byte = iota
+ criteriaEq
+ criteriaLe
+ criteriaGe
+ criteriaNe
+ criteriaL
+ criteriaG
+ criteriaErr
+ criteriaRegexp
+
+ categoryWeightAndMass
+ categoryDistance
+ categoryTime
+ categoryPressure
+ categoryForce
+ categoryEnergy
+ categoryPower
+ categoryMagnetism
+ categoryTemperature
+ categoryVolumeAndLiquidMeasure
+ categoryArea
+ categoryInformation
+ categorySpeed
+
+ matchModeExact = 0
+ matchModeMinGreater = 1
+ matchModeMaxLess = -1
+ matchModeWildcard = 2
+
+ searchModeLinear = 1
+ searchModeReverseLinear = -1
+ searchModeAscBinary = 2
+ searchModeDescBinary = -2
+
+ maxFinancialIterations = 128
+ financialPrecision = 1.0e-08
+ // Date and time format regular expressions
+ monthRe = `((jan|january)|(feb|february)|(mar|march)|(apr|april)|(may)|(jun|june)|(jul|july)|(aug|august)|(sep|september)|(oct|october)|(nov|november)|(dec|december))`
+ df1 = `(([0-9])+)/(([0-9])+)/(([0-9])+)`
+ df2 = monthRe + ` (([0-9])+), (([0-9])+)`
+ df3 = `(([0-9])+)-(([0-9])+)-(([0-9])+)`
+ df4 = `(([0-9])+)-` + monthRe + `-(([0-9])+)`
+ datePrefix = `^((` + df1 + `|` + df2 + `|` + df3 + `|` + df4 + `) )?`
+ tfhh = `(([0-9])+) (am|pm)`
+ tfhhmm = `(([0-9])+):(([0-9])+)( (am|pm))?`
+ tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?`
+ tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?`
+ timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$`
+)
+
+var (
+ // tokenPriority defined basic arithmetic operator priority
+ tokenPriority = map[string]int{
+ "^": 5,
+ "*": 4,
+ "/": 4,
+ "+": 3,
+ "-": 3,
+ "&": 2,
+ "=": 1,
+ "<>": 1,
+ "<": 1,
+ "<=": 1,
+ ">": 1,
+ ">=": 1,
+ }
+ month2num = map[string]int{
+ "january": 1,
+ "february": 2,
+ "march": 3,
+ "april": 4,
+ "may": 5,
+ "june": 6,
+ "july": 7,
+ "august": 8,
+ "september": 9,
+ "october": 10,
+ "november": 11,
+ "december": 12,
+ "jan": 1,
+ "feb": 2,
+ "mar": 3,
+ "apr": 4,
+ "jun": 6,
+ "jul": 7,
+ "aug": 8,
+ "sep": 9,
+ "oct": 10,
+ "nov": 11,
+ "dec": 12,
+ }
+ dateFormats = map[string]*regexp.Regexp{
+ "mm/dd/yy": regexp.MustCompile(`^` + df1 + timeSuffix),
+ "mm dd, yy": regexp.MustCompile(`^` + df2 + timeSuffix),
+ "yy-mm-dd": regexp.MustCompile(`^` + df3 + timeSuffix),
+ "yy-mmStr-dd": regexp.MustCompile(`^` + df4 + timeSuffix),
+ }
+ timeFormats = map[string]*regexp.Regexp{
+ "hh": regexp.MustCompile(datePrefix + tfhh + `$`),
+ "hh:mm": regexp.MustCompile(datePrefix + tfhhmm + `$`),
+ "mm:ss": regexp.MustCompile(datePrefix + tfmmss + `$`),
+ "hh:mm:ss": regexp.MustCompile(datePrefix + tfhhmmss + `$`),
+ }
+ dateOnlyFormats = []*regexp.Regexp{
+ regexp.MustCompile(`^` + df1 + `$`),
+ regexp.MustCompile(`^` + df2 + `$`),
+ regexp.MustCompile(`^` + df3 + `$`),
+ regexp.MustCompile(`^` + df4 + `$`),
+ }
+ addressFmtMaps = map[string]func(col, row int) (string, error){
+ "1_TRUE": func(col, row int) (string, error) {
+ return CoordinatesToCellName(col, row, true)
+ },
+ "1_FALSE": func(col, row int) (string, error) {
+ return fmt.Sprintf("R%dC%d", row, col), nil
+ },
+ "2_TRUE": func(col, row int) (string, error) {
+ column, err := ColumnNumberToName(col)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("%s$%d", column, row), nil
+ },
+ "2_FALSE": func(col, row int) (string, error) {
+ return fmt.Sprintf("R%dC[%d]", row, col), nil
+ },
+ "3_TRUE": func(col, row int) (string, error) {
+ column, err := ColumnNumberToName(col)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("$%s%d", column, row), nil
+ },
+ "3_FALSE": func(col, row int) (string, error) {
+ return fmt.Sprintf("R[%d]C%d", row, col), nil
+ },
+ "4_TRUE": func(col, row int) (string, error) {
+ return CoordinatesToCellName(col, row, false)
+ },
+ "4_FALSE": func(col, row int) (string, error) {
+ return fmt.Sprintf("R[%d]C[%d]", row, col), nil
+ },
+ }
+ formulaFormats = []*regexp.Regexp{
+ regexp.MustCompile(`^(\d+)$`),
+ regexp.MustCompile(`^=(.*)$`),
+ regexp.MustCompile(`^<>(.*)$`),
+ regexp.MustCompile(`^<=(.*)$`),
+ regexp.MustCompile(`^>=(.*)$`),
+ regexp.MustCompile(`^<(.*)$`),
+ regexp.MustCompile(`^>(.*)$`),
+ }
+ formulaCriterias = []byte{
+ criteriaEq,
+ criteriaEq,
+ criteriaNe,
+ criteriaLe,
+ criteriaGe,
+ criteriaL,
+ criteriaG,
+ }
)
+// calcContext defines the formula execution context.
+type calcContext struct {
+ mu sync.Mutex
+ entry string
+ maxCalcIterations uint
+ iterations map[string]uint
+ iterationsCache map[string]formulaArg
+}
+
// cellRef defines the structure of a cell reference.
type cellRef struct {
Col int
@@ -55,131 +235,733 @@ type cellRange struct {
To cellRef
}
-// formula criteria condition enumeration.
-const (
- _ byte = iota
- criteriaEq
- criteriaLe
- criteriaGe
- criteriaL
- criteriaG
- criteriaBeg
- criteriaEnd
-)
-
// formulaCriteria defined formula criteria parser result.
type formulaCriteria struct {
Type byte
- Condition string
+ Condition formulaArg
}
-// ArgType is the type if formula argument type.
+// ArgType is the type of formula argument type.
type ArgType byte
// Formula argument types enumeration.
const (
ArgUnknown ArgType = iota
+ ArgNumber
ArgString
+ ArgList
ArgMatrix
+ ArgError
+ ArgEmpty
)
// formulaArg is the argument of a formula or function.
type formulaArg struct {
- String string
- Matrix [][]formulaArg
- Type ArgType
+ SheetName string
+ Number float64
+ String string
+ List []formulaArg
+ Matrix [][]formulaArg
+ Boolean bool
+ Error string
+ Type ArgType
+ cellRefs, cellRanges *list.List
+}
+
+// Value returns a string data type of the formula argument.
+func (fa formulaArg) Value() (value string) {
+ switch fa.Type {
+ case ArgNumber:
+ if fa.Boolean {
+ if fa.Number == 0 {
+ return "FALSE"
+ }
+ return "TRUE"
+ }
+ return fmt.Sprintf("%g", fa.Number)
+ case ArgString:
+ return fa.String
+ case ArgMatrix:
+ if args := fa.ToList(); len(args) > 0 {
+ return args[0].Value()
+ }
+ case ArgError:
+ return fa.Error
+ }
+ return
+}
+
+// ToNumber returns a formula argument with number data type.
+func (fa formulaArg) ToNumber() formulaArg {
+ var n float64
+ var err error
+ switch fa.Type {
+ case ArgString:
+ n, err = strconv.ParseFloat(fa.String, 64)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ case ArgNumber:
+ n = fa.Number
+ case ArgMatrix:
+ if args := fa.ToList(); len(args) > 0 {
+ return args[0].ToNumber()
+ }
+ }
+ return newNumberFormulaArg(n)
+}
+
+// ToBool returns a formula argument with boolean data type.
+func (fa formulaArg) ToBool() formulaArg {
+ var b bool
+ var err error
+ switch fa.Type {
+ case ArgString:
+ b, err = strconv.ParseBool(fa.String)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ case ArgNumber:
+ if fa.Number == 1 {
+ b = true
+ }
+ }
+ return newBoolFormulaArg(b)
+}
+
+// ToList returns a formula argument with array data type.
+func (fa formulaArg) ToList() []formulaArg {
+ switch fa.Type {
+ case ArgMatrix:
+ var args []formulaArg
+ for _, row := range fa.Matrix {
+ args = append(args, row...)
+ }
+ return args
+ case ArgList:
+ return fa.List
+ case ArgNumber, ArgString, ArgError, ArgUnknown:
+ return []formulaArg{fa}
+ }
+ return nil
}
// formulaFuncs is the type of the formula functions.
-type formulaFuncs struct{}
+type formulaFuncs struct {
+ f *File
+ ctx *calcContext
+ sheet, cell string
+}
-// CalcCellValue provides a function to get calculated cell value. This
-// feature is currently in working processing. Array formula, table formula
-// and some other formulas are not supported currently.
+// CalcCellValue provides a function to get calculated cell value. This feature
+// is currently in working processing. Iterative calculation, implicit
+// intersection, explicit intersection, array formula, table formula and some
+// other formulas are not supported currently.
//
-// Supported formulas:
+// Supported formula functions:
//
-// ABS, ACOS, ACOSH, ACOT, ACOTH, ARABIC, ASIN, ASINH, ATAN2, ATANH, BASE,
-// CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, COSH, COT,
-// COTH, COUNTA, CSC, CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, FACTDOUBLE,
-// FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, ISERR, ISERROR,
-// ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, LCM, LN, LOG,
-// LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, NA, ODD, PI,
-// POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, ROUND, ROUNDDOWN,
-// ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, SUM, SUMIF, SUMSQ,
-// TAN, TANH, TRUNC
-//
-func (f *File) CalcCellValue(sheet, cell string) (result string, err error) {
+// ABS
+// ACCRINT
+// ACCRINTM
+// ACOS
+// ACOSH
+// ACOT
+// ACOTH
+// ADDRESS
+// AGGREGATE
+// AMORDEGRC
+// AMORLINC
+// AND
+// ARABIC
+// ARRAYTOTEXT
+// ASIN
+// ASINH
+// ATAN
+// ATAN2
+// ATANH
+// AVEDEV
+// AVERAGE
+// AVERAGEA
+// AVERAGEIF
+// AVERAGEIFS
+// BASE
+// BESSELI
+// BESSELJ
+// BESSELK
+// BESSELY
+// BETA.DIST
+// BETA.INV
+// BETADIST
+// BETAINV
+// BIN2DEC
+// BIN2HEX
+// BIN2OCT
+// BINOM.DIST
+// BINOM.DIST.RANGE
+// BINOM.INV
+// BINOMDIST
+// BITAND
+// BITLSHIFT
+// BITOR
+// BITRSHIFT
+// BITXOR
+// CEILING
+// CEILING.MATH
+// CEILING.PRECISE
+// CHAR
+// CHIDIST
+// CHIINV
+// CHISQ.DIST
+// CHISQ.DIST.RT
+// CHISQ.INV
+// CHISQ.INV.RT
+// CHISQ.TEST
+// CHITEST
+// CHOOSE
+// CLEAN
+// CODE
+// COLUMN
+// COLUMNS
+// COMBIN
+// COMBINA
+// COMPLEX
+// CONCAT
+// CONCATENATE
+// CONFIDENCE
+// CONFIDENCE.NORM
+// CONFIDENCE.T
+// CONVERT
+// CORREL
+// COS
+// COSH
+// COT
+// COTH
+// COUNT
+// COUNTA
+// COUNTBLANK
+// COUNTIF
+// COUNTIFS
+// COUPDAYBS
+// COUPDAYS
+// COUPDAYSNC
+// COUPNCD
+// COUPNUM
+// COUPPCD
+// COVAR
+// COVARIANCE.P
+// COVARIANCE.S
+// CRITBINOM
+// CSC
+// CSCH
+// CUMIPMT
+// CUMPRINC
+// DATE
+// DATEDIF
+// DATEVALUE
+// DAVERAGE
+// DAY
+// DAYS
+// DAYS360
+// DB
+// DBCS
+// DCOUNT
+// DCOUNTA
+// DDB
+// DEC2BIN
+// DEC2HEX
+// DEC2OCT
+// DECIMAL
+// DEGREES
+// DELTA
+// DEVSQ
+// DGET
+// DISC
+// DMAX
+// DMIN
+// DOLLAR
+// DOLLARDE
+// DOLLARFR
+// DPRODUCT
+// DSTDEV
+// DSTDEVP
+// DSUM
+// DURATION
+// DVAR
+// DVARP
+// EDATE
+// EFFECT
+// ENCODEURL
+// EOMONTH
+// ERF
+// ERF.PRECISE
+// ERFC
+// ERFC.PRECISE
+// ERROR.TYPE
+// EUROCONVERT
+// EVEN
+// EXACT
+// EXP
+// EXPON.DIST
+// EXPONDIST
+// F.DIST
+// F.DIST.RT
+// F.INV
+// F.INV.RT
+// F.TEST
+// FACT
+// FACTDOUBLE
+// FALSE
+// FDIST
+// FIND
+// FINDB
+// FINV
+// FISHER
+// FISHERINV
+// FIXED
+// FLOOR
+// FLOOR.MATH
+// FLOOR.PRECISE
+// FORECAST
+// FORECAST.LINEAR
+// FORMULATEXT
+// FREQUENCY
+// FTEST
+// FV
+// FVSCHEDULE
+// GAMMA
+// GAMMA.DIST
+// GAMMA.INV
+// GAMMADIST
+// GAMMAINV
+// GAMMALN
+// GAMMALN.PRECISE
+// GAUSS
+// GCD
+// GEOMEAN
+// GESTEP
+// GROWTH
+// HARMEAN
+// HEX2BIN
+// HEX2DEC
+// HEX2OCT
+// HLOOKUP
+// HOUR
+// HYPERLINK
+// HYPGEOM.DIST
+// HYPGEOMDIST
+// IF
+// IFERROR
+// IFNA
+// IFS
+// IMABS
+// IMAGINARY
+// IMARGUMENT
+// IMCONJUGATE
+// IMCOS
+// IMCOSH
+// IMCOT
+// IMCSC
+// IMCSCH
+// IMDIV
+// IMEXP
+// IMLN
+// IMLOG10
+// IMLOG2
+// IMPOWER
+// IMPRODUCT
+// IMREAL
+// IMSEC
+// IMSECH
+// IMSIN
+// IMSINH
+// IMSQRT
+// IMSUB
+// IMSUM
+// IMTAN
+// INDEX
+// INDIRECT
+// INT
+// INTERCEPT
+// INTRATE
+// IPMT
+// IRR
+// ISBLANK
+// ISERR
+// ISERROR
+// ISEVEN
+// ISFORMULA
+// ISLOGICAL
+// ISNA
+// ISNONTEXT
+// ISNUMBER
+// ISO.CEILING
+// ISODD
+// ISOWEEKNUM
+// ISPMT
+// ISREF
+// ISTEXT
+// KURT
+// LARGE
+// LCM
+// LEFT
+// LEFTB
+// LEN
+// LENB
+// LN
+// LOG
+// LOG10
+// LOGINV
+// LOGNORM.DIST
+// LOGNORM.INV
+// LOGNORMDIST
+// LOOKUP
+// LOWER
+// MATCH
+// MAX
+// MAXA
+// MAXIFS
+// MDETERM
+// MDURATION
+// MEDIAN
+// MID
+// MIDB
+// MIN
+// MINA
+// MINIFS
+// MINUTE
+// MINVERSE
+// MIRR
+// MMULT
+// MOD
+// MODE
+// MODE.MULT
+// MODE.SNGL
+// MONTH
+// MROUND
+// MULTINOMIAL
+// MUNIT
+// N
+// NA
+// NEGBINOM.DIST
+// NEGBINOMDIST
+// NETWORKDAYS
+// NETWORKDAYS.INTL
+// NOMINAL
+// NORM.DIST
+// NORM.INV
+// NORM.S.DIST
+// NORM.S.INV
+// NORMDIST
+// NORMINV
+// NORMSDIST
+// NORMSINV
+// NOT
+// NOW
+// NPER
+// NPV
+// OCT2BIN
+// OCT2DEC
+// OCT2HEX
+// ODD
+// ODDFPRICE
+// ODDFYIELD
+// ODDLPRICE
+// ODDLYIELD
+// OR
+// PDURATION
+// PEARSON
+// PERCENTILE
+// PERCENTILE.EXC
+// PERCENTILE.INC
+// PERCENTRANK
+// PERCENTRANK.EXC
+// PERCENTRANK.INC
+// PERMUT
+// PERMUTATIONA
+// PHI
+// PI
+// PMT
+// POISSON
+// POISSON.DIST
+// POWER
+// PPMT
+// PRICE
+// PRICEDISC
+// PRICEMAT
+// PROB
+// PRODUCT
+// PROPER
+// PV
+// QUARTILE
+// QUARTILE.EXC
+// QUARTILE.INC
+// QUOTIENT
+// RADIANS
+// RAND
+// RANDBETWEEN
+// RANK
+// RANK.EQ
+// RATE
+// RECEIVED
+// REPLACE
+// REPLACEB
+// REPT
+// RIGHT
+// RIGHTB
+// ROMAN
+// ROUND
+// ROUNDDOWN
+// ROUNDUP
+// ROW
+// ROWS
+// RRI
+// RSQ
+// SEARCH
+// SEARCHB
+// SEC
+// SECH
+// SECOND
+// SERIESSUM
+// SHEET
+// SHEETS
+// SIGN
+// SIN
+// SINH
+// SKEW
+// SKEW.P
+// SLN
+// SLOPE
+// SMALL
+// SQRT
+// SQRTPI
+// STANDARDIZE
+// STDEV
+// STDEV.P
+// STDEV.S
+// STDEVA
+// STDEVP
+// STDEVPA
+// STEYX
+// SUBSTITUTE
+// SUBTOTAL
+// SUM
+// SUMIF
+// SUMIFS
+// SUMPRODUCT
+// SUMSQ
+// SUMX2MY2
+// SUMX2PY2
+// SUMXMY2
+// SWITCH
+// SYD
+// T
+// T.DIST
+// T.DIST.2T
+// T.DIST.RT
+// T.INV
+// T.INV.2T
+// T.TEST
+// TAN
+// TANH
+// TBILLEQ
+// TBILLPRICE
+// TBILLYIELD
+// TDIST
+// TEXT
+// TEXTAFTER
+// TEXTBEFORE
+// TEXTJOIN
+// TIME
+// TIMEVALUE
+// TINV
+// TODAY
+// TRANSPOSE
+// TREND
+// TRIM
+// TRIMMEAN
+// TRUE
+// TRUNC
+// TTEST
+// TYPE
+// UNICHAR
+// UNICODE
+// UPPER
+// VALUE
+// VALUETOTEXT
+// VAR
+// VAR.P
+// VAR.S
+// VARA
+// VARP
+// VARPA
+// VDB
+// VLOOKUP
+// WEEKDAY
+// WEEKNUM
+// WEIBULL
+// WEIBULL.DIST
+// WORKDAY
+// WORKDAY.INTL
+// XIRR
+// XLOOKUP
+// XNPV
+// XOR
+// YEAR
+// YEARFRAC
+// YIELD
+// YIELDDISC
+// YIELDMAT
+// Z.TEST
+// ZTEST
+func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) {
+ options := f.getOptions(opts...)
var (
- formula string
- token efp.Token
+ rawCellValue = options.RawCellValue
+ styleIdx int
+ token formulaArg
)
- if formula, err = f.GetCellFormula(sheet, cell); err != nil {
+ if token, err = f.calcCellValue(&calcContext{
+ entry: fmt.Sprintf("%s!%s", sheet, cell),
+ maxCalcIterations: options.MaxCalcIterations,
+ iterations: make(map[string]uint),
+ iterationsCache: make(map[string]formulaArg),
+ }, sheet, cell); err != nil {
+ result = token.String
return
}
- ps := efp.ExcelParser()
- tokens := ps.Parse(formula)
- if tokens == nil {
+ if !rawCellValue {
+ styleIdx, _ = f.GetCellStyle(sheet, cell)
+ }
+ if token.Type == ArgNumber && !token.Boolean {
+ _, precision, decimal := isNumeric(token.Value())
+ if precision > 15 {
+ result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber)
+ return
+ }
+ if !strings.HasPrefix(result, "0") {
+ result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber)
+ }
return
}
- if token, err = f.evalInfixExp(sheet, tokens); err != nil {
+ result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString)
+ return
+}
+
+// calcCellValue calculate cell value by given context, worksheet name and cell
+// reference.
+func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) {
+ var formula string
+ if formula, err = f.getCellFormula(sheet, cell, true); err != nil {
return
}
- result = token.TValue
+ ps := efp.ExcelParser()
+ tokens := ps.Parse(formula)
+ if tokens == nil {
+ return f.cellResolver(ctx, sheet, cell)
+ }
+ result, err = f.evalInfixExp(ctx, sheet, cell, tokens)
return
}
// getPriority calculate arithmetic operator priority.
func getPriority(token efp.Token) (pri int) {
- var priority = map[string]int{
- "*": 2,
- "/": 2,
- "+": 1,
- "-": 1,
- }
- pri, _ = priority[token.TValue]
+ pri = tokenPriority[token.TValue]
if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix {
- pri = 3
+ pri = 6
}
- if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // (
+ if isBeginParenthesesToken(token) { // (
pri = 0
}
return
}
+// newNumberFormulaArg constructs a number formula argument.
+func newNumberFormulaArg(n float64) formulaArg {
+ if math.IsNaN(n) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return formulaArg{Type: ArgNumber, Number: n}
+}
+
+// newStringFormulaArg constructs a string formula argument.
+func newStringFormulaArg(s string) formulaArg {
+ return formulaArg{Type: ArgString, String: s}
+}
+
+// newMatrixFormulaArg constructs a matrix formula argument.
+func newMatrixFormulaArg(m [][]formulaArg) formulaArg {
+ return formulaArg{Type: ArgMatrix, Matrix: m}
+}
+
+// newListFormulaArg create a list formula argument.
+func newListFormulaArg(l []formulaArg) formulaArg {
+ return formulaArg{Type: ArgList, List: l}
+}
+
+// newBoolFormulaArg constructs a boolean formula argument.
+func newBoolFormulaArg(b bool) formulaArg {
+ var n float64
+ if b {
+ n = 1
+ }
+ return formulaArg{Type: ArgNumber, Number: n, Boolean: true}
+}
+
+// newErrorFormulaArg create an error formula argument of a given type with a
+// specified error message.
+func newErrorFormulaArg(formulaError, msg string) formulaArg {
+ return formulaArg{Type: ArgError, String: formulaError, Error: msg}
+}
+
+// newEmptyFormulaArg create an empty formula argument.
+func newEmptyFormulaArg() formulaArg {
+ return formulaArg{Type: ArgEmpty}
+}
+
// evalInfixExp evaluate syntax analysis by given infix expression after
// lexical analysis. Evaluate an infix expression containing formulas by
// stacks:
//
-// opd - Operand
-// opt - Operator
-// opf - Operation formula
-// opfd - Operand of the operation formula
-// opft - Operator of the operation formula
-//
-// Evaluate arguments of the operation formula by list:
-//
-// args - Arguments of the operation formula
+// opd - Operand
+// opt - Operator
+// opf - Operation formula
+// opfd - Operand of the operation formula
+// opft - Operator of the operation formula
+// args - Arguments list of the operation formula
//
// TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union
-//
-func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) {
- var err error
- opdStack, optStack, opfStack, opfdStack, opftStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack()
- argsList := list.New()
+func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.Token) (formulaArg, error) {
+ var (
+ err error
+ inArray, inArrayRow bool
+ formulaArray [][]formulaArg
+ formulaArrayRow []formulaArg
+ opdStack, optStack, opfStack = NewStack(), NewStack(), NewStack()
+ opfdStack, opftStack, argsStack = NewStack(), NewStack(), NewStack()
+ )
for i := 0; i < len(tokens); i++ {
token := tokens[i]
// out of function stack
if opfStack.Len() == 0 {
- if err = f.parseToken(sheet, token, opdStack, optStack); err != nil {
- return efp.Token{}, err
+ if err = f.parseToken(ctx, sheet, token, opdStack, optStack); err != nil {
+ return newEmptyFormulaArg(), err
}
}
// function start
- if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart {
+ if isFunctionStartToken(token) {
+ if token.TValue == "ARRAY" {
+ inArray, formulaArray = true, [][]formulaArg{}
+ continue
+ }
+ if token.TValue == "ARRAYROW" {
+ inArrayRow, formulaArrayRow = true, []formulaArg{}
+ continue
+ }
opfStack.Push(token)
+ argsStack.Push(list.New().Init())
+ opftStack.Push(token) // to know which operators belong to a function use the function as a separator
continue
}
@@ -192,200 +974,316 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error)
// current token is args or range, skip next token, order required: parse reference first
if token.TSubType == efp.TokenSubTypeRange {
- if !opftStack.Empty() {
+ if opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) {
+ refTo := f.getDefinedNameRefTo(token.TValue, sheet)
+ if refTo != "" {
+ token.TValue = refTo
+ }
// parse reference: must reference at here
- result, err := f.parseReference(sheet, token.TValue)
+ result, err := f.parseReference(ctx, sheet, token.TValue)
if err != nil {
- return efp.Token{TValue: formulaErrorNAME}, err
- }
- if result.Type != ArgString {
- return efp.Token{}, errors.New(formulaErrorVALUE)
+ return result, err
}
- opfdStack.Push(efp.Token{
- TType: efp.TokenTypeOperand,
- TSubType: efp.TokenSubTypeNumber,
- TValue: result.String,
- })
+ opfdStack.Push(result)
continue
}
if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction {
// parse reference: reference or range at here
- result, err := f.parseReference(sheet, token.TValue)
+ refTo := f.getDefinedNameRefTo(token.TValue, sheet)
+ if refTo != "" {
+ token.TValue = refTo
+ }
+ result, err := f.parseReference(ctx, sheet, token.TValue)
if err != nil {
- return efp.Token{TValue: formulaErrorNAME}, err
+ return result, err
}
- if result.Type == ArgUnknown {
- return efp.Token{}, errors.New(formulaErrorVALUE)
+ // when current token is range, next token is argument and opfdStack not empty,
+ // should push value to opfdStack and continue
+ if nextToken.TType == efp.TokenTypeArgument && !opfdStack.Empty() {
+ opfdStack.Push(result)
+ continue
}
- argsList.PushBack(result)
+ argsStack.Peek().(*list.List).PushBack(result)
continue
}
}
// check current token is opft
- if err = f.parseToken(sheet, token, opfdStack, opftStack); err != nil {
- return efp.Token{}, err
+ if err = f.parseToken(ctx, sheet, token, opfdStack, opftStack); err != nil {
+ return newEmptyFormulaArg(), err
}
// current token is arg
if token.TType == efp.TokenTypeArgument {
- for !opftStack.Empty() {
+ for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) {
// calculate trigger
topOpt := opftStack.Peek().(efp.Token)
if err := calculate(opfdStack, topOpt); err != nil {
- return efp.Token{}, err
+ argsStack.Peek().(*list.List).PushFront(newErrorFormulaArg(formulaErrorVALUE, err.Error()))
}
opftStack.Pop()
}
if !opfdStack.Empty() {
- argsList.PushBack(formulaArg{
- String: opfdStack.Pop().(efp.Token).TValue,
- Type: ArgString,
- })
+ argsStack.Peek().(*list.List).PushBack(opfdStack.Pop().(formulaArg))
}
continue
}
- // current token is logical
- if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical {
+ if inArrayRow && isOperand(token) {
+ formulaArrayRow = append(formulaArrayRow, opfdStack.Pop().(formulaArg))
+ continue
}
-
- // current token is text
- if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
- argsList.PushBack(formulaArg{
- String: token.TValue,
- Type: ArgString,
- })
+ if inArrayRow && isFunctionStopToken(token) {
+ formulaArray = append(formulaArray, formulaArrayRow)
+ inArrayRow = false
+ continue
}
-
- // current token is function stop
- if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop {
- for !opftStack.Empty() {
- // calculate trigger
- topOpt := opftStack.Peek().(efp.Token)
- if err := calculate(opfdStack, topOpt); err != nil {
- return efp.Token{}, err
- }
- opftStack.Pop()
- }
-
- // push opfd to args
- if opfdStack.Len() > 0 {
- argsList.PushBack(formulaArg{
- String: opfdStack.Pop().(efp.Token).TValue,
- Type: ArgString,
- })
- }
- // call formula function to evaluate
- result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer(
- "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue),
- []reflect.Value{reflect.ValueOf(argsList)})
- if err != nil {
- return efp.Token{}, err
- }
- argsList.Init()
- opfStack.Pop()
- if opfStack.Len() > 0 { // still in function stack
- opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
- } else {
- opdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
- }
+ if inArray && isFunctionStopToken(token) {
+ argsStack.Peek().(*list.List).PushBack(newMatrixFormulaArg(formulaArray))
+ inArray = false
+ continue
+ }
+ if errArg := f.evalInfixExpFunc(ctx, sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); errArg.Type == ArgError {
+ return errArg, errors.New(errArg.Error)
}
}
}
for optStack.Len() != 0 {
topOpt := optStack.Peek().(efp.Token)
if err = calculate(opdStack, topOpt); err != nil {
- return efp.Token{}, err
+ return newEmptyFormulaArg(), err
}
optStack.Pop()
}
if opdStack.Len() == 0 {
- return efp.Token{}, errors.New("formula not valid")
+ return newEmptyFormulaArg(), ErrInvalidFormula
}
- return opdStack.Peek().(efp.Token), err
+ return opdStack.Peek().(formulaArg), err
}
-// calcAdd evaluate addition arithmetic operations.
-func calcAdd(opdStack *Stack) error {
- if opdStack.Len() < 2 {
- return errors.New("formula not valid")
+// evalInfixExpFunc evaluate formula function in the infix expression.
+func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) formulaArg {
+ if !isFunctionStopToken(token) {
+ return newEmptyFormulaArg()
+ }
+ prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack)
+ // call formula function to evaluate
+ arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, strings.NewReplacer(
+ "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue),
+ []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))})
+ if arg.Type == ArgError && opfStack.Len() == 1 {
+ return arg
+ }
+ argsStack.Pop()
+ opftStack.Pop() // remove current function separator
+ opfStack.Pop()
+ if opfStack.Len() > 0 { // still in function stack
+ if nextToken.TType == efp.TokenTypeOperatorInfix || opftStack.Len() > 1 {
+ // mathematics calculate in formula function
+ opfdStack.Push(arg)
+ return newEmptyFormulaArg()
+ }
+ argsStack.Peek().(*list.List).PushBack(arg)
+ return newEmptyFormulaArg()
}
- rOpd := opdStack.Pop().(efp.Token)
- lOpd := opdStack.Pop().(efp.Token)
- lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
- if err != nil {
- return err
+ if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 {
+ opdStack.Push(arg.Matrix[0][0])
+ return newEmptyFormulaArg()
}
- rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
- if err != nil {
- return err
+ opdStack.Push(arg)
+ return newEmptyFormulaArg()
+}
+
+// prepareEvalInfixExp check the token and stack state for formula function
+// evaluate.
+func prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack *Stack) {
+ // current token is function stop
+ for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) {
+ // calculate trigger
+ topOpt := opftStack.Peek().(efp.Token)
+ if err := calculate(opfdStack, topOpt); err != nil {
+ argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error()))
+ opftStack.Pop()
+ continue
+ }
+ opftStack.Pop()
+ }
+ argument := true
+ if opftStack.Len() > 2 && opfdStack.Len() == 1 {
+ topOpt := opftStack.Pop()
+ if opftStack.Peek().(efp.Token).TType == efp.TokenTypeOperatorInfix {
+ argument = false
+ }
+ opftStack.Push(topOpt)
+ }
+ // push opfd to args
+ if argument && opfdStack.Len() > 0 {
+ argsStack.Peek().(*list.List).PushBack(opfdStack.Pop().(formulaArg))
+ }
+}
+
+// calcPow evaluate exponentiation arithmetic operations.
+func calcPow(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ lOpdVal := lOpd.ToNumber()
+ if lOpdVal.Type != ArgNumber {
+ return errors.New(lOpdVal.Value())
+ }
+ rOpdVal := rOpd.ToNumber()
+ if rOpdVal.Type != ArgNumber {
+ return errors.New(rOpdVal.Value())
}
- result := lOpdVal + rOpdVal
- opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
+ opdStack.Push(newNumberFormulaArg(math.Pow(lOpdVal.Number, rOpdVal.Number)))
return nil
}
-// calcSubtract evaluate subtraction arithmetic operations.
-func calcSubtract(opdStack *Stack) error {
- if opdStack.Len() < 2 {
- return errors.New("formula not valid")
+// calcEq evaluate equal arithmetic operations.
+func calcEq(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ opdStack.Push(newBoolFormulaArg(rOpd.Value() == lOpd.Value()))
+ return nil
+}
+
+// calcNEq evaluate not equal arithmetic operations.
+func calcNEq(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ opdStack.Push(newBoolFormulaArg(rOpd.Value() != lOpd.Value()))
+ return nil
+}
+
+// calcL evaluate less than arithmetic operations.
+func calcL(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(lOpd.Number < rOpd.Number))
}
- rOpd := opdStack.Pop().(efp.Token)
- lOpd := opdStack.Pop().(efp.Token)
- lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgString && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) == -1))
}
- rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(false))
+ }
+ if rOpd.Type == ArgString && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(true))
}
- result := lOpdVal - rOpdVal
- opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}
-// calcMultiply evaluate multiplication arithmetic operations.
-func calcMultiply(opdStack *Stack) error {
- if opdStack.Len() < 2 {
- return errors.New("formula not valid")
+// calcLe evaluate less than or equal arithmetic operations.
+func calcLe(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(lOpd.Number <= rOpd.Number))
}
- rOpd := opdStack.Pop().(efp.Token)
- lOpd := opdStack.Pop().(efp.Token)
- lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgString && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) != 1))
}
- rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(false))
+ }
+ if rOpd.Type == ArgString && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(true))
}
- result := lOpdVal * rOpdVal
- opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
return nil
}
-// calcDivide evaluate division arithmetic operations.
-func calcDivide(opdStack *Stack) error {
- if opdStack.Len() < 2 {
- return errors.New("formula not valid")
+// calcG evaluate greater than arithmetic operations.
+func calcG(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(lOpd.Number > rOpd.Number))
}
- rOpd := opdStack.Pop().(efp.Token)
- lOpd := opdStack.Pop().(efp.Token)
- lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgString && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) == 1))
}
- rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64)
- if err != nil {
- return err
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(true))
+ }
+ if rOpd.Type == ArgString && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(false))
+ }
+ return nil
+}
+
+// calcGe evaluate greater than or equal arithmetic operations.
+func calcGe(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(lOpd.Number >= rOpd.Number))
+ }
+ if rOpd.Type == ArgString && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(strings.Compare(lOpd.Value(), rOpd.Value()) != -1))
+ }
+ if rOpd.Type == ArgNumber && lOpd.Type == ArgString {
+ opdStack.Push(newBoolFormulaArg(true))
+ }
+ if rOpd.Type == ArgString && lOpd.Type == ArgNumber {
+ opdStack.Push(newBoolFormulaArg(false))
+ }
+ return nil
+}
+
+// calcSplice evaluate splice '&' operations.
+func calcSplice(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ opdStack.Push(newStringFormulaArg(lOpd.Value() + rOpd.Value()))
+ return nil
+}
+
+// calcAdd evaluate addition arithmetic operations.
+func calcAdd(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ lOpdVal := lOpd.ToNumber()
+ if lOpdVal.Type != ArgNumber {
+ return errors.New(lOpdVal.Value())
+ }
+ rOpdVal := rOpd.ToNumber()
+ if rOpdVal.Type != ArgNumber {
+ return errors.New(rOpdVal.Value())
+ }
+ opdStack.Push(newNumberFormulaArg(lOpdVal.Number + rOpdVal.Number))
+ return nil
+}
+
+// calcSubtract evaluate subtraction arithmetic operations.
+func calcSubtract(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ if rOpd.Value() == "" {
+ rOpd = newNumberFormulaArg(0)
+ }
+ if lOpd.Value() == "" {
+ lOpd = newNumberFormulaArg(0)
+ }
+ lOpdVal := lOpd.ToNumber()
+ if lOpdVal.Type != ArgNumber {
+ return errors.New(lOpdVal.Value())
+ }
+ rOpdVal := rOpd.ToNumber()
+ if rOpdVal.Type != ArgNumber {
+ return errors.New(rOpdVal.Value())
+ }
+ opdStack.Push(newNumberFormulaArg(lOpdVal.Number - rOpdVal.Number))
+ return nil
+}
+
+// calcMultiply evaluate multiplication arithmetic operations.
+func calcMultiply(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ lOpdVal := lOpd.ToNumber()
+ if lOpdVal.Type != ArgNumber {
+ return errors.New(lOpdVal.Value())
+ }
+ rOpdVal := rOpd.ToNumber()
+ if rOpdVal.Type != ArgNumber {
+ return errors.New(rOpdVal.Value())
+ }
+ opdStack.Push(newNumberFormulaArg(lOpdVal.Number * rOpdVal.Number))
+ return nil
+}
+
+// calcDiv evaluate division arithmetic operations.
+func calcDiv(rOpd, lOpd formulaArg, opdStack *Stack) error {
+ lOpdVal := lOpd.ToNumber()
+ if lOpdVal.Type != ArgNumber {
+ return errors.New(lOpdVal.Value())
+ }
+ rOpdVal := rOpd.ToNumber()
+ if rOpdVal.Type != ArgNumber {
+ return errors.New(rOpdVal.Value())
}
- result := lOpdVal / rOpdVal
- if rOpdVal == 0 {
+ if rOpdVal.Number == 0 {
return errors.New(formulaErrorDIV)
}
- opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
+ opdStack.Push(newNumberFormulaArg(lOpdVal.Number / rOpdVal.Number))
return nil
}
@@ -393,36 +1291,55 @@ func calcDivide(opdStack *Stack) error {
func calculate(opdStack *Stack, opt efp.Token) error {
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix {
if opdStack.Len() < 1 {
- return errors.New("formula not valid")
- }
- opd := opdStack.Pop().(efp.Token)
- opdVal, err := strconv.ParseFloat(opd.TValue, 64)
- if err != nil {
- return err
- }
- result := 0 - opdVal
- opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
- }
-
- if opt.TValue == "+" {
- if err := calcAdd(opdStack); err != nil {
- return err
+ return ErrInvalidFormula
}
+ opd := opdStack.Pop().(formulaArg)
+ opdStack.Push(newNumberFormulaArg(0 - opd.ToNumber().Number))
}
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix {
- if err := calcSubtract(opdStack); err != nil {
- return err
+ if opdStack.Len() < 2 {
+ return ErrInvalidFormula
}
- }
- if opt.TValue == "*" {
- if err := calcMultiply(opdStack); err != nil {
+ rOpd := opdStack.Pop().(formulaArg)
+ lOpd := opdStack.Pop().(formulaArg)
+ if err := calcSubtract(rOpd, lOpd, opdStack); err != nil {
return err
}
}
- if opt.TValue == "/" {
- if err := calcDivide(opdStack); err != nil {
- return err
+ tokenCalcFunc := map[string]func(rOpd, lOpd formulaArg, opdStack *Stack) error{
+ "^": calcPow,
+ "*": calcMultiply,
+ "/": calcDiv,
+ "+": calcAdd,
+ "=": calcEq,
+ "<>": calcNEq,
+ "<": calcL,
+ "<=": calcLe,
+ ">": calcG,
+ ">=": calcGe,
+ "&": calcSplice,
+ }
+ if fn, ok := tokenCalcFunc[opt.TValue]; ok {
+ if opdStack.Len() < 2 {
+ return ErrInvalidFormula
+ }
+ rOpd := opdStack.Pop().(formulaArg)
+ lOpd := opdStack.Pop().(formulaArg)
+ if opt.TValue != "&" {
+ if rOpd.Value() == "" {
+ rOpd = newNumberFormulaArg(0)
+ }
+ if lOpd.Value() == "" {
+ lOpd = newNumberFormulaArg(0)
+ }
+ }
+ if rOpd.Type == ArgError {
+ return errors.New(rOpd.Value())
}
+ if lOpd.Type == ArgError {
+ return errors.New(lOpd.Value())
+ }
+ return fn(rOpd, lOpd, opdStack)
}
return nil
}
@@ -431,84 +1348,118 @@ func calculate(opdStack *Stack, opt efp.Token) error {
func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Token) (err error) {
if optStack.Len() == 0 {
optStack.Push(token)
- } else {
- tokenPriority := getPriority(token)
- topOpt := optStack.Peek().(efp.Token)
- topOptPriority := getPriority(topOpt)
- if tokenPriority > topOptPriority {
- optStack.Push(token)
- } else {
- for tokenPriority <= topOptPriority {
- optStack.Pop()
- if err = calculate(opdStack, topOpt); err != nil {
- return
- }
- if optStack.Len() > 0 {
- topOpt = optStack.Peek().(efp.Token)
- topOptPriority = getPriority(topOpt)
- continue
- }
- break
- }
- optStack.Push(token)
+ return
+ }
+ tokenPriority := getPriority(token)
+ topOpt := optStack.Peek().(efp.Token)
+ topOptPriority := getPriority(topOpt)
+ if topOpt.TValue == "-" && topOpt.TType == efp.TokenTypeOperatorPrefix && token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix {
+ optStack.Pop()
+ return
+ }
+ if tokenPriority > topOptPriority {
+ optStack.Push(token)
+ return
+ }
+ for tokenPriority <= topOptPriority {
+ optStack.Pop()
+ if err = calculate(opdStack, topOpt); err != nil {
+ return
}
+ if optStack.Len() > 0 {
+ topOpt = optStack.Peek().(efp.Token)
+ topOptPriority = getPriority(topOpt)
+ continue
+ }
+ break
}
+ optStack.Push(token)
return
}
+// isFunctionStartToken determine if the token is function start.
+func isFunctionStartToken(token efp.Token) bool {
+ return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
+}
+
+// isFunctionStopToken determine if the token is function stop.
+func isFunctionStopToken(token efp.Token) bool {
+ return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop
+}
+
+// isBeginParenthesesToken determine if the token is begin parentheses: (.
+func isBeginParenthesesToken(token efp.Token) bool {
+ return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart
+}
+
+// isEndParenthesesToken determine if the token is end parentheses: ).
+func isEndParenthesesToken(token efp.Token) bool {
+ return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop
+}
+
// isOperatorPrefixToken determine if the token is parse operator prefix
// token.
func isOperatorPrefixToken(token efp.Token) bool {
- if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) ||
- token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" {
- return true
+ _, ok := tokenPriority[token.TValue]
+ return (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || (ok && token.TType == efp.TokenTypeOperatorInfix)
+}
+
+// isOperand determine if the token is parse operand.
+func isOperand(token efp.Token) bool {
+ return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText || token.TSubType == efp.TokenSubTypeLogical)
+}
+
+// tokenToFormulaArg create a formula argument by given token.
+func tokenToFormulaArg(token efp.Token) formulaArg {
+ switch token.TSubType {
+ case efp.TokenSubTypeLogical:
+ return newBoolFormulaArg(strings.EqualFold(token.TValue, "TRUE"))
+ case efp.TokenSubTypeNumber:
+ num, _ := strconv.ParseFloat(token.TValue, 64)
+ return newNumberFormulaArg(num)
+ default:
+ return newStringFormulaArg(token.TValue)
}
- return false
}
-func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) {
- for _, definedName := range f.GetDefinedName() {
- if definedName.Name == definedNameName {
- refTo = definedName.RefersTo
- // worksheet scope takes precedence over scope workbook when both definedNames exist
- if definedName.Scope == currentSheet {
- break
- }
+// formulaArgToToken create a token by given formula argument.
+func formulaArgToToken(arg formulaArg) efp.Token {
+ switch arg.Type {
+ case ArgNumber:
+ if arg.Boolean {
+ return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeLogical}
}
+ return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}
+ default:
+ return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeText}
}
- return refTo
}
// parseToken parse basic arithmetic operator priority and evaluate based on
// operators and operands.
-func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error {
+func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdStack, optStack *Stack) error {
// parse reference: must reference at here
if token.TSubType == efp.TokenSubTypeRange {
refTo := f.getDefinedNameRefTo(token.TValue, sheet)
if refTo != "" {
token.TValue = refTo
}
- result, err := f.parseReference(sheet, token.TValue)
+ result, err := f.parseReference(ctx, sheet, token.TValue)
if err != nil {
return errors.New(formulaErrorNAME)
}
- if result.Type != ArgString {
- return errors.New(formulaErrorVALUE)
- }
- token.TValue = result.String
- token.TType = efp.TokenTypeOperand
- token.TSubType = efp.TokenSubTypeNumber
+ token = formulaArgToToken(result)
}
if isOperatorPrefixToken(token) {
if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil {
return err
}
}
- if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // (
+ if isBeginParenthesesToken(token) { // (
optStack.Push(token)
}
- if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop { // )
- for optStack.Peek().(efp.Token).TSubType != efp.TokenSubTypeStart && optStack.Peek().(efp.Token).TType != efp.TokenTypeSubexpression { // != (
+ if isEndParenthesesToken(token) { // )
+ for !isBeginParenthesesToken(optStack.Peek().(efp.Token)) { // != (
topOpt := optStack.Peek().(efp.Token)
if err := calculate(opdStack, topOpt); err != nil {
return err
@@ -517,56 +1468,110 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta
}
optStack.Pop()
}
+ if token.TType == efp.TokenTypeOperatorPostfix && !opdStack.Empty() {
+ topOpd := opdStack.Pop().(formulaArg)
+ opdStack.Push(newNumberFormulaArg(topOpd.Number / 100))
+ }
// opd
- if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeNumber {
- opdStack.Push(token)
+ if isOperand(token) {
+ opdStack.Push(tokenToFormulaArg(token))
}
return nil
}
-// parseReference parse reference and extract values by given reference
-// characters and default sheet name.
-func (f *File) parseReference(sheet, reference string) (arg formulaArg, err error) {
- reference = strings.Replace(reference, "$", "", -1)
- refs, cellRanges, cellRefs := list.New(), list.New(), list.New()
- for _, ref := range strings.Split(reference, ":") {
- tokens := strings.Split(ref, "!")
- cr := cellRef{}
- if len(tokens) == 2 { // have a worksheet name
- cr.Sheet = tokens[0]
- if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil {
- return
- }
- if refs.Len() > 0 {
- e := refs.Back()
- cellRefs.PushBack(e.Value.(cellRef))
- refs.Remove(e)
- }
- refs.PushBack(cr)
- continue
+// parseRef parse reference for a cell, column name or row number.
+func parseRef(ref string) (cellRef, bool, bool, error) {
+ var (
+ err, colErr, rowErr error
+ cr cellRef
+ cell = ref
+ tokens = strings.Split(ref, "!")
+ )
+ if len(tokens) == 2 { // have a worksheet
+ cr.Sheet, cell = tokens[0], tokens[1]
+ }
+ if cr.Col, cr.Row, err = CellNameToCoordinates(cell); err != nil {
+ if cr.Col, colErr = ColumnNameToNumber(cell); colErr == nil { // cast to column
+ return cr, true, false, nil
}
- if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil {
- return
+ if cr.Row, rowErr = strconv.Atoi(cell); rowErr == nil { // cast to row
+ return cr, false, true, nil
}
- e := refs.Back()
- if e == nil {
- cr.Sheet = sheet
- refs.PushBack(cr)
- continue
+ return cr, false, false, err
+ }
+ return cr, false, false, err
+}
+
+// prepareCellRange checking and convert cell reference to a cell range.
+func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error {
+ if col {
+ cellRef.Row = TotalRows
+ }
+ if row {
+ cellRef.Col = MaxColumns
+ }
+ if cellRef.Sheet == "" {
+ cellRef.Sheet = cr.From.Sheet
+ }
+ if cr.From.Sheet != cellRef.Sheet || cr.To.Sheet != cellRef.Sheet {
+ return errors.New("invalid reference")
+ }
+ if cr.From.Col > cellRef.Col {
+ cr.From.Col = cellRef.Col
+ }
+ if cr.From.Row > cellRef.Row {
+ cr.From.Row = cellRef.Row
+ }
+ if cr.To.Col < cellRef.Col {
+ cr.To.Col = cellRef.Col
+ }
+ if cr.To.Row < cellRef.Row {
+ cr.To.Row = cellRef.Row
+ }
+ return nil
+}
+
+// parseReference parse reference and extract values by given reference
+// characters and default sheet name.
+func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) {
+ reference = strings.ReplaceAll(reference, "$", "")
+ ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New()
+ if len(ranges) > 1 {
+ var cr cellRange
+ for i, ref := range ranges {
+ cellRef, col, row, err := parseRef(ref)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
+ }
+ if i == 0 {
+ if col {
+ cellRef.Row = 1
+ }
+ if row {
+ cellRef.Col = 1
+ }
+ if cellRef.Sheet == "" {
+ cellRef.Sheet = sheet
+ }
+ cr.From, cr.To = cellRef, cellRef
+ continue
+ }
+ if err := cr.prepareCellRange(col, row, cellRef); err != nil {
+ return newErrorFormulaArg(formulaErrorNAME, err.Error()), err
+ }
}
- cellRanges.PushBack(cellRange{
- From: e.Value.(cellRef),
- To: cr,
- })
- refs.Remove(e)
+ cellRanges.PushBack(cr)
+ return f.rangeResolver(ctx, cellRefs, cellRanges)
+ }
+ cellRef, _, _, err := parseRef(reference)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference")
}
- if refs.Len() > 0 {
- e := refs.Back()
- cellRefs.PushBack(e.Value.(cellRef))
- refs.Remove(e)
+ if cellRef.Sheet == "" {
+ cellRef.Sheet = sheet
}
- arg, err = f.rangeResolver(cellRefs, cellRanges)
- return
+ cellRefs.PushBack(cellRef)
+ return f.rangeResolver(ctx, cellRefs, cellRanges)
}
// prepareValueRange prepare value range.
@@ -601,21 +1606,74 @@ func prepareValueRef(cr cellRef, valueRange []int) {
}
}
+// cellResolver calc cell value by given worksheet name, cell reference and context.
+func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, error) {
+ var (
+ arg formulaArg
+ value string
+ err error
+ )
+ ref := fmt.Sprintf("%s!%s", sheet, cell)
+ if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 {
+ ctx.mu.Lock()
+ if ctx.entry != ref {
+ if ctx.iterations[ref] <= f.options.MaxCalcIterations {
+ ctx.iterations[ref]++
+ ctx.mu.Unlock()
+ arg, _ = f.calcCellValue(ctx, sheet, cell)
+ ctx.iterationsCache[ref] = arg
+ return arg, nil
+ }
+ ctx.mu.Unlock()
+ return ctx.iterationsCache[ref], nil
+ }
+ ctx.mu.Unlock()
+ }
+ if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil {
+ return arg, err
+ }
+ arg = newStringFormulaArg(value)
+ cellType, _ := f.GetCellType(sheet, cell)
+ switch cellType {
+ case CellTypeBool:
+ return arg.ToBool(), err
+ case CellTypeNumber, CellTypeUnset:
+ if arg.Value() == "" {
+ return newEmptyFormulaArg(), err
+ }
+ return arg.ToNumber(), err
+ case CellTypeInlineString, CellTypeSharedString:
+ return arg, err
+ case CellTypeFormula:
+ if value != "" {
+ return arg, err
+ }
+ return newEmptyFormulaArg(), err
+ case CellTypeDate:
+ if value, err = f.GetCellValue(sheet, cell); err == nil {
+ if num := newStringFormulaArg(value).ToNumber(); num.Type == ArgNumber {
+ return num, err
+ }
+ }
+ return arg, err
+ default:
+ return newErrorFormulaArg(value, value), err
+ }
+}
+
// rangeResolver extract value as string from given reference and range list.
// This function will not ignore the empty cell. For example, A1:A2:A2:B3 will
// be reference A1:B3.
-func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, err error) {
+func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) (arg formulaArg, err error) {
+ arg.cellRefs, arg.cellRanges = cellRefs, cellRanges
// value range order: from row, to row, from column, to column
valueRange := []int{0, 0, 0, 0}
var sheet string
// prepare value range
for temp := cellRanges.Front(); temp != nil; temp = temp.Next() {
cr := temp.Value.(cellRange)
- if cr.From.Sheet != cr.To.Sheet {
- err = errors.New(formulaErrorVALUE)
- }
rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row}
- sortCoordinates(rng)
+ _ = sortCoordinates(rng)
cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row = rng[0], rng[1], rng[2], rng[3]
prepareValueRange(cr, valueRange)
if cr.From.Sheet != "" {
@@ -632,20 +1690,33 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e
// extract value from ranges
if cellRanges.Len() > 0 {
arg.Type = ArgMatrix
+
+ var ws *xlsxWorksheet
+ ws, err = f.workSheetReader(sheet)
+ if err != nil {
+ return
+ }
+
for row := valueRange[0]; row <= valueRange[1]; row++ {
- var matrixRow = []formulaArg{}
+ colMax := 0
+ if row <= len(ws.SheetData.Row) {
+ rowData := &ws.SheetData.Row[row-1]
+ colMax = min(valueRange[3], len(rowData.C))
+ }
+
+ var matrixRow []formulaArg
for col := valueRange[2]; col <= valueRange[3]; col++ {
- var cell, value string
- if cell, err = CoordinatesToCellName(col, row); err != nil {
- return
- }
- if value, err = f.GetCellValue(sheet, cell); err != nil {
- return
+ value := newEmptyFormulaArg()
+ if col <= colMax {
+ var cell string
+ if cell, err = CoordinatesToCellName(col, row); err != nil {
+ return
+ }
+ if value, err = f.cellResolver(ctx, sheet, cell); err != nil {
+ return
+ }
}
- matrixRow = append(matrixRow, formulaArg{
- String: value,
- Type: ArgString,
- })
+ matrixRow = append(matrixRow, value)
}
arg.Matrix = append(arg.Matrix, matrixRow)
}
@@ -658,1152 +1729,2306 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e
if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil {
return
}
- if arg.String, err = f.GetCellValue(cr.Sheet, cell); err != nil {
+ if arg, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil {
return
}
- arg.Type = ArgString
+ arg.cellRefs, arg.cellRanges = cellRefs, cellRanges
}
return
}
// callFuncByName calls the no error or only error return function with
// reflect by given receiver, name and parameters.
-func callFuncByName(receiver interface{}, name string, params []reflect.Value) (result string, err error) {
+func callFuncByName(receiver interface{}, name string, params []reflect.Value) (arg formulaArg) {
function := reflect.ValueOf(receiver).MethodByName(name)
if function.IsValid() {
rt := function.Call(params)
if len(rt) == 0 {
return
}
- if !rt[1].IsNil() {
- err = rt[1].Interface().(error)
- return
- }
- result = rt[0].Interface().(string)
+ arg = rt[0].Interface().(formulaArg)
return
}
- err = fmt.Errorf("not support %s function", name)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("not support %s function", name))
}
// formulaCriteriaParser parse formula criteria.
-func formulaCriteriaParser(exp string) (fc *formulaCriteria) {
- fc = &formulaCriteria{}
- if exp == "" {
- return
- }
- if match := regexp.MustCompile(`^([0-9]+)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaEq, match[1]
- return
- }
- if match := regexp.MustCompile(`^=(.*)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaEq, match[1]
+func formulaCriteriaParser(exp formulaArg) *formulaCriteria {
+ prepareValue := func(cond string) (expected float64, err error) {
+ percentile := 1.0
+ if strings.HasSuffix(cond, "%") {
+ cond = strings.TrimSuffix(cond, "%")
+ percentile /= 100
+ }
+ if expected, err = strconv.ParseFloat(cond, 64); err != nil {
+ return
+ }
+ expected *= percentile
return
}
- if match := regexp.MustCompile(`^<(.*)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaLe, match[1]
- return
+ fc, val := &formulaCriteria{}, exp.Value()
+ if val == "" {
+ return fc
}
- if match := regexp.MustCompile(`^>(.*)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaGe, match[1]
- return
+ for i, re := range formulaFormats {
+ if match := re.FindStringSubmatch(val); len(match) > 1 {
+ fc.Condition = newStringFormulaArg(match[1])
+ if num, err := prepareValue(match[1]); err == nil {
+ fc.Condition = newNumberFormulaArg(num)
+ }
+ fc.Type = formulaCriterias[i]
+ return fc
+ }
}
- if match := regexp.MustCompile(`^<=(.*)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaL, match[1]
- return
+ if strings.Contains(val, "?") {
+ val = strings.ReplaceAll(val, "?", ".")
}
- if match := regexp.MustCompile(`^>=(.*)$`).FindStringSubmatch(exp); len(match) > 1 {
- fc.Type, fc.Condition = criteriaG, match[1]
- return
+ if strings.Contains(val, "*") {
+ val = strings.ReplaceAll(val, "*", ".*")
}
- if strings.Contains(exp, "*") {
- if strings.HasPrefix(exp, "*") {
- fc.Type, fc.Condition = criteriaEnd, strings.TrimPrefix(exp, "*")
- }
- if strings.HasSuffix(exp, "*") {
- fc.Type, fc.Condition = criteriaBeg, strings.TrimSuffix(exp, "*")
- }
- return
+ fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg(val)
+ if num := fc.Condition.ToNumber(); num.Type == ArgNumber {
+ fc.Condition = num
}
- fc.Type, fc.Condition = criteriaEq, exp
- return
+ return fc
}
// formulaCriteriaEval evaluate formula criteria expression.
-func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, err error) {
- var value, expected float64
- var prepareValue = func(val, cond string) (value float64, expected float64, err error) {
- value, _ = strconv.ParseFloat(val, 64)
- if expected, err = strconv.ParseFloat(criteria.Condition, 64); err != nil {
- return
- }
- return
+func formulaCriteriaEval(val formulaArg, criteria *formulaCriteria) (result bool, err error) {
+ s := NewStack()
+ tokenCalcFunc := map[byte]func(rOpd, lOpd formulaArg, opdStack *Stack) error{
+ criteriaEq: calcEq,
+ criteriaNe: calcNEq,
+ criteriaL: calcL,
+ criteriaLe: calcLe,
+ criteriaG: calcG,
+ criteriaGe: calcGe,
}
switch criteria.Type {
- case criteriaEq:
- return val == criteria.Condition, err
- case criteriaLe:
- if value, expected, err = prepareValue(val, criteria.Condition); err != nil {
- return
- }
- return value <= expected, err
- case criteriaGe:
- if value, expected, err = prepareValue(val, criteria.Condition); err != nil {
- return
- }
- return value >= expected, err
- case criteriaL:
- if value, expected, err = prepareValue(val, criteria.Condition); err != nil {
- return
- }
- return value < expected, err
- case criteriaG:
- if value, expected, err = prepareValue(val, criteria.Condition); err != nil {
- return
+ case criteriaEq, criteriaLe, criteriaGe, criteriaNe, criteriaL, criteriaG:
+ if fn, ok := tokenCalcFunc[criteria.Type]; ok {
+ if _ = fn(criteria.Condition, val, s); s.Len() > 0 {
+ return s.Pop().(formulaArg).Number == 1, err
+ }
}
- return value > expected, err
- case criteriaBeg:
- return strings.HasPrefix(val, criteria.Condition), err
- case criteriaEnd:
- return strings.HasSuffix(val, criteria.Condition), err
+ case criteriaRegexp:
+ return regexp.MatchString(criteria.Condition.Value(), val.Value())
}
return
}
-// Math and Trigonometric functions
+// Engineering Functions
-// ABS function returns the absolute value of any supplied number. The syntax
-// of the function is:
-//
-// ABS(number)
+// BESSELI function the modified Bessel function, which is equivalent to the
+// Bessel function evaluated for purely imaginary arguments. The syntax of
+// the Besseli function is:
//
-func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ABS requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+// BESSELI(x,n)
+func (fn *formulaFuncs) BESSELI(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BESSELI requires 2 numeric arguments")
}
- result = fmt.Sprintf("%g", math.Abs(val))
- return
+ return fn.bassel(argsList, true)
}
-// ACOS function calculates the arccosine (i.e. the inverse cosine) of a given
-// number, and returns an angle, in radians, between 0 and π. The syntax of
-// the function is:
-//
-// ACOS(number)
+// BESSELJ function returns the Bessel function, Jn(x), for a specified order
+// and value of x. The syntax of the function is:
//
-func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ACOS requires 1 numeric argument")
- return
+// BESSELJ(x,n)
+func (fn *formulaFuncs) BESSELJ(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BESSELJ requires 2 numeric arguments")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return fn.bassel(argsList, false)
+}
+
+// bassel is an implementation of the formula functions BESSELI and BESSELJ.
+func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg {
+ x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
}
- result = fmt.Sprintf("%g", math.Acos(val))
- return
+ if n.Type != ArgNumber {
+ return n
+ }
+ maxVal, x1 := 100, x.Number*0.5
+ x2 := x1 * x1
+ x1 = math.Pow(x1, n.Number)
+ n1, n2, n3, n4, add := fact(n.Number), 1.0, 0.0, n.Number, false
+ result := x1 / n1
+ t := result * 0.9
+ for result != t && maxVal != 0 {
+ x1 *= x2
+ n3++
+ n1 *= n3
+ n4++
+ n2 *= n4
+ t = result
+ r := x1 / n1 / n2
+ if modfied || add {
+ result += r
+ } else {
+ result -= r
+ }
+ maxVal--
+ add = !add
+ }
+ return newNumberFormulaArg(result)
}
-// ACOSH function calculates the inverse hyperbolic cosine of a supplied number.
+// BESSELK function calculates the modified Bessel functions, Kn(x), which are
+// also known as the hyperbolic Bessel Functions. These are the equivalent of
+// the Bessel functions, evaluated for purely imaginary arguments. The syntax
// of the function is:
//
-// ACOSH(number)
-//
-func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ACOSH requires 1 numeric argument")
- return
+// BESSELK(x,n)
+func (fn *formulaFuncs) BESSELK(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BESSELK requires 2 numeric arguments")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
}
- result = fmt.Sprintf("%g", math.Acosh(val))
- return
+ if n.Type != ArgNumber {
+ return n
+ }
+ if x.Number <= 0 || n.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ var result float64
+ switch math.Floor(n.Number) {
+ case 0:
+ result = fn.besselK0(x)
+ case 1:
+ result = fn.besselK1(x)
+ default:
+ result = fn.besselK2(x, n)
+ }
+ return newNumberFormulaArg(result)
}
-// ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a
-// given number, and returns an angle, in radians, between 0 and π. The syntax
-// of the function is:
-//
-// ACOT(number)
+// besselK0 is an implementation of the formula function BESSELK.
+func (fn *formulaFuncs) besselK0(x formulaArg) float64 {
+ var y float64
+ if x.Number <= 2 {
+ n2 := x.Number * 0.5
+ y = n2 * n2
+ args := list.New()
+ args.PushBack(x)
+ args.PushBack(newNumberFormulaArg(0))
+ return -math.Log(n2)*fn.BESSELI(args).Number +
+ (-0.57721566 + y*(0.42278420+y*(0.23069756+y*(0.3488590e-1+y*(0.262698e-2+y*
+ (0.10750e-3+y*0.74e-5))))))
+ }
+ y = 2 / x.Number
+ return math.Exp(-x.Number) / math.Sqrt(x.Number) *
+ (1.25331414 + y*(-0.7832358e-1+y*(0.2189568e-1+y*(-0.1062446e-1+y*
+ (0.587872e-2+y*(-0.251540e-2+y*0.53208e-3))))))
+}
+
+// besselK1 is an implementation of the formula function BESSELK.
+func (fn *formulaFuncs) besselK1(x formulaArg) float64 {
+ var n2, y float64
+ if x.Number <= 2 {
+ n2 = x.Number * 0.5
+ y = n2 * n2
+ args := list.New()
+ args.PushBack(x)
+ args.PushBack(newNumberFormulaArg(1))
+ return math.Log(n2)*fn.BESSELI(args).Number +
+ (1+y*(0.15443144+y*(-0.67278579+y*(-0.18156897+y*(-0.1919402e-1+y*(-0.110404e-2+y*(-0.4686e-4)))))))/x.Number
+ }
+ y = 2 / x.Number
+ return math.Exp(-x.Number) / math.Sqrt(x.Number) *
+ (1.25331414 + y*(0.23498619+y*(-0.3655620e-1+y*(0.1504268e-1+y*(-0.780353e-2+y*
+ (0.325614e-2+y*(-0.68245e-3)))))))
+}
+
+// besselK2 is an implementation of the formula function BESSELK.
+func (fn *formulaFuncs) besselK2(x, n formulaArg) float64 {
+ tox, bkm, bk, bkp := 2/x.Number, fn.besselK0(x), fn.besselK1(x), 0.0
+ for i := 1.0; i < n.Number; i++ {
+ bkp = bkm + i*tox*bk
+ bkm = bk
+ bk = bkp
+ }
+ return bk
+}
+
+// BESSELY function returns the Bessel function, Yn(x), (also known as the
+// Weber function or the Neumann function), for a specified order and value
+// of x. The syntax of the function is:
//
-func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ACOT requires 1 numeric argument")
- return
+// BESSELY(x,n)
+func (fn *formulaFuncs) BESSELY(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BESSELY requires 2 numeric arguments")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
}
- result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val))
- return
+ if n.Type != ArgNumber {
+ return n
+ }
+ if x.Number <= 0 || n.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ var result float64
+ switch math.Floor(n.Number) {
+ case 0:
+ result = fn.besselY0(x)
+ case 1:
+ result = fn.besselY1(x)
+ default:
+ result = fn.besselY2(x, n)
+ }
+ return newNumberFormulaArg(result)
}
-// ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied
-// value. The syntax of the function is:
-//
-// ACOTH(number)
+// besselY0 is an implementation of the formula function BESSELY.
+func (fn *formulaFuncs) besselY0(x formulaArg) float64 {
+ var y float64
+ if x.Number < 8 {
+ y = x.Number * x.Number
+ f1 := -2957821389.0 + y*(7062834065.0+y*(-512359803.6+y*(10879881.29+y*
+ (-86327.92757+y*228.4622733))))
+ f2 := 40076544269.0 + y*(745249964.8+y*(7189466.438+y*
+ (47447.26470+y*(226.1030244+y))))
+ args := list.New()
+ args.PushBack(x)
+ args.PushBack(newNumberFormulaArg(0))
+ return f1/f2 + 0.636619772*fn.BESSELJ(args).Number*math.Log(x.Number)
+ }
+ z := 8.0 / x.Number
+ y = z * z
+ xx := x.Number - 0.785398164
+ f1 := 1 + y*(-0.1098628627e-2+y*(0.2734510407e-4+y*(-0.2073370639e-5+y*0.2093887211e-6)))
+ f2 := -0.1562499995e-1 + y*(0.1430488765e-3+y*(-0.6911147651e-5+y*(0.7621095161e-6+y*
+ (-0.934945152e-7))))
+ return math.Sqrt(0.636619772/x.Number) * (math.Sin(xx)*f1 + z*math.Cos(xx)*f2)
+}
+
+// besselY1 is an implementation of the formula function BESSELY.
+func (fn *formulaFuncs) besselY1(x formulaArg) float64 {
+ if x.Number < 8 {
+ y := x.Number * x.Number
+ f1 := x.Number * (-0.4900604943e13 + y*(0.1275274390e13+y*(-0.5153438139e11+y*
+ (0.7349264551e9+y*(-0.4237922726e7+y*0.8511937935e4)))))
+ f2 := 0.2499580570e14 + y*(0.4244419664e12+y*(0.3733650367e10+y*(0.2245904002e8+y*
+ (0.1020426050e6+y*(0.3549632885e3+y)))))
+ args := list.New()
+ args.PushBack(x)
+ args.PushBack(newNumberFormulaArg(1))
+ return f1/f2 + 0.636619772*(fn.BESSELJ(args).Number*math.Log(x.Number)-1/x.Number)
+ }
+ return math.Sqrt(0.636619772/x.Number) * math.Sin(x.Number-2.356194491)
+}
+
+// besselY2 is an implementation of the formula function BESSELY.
+func (fn *formulaFuncs) besselY2(x, n formulaArg) float64 {
+ tox, bym, by, byp := 2/x.Number, fn.besselY0(x), fn.besselY1(x), 0.0
+ for i := 1.0; i < n.Number; i++ {
+ byp = i*tox*by - bym
+ bym = by
+ by = byp
+ }
+ return by
+}
+
+// BIN2DEC function converts a Binary (a base-2 number) into a decimal number.
+// The syntax of the function is:
//
-func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) {
+// BIN2DEC(number)
+func (fn *formulaFuncs) BIN2DEC(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("ACOTH requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "BIN2DEC requires 1 numeric argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
}
- result = fmt.Sprintf("%g", math.Atanh(1/val))
- return
+ return fn.bin2dec(token.Value())
}
-// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax
-// of the function is:
+// BIN2HEX function converts a Binary (Base 2) number into a Hexadecimal
+// (Base 16) number. The syntax of the function is:
//
-// ARABIC(text)
+// BIN2HEX(number,[places])
+func (fn *formulaFuncs) BIN2HEX(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BIN2HEX requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BIN2HEX allows at most 2 arguments")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
+ }
+ decimal, newList := fn.bin2dec(token.Value()), list.New()
+ if decimal.Type != ArgNumber {
+ return decimal
+ }
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.dec2x("BIN2HEX", newList)
+}
+
+// BIN2OCT function converts a Binary (Base 2) number into an Octal (Base 8)
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ARABIC requires 1 numeric argument")
- return
+// BIN2OCT(number,[places])
+func (fn *formulaFuncs) BIN2OCT(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BIN2OCT requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BIN2OCT allows at most 2 arguments")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
+ }
+ decimal, newList := fn.bin2dec(token.Value()), list.New()
+ if decimal.Type != ArgNumber {
+ return decimal
}
- charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
- val, last, prefix := 0.0, 0.0, 1.0
- for _, char := range argsList.Front().Value.(formulaArg).String {
- digit := 0.0
- if char == '-' {
- prefix = -1
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.dec2x("BIN2OCT", newList)
+}
+
+// bin2dec is an implementation of the formula function BIN2DEC.
+func (fn *formulaFuncs) bin2dec(number string) formulaArg {
+ decimal, length := 0.0, len(number)
+ for i := length; i > 0; i-- {
+ s := string(number[length-i])
+ if i == 10 && s == "1" {
+ decimal += math.Pow(-2.0, float64(i-1))
continue
}
- digit, _ = charMap[char]
- val += digit
- switch {
- case last == digit && (last == 5 || last == 50 || last == 500):
- result = formulaErrorVALUE
- return
- case 2*last == digit:
- result = formulaErrorVALUE
- return
+ if s == "1" {
+ decimal += math.Pow(2.0, float64(i-1))
+ continue
}
- if last < digit {
- val -= 2 * last
+ if s != "0" {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- last = digit
}
- result = fmt.Sprintf("%g", prefix*val)
- return
+ return newNumberFormulaArg(decimal)
}
-// ASIN function calculates the arcsine (i.e. the inverse sine) of a given
-// number, and returns an angle, in radians, between -π/2 and π/2. The syntax
-// of the function is:
-//
-// ASIN(number)
+// BITAND function returns the bitwise 'AND' for two supplied integers. The
+// syntax of the function is:
//
-func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ASIN requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- result = fmt.Sprintf("%g", math.Asin(val))
- return
+// BITAND(number1,number2)
+func (fn *formulaFuncs) BITAND(argsList *list.List) formulaArg {
+ return fn.bitwise("BITAND", argsList)
}
-// ASINH function calculates the inverse hyperbolic sine of a supplied number.
-// The syntax of the function is:
+// BITLSHIFT function returns a supplied integer, shifted left by a specified
+// number of bits. The syntax of the function is:
//
-// ASINH(number)
-//
-func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ASINH requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- result = fmt.Sprintf("%g", math.Asinh(val))
- return
+// BITLSHIFT(number1,shift_amount)
+func (fn *formulaFuncs) BITLSHIFT(argsList *list.List) formulaArg {
+ return fn.bitwise("BITLSHIFT", argsList)
}
-// ATAN function calculates the arctangent (i.e. the inverse tangent) of a
-// given number, and returns an angle, in radians, between -π/2 and +π/2. The
+// BITOR function returns the bitwise 'OR' for two supplied integers. The
// syntax of the function is:
//
-// ATAN(number)
-//
-func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ATAN requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- result = fmt.Sprintf("%g", math.Atan(val))
- return
+// BITOR(number1,number2)
+func (fn *formulaFuncs) BITOR(argsList *list.List) formulaArg {
+ return fn.bitwise("BITOR", argsList)
}
-// ATANH function calculates the inverse hyperbolic tangent of a supplied
-// number. The syntax of the function is:
+// BITRSHIFT function returns a supplied integer, shifted right by a specified
+// number of bits. The syntax of the function is:
//
-// ATANH(number)
-//
-func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ATANH requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- result = fmt.Sprintf("%g", math.Atanh(val))
- return
+// BITRSHIFT(number1,shift_amount)
+func (fn *formulaFuncs) BITRSHIFT(argsList *list.List) formulaArg {
+ return fn.bitwise("BITRSHIFT", argsList)
}
-// ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a
-// given set of x and y coordinates, and returns an angle, in radians, between
-// -π/2 and +π/2. The syntax of the function is:
-//
-// ATAN2(x_num,y_num)
+// BITXOR function returns the bitwise 'XOR' (exclusive 'OR') for two supplied
+// integers. The syntax of the function is:
//
-func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) {
+// BITXOR(number1,number2)
+func (fn *formulaFuncs) BITXOR(argsList *list.List) formulaArg {
+ return fn.bitwise("BITXOR", argsList)
+}
+
+// bitwise is an implementation of the formula functions BITAND, BITLSHIFT,
+// BITOR, BITRSHIFT and BITXOR.
+func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg {
if argsList.Len() != 2 {
- err = errors.New("ATAN2 requires 2 numeric arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 numeric arguments", name))
}
- var x, y float64
- if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ num1, num2 := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber()
+ if num1.Type != ArgNumber || num2.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ maxVal := math.Pow(2, 48) - 1
+ if num1.Number < 0 || num1.Number > maxVal || num2.Number < 0 || num2.Number > maxVal {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = fmt.Sprintf("%g", math.Atan2(x, y))
- return
+ bitwiseFuncMap := map[string]func(a, b int) int{
+ "BITAND": func(a, b int) int { return a & b },
+ "BITLSHIFT": func(a, b int) int { return a << uint(b) },
+ "BITOR": func(a, b int) int { return a | b },
+ "BITRSHIFT": func(a, b int) int { return a >> uint(b) },
+ "BITXOR": func(a, b int) int { return a ^ b },
+ }
+ bitwiseFunc := bitwiseFuncMap[name]
+ return newNumberFormulaArg(float64(bitwiseFunc(int(num1.Number), int(num2.Number))))
}
-// BASE function converts a number into a supplied base (radix), and returns a
-// text representation of the calculated value. The syntax of the function is:
-//
-// BASE(number,radix,[min_length])
+// COMPLEX function takes two arguments, representing the real and the
+// imaginary coefficients of a complex number, and from these, creates a
+// complex number. The syntax of the function is:
//
-func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) {
+// COMPLEX(real_num,i_num,[suffix])
+func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg {
if argsList.Len() < 2 {
- err = errors.New("BASE requires at least 2 arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "COMPLEX requires at least 2 arguments")
}
if argsList.Len() > 3 {
- err = errors.New("BASE allows at most 3 arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "COMPLEX allows at most 3 arguments")
}
- var number float64
- var radix, minLength int
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ realNum, i, suffix := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber(), "i"
+ if realNum.Type != ArgNumber {
+ return realNum
}
- if radix < 2 || radix > 36 {
- err = errors.New("radix must be an integer >= 2 and <= 36")
- return
+ if i.Type != ArgNumber {
+ return i
}
- if argsList.Len() > 2 {
- if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if argsList.Len() == 3 {
+ if suffix = strings.ToLower(argsList.Back().Value.(formulaArg).Value()); suffix != "i" && suffix != "j" {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
}
- result = strconv.FormatInt(int64(number), radix)
- if len(result) < minLength {
- result = strings.Repeat("0", minLength-len(result)) + result
- }
- result = strings.ToUpper(result)
- return
+ return newStringFormulaArg(cmplx2str(complex(realNum.Number, i.Number), suffix))
}
-// CEILING function rounds a supplied number away from zero, to the nearest
-// multiple of a given number. The syntax of the function is:
-//
-// CEILING(number,significance)
-//
-func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("CEILING requires at least 1 argument")
- return
- }
- if argsList.Len() > 2 {
- err = errors.New("CEILING allows at most 2 arguments")
- return
- }
- number, significance, res := 0.0, 1.0, 0.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
+// cmplx2str replace complex number string characters.
+func cmplx2str(num complex128, suffix string) string {
+ realPart, imagPart := fmt.Sprint(real(num)), fmt.Sprint(imag(num))
+ isNum, i, decimal := isNumeric(realPart)
+ if isNum && i > 15 {
+ realPart = strconv.FormatFloat(decimal, 'G', 15, 64)
+ }
+ isNum, i, decimal = isNumeric(imagPart)
+ if isNum && i > 15 {
+ imagPart = strconv.FormatFloat(decimal, 'G', 15, 64)
+ }
+ c := realPart
+ if imag(num) > 0 {
+ c += "+"
+ }
+ if imag(num) != 0 {
+ c += imagPart + "i"
+ }
+ c = strings.TrimPrefix(c, "(")
+ c = strings.TrimPrefix(c, "+0+")
+ c = strings.TrimPrefix(c, "-0+")
+ c = strings.TrimSuffix(c, ")")
+ c = strings.TrimPrefix(c, "0+")
+ if strings.HasPrefix(c, "0-") {
+ c = "-" + strings.TrimPrefix(c, "0-")
+ }
+ c = strings.TrimPrefix(c, "0+")
+ c = strings.TrimSuffix(c, "+0i")
+ c = strings.TrimSuffix(c, "-0i")
+ c = strings.NewReplacer("+1i", "+i", "-1i", "-i").Replace(c)
+ c = strings.ReplaceAll(c, "i", suffix)
+ return c
+}
+
+// str2cmplx convert complex number string characters.
+func str2cmplx(c string) string {
+ c = strings.ReplaceAll(c, "j", "i")
+ if c == "i" {
+ c = "1i"
+ }
+ c = strings.NewReplacer("+i", "+1i", "-i", "-1i").Replace(c)
+ return c
+}
+
+// conversionUnit defined unit info for conversion.
+type conversionUnit struct {
+ group uint8
+ allowPrefix bool
+}
+
+// conversionUnits maps info list for unit conversion, that can be used in
+// formula function CONVERT.
+var conversionUnits = map[string]conversionUnit{
+ // weight and mass
+ "g": {group: categoryWeightAndMass, allowPrefix: true},
+ "sg": {group: categoryWeightAndMass, allowPrefix: false},
+ "lbm": {group: categoryWeightAndMass, allowPrefix: false},
+ "u": {group: categoryWeightAndMass, allowPrefix: true},
+ "ozm": {group: categoryWeightAndMass, allowPrefix: false},
+ "grain": {group: categoryWeightAndMass, allowPrefix: false},
+ "cwt": {group: categoryWeightAndMass, allowPrefix: false},
+ "shweight": {group: categoryWeightAndMass, allowPrefix: false},
+ "uk_cwt": {group: categoryWeightAndMass, allowPrefix: false},
+ "lcwt": {group: categoryWeightAndMass, allowPrefix: false},
+ "hweight": {group: categoryWeightAndMass, allowPrefix: false},
+ "stone": {group: categoryWeightAndMass, allowPrefix: false},
+ "ton": {group: categoryWeightAndMass, allowPrefix: false},
+ "uk_ton": {group: categoryWeightAndMass, allowPrefix: false},
+ "LTON": {group: categoryWeightAndMass, allowPrefix: false},
+ "brton": {group: categoryWeightAndMass, allowPrefix: false},
+ // distance
+ "m": {group: categoryDistance, allowPrefix: true},
+ "mi": {group: categoryDistance, allowPrefix: false},
+ "Nmi": {group: categoryDistance, allowPrefix: false},
+ "in": {group: categoryDistance, allowPrefix: false},
+ "ft": {group: categoryDistance, allowPrefix: false},
+ "yd": {group: categoryDistance, allowPrefix: false},
+ "ang": {group: categoryDistance, allowPrefix: true},
+ "ell": {group: categoryDistance, allowPrefix: false},
+ "ly": {group: categoryDistance, allowPrefix: false},
+ "parsec": {group: categoryDistance, allowPrefix: false},
+ "pc": {group: categoryDistance, allowPrefix: false},
+ "Pica": {group: categoryDistance, allowPrefix: false},
+ "Picapt": {group: categoryDistance, allowPrefix: false},
+ "pica": {group: categoryDistance, allowPrefix: false},
+ "survey_mi": {group: categoryDistance, allowPrefix: false},
+ // time
+ "yr": {group: categoryTime, allowPrefix: false},
+ "day": {group: categoryTime, allowPrefix: false},
+ "d": {group: categoryTime, allowPrefix: false},
+ "hr": {group: categoryTime, allowPrefix: false},
+ "mn": {group: categoryTime, allowPrefix: false},
+ "min": {group: categoryTime, allowPrefix: false},
+ "sec": {group: categoryTime, allowPrefix: true},
+ "s": {group: categoryTime, allowPrefix: true},
+ // pressure
+ "Pa": {group: categoryPressure, allowPrefix: true},
+ "p": {group: categoryPressure, allowPrefix: true},
+ "atm": {group: categoryPressure, allowPrefix: true},
+ "at": {group: categoryPressure, allowPrefix: true},
+ "mmHg": {group: categoryPressure, allowPrefix: true},
+ "psi": {group: categoryPressure, allowPrefix: true},
+ "Torr": {group: categoryPressure, allowPrefix: true},
+ // force
+ "N": {group: categoryForce, allowPrefix: true},
+ "dyn": {group: categoryForce, allowPrefix: true},
+ "dy": {group: categoryForce, allowPrefix: true},
+ "lbf": {group: categoryForce, allowPrefix: false},
+ "pond": {group: categoryForce, allowPrefix: true},
+ // energy
+ "J": {group: categoryEnergy, allowPrefix: true},
+ "e": {group: categoryEnergy, allowPrefix: true},
+ "c": {group: categoryEnergy, allowPrefix: true},
+ "cal": {group: categoryEnergy, allowPrefix: true},
+ "eV": {group: categoryEnergy, allowPrefix: true},
+ "ev": {group: categoryEnergy, allowPrefix: true},
+ "HPh": {group: categoryEnergy, allowPrefix: false},
+ "hh": {group: categoryEnergy, allowPrefix: false},
+ "Wh": {group: categoryEnergy, allowPrefix: true},
+ "wh": {group: categoryEnergy, allowPrefix: true},
+ "flb": {group: categoryEnergy, allowPrefix: false},
+ "BTU": {group: categoryEnergy, allowPrefix: false},
+ "btu": {group: categoryEnergy, allowPrefix: false},
+ // power
+ "HP": {group: categoryPower, allowPrefix: false},
+ "h": {group: categoryPower, allowPrefix: false},
+ "W": {group: categoryPower, allowPrefix: true},
+ "w": {group: categoryPower, allowPrefix: true},
+ "PS": {group: categoryPower, allowPrefix: false},
+ "T": {group: categoryMagnetism, allowPrefix: true},
+ "ga": {group: categoryMagnetism, allowPrefix: true},
+ // temperature
+ "C": {group: categoryTemperature, allowPrefix: false},
+ "cel": {group: categoryTemperature, allowPrefix: false},
+ "F": {group: categoryTemperature, allowPrefix: false},
+ "fah": {group: categoryTemperature, allowPrefix: false},
+ "K": {group: categoryTemperature, allowPrefix: false},
+ "kel": {group: categoryTemperature, allowPrefix: false},
+ "Rank": {group: categoryTemperature, allowPrefix: false},
+ "Reau": {group: categoryTemperature, allowPrefix: false},
+ // volume
+ "l": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "L": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "lt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "tsp": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "tspm": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "tbs": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "oz": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "cup": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "us_pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "uk_pt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "qt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "uk_qt": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "gal": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "uk_gal": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "ang3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "ang^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "barrel": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "bushel": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "in3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "in^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "ft3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "ft^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "ly3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "ly^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "m3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "m^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: true},
+ "mi3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "mi^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "yd3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "yd^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Nmi3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Nmi^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Pica3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Pica^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Picapt3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "Picapt^3": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "GRT": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "regton": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ "MTON": {group: categoryVolumeAndLiquidMeasure, allowPrefix: false},
+ // area
+ "ha": {group: categoryArea, allowPrefix: true},
+ "uk_acre": {group: categoryArea, allowPrefix: false},
+ "us_acre": {group: categoryArea, allowPrefix: false},
+ "ang2": {group: categoryArea, allowPrefix: true},
+ "ang^2": {group: categoryArea, allowPrefix: true},
+ "ar": {group: categoryArea, allowPrefix: true},
+ "ft2": {group: categoryArea, allowPrefix: false},
+ "ft^2": {group: categoryArea, allowPrefix: false},
+ "in2": {group: categoryArea, allowPrefix: false},
+ "in^2": {group: categoryArea, allowPrefix: false},
+ "ly2": {group: categoryArea, allowPrefix: false},
+ "ly^2": {group: categoryArea, allowPrefix: false},
+ "m2": {group: categoryArea, allowPrefix: true},
+ "m^2": {group: categoryArea, allowPrefix: true},
+ "Morgen": {group: categoryArea, allowPrefix: false},
+ "mi2": {group: categoryArea, allowPrefix: false},
+ "mi^2": {group: categoryArea, allowPrefix: false},
+ "Nmi2": {group: categoryArea, allowPrefix: false},
+ "Nmi^2": {group: categoryArea, allowPrefix: false},
+ "Pica2": {group: categoryArea, allowPrefix: false},
+ "Pica^2": {group: categoryArea, allowPrefix: false},
+ "Picapt2": {group: categoryArea, allowPrefix: false},
+ "Picapt^2": {group: categoryArea, allowPrefix: false},
+ "yd2": {group: categoryArea, allowPrefix: false},
+ "yd^2": {group: categoryArea, allowPrefix: false},
+ // information
+ "byte": {group: categoryInformation, allowPrefix: true},
+ "bit": {group: categoryInformation, allowPrefix: true},
+ // speed
+ "m/s": {group: categorySpeed, allowPrefix: true},
+ "m/sec": {group: categorySpeed, allowPrefix: true},
+ "m/h": {group: categorySpeed, allowPrefix: true},
+ "m/hr": {group: categorySpeed, allowPrefix: true},
+ "mph": {group: categorySpeed, allowPrefix: false},
+ "admkn": {group: categorySpeed, allowPrefix: false},
+ "kn": {group: categorySpeed, allowPrefix: false},
+}
+
+// unitConversions maps details of the Units of measure conversion factors,
+// organised by group.
+var unitConversions = map[byte]map[string]float64{
+ // conversion uses gram (g) as an intermediate unit
+ categoryWeightAndMass: {
+ "g": 1,
+ "sg": 6.85217658567918e-05,
+ "lbm": 2.20462262184878e-03,
+ "u": 6.02214179421676e+23,
+ "ozm": 3.52739619495804e-02,
+ "grain": 1.54323583529414e+01,
+ "cwt": 2.20462262184878e-05,
+ "shweight": 2.20462262184878e-05,
+ "uk_cwt": 1.96841305522212e-05,
+ "lcwt": 1.96841305522212e-05,
+ "hweight": 1.96841305522212e-05,
+ "stone": 1.57473044417770e-04,
+ "ton": 1.10231131092439e-06,
+ "uk_ton": 9.84206527611061e-07,
+ "LTON": 9.84206527611061e-07,
+ "brton": 9.84206527611061e-07,
+ },
+ // conversion uses meter (m) as an intermediate unit
+ categoryDistance: {
+ "m": 1,
+ "mi": 6.21371192237334e-04,
+ "Nmi": 5.39956803455724e-04,
+ "in": 3.93700787401575e+01,
+ "ft": 3.28083989501312e+00,
+ "yd": 1.09361329833771e+00,
+ "ang": 1.0e+10,
+ "ell": 8.74890638670166e-01,
+ "ly": 1.05700083402462e-16,
+ "parsec": 3.24077928966473e-17,
+ "pc": 3.24077928966473e-17,
+ "Pica": 2.83464566929134e+03,
+ "Picapt": 2.83464566929134e+03,
+ "pica": 2.36220472440945e+02,
+ "survey_mi": 6.21369949494950e-04,
+ },
+ // conversion uses second (s) as an intermediate unit
+ categoryTime: {
+ "yr": 3.16880878140289e-08,
+ "day": 1.15740740740741e-05,
+ "d": 1.15740740740741e-05,
+ "hr": 2.77777777777778e-04,
+ "mn": 1.66666666666667e-02,
+ "min": 1.66666666666667e-02,
+ "sec": 1,
+ "s": 1,
+ },
+ // conversion uses Pascal (Pa) as an intermediate unit
+ categoryPressure: {
+ "Pa": 1,
+ "p": 1,
+ "atm": 9.86923266716013e-06,
+ "at": 9.86923266716013e-06,
+ "mmHg": 7.50063755419211e-03,
+ "psi": 1.45037737730209e-04,
+ "Torr": 7.50061682704170e-03,
+ },
+ // conversion uses Newton (N) as an intermediate unit
+ categoryForce: {
+ "N": 1,
+ "dyn": 1.0e+5,
+ "dy": 1.0e+5,
+ "lbf": 2.24808923655339e-01,
+ "pond": 1.01971621297793e+02,
+ },
+ // conversion uses Joule (J) as an intermediate unit
+ categoryEnergy: {
+ "J": 1,
+ "e": 9.99999519343231e+06,
+ "c": 2.39006249473467e-01,
+ "cal": 2.38846190642017e-01,
+ "eV": 6.24145700000000e+18,
+ "ev": 6.24145700000000e+18,
+ "HPh": 3.72506430801000e-07,
+ "hh": 3.72506430801000e-07,
+ "Wh": 2.77777916238711e-04,
+ "wh": 2.77777916238711e-04,
+ "flb": 2.37304222192651e+01,
+ "BTU": 9.47815067349015e-04,
+ "btu": 9.47815067349015e-04,
+ },
+ // conversion uses Horsepower (HP) as an intermediate unit
+ categoryPower: {
+ "HP": 1,
+ "h": 1,
+ "W": 7.45699871582270e+02,
+ "w": 7.45699871582270e+02,
+ "PS": 1.01386966542400e+00,
+ },
+ // conversion uses Tesla (T) as an intermediate unit
+ categoryMagnetism: {
+ "T": 1,
+ "ga": 10000,
+ },
+ // conversion uses litre (l) as an intermediate unit
+ categoryVolumeAndLiquidMeasure: {
+ "l": 1,
+ "L": 1,
+ "lt": 1,
+ "tsp": 2.02884136211058e+02,
+ "tspm": 2.0e+02,
+ "tbs": 6.76280454036860e+01,
+ "oz": 3.38140227018430e+01,
+ "cup": 4.22675283773038e+00,
+ "pt": 2.11337641886519e+00,
+ "us_pt": 2.11337641886519e+00,
+ "uk_pt": 1.75975398639270e+00,
+ "qt": 1.05668820943259e+00,
+ "uk_qt": 8.79876993196351e-01,
+ "gal": 2.64172052358148e-01,
+ "uk_gal": 2.19969248299088e-01,
+ "ang3": 1.0e+27,
+ "ang^3": 1.0e+27,
+ "barrel": 6.28981077043211e-03,
+ "bushel": 2.83775932584017e-02,
+ "in3": 6.10237440947323e+01,
+ "in^3": 6.10237440947323e+01,
+ "ft3": 3.53146667214886e-02,
+ "ft^3": 3.53146667214886e-02,
+ "ly3": 1.18093498844171e-51,
+ "ly^3": 1.18093498844171e-51,
+ "m3": 1.0e-03,
+ "m^3": 1.0e-03,
+ "mi3": 2.39912758578928e-13,
+ "mi^3": 2.39912758578928e-13,
+ "yd3": 1.30795061931439e-03,
+ "yd^3": 1.30795061931439e-03,
+ "Nmi3": 1.57426214685811e-13,
+ "Nmi^3": 1.57426214685811e-13,
+ "Pica3": 2.27769904358706e+07,
+ "Pica^3": 2.27769904358706e+07,
+ "Picapt3": 2.27769904358706e+07,
+ "Picapt^3": 2.27769904358706e+07,
+ "GRT": 3.53146667214886e-04,
+ "regton": 3.53146667214886e-04,
+ "MTON": 8.82866668037215e-04,
+ },
+ // conversion uses hectare (ha) as an intermediate unit
+ categoryArea: {
+ "ha": 1,
+ "uk_acre": 2.47105381467165e+00,
+ "us_acre": 2.47104393046628e+00,
+ "ang2": 1.0e+24,
+ "ang^2": 1.0e+24,
+ "ar": 1.0e+02,
+ "ft2": 1.07639104167097e+05,
+ "ft^2": 1.07639104167097e+05,
+ "in2": 1.55000310000620e+07,
+ "in^2": 1.55000310000620e+07,
+ "ly2": 1.11725076312873e-28,
+ "ly^2": 1.11725076312873e-28,
+ "m2": 1.0e+04,
+ "m^2": 1.0e+04,
+ "Morgen": 4.0e+00,
+ "mi2": 3.86102158542446e-03,
+ "mi^2": 3.86102158542446e-03,
+ "Nmi2": 2.91553349598123e-03,
+ "Nmi^2": 2.91553349598123e-03,
+ "Pica2": 8.03521607043214e+10,
+ "Pica^2": 8.03521607043214e+10,
+ "Picapt2": 8.03521607043214e+10,
+ "Picapt^2": 8.03521607043214e+10,
+ "yd2": 1.19599004630108e+04,
+ "yd^2": 1.19599004630108e+04,
+ },
+ // conversion uses bit (bit) as an intermediate unit
+ categoryInformation: {
+ "bit": 1,
+ "byte": 0.125,
+ },
+ // conversion uses Meters per Second (m/s) as an intermediate unit
+ categorySpeed: {
+ "m/s": 1,
+ "m/sec": 1,
+ "m/h": 3.60e+03,
+ "m/hr": 3.60e+03,
+ "mph": 2.23693629205440e+00,
+ "admkn": 1.94260256941567e+00,
+ "kn": 1.94384449244060e+00,
+ },
+}
+
+// conversionMultipliers maps details of the Multiplier prefixes that can be
+// used with Units of Measure in CONVERT.
+var conversionMultipliers = map[string]float64{
+ "Y": 1e24,
+ "Z": 1e21,
+ "E": 1e18,
+ "P": 1e15,
+ "T": 1e12,
+ "G": 1e9,
+ "M": 1e6,
+ "k": 1e3,
+ "h": 1e2,
+ "e": 1e1,
+ "da": 1e1,
+ "d": 1e-1,
+ "c": 1e-2,
+ "m": 1e-3,
+ "u": 1e-6,
+ "n": 1e-9,
+ "p": 1e-12,
+ "f": 1e-15,
+ "a": 1e-18,
+ "z": 1e-21,
+ "y": 1e-24,
+ "Yi": math.Pow(2, 80),
+ "Zi": math.Pow(2, 70),
+ "Ei": math.Pow(2, 60),
+ "Pi": math.Pow(2, 50),
+ "Ti": math.Pow(2, 40),
+ "Gi": math.Pow(2, 30),
+ "Mi": math.Pow(2, 20),
+ "ki": math.Pow(2, 10),
+}
+
+// getUnitDetails check and returns the unit of measure details.
+func getUnitDetails(uom string) (unit string, catgory byte, res float64, ok bool) {
+ if len(uom) == 0 {
+ ok = false
return
}
- if number < 0 {
- significance = -1
- }
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
+ if unit, ok := conversionUnits[uom]; ok {
+ return uom, unit.group, 1, ok
+ }
+ // 1 character standard metric multiplier prefixes
+ multiplierType := uom[:1]
+ uom = uom[1:]
+ conversionUnit, ok1 := conversionUnits[uom]
+ multiplier, ok2 := conversionMultipliers[multiplierType]
+ if ok1 && ok2 {
+ if !conversionUnit.allowPrefix {
+ ok = false
return
}
+ unitCategory := conversionUnit.group
+ return uom, unitCategory, multiplier, true
+ }
+ // 2 character standard and binary metric multiplier prefixes
+ if len(uom) > 0 {
+ multiplierType += uom[:1]
+ uom = uom[1:]
+ }
+ conversionUnit, ok1 = conversionUnits[uom]
+ multiplier, ok2 = conversionMultipliers[multiplierType]
+ if ok1 && ok2 {
+ if !conversionUnit.allowPrefix {
+ ok = false
+ return
+ }
+ unitCategory := conversionUnit.group
+ return uom, unitCategory, multiplier, true
}
- if significance < 0 && number > 0 {
- err = errors.New("negative sig to CEILING invalid")
- return
- }
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Ceil(number))
- return
- }
- number, res = math.Modf(number / significance)
- if res > 0 {
- number++
- }
- result = fmt.Sprintf("%g", number*significance)
+ ok = false
return
}
-// CEILINGMATH function rounds a supplied number up to a supplied multiple of
-// significance. The syntax of the function is:
+// resolveTemperatureSynonyms returns unit of measure according to a given
+// temperature synonyms.
+func resolveTemperatureSynonyms(uom string) string {
+ switch uom {
+ case "fah":
+ return "F"
+ case "cel":
+ return "C"
+ case "kel":
+ return "K"
+ }
+ return uom
+}
+
+// convertTemperature returns converted temperature by a given unit of measure.
+func convertTemperature(fromUOM, toUOM string, value float64) float64 {
+ fromUOM = resolveTemperatureSynonyms(fromUOM)
+ toUOM = resolveTemperatureSynonyms(toUOM)
+ if fromUOM == toUOM {
+ return value
+ }
+ // convert to Kelvin
+ switch fromUOM {
+ case "F":
+ value = (value-32)/1.8 + 273.15
+ case "C":
+ value += 273.15
+ case "Rank":
+ value /= 1.8
+ case "Reau":
+ value = value*1.25 + 273.15
+ }
+ // convert from Kelvin
+ switch toUOM {
+ case "F":
+ value = (value-273.15)*1.8 + 32
+ case "C":
+ value -= 273.15
+ case "Rank":
+ value *= 1.8
+ case "Reau":
+ value = (value - 273.15) * 0.8
+ }
+ return value
+}
+
+// CONVERT function converts a number from one unit type (e.g. Yards) to
+// another unit type (e.g. Meters). The syntax of the function is:
//
-// CEILING.MATH(number,[significance],[mode])
+// CONVERT(number,from_unit,to_unit)
+func (fn *formulaFuncs) CONVERT(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CONVERT requires 3 arguments")
+ }
+ num := argsList.Front().Value.(formulaArg).ToNumber()
+ if num.Type != ArgNumber {
+ return num
+ }
+ fromUOM, fromCategory, fromMultiplier, ok1 := getUnitDetails(argsList.Front().Next().Value.(formulaArg).Value())
+ toUOM, toCategory, toMultiplier, ok2 := getUnitDetails(argsList.Back().Value.(formulaArg).Value())
+ if !ok1 || !ok2 || fromCategory != toCategory {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ val := num.Number * fromMultiplier
+ if fromUOM == toUOM && fromMultiplier == toMultiplier {
+ return newNumberFormulaArg(val / fromMultiplier)
+ } else if fromUOM == toUOM {
+ return newNumberFormulaArg(val / toMultiplier)
+ } else if fromCategory == categoryTemperature {
+ return newNumberFormulaArg(convertTemperature(fromUOM, toUOM, val))
+ }
+ fromConversion := unitConversions[fromCategory][fromUOM]
+ toConversion := unitConversions[fromCategory][toUOM]
+ baseValue := val * (1 / fromConversion)
+ return newNumberFormulaArg((baseValue * toConversion) / toMultiplier)
+}
+
+// DEC2BIN function converts a decimal number into a Binary (Base 2) number.
+// The syntax of the function is:
//
-func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("CEILING.MATH requires at least 1 argument")
- return
- }
- if argsList.Len() > 3 {
- err = errors.New("CEILING.MATH allows at most 3 arguments")
- return
- }
- number, significance, mode := 0.0, 1.0, 1.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if number < 0 {
- significance = -1
+// DEC2BIN(number,[places])
+func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg {
+ return fn.dec2x("DEC2BIN", argsList)
+}
+
+// DEC2HEX function converts a decimal number into a Hexadecimal (Base 16)
+// number. The syntax of the function is:
+//
+// DEC2HEX(number,[places])
+func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg {
+ return fn.dec2x("DEC2HEX", argsList)
+}
+
+// DEC2OCT function converts a decimal number into an Octal (Base 8) number.
+// The syntax of the function is:
+//
+// DEC2OCT(number,[places])
+func (fn *formulaFuncs) DEC2OCT(argsList *list.List) formulaArg {
+ return fn.dec2x("DEC2OCT", argsList)
+}
+
+// dec2x is an implementation of the formula functions DEC2BIN, DEC2HEX and
+// DEC2OCT.
+func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
}
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name))
+ }
+ decimal := argsList.Front().Value.(formulaArg).ToNumber()
+ if decimal.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, decimal.Error)
+ }
+ maxLimitMap := map[string]float64{
+ "DEC2BIN": 511,
+ "HEX2BIN": 511,
+ "OCT2BIN": 511,
+ "BIN2HEX": 549755813887,
+ "DEC2HEX": 549755813887,
+ "OCT2HEX": 549755813887,
+ "BIN2OCT": 536870911,
+ "DEC2OCT": 536870911,
+ "HEX2OCT": 536870911,
+ }
+ minLimitMap := map[string]float64{
+ "DEC2BIN": -512,
+ "HEX2BIN": -512,
+ "OCT2BIN": -512,
+ "BIN2HEX": -549755813888,
+ "DEC2HEX": -549755813888,
+ "OCT2HEX": -549755813888,
+ "BIN2OCT": -536870912,
+ "DEC2OCT": -536870912,
+ "HEX2OCT": -536870912,
+ }
+ baseMap := map[string]int{
+ "DEC2BIN": 2,
+ "HEX2BIN": 2,
+ "OCT2BIN": 2,
+ "BIN2HEX": 16,
+ "DEC2HEX": 16,
+ "OCT2HEX": 16,
+ "BIN2OCT": 8,
+ "DEC2OCT": 8,
+ "HEX2OCT": 8,
+ }
+ maxLimit, minLimit := maxLimitMap[name], minLimitMap[name]
+ base := baseMap[name]
+ if decimal.Number < minLimit || decimal.Number > maxLimit {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ n := int64(decimal.Number)
+ binary := strconv.FormatUint(*(*uint64)(unsafe.Pointer(&n)), base)
+ if argsList.Len() == 2 {
+ places := argsList.Back().Value.(formulaArg).ToNumber()
+ if places.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, places.Error)
}
+ binaryPlaces := len(binary)
+ if places.Number < 0 || places.Number > 10 || binaryPlaces > int(places.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%s%s", strings.Repeat("0", int(places.Number)-binaryPlaces), binary)))
}
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Ceil(number))
- return
+ if decimal.Number < 0 && len(binary) > 10 {
+ return newStringFormulaArg(strings.ToUpper(binary[len(binary)-10:]))
+ }
+ return newStringFormulaArg(strings.ToUpper(binary))
+}
+
+// DELTA function tests two numbers for equality and returns the Kronecker
+// Delta. i.e. the function returns 1 if the two supplied numbers are equal
+// and 0 otherwise. The syntax of the function is:
+//
+// DELTA(number1,[number2])
+func (fn *formulaFuncs) DELTA(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DELTA requires at least 1 argument")
}
if argsList.Len() > 2 {
- if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
+ return newErrorFormulaArg(formulaErrorVALUE, "DELTA allows at most 2 arguments")
}
- val, res := math.Modf(number / significance)
- if res != 0 {
- if number > 0 {
- val++
- } else if mode < 0 {
- val--
+ number1 := argsList.Front().Value.(formulaArg).ToNumber()
+ if number1.Type != ArgNumber {
+ return number1
+ }
+ number2 := newNumberFormulaArg(0)
+ if argsList.Len() == 2 {
+ if number2 = argsList.Back().Value.(formulaArg).ToNumber(); number2.Type != ArgNumber {
+ return number2
}
}
- result = fmt.Sprintf("%g", val*significance)
- return
+ return newBoolFormulaArg(number1.Number == number2.Number).ToNumber()
}
-// CEILINGPRECISE function rounds a supplied number up (regardless of the
-// number's sign), to the nearest multiple of a given number. The syntax of
-// the function is:
+// ERF function calculates the Error Function, integrated between two supplied
+// limits. The syntax of the function is:
//
-// CEILING.PRECISE(number,[significance])
-//
-func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("CEILING.PRECISE requires at least 1 argument")
- return
+// ERF(lower_limit,[upper_limit])
+func (fn *formulaFuncs) ERF(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ERF requires at least 1 argument")
}
if argsList.Len() > 2 {
- err = errors.New("CEILING.PRECISE allows at most 2 arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "ERF allows at most 2 arguments")
}
- number, significance := 0.0, 1.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ lower := argsList.Front().Value.(formulaArg).ToNumber()
+ if lower.Type != ArgNumber {
+ return lower
}
- if number < 0 {
- significance = -1
+ if argsList.Len() == 2 {
+ upper := argsList.Back().Value.(formulaArg).ToNumber()
+ if upper.Type != ArgNumber {
+ return upper
+ }
+ return newNumberFormulaArg(math.Erf(upper.Number) - math.Erf(lower.Number))
}
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Ceil(number))
- return
+ return newNumberFormulaArg(math.Erf(lower.Number))
+}
+
+// ERFdotPRECISE function calculates the Error Function, integrated between a
+// supplied lower or upper limit and 0. The syntax of the function is:
+//
+// ERF.PRECISE(x)
+func (fn *formulaFuncs) ERFdotPRECISE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ERF.PRECISE requires 1 argument")
}
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- significance = math.Abs(significance)
- if significance == 0 {
- result = "0"
- return
- }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
}
- val, res := math.Modf(number / significance)
- if res != 0 {
- if number > 0 {
- val++
- }
+ return newNumberFormulaArg(math.Erf(x.Number))
+}
+
+// erfc is an implementation of the formula functions ERFC and ERFC.PRECISE.
+func (fn *formulaFuncs) erfc(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 1 argument", name))
}
- result = fmt.Sprintf("%g", val*significance)
- return
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ return newNumberFormulaArg(math.Erfc(x.Number))
}
-// COMBIN function calculates the number of combinations (in any order) of a
-// given number objects from a set. The syntax of the function is:
+// ERFC function calculates the Complementary Error Function, integrated
+// between a supplied lower limit and infinity. The syntax of the function
+// is:
//
-// COMBIN(number,number_chosen)
+// ERFC(x)
+func (fn *formulaFuncs) ERFC(argsList *list.List) formulaArg {
+ return fn.erfc("ERFC", argsList)
+}
+
+// ERFCdotPRECISE function calculates the Complementary Error Function,
+// integrated between a supplied lower limit and infinity. The syntax of the
+// function is:
//
-func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("COMBIN requires 2 argument")
- return
+// ERFC(x)
+func (fn *formulaFuncs) ERFCdotPRECISE(argsList *list.List) formulaArg {
+ return fn.erfc("ERFC.PRECISE", argsList)
+}
+
+// GESTEP unction tests whether a supplied number is greater than a supplied
+// step size and returns. The syntax of the function is:
+//
+// GESTEP(number,[step])
+func (fn *formulaFuncs) GESTEP(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GESTEP requires at least 1 argument")
}
- number, chosen, val := 0.0, 0.0, 1.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GESTEP allows at most 2 arguments")
}
- if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type != ArgNumber {
+ return number
}
- number, chosen = math.Trunc(number), math.Trunc(chosen)
- if chosen > number {
- err = errors.New("COMBIN requires number >= number_chosen")
- return
+ step := newNumberFormulaArg(0)
+ if argsList.Len() == 2 {
+ if step = argsList.Back().Value.(formulaArg).ToNumber(); step.Type != ArgNumber {
+ return step
+ }
}
- if chosen == number || chosen == 0 {
- result = "1"
- return
+ return newBoolFormulaArg(number.Number >= step.Number).ToNumber()
+}
+
+// HEX2BIN function converts a Hexadecimal (Base 16) number into a Binary
+// (Base 2) number. The syntax of the function is:
+//
+// HEX2BIN(number,[places])
+func (fn *formulaFuncs) HEX2BIN(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HEX2BIN requires at least 1 argument")
}
- for c := float64(1); c <= chosen; c++ {
- val *= (number + 1 - c) / c
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HEX2BIN allows at most 2 arguments")
}
- result = fmt.Sprintf("%g", math.Ceil(val))
- return
+ decimal, newList := fn.hex2dec(argsList.Front().Value.(formulaArg).Value()), list.New()
+ if decimal.Type != ArgNumber {
+ return decimal
+ }
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.dec2x("HEX2BIN", newList)
}
-// COMBINA function calculates the number of combinations, with repetitions,
-// of a given number objects from a set. The syntax of the function is:
+// HEX2DEC function converts a hexadecimal (a base-16 number) into a decimal
+// number. The syntax of the function is:
//
-// COMBINA(number,number_chosen)
+// HEX2DEC(number)
+func (fn *formulaFuncs) HEX2DEC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HEX2DEC requires 1 numeric argument")
+ }
+ return fn.hex2dec(argsList.Front().Value.(formulaArg).Value())
+}
+
+// HEX2OCT function converts a Hexadecimal (Base 16) number into an Octal
+// (Base 8) number. The syntax of the function is:
//
-func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("COMBINA requires 2 argument")
- return
+// HEX2OCT(number,[places])
+func (fn *formulaFuncs) HEX2OCT(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HEX2OCT requires at least 1 argument")
}
- var number, chosen float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HEX2OCT allows at most 2 arguments")
}
- if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ decimal, newList := fn.hex2dec(argsList.Front().Value.(formulaArg).Value()), list.New()
+ if decimal.Type != ArgNumber {
+ return decimal
}
- number, chosen = math.Trunc(number), math.Trunc(chosen)
- if number < chosen {
- err = errors.New("COMBINA requires number > number_chosen")
- return
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
}
- if number == 0 {
- result = "0"
- return
+ return fn.dec2x("HEX2OCT", newList)
+}
+
+// hex2dec is an implementation of the formula function HEX2DEC.
+func (fn *formulaFuncs) hex2dec(number string) formulaArg {
+ decimal, length := 0.0, len(number)
+ for i := length; i > 0; i-- {
+ num, err := strconv.ParseInt(string(number[length-i]), 16, 64)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ if i == 10 && string(number[length-i]) == "F" {
+ decimal += math.Pow(-16.0, float64(i-1))
+ continue
+ }
+ decimal += float64(num) * math.Pow(16.0, float64(i-1))
}
- args := list.New()
- args.PushBack(formulaArg{
- String: fmt.Sprintf("%g", number+chosen-1),
- Type: ArgString,
- })
- args.PushBack(formulaArg{
- String: fmt.Sprintf("%g", number-1),
- Type: ArgString,
- })
- return fn.COMBIN(args)
+ return newNumberFormulaArg(decimal)
}
-// COS function calculates the cosine of a given angle. The syntax of the
-// function is:
-//
-// COS(number)
+// IMABS function returns the absolute value (the modulus) of a complex
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) {
+// IMABS(inumber)
+func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("COS requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMABS requires 1 argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", math.Cos(val))
- return
+ return newNumberFormulaArg(cmplx.Abs(inumber))
}
-// COSH function calculates the hyperbolic cosine (cosh) of a supplied number.
-// The syntax of the function is:
-//
-// COSH(number)
+// IMAGINARY function returns the imaginary coefficient of a supplied complex
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) {
+// IMAGINARY(inumber)
+func (fn *formulaFuncs) IMAGINARY(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("COSH requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMAGINARY requires 1 argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", math.Cosh(val))
- return
+ return newNumberFormulaArg(imag(inumber))
}
-// COT function calculates the cotangent of a given angle. The syntax of the
-// function is:
+// IMARGUMENT function returns the phase (also called the argument) of a
+// supplied complex number. The syntax of the function is:
//
-// COT(number)
-//
-func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) {
+// IMARGUMENT(inumber)
+func (fn *formulaFuncs) IMARGUMENT(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("COT requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMARGUMENT requires 1 argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if val == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", math.Tan(val))
- return
+ return newNumberFormulaArg(cmplx.Phase(inumber))
}
-// COTH function calculates the hyperbolic cotangent (coth) of a supplied
-// angle. The syntax of the function is:
-//
-// COTH(number)
+// IMCONJUGATE function returns the complex conjugate of a supplied complex
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) {
+// IMCONJUGATE(inumber)
+func (fn *formulaFuncs) IMCONJUGATE(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("COTH requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCONJUGATE requires 1 argument")
}
- if val == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", math.Tanh(val))
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Conj(inumber), value[len(value)-1:]))
}
-// CSC function calculates the cosecant of a given angle. The syntax of the
-// function is:
-//
-// CSC(number)
+// IMCOS function returns the cosine of a supplied complex number. The syntax
+// of the function is:
//
-func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) {
+// IMCOS(inumber)
+func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("CSC requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCOS requires 1 argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if val == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", 1/math.Sin(val))
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Cos(inumber), value[len(value)-1:]))
}
-// CSCH function calculates the hyperbolic cosecant (csch) of a supplied
-// angle. The syntax of the function is:
-//
-// CSCH(number)
+// IMCOSH function returns the hyperbolic cosine of a supplied complex number. The syntax
+// of the function is:
//
-func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) {
+// IMCOSH(inumber)
+func (fn *formulaFuncs) IMCOSH(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("CSCH requires 1 numeric argument")
- return
- }
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCOSH requires 1 argument")
}
- if val == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", 1/math.Sinh(val))
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Cosh(inumber), value[len(value)-1:]))
}
-// DECIMAL function converts a text representation of a number in a specified
-// base, into a decimal value. The syntax of the function is:
-//
-// DECIMAL(text,radix)
+// IMCOT function returns the cotangent of a supplied complex number. The syntax
+// of the function is:
//
-func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("DECIMAL requires 2 numeric arguments")
- return
- }
- var text = argsList.Front().Value.(formulaArg).String
- var radix int
- if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) {
- text = text[2:]
+// IMCOT(inumber)
+func (fn *formulaFuncs) IMCOT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCOT requires 1 argument")
}
- val, err := strconv.ParseInt(text, radix, 64)
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
if err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", float64(val))
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Cot(inumber), value[len(value)-1:]))
}
-// DEGREES function converts radians into degrees. The syntax of the function
-// is:
+// IMCSC function returns the cosecant of a supplied complex number. The syntax
+// of the function is:
//
-// DEGREES(angle)
-//
-func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) {
+// IMCSC(inumber)
+func (fn *formulaFuncs) IMCSC(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("DEGREES requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCSC requires 1 argument")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if val == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ num := 1 / cmplx.Sin(inumber)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = fmt.Sprintf("%g", 180.0/math.Pi*val)
- return
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
}
-// EVEN function rounds a supplied number away from zero (i.e. rounds a
-// positive number up and a negative number down), to the next even number.
-// The syntax of the function is:
-//
-// EVEN(number)
+// IMCSCH function returns the hyperbolic cosecant of a supplied complex
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) {
+// IMCSCH(inumber)
+func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("EVEN requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMCSCH requires 1 argument")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- sign := math.Signbit(number)
- m, frac := math.Modf(number / 2)
- val := m * 2
- if frac != 0 {
- if !sign {
- val += 2
- } else {
- val -= 2
- }
+ num := 1 / cmplx.Sinh(inumber)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = fmt.Sprintf("%g", val)
- return
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
}
-// EXP function calculates the value of the mathematical constant e, raised to
-// the power of a given number. The syntax of the function is:
+// IMDIV function calculates the quotient of two complex numbers (i.e. divides
+// one complex number by another). The syntax of the function is:
//
-// EXP(number)
+// IMDIV(inumber1,inumber2)
+func (fn *formulaFuncs) IMDIV(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMDIV requires 2 arguments")
+ }
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber1, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ inumber2, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ num := inumber1 / inumber2
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
+}
+
+// IMEXP function returns the exponential of a supplied complex number. The
+// syntax of the function is:
//
-func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) {
+// IMEXP(inumber)
+func (fn *formulaFuncs) IMEXP(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("EXP requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMEXP requires 1 argument")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number)))
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Exp(inumber), value[len(value)-1:]))
}
-// fact returns the factorial of a supplied number.
-func fact(number float64) float64 {
- val := float64(1)
- for i := float64(2); i <= number; i++ {
- val *= i
+// IMLN function returns the natural logarithm of a supplied complex number.
+// The syntax of the function is:
+//
+// IMLN(inumber)
+func (fn *formulaFuncs) IMLN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMLN requires 1 argument")
}
- return val
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ num := cmplx.Log(inumber)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
}
-// FACT function returns the factorial of a supplied number. The syntax of the
-// function is:
+// IMLOG10 function returns the common (base 10) logarithm of a supplied
+// complex number. The syntax of the function is:
//
-// FACT(number)
-//
-func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) {
+// IMLOG10(inumber)
+func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("FACT requires 1 numeric argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMLOG10 requires 1 argument")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if number < 0 {
- err = errors.New(formulaErrorNUM)
+ num := cmplx.Log10(inumber)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = strings.ToUpper(fmt.Sprintf("%g", fact(number)))
- return
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
}
-// FACTDOUBLE function returns the double factorial of a supplied number. The
-// syntax of the function is:
-//
-// FACTDOUBLE(number)
+// IMLOG2 function calculates the base 2 logarithm of a supplied complex
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err error) {
+// IMLOG2(inumber)
+func (fn *formulaFuncs) IMLOG2(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("FACTDOUBLE requires 1 numeric argument")
- return
- }
- number, val := 0.0, 1.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMLOG2 requires 1 argument")
}
- if number < 0 {
- err = errors.New(formulaErrorNUM)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- for i := math.Trunc(number); i > 1; i -= 2 {
- val *= i
+ num := cmplx.Log(inumber)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = strings.ToUpper(fmt.Sprintf("%g", val))
- return
+ return newStringFormulaArg(cmplx2str(num/cmplx.Log(2), value[len(value)-1:]))
}
-// FLOOR function rounds a supplied number towards zero to the nearest
-// multiple of a specified significance. The syntax of the function is:
+// IMPOWER function returns a supplied complex number, raised to a given
+// power. The syntax of the function is:
//
-// FLOOR(number,significance)
-//
-func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) {
+// IMPOWER(inumber,number)
+func (fn *formulaFuncs) IMPOWER(argsList *list.List) formulaArg {
if argsList.Len() != 2 {
- err = errors.New("FLOOR requires 2 numeric arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMPOWER requires 2 arguments")
}
- var number, significance float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ number, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if significance < 0 && number >= 0 {
- err = errors.New(formulaErrorNUM)
- return
+ if inumber == 0 && number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- val := number
- val, res := math.Modf(val / significance)
- if res != 0 {
- if number < 0 && res < 0 {
- val--
- }
+ num := cmplx.Pow(inumber, number)
+ if cmplx.IsInf(num) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = strings.ToUpper(fmt.Sprintf("%g", val*significance))
- return
+ return newStringFormulaArg(cmplx2str(num, value[len(value)-1:]))
}
-// FLOORMATH function rounds a supplied number down to a supplied multiple of
-// significance. The syntax of the function is:
-//
-// FLOOR.MATH(number,[significance],[mode])
+// IMPRODUCT function calculates the product of two or more complex numbers.
+// The syntax of the function is:
//
-func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("FLOOR.MATH requires at least 1 argument")
- return
- }
- if argsList.Len() > 3 {
- err = errors.New("FLOOR.MATH allows at most 3 arguments")
- return
+// IMPRODUCT(number1,[number2],...)
+func (fn *formulaFuncs) IMPRODUCT(argsList *list.List) formulaArg {
+ product := complex128(1)
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ if token.Value() == "" {
+ continue
+ }
+ val, err := strconv.ParseComplex(str2cmplx(token.Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ product = product * val
+ case ArgNumber:
+ product = product * complex(token.Number, 0)
+ case ArgMatrix:
+ for _, row := range token.Matrix {
+ for _, value := range row {
+ if value.Value() == "" {
+ continue
+ }
+ val, err := strconv.ParseComplex(str2cmplx(value.Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
+ }
+ product = product * val
+ }
+ }
+ }
}
- number, significance, mode := 0.0, 1.0, 1.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newStringFormulaArg(cmplx2str(product, "i"))
+}
+
+// IMREAL function returns the real coefficient of a supplied complex number.
+// The syntax of the function is:
+//
+// IMREAL(inumber)
+func (fn *formulaFuncs) IMREAL(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMREAL requires 1 argument")
}
- if number < 0 {
- significance = -1
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
+ return newStringFormulaArg(fmt.Sprint(real(inumber)))
+}
+
+// IMSEC function returns the secant of a supplied complex number. The syntax
+// of the function is:
+//
+// IMSEC(inumber)
+func (fn *formulaFuncs) IMSEC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSEC requires 1 argument")
}
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Floor(number))
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if argsList.Len() > 2 {
- if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
+ return newStringFormulaArg(cmplx2str(1/cmplx.Cos(inumber), value[len(value)-1:]))
+}
+
+// IMSECH function returns the hyperbolic secant of a supplied complex number.
+// The syntax of the function is:
+//
+// IMSECH(inumber)
+func (fn *formulaFuncs) IMSECH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSECH requires 1 argument")
}
- val, res := math.Modf(number / significance)
- if res != 0 && number < 0 && mode > 0 {
- val--
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", val*significance)
- return
+ return newStringFormulaArg(cmplx2str(1/cmplx.Cosh(inumber), value[len(value)-1:]))
}
-// FLOORPRECISE function rounds a supplied number down to a supplied multiple
-// of significance. The syntax of the function is:
-//
-// FLOOR.PRECISE(number,[significance])
+// IMSIN function returns the Sine of a supplied complex number. The syntax of
+// the function is:
//
-func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("FLOOR.PRECISE requires at least 1 argument")
- return
- }
- if argsList.Len() > 2 {
- err = errors.New("FLOOR.PRECISE allows at most 2 arguments")
- return
+// IMSIN(inumber)
+func (fn *formulaFuncs) IMSIN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSIN requires 1 argument")
}
- var number, significance float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if number < 0 {
- significance = -1
+ return newStringFormulaArg(cmplx2str(cmplx.Sin(inumber), value[len(value)-1:]))
+}
+
+// IMSINH function returns the hyperbolic sine of a supplied complex number.
+// The syntax of the function is:
+//
+// IMSINH(inumber)
+func (fn *formulaFuncs) IMSINH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSINH requires 1 argument")
}
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Floor(number))
- return
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- significance = math.Abs(significance)
- if significance == 0 {
- result = "0"
- return
- }
+ return newStringFormulaArg(cmplx2str(cmplx.Sinh(inumber), value[len(value)-1:]))
+}
+
+// IMSQRT function returns the square root of a supplied complex number. The
+// syntax of the function is:
+//
+// IMSQRT(inumber)
+func (fn *formulaFuncs) IMSQRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSQRT requires 1 argument")
}
- val, res := math.Modf(number / significance)
- if res != 0 {
- if number < 0 {
- val--
- }
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", val*significance)
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Sqrt(inumber), value[len(value)-1:]))
}
-// gcd returns the greatest common divisor of two supplied integers.
-func gcd(x, y float64) float64 {
- x, y = math.Trunc(x), math.Trunc(y)
- if x == 0 {
- return y
+// IMSUB function calculates the difference between two complex numbers
+// (i.e. subtracts one complex number from another). The syntax of the
+// function is:
+//
+// IMSUB(inumber1,inumber2)
+func (fn *formulaFuncs) IMSUB(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSUB requires 2 arguments")
}
- if y == 0 {
- return x
+ i1, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- for x != y {
- if x > y {
- x = x - y
- } else {
- y = y - x
- }
+ i2, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- return x
+ return newStringFormulaArg(cmplx2str(i1-i2, "i"))
}
-// GCD function returns the greatest common divisor of two or more supplied
-// integers. The syntax of the function is:
-//
-// GCD(number1,[number2],...)
+// IMSUM function calculates the sum of two or more complex numbers. The
+// syntax of the function is:
//
-func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("GCD requires at least 1 argument")
- return
+// IMSUM(inumber1,inumber2,...)
+func (fn *formulaFuncs) IMSUM(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IMSUM requires at least 1 argument")
}
- var (
- val float64
- nums = []float64{}
- )
+ var result complex128
for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg).String
- if token == "" {
- continue
- }
- if val, err = strconv.ParseFloat(token, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- nums = append(nums, val)
- }
- if nums[0] < 0 {
- err = errors.New("GCD only accepts positive arguments")
- return
- }
- if len(nums) == 1 {
- result = fmt.Sprintf("%g", nums[0])
- return
- }
- cd := nums[0]
- for i := 1; i < len(nums); i++ {
- if nums[i] < 0 {
- err = errors.New("GCD only accepts positive arguments")
- return
+ token := arg.Value.(formulaArg)
+ num, err := strconv.ParseComplex(str2cmplx(token.Value()), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- cd = gcd(cd, nums[i])
+ result += num
}
- result = fmt.Sprintf("%g", cd)
- return
+ return newStringFormulaArg(cmplx2str(result, "i"))
}
-// INT function truncates a supplied number down to the closest integer. The
-// syntax of the function is:
-//
-// INT(number)
+// IMTAN function returns the tangent of a supplied complex number. The syntax
+// of the function is:
//
-func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) {
+// IMTAN(inumber)
+func (fn *formulaFuncs) IMTAN(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
- err = errors.New("INT requires 1 numeric argument")
- return
- }
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "IMTAN requires 1 argument")
}
- val, frac := math.Modf(number)
- if frac < 0 {
- val--
+ value := argsList.Front().Value.(formulaArg).Value()
+ inumber, err := strconv.ParseComplex(str2cmplx(value), 128)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorNUM, err.Error())
}
- result = fmt.Sprintf("%g", val)
- return
+ return newStringFormulaArg(cmplx2str(cmplx.Tan(inumber), value[len(value)-1:]))
}
-// ISOCEILING function rounds a supplied number up (regardless of the number's
-// sign), to the nearest multiple of a supplied significance. The syntax of
-// the function is:
-//
-// ISO.CEILING(number,[significance])
+// OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2)
+// number. The syntax of the function is:
//
-func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("ISO.CEILING requires at least 1 argument")
- return
+// OCT2BIN(number,[places])
+func (fn *formulaFuncs) OCT2BIN(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OCT2BIN requires at least 1 argument")
}
if argsList.Len() > 2 {
- err = errors.New("ISO.CEILING allows at most 2 arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "OCT2BIN allows at most 2 arguments")
}
- var number, significance float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if number < 0 {
- significance = -1
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
}
- if argsList.Len() == 1 {
- result = fmt.Sprintf("%g", math.Ceil(number))
- return
+ decimal, newList := fn.oct2dec(token.Value()), list.New()
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
}
- if argsList.Len() > 1 {
- if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return fn.dec2x("OCT2BIN", newList)
+}
+
+// OCT2DEC function converts an Octal (a base-8 number) into a decimal number.
+// The syntax of the function is:
+//
+// OCT2DEC(number)
+func (fn *formulaFuncs) OCT2DEC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OCT2DEC requires 1 numeric argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
+ }
+ return fn.oct2dec(token.Value())
+}
+
+// OCT2HEX function converts an Octal (Base 8) number into a Hexadecimal
+// (Base 16) number. The syntax of the function is:
+//
+// OCT2HEX(number,[places])
+func (fn *formulaFuncs) OCT2HEX(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OCT2HEX requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OCT2HEX allows at most 2 arguments")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ number := token.ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, number.Error)
+ }
+ decimal, newList := fn.oct2dec(token.Value()), list.New()
+ newList.PushBack(decimal)
+ if argsList.Len() == 2 {
+ newList.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.dec2x("OCT2HEX", newList)
+}
+
+// oct2dec is an implementation of the formula function OCT2DEC.
+func (fn *formulaFuncs) oct2dec(number string) formulaArg {
+ decimal, length := 0.0, len(number)
+ for i := length; i > 0; i-- {
+ num, _ := strconv.Atoi(string(number[length-i]))
+ if i == 10 && string(number[length-i]) == "7" {
+ decimal += math.Pow(-8.0, float64(i-1))
+ continue
+ }
+ decimal += float64(num) * math.Pow(8.0, float64(i-1))
+ }
+ return newNumberFormulaArg(decimal)
+}
+
+// Math and Trigonometric Functions
+
+// ABS function returns the absolute value of any supplied number. The syntax
+// of the function is:
+//
+// ABS(number)
+func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ABS requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Abs(arg.Number))
+}
+
+// ACOS function calculates the arccosine (i.e. the inverse cosine) of a given
+// number, and returns an angle, in radians, between 0 and π. The syntax of
+// the function is:
+//
+// ACOS(number)
+func (fn *formulaFuncs) ACOS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACOS requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Acos(arg.Number))
+}
+
+// ACOSH function calculates the inverse hyperbolic cosine of a supplied number.
+// of the function is:
+//
+// ACOSH(number)
+func (fn *formulaFuncs) ACOSH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACOSH requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Acosh(arg.Number))
+}
+
+// ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a
+// given number, and returns an angle, in radians, between 0 and π. The syntax
+// of the function is:
+//
+// ACOT(number)
+func (fn *formulaFuncs) ACOT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACOT requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Pi/2 - math.Atan(arg.Number))
+}
+
+// ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied
+// value. The syntax of the function is:
+//
+// ACOTH(number)
+func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACOTH requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Atanh(1 / arg.Number))
+}
+
+// AGGREGATE function returns the result of a specified operation or function,
+// applied to a list or database of values. The syntax of the function is:
+//
+// AGGREGATE(function_num,options,ref1,[ref2],...)
+func (fn *formulaFuncs) AGGREGATE(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE requires at least 3 arguments")
+ }
+ var fnNum, opts formulaArg
+ if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
+ return fnNum
+ }
+ subFn, ok := map[int]func(argsList *list.List) formulaArg{
+ 1: fn.AVERAGE,
+ 2: fn.COUNT,
+ 3: fn.COUNTA,
+ 4: fn.MAX,
+ 5: fn.MIN,
+ 6: fn.PRODUCT,
+ 7: fn.STDEVdotS,
+ 8: fn.STDEVdotP,
+ 9: fn.SUM,
+ 10: fn.VARdotS,
+ 11: fn.VARdotP,
+ 12: fn.MEDIAN,
+ 13: fn.MODEdotSNGL,
+ 14: fn.LARGE,
+ 15: fn.SMALL,
+ 16: fn.PERCENTILEdotINC,
+ 17: fn.QUARTILEdotINC,
+ 18: fn.PERCENTILEdotEXC,
+ 19: fn.QUARTILEdotEXC,
+ }[int(fnNum.Number)]
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid function_num")
+ }
+ if opts = argsList.Front().Next().Value.(formulaArg).ToNumber(); opts.Type != ArgNumber {
+ return opts
+ }
+ // TODO: apply option argument values to be ignored during the calculation
+ if int(opts.Number) < 0 || int(opts.Number) > 7 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AGGREGATE has invalid options")
+ }
+ subArgList := list.New().Init()
+ for arg := argsList.Front().Next().Next(); arg != nil; arg = arg.Next() {
+ subArgList.PushBack(arg.Value.(formulaArg))
+ }
+ return subFn(subArgList)
+}
+
+// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax
+// of the function is:
+//
+// ARABIC(text)
+func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument")
+ }
+ text := argsList.Front().Value.(formulaArg).Value()
+ if len(text) > MaxFieldLength {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ text = strings.ToUpper(text)
+ number, actualStart, index, isNegative := 0, 0, len(text)-1, false
+ startIndex, subtractNumber, currentPartValue, currentCharValue, prevCharValue := 0, 0, 0, 0, -1
+ for index >= 0 && text[index] == ' ' {
+ index--
+ }
+ for actualStart <= index && text[actualStart] == ' ' {
+ actualStart++
+ }
+ if actualStart <= index && text[actualStart] == '-' {
+ isNegative = true
+ actualStart++
+ }
+ charMap := map[rune]int{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
+ for index >= actualStart {
+ startIndex = index
+ startChar := text[startIndex]
+ index--
+ for index >= actualStart && (text[index]|' ') == startChar {
+ index--
+ }
+ currentCharValue = charMap[rune(startChar)]
+ currentPartValue = (startIndex - index) * currentCharValue
+ if currentCharValue >= prevCharValue {
+ number += currentPartValue - subtractNumber
+ prevCharValue = currentCharValue
+ subtractNumber = 0
+ continue
+ }
+ subtractNumber += currentPartValue
+ }
+ if subtractNumber != 0 {
+ number -= subtractNumber
+ }
+ if isNegative {
+ number = -number
+ }
+ return newNumberFormulaArg(float64(number))
+}
+
+// ASIN function calculates the arcsine (i.e. the inverse sine) of a given
+// number, and returns an angle, in radians, between -π/2 and π/2. The syntax
+// of the function is:
+//
+// ASIN(number)
+func (fn *formulaFuncs) ASIN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ASIN requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Asin(arg.Number))
+}
+
+// ASINH function calculates the inverse hyperbolic sine of a supplied number.
+// The syntax of the function is:
+//
+// ASINH(number)
+func (fn *formulaFuncs) ASINH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ASINH requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Asinh(arg.Number))
+}
+
+// ATAN function calculates the arctangent (i.e. the inverse tangent) of a
+// given number, and returns an angle, in radians, between -π/2 and +π/2. The
+// syntax of the function is:
+//
+// ATAN(number)
+func (fn *formulaFuncs) ATAN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ATAN requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Atan(arg.Number))
+}
+
+// ATANH function calculates the inverse hyperbolic tangent of a supplied
+// number. The syntax of the function is:
+//
+// ATANH(number)
+func (fn *formulaFuncs) ATANH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ATANH requires 1 numeric argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type == ArgError {
+ return arg
+ }
+ return newNumberFormulaArg(math.Atanh(arg.Number))
+}
+
+// ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a
+// given set of x and y coordinates, and returns an angle, in radians, between
+// -π/2 and +π/2. The syntax of the function is:
+//
+// ATAN2(x_num,y_num)
+func (fn *formulaFuncs) ATAN2(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ATAN2 requires 2 numeric arguments")
+ }
+ x := argsList.Back().Value.(formulaArg).ToNumber()
+ if x.Type == ArgError {
+ return x
+ }
+ y := argsList.Front().Value.(formulaArg).ToNumber()
+ if y.Type == ArgError {
+ return y
+ }
+ return newNumberFormulaArg(math.Atan2(x.Number, y.Number))
+}
+
+// BASE function converts a number into a supplied base (radix), and returns a
+// text representation of the calculated value. The syntax of the function is:
+//
+// BASE(number,radix,[min_length])
+func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BASE requires at least 2 arguments")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BASE allows at most 3 arguments")
+ }
+ var minLength int
+ var err error
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ radix := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if radix.Type == ArgError {
+ return radix
+ }
+ if int(radix.Number) < 2 || int(radix.Number) > 36 {
+ return newErrorFormulaArg(formulaErrorVALUE, "radix must be an integer >= 2 and <= 36")
+ }
+ if argsList.Len() > 2 {
+ if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value()); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ }
+ result := strconv.FormatInt(int64(number.Number), int(radix.Number))
+ if len(result) < minLength {
+ result = strings.Repeat("0", minLength-len(result)) + result
+ }
+ return newStringFormulaArg(strings.ToUpper(result))
+}
+
+// CEILING function rounds a supplied number away from zero, to the nearest
+// multiple of a given number. The syntax of the function is:
+//
+// CEILING(number,significance)
+func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING allows at most 2 arguments")
+ }
+ number, significance, res := 0.0, 1.0, 0.0
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ number = n.Number
+ if number < 0 {
+ significance = -1
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Back().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
+ }
+ if significance < 0 && number > 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "negative sig to CEILING invalid")
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Ceil(number))
+ }
+ number, res = math.Modf(number / significance)
+ if res > 0 {
+ number++
+ }
+ return newNumberFormulaArg(number * significance)
+}
+
+// CEILINGdotMATH function rounds a supplied number up to a supplied multiple
+// of significance. The syntax of the function is:
+//
+// CEILING.MATH(number,[significance],[mode])
+func (fn *formulaFuncs) CEILINGdotMATH(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH requires at least 1 argument")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH allows at most 3 arguments")
+ }
+ number, significance, mode := 0.0, 1.0, 1.0
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ number = n.Number
+ if number < 0 {
+ significance = -1
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Ceil(number))
+ }
+ if argsList.Len() > 2 {
+ m := argsList.Back().Value.(formulaArg).ToNumber()
+ if m.Type == ArgError {
+ return m
}
+ mode = m.Number
+ }
+ val, res := math.Modf(number / significance)
+ if res != 0 {
+ if number > 0 {
+ val++
+ } else if mode < 0 {
+ val--
+ }
+ }
+ return newNumberFormulaArg(val * significance)
+}
+
+// CEILINGdotPRECISE function rounds a supplied number up (regardless of the
+// number's sign), to the nearest multiple of a given number. The syntax of
+// the function is:
+//
+// CEILING.PRECISE(number,[significance])
+func (fn *formulaFuncs) CEILINGdotPRECISE(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE allows at most 2 arguments")
+ }
+ number, significance := 0.0, 1.0
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ number = n.Number
+ if number < 0 {
+ significance = -1
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Ceil(number))
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Back().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
significance = math.Abs(significance)
if significance == 0 {
- result = "0"
- return
+ return newNumberFormulaArg(significance)
}
}
val, res := math.Modf(number / significance)
@@ -1812,1336 +4037,14810 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro
val++
}
}
- result = fmt.Sprintf("%g", val*significance)
- return
+ return newNumberFormulaArg(val * significance)
+}
+
+// COMBIN function calculates the number of combinations (in any order) of a
+// given number objects from a set. The syntax of the function is:
+//
+// COMBIN(number,number_chosen)
+func (fn *formulaFuncs) COMBIN(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires 2 argument")
+ }
+ number, chosen, val := 0.0, 0.0, 1.0
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ number = n.Number
+ c := argsList.Back().Value.(formulaArg).ToNumber()
+ if c.Type == ArgError {
+ return c
+ }
+ chosen = c.Number
+ number, chosen = math.Trunc(number), math.Trunc(chosen)
+ if chosen > number {
+ return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires number >= number_chosen")
+ }
+ if chosen == number || chosen == 0 {
+ return newNumberFormulaArg(1)
+ }
+ for c := float64(1); c <= chosen; c++ {
+ val *= (number + 1 - c) / c
+ }
+ return newNumberFormulaArg(math.Ceil(val))
+}
+
+// COMBINA function calculates the number of combinations, with repetitions,
+// of a given number objects from a set. The syntax of the function is:
+//
+// COMBINA(number,number_chosen)
+func (fn *formulaFuncs) COMBINA(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires 2 argument")
+ }
+ var number, chosen float64
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ number = n.Number
+ c := argsList.Back().Value.(formulaArg).ToNumber()
+ if c.Type == ArgError {
+ return c
+ }
+ chosen = c.Number
+ number, chosen = math.Trunc(number), math.Trunc(chosen)
+ if number < chosen {
+ return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires number > number_chosen")
+ }
+ if number == 0 {
+ return newNumberFormulaArg(number)
+ }
+ args := list.New()
+ args.PushBack(formulaArg{
+ String: fmt.Sprintf("%g", number+chosen-1),
+ Type: ArgString,
+ })
+ args.PushBack(formulaArg{
+ String: fmt.Sprintf("%g", number-1),
+ Type: ArgString,
+ })
+ return fn.COMBIN(args)
+}
+
+// COS function calculates the cosine of a given angle. The syntax of the
+// function is:
+//
+// COS(number)
+func (fn *formulaFuncs) COS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COS requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ return newNumberFormulaArg(math.Cos(val.Number))
+}
+
+// COSH function calculates the hyperbolic cosine (cosh) of a supplied number.
+// The syntax of the function is:
+//
+// COSH(number)
+func (fn *formulaFuncs) COSH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COSH requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ return newNumberFormulaArg(math.Cosh(val.Number))
+}
+
+// COT function calculates the cotangent of a given angle. The syntax of the
+// function is:
+//
+// COT(number)
+func (fn *formulaFuncs) COT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COT requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(1 / math.Tan(val.Number))
+}
+
+// COTH function calculates the hyperbolic cotangent (coth) of a supplied
+// angle. The syntax of the function is:
+//
+// COTH(number)
+func (fn *formulaFuncs) COTH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COTH requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg((math.Exp(val.Number) + math.Exp(-val.Number)) / (math.Exp(val.Number) - math.Exp(-val.Number)))
+}
+
+// CSC function calculates the cosecant of a given angle. The syntax of the
+// function is:
+//
+// CSC(number)
+func (fn *formulaFuncs) CSC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CSC requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(1 / math.Sin(val.Number))
+}
+
+// CSCH function calculates the hyperbolic cosecant (csch) of a supplied
+// angle. The syntax of the function is:
+//
+// CSCH(number)
+func (fn *formulaFuncs) CSCH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CSCH requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(1 / math.Sinh(val.Number))
+}
+
+// DECIMAL function converts a text representation of a number in a specified
+// base, into a decimal value. The syntax of the function is:
+//
+// DECIMAL(text,radix)
+func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DECIMAL requires 2 numeric arguments")
+ }
+ text := argsList.Front().Value.(formulaArg).Value()
+ var err error
+ radix := argsList.Back().Value.(formulaArg).ToNumber()
+ if radix.Type != ArgNumber {
+ return radix
+ }
+ if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) {
+ text = text[2:]
+ }
+ val, err := strconv.ParseInt(text, int(radix.Number), 64)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ return newNumberFormulaArg(float64(val))
+}
+
+// DEGREES function converts radians into degrees. The syntax of the function
+// is:
+//
+// DEGREES(angle)
+func (fn *formulaFuncs) DEGREES(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DEGREES requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(180.0 / math.Pi * val.Number)
+}
+
+// EVEN function rounds a supplied number away from zero (i.e. rounds a
+// positive number up and a negative number down), to the next even number.
+// The syntax of the function is:
+//
+// EVEN(number)
+func (fn *formulaFuncs) EVEN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EVEN requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ sign := math.Signbit(number.Number)
+ m, frac := math.Modf(number.Number / 2)
+ val := m * 2
+ if frac != 0 {
+ if !sign {
+ val += 2
+ } else {
+ val -= 2
+ }
+ }
+ return newNumberFormulaArg(val)
+}
+
+// EXP function calculates the value of the mathematical constant e, raised to
+// the power of a given number. The syntax of the function is:
+//
+// EXP(number)
+func (fn *formulaFuncs) EXP(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EXP requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Exp(number.Number))
+}
+
+// fact returns the factorial of a supplied number.
+func fact(number float64) float64 {
+ val := float64(1)
+ for i := float64(2); i <= number; i++ {
+ val *= i
+ }
+ return val
+}
+
+// FACT function returns the factorial of a supplied number. The syntax of the
+// function is:
+//
+// FACT(number)
+func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FACT requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(fact(number.Number))
+}
+
+// FACTDOUBLE function returns the double factorial of a supplied number. The
+// syntax of the function is:
+//
+// FACTDOUBLE(number)
+func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FACTDOUBLE requires 1 numeric argument")
+ }
+ val := 1.0
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ for i := math.Trunc(number.Number); i > 1; i -= 2 {
+ val *= i
+ }
+ return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val)))
+}
+
+// FLOOR function rounds a supplied number towards zero to the nearest
+// multiple of a specified significance. The syntax of the function is:
+//
+// FLOOR(number,significance)
+func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FLOOR requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ significance := argsList.Back().Value.(formulaArg).ToNumber()
+ if significance.Type == ArgError {
+ return significance
+ }
+ if significance.Number < 0 && number.Number >= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "invalid arguments to FLOOR")
+ }
+ val := number.Number
+ val, res := math.Modf(val / significance.Number)
+ if res != 0 {
+ if number.Number < 0 && res < 0 {
+ val--
+ }
+ }
+ return newNumberFormulaArg(val * significance.Number)
+}
+
+// FLOORdotMATH function rounds a supplied number down to a supplied multiple
+// of significance. The syntax of the function is:
+//
+// FLOOR.MATH(number,[significance],[mode])
+func (fn *formulaFuncs) FLOORdotMATH(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH requires at least 1 argument")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH allows at most 3 arguments")
+ }
+ significance, mode := 1.0, 1.0
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number < 0 {
+ significance = -1
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Floor(number.Number))
+ }
+ if argsList.Len() > 2 {
+ m := argsList.Back().Value.(formulaArg).ToNumber()
+ if m.Type == ArgError {
+ return m
+ }
+ mode = m.Number
+ }
+ val, res := math.Modf(number.Number / significance)
+ if res != 0 && number.Number < 0 && mode > 0 {
+ val--
+ }
+ return newNumberFormulaArg(val * significance)
+}
+
+// FLOORdotPRECISE function rounds a supplied number down to a supplied
+// multiple of significance. The syntax of the function is:
+//
+// FLOOR.PRECISE(number,[significance])
+func (fn *formulaFuncs) FLOORdotPRECISE(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE allows at most 2 arguments")
+ }
+ var significance float64
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number < 0 {
+ significance = -1
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Floor(number.Number))
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Back().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
+ significance = math.Abs(significance)
+ if significance == 0 {
+ return newNumberFormulaArg(significance)
+ }
+ }
+ val, res := math.Modf(number.Number / significance)
+ if res != 0 {
+ if number.Number < 0 {
+ val--
+ }
+ }
+ return newNumberFormulaArg(val * significance)
+}
+
+// gcd returns the greatest common divisor of two supplied integers.
+func gcd(x, y float64) float64 {
+ x, y = math.Trunc(x), math.Trunc(y)
+ if x == 0 {
+ return y
+ }
+ if y == 0 {
+ return x
+ }
+ for x != y {
+ if x > y {
+ x = x - y
+ } else {
+ y = y - x
+ }
+ }
+ return x
+}
+
+// GCD function returns the greatest common divisor of two or more supplied
+// integers. The syntax of the function is:
+//
+// GCD(number1,[number2],...)
+func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GCD requires at least 1 argument")
+ }
+ var (
+ val float64
+ nums []float64
+ )
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ num := token.ToNumber()
+ if num.Type == ArgError {
+ return num
+ }
+ val = num.Number
+ case ArgNumber:
+ val = token.Number
+ }
+ nums = append(nums, val)
+ }
+ if nums[0] < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GCD only accepts positive arguments")
+ }
+ if len(nums) == 1 {
+ return newNumberFormulaArg(nums[0])
+ }
+ cd := nums[0]
+ for i := 1; i < len(nums); i++ {
+ if nums[i] < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GCD only accepts positive arguments")
+ }
+ cd = gcd(cd, nums[i])
+ }
+ return newNumberFormulaArg(cd)
+}
+
+// INT function truncates a supplied number down to the closest integer. The
+// syntax of the function is:
+//
+// INT(number)
+func (fn *formulaFuncs) INT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "INT requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ val, frac := math.Modf(number.Number)
+ if frac < 0 {
+ val--
+ }
+ return newNumberFormulaArg(val)
+}
+
+// ISOdotCEILING function rounds a supplied number up (regardless of the
+// number's sign), to the nearest multiple of a supplied significance. The
+// syntax of the function is:
+//
+// ISO.CEILING(number,[significance])
+func (fn *formulaFuncs) ISOdotCEILING(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING allows at most 2 arguments")
+ }
+ var significance float64
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number < 0 {
+ significance = -1
+ }
+ if argsList.Len() == 1 {
+ return newNumberFormulaArg(math.Ceil(number.Number))
+ }
+ if argsList.Len() > 1 {
+ s := argsList.Back().Value.(formulaArg).ToNumber()
+ if s.Type == ArgError {
+ return s
+ }
+ significance = s.Number
+ significance = math.Abs(significance)
+ if significance == 0 {
+ return newNumberFormulaArg(significance)
+ }
+ }
+ val, res := math.Modf(number.Number / significance)
+ if res != 0 {
+ if number.Number > 0 {
+ val++
+ }
+ }
+ return newNumberFormulaArg(val * significance)
+}
+
+// lcm returns the least common multiple of two supplied integers.
+func lcm(a, b float64) float64 {
+ a = math.Trunc(a)
+ b = math.Trunc(b)
+ if a == 0 && b == 0 {
+ return 0
+ }
+ return a * b / gcd(a, b)
+}
+
+// LCM function returns the least common multiple of two or more supplied
+// integers. The syntax of the function is:
+//
+// LCM(number1,[number2],...)
+func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LCM requires at least 1 argument")
+ }
+ var (
+ val float64
+ nums []float64
+ err error
+ )
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ if token.String == "" {
+ continue
+ }
+ if val, err = strconv.ParseFloat(token.String, 64); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ case ArgNumber:
+ val = token.Number
+ }
+ nums = append(nums, val)
+ }
+ if nums[0] < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LCM only accepts positive arguments")
+ }
+ if len(nums) == 1 {
+ return newNumberFormulaArg(nums[0])
+ }
+ cm := nums[0]
+ for i := 1; i < len(nums); i++ {
+ if nums[i] < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LCM only accepts positive arguments")
+ }
+ cm = lcm(cm, nums[i])
+ }
+ return newNumberFormulaArg(cm)
+}
+
+// LN function calculates the natural logarithm of a given number. The syntax
+// of the function is:
+//
+// LN(number)
+func (fn *formulaFuncs) LN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LN requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Log(number.Number))
+}
+
+// LOG function calculates the logarithm of a given number, to a supplied
+// base. The syntax of the function is:
+//
+// LOG(number,[base])
+func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOG requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOG allows at most 2 arguments")
+ }
+ base := 10.0
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if argsList.Len() > 1 {
+ b := argsList.Back().Value.(formulaArg).ToNumber()
+ if b.Type == ArgError {
+ return b
+ }
+ base = b.Number
+ }
+ if number.Number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorDIV)
+ }
+ if base == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorDIV)
+ }
+ if base == 1 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(math.Log(number.Number) / math.Log(base))
+}
+
+// LOG10 function calculates the base 10 logarithm of a given number. The
+// syntax of the function is:
+//
+// LOG10(number)
+func (fn *formulaFuncs) LOG10(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOG10 requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Log10(number.Number))
+}
+
+// minor function implement a minor of a matrix A is the determinant of some
+// smaller square matrix.
+func minor(sqMtx [][]float64, idx int) [][]float64 {
+ var ret [][]float64
+ for i := range sqMtx {
+ if i == 0 {
+ continue
+ }
+ var row []float64
+ for j := range sqMtx {
+ if j == idx {
+ continue
+ }
+ row = append(row, sqMtx[i][j])
+ }
+ ret = append(ret, row)
+ }
+ return ret
+}
+
+// det determinant of the 2x2 matrix.
+func det(sqMtx [][]float64) float64 {
+ if len(sqMtx) == 2 {
+ m00 := sqMtx[0][0]
+ m01 := sqMtx[0][1]
+ m10 := sqMtx[1][0]
+ m11 := sqMtx[1][1]
+ return m00*m11 - m10*m01
+ }
+ var res, sgn float64 = 0, 1
+ for j := range sqMtx {
+ res += sgn * sqMtx[0][j] * det(minor(sqMtx, j))
+ sgn *= -1
+ }
+ return res
+}
+
+// newNumberMatrix converts a formula arguments matrix to a number matrix.
+func newNumberMatrix(arg formulaArg, phalanx bool) (numMtx [][]float64, ele formulaArg) {
+ rows := len(arg.Matrix)
+ for r, row := range arg.Matrix {
+ if phalanx && len(row) != rows {
+ ele = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ return
+ }
+ numMtx = append(numMtx, make([]float64, len(row)))
+ for c, cell := range row {
+ if cell.Type != ArgNumber {
+ ele = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ return
+ }
+ numMtx[r][c] = cell.Number
+ }
+ }
+ return
+}
+
+// newFormulaArgMatrix converts the number formula arguments matrix to a
+// formula arguments matrix.
+func newFormulaArgMatrix(numMtx [][]float64) (arg [][]formulaArg) {
+ for r, row := range numMtx {
+ arg = append(arg, make([]formulaArg, len(row)))
+ for c, cell := range row {
+ arg[r][c] = newNumberFormulaArg(cell)
+ }
+ }
+ return
+}
+
+// MDETERM calculates the determinant of a square matrix. The
+// syntax of the function is:
+//
+// MDETERM(array)
+func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires 1 argument")
+ }
+ numMtx, errArg := newNumberMatrix(argsList.Front().Value.(formulaArg), true)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ return newNumberFormulaArg(det(numMtx))
+}
+
+// cofactorMatrix returns the matrix A of cofactors.
+func cofactorMatrix(i, j int, A [][]float64) float64 {
+ N, sign := len(A), -1.0
+ if (i+j)%2 == 0 {
+ sign = 1
+ }
+ var B [][]float64
+ B = append(B, A...)
+ for m := 0; m < N; m++ {
+ for n := j + 1; n < N; n++ {
+ B[m][n-1] = B[m][n]
+ }
+ B[m] = B[m][:len(B[m])-1]
+ }
+ for k := i + 1; k < N; k++ {
+ B[k-1] = B[k]
+ }
+ B = B[:len(B)-1]
+ return sign * det(B)
+}
+
+// adjugateMatrix returns transpose of the cofactor matrix A with Cramer's
+// rule.
+func adjugateMatrix(A [][]float64) (adjA [][]float64) {
+ N := len(A)
+ var B [][]float64
+ for i := 0; i < N; i++ {
+ adjA = append(adjA, make([]float64, N))
+ for j := 0; j < N; j++ {
+ for m := 0; m < N; m++ {
+ for n := 0; n < N; n++ {
+ for x := len(B); x <= m; x++ {
+ B = append(B, []float64{})
+ }
+ for k := len(B[m]); k <= n; k++ {
+ B[m] = append(B[m], 0)
+ }
+ B[m][n] = A[m][n]
+ }
+ }
+ adjA[i][j] = cofactorMatrix(j, i, B)
+ }
+ }
+ return
+}
+
+// MINVERSE function calculates the inverse of a square matrix. The syntax of
+// the function is:
+//
+// MINVERSE(array)
+func (fn *formulaFuncs) MINVERSE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MINVERSE requires 1 argument")
+ }
+ numMtx, errArg := newNumberMatrix(argsList.Front().Value.(formulaArg), true)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ if detM := det(numMtx); detM != 0 {
+ datM, invertM := 1/detM, adjugateMatrix(numMtx)
+ for i := 0; i < len(invertM); i++ {
+ for j := 0; j < len(invertM[i]); j++ {
+ invertM[i][j] *= datM
+ }
+ }
+ return newMatrixFormulaArg(newFormulaArgMatrix(invertM))
+ }
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+}
+
+// MMULT function calculates the matrix product of two arrays
+// (representing matrices). The syntax of the function is:
+//
+// MMULT(array1,array2)
+func (fn *formulaFuncs) MMULT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MMULT requires 2 argument")
+ }
+ arr1 := argsList.Front().Value.(formulaArg)
+ arr2 := argsList.Back().Value.(formulaArg)
+ if arr1.Type == ArgNumber && arr2.Type == ArgNumber {
+ return newNumberFormulaArg(arr1.Number * arr2.Number)
+ }
+ numMtx1, errArg1 := newNumberMatrix(arr1, false)
+ if errArg1.Type == ArgError {
+ return errArg1
+ }
+ numMtx2, errArg2 := newNumberMatrix(arr2, false)
+ if errArg2.Type == ArgError {
+ return errArg2
+ }
+ array2Rows, array2Cols := len(numMtx2), len(numMtx2[0])
+ if len(numMtx1[0]) != array2Rows {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ var numMtx [][]float64
+ var row1, row []float64
+ var sum float64
+ for i := 0; i < len(numMtx1); i++ {
+ numMtx = append(numMtx, []float64{})
+ row = []float64{}
+ row1 = numMtx1[i]
+ for j := 0; j < array2Cols; j++ {
+ sum = 0
+ for k := 0; k < array2Rows; k++ {
+ sum += row1[k] * numMtx2[k][j]
+ }
+ for l := len(row); l <= j; l++ {
+ row = append(row, 0)
+ }
+ row[j] = sum
+ numMtx[i] = row
+ }
+ }
+ return newMatrixFormulaArg(newFormulaArgMatrix(numMtx))
+}
+
+// MOD function returns the remainder of a division between two supplied
+// numbers. The syntax of the function is:
+//
+// MOD(number,divisor)
+func (fn *formulaFuncs) MOD(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MOD requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ divisor := argsList.Back().Value.(formulaArg).ToNumber()
+ if divisor.Type == ArgError {
+ return divisor
+ }
+ if divisor.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, "MOD divide by zero")
+ }
+ trunc, rem := math.Modf(number.Number / divisor.Number)
+ if rem < 0 {
+ trunc--
+ }
+ return newNumberFormulaArg(number.Number - divisor.Number*trunc)
+}
+
+// MROUND function rounds a supplied number up or down to the nearest multiple
+// of a given number. The syntax of the function is:
+//
+// MROUND(number,multiple)
+func (fn *formulaFuncs) MROUND(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MROUND requires 2 numeric arguments")
+ }
+ n := argsList.Front().Value.(formulaArg).ToNumber()
+ if n.Type == ArgError {
+ return n
+ }
+ multiple := argsList.Back().Value.(formulaArg).ToNumber()
+ if multiple.Type == ArgError {
+ return multiple
+ }
+ if multiple.Number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if multiple.Number < 0 && n.Number > 0 ||
+ multiple.Number > 0 && n.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ number, res := math.Modf(n.Number / multiple.Number)
+ if math.Trunc(res+0.5) > 0 {
+ number++
+ }
+ return newNumberFormulaArg(number * multiple.Number)
+}
+
+// MULTINOMIAL function calculates the ratio of the factorial of a sum of
+// supplied values to the product of factorials of those values. The syntax of
+// the function is:
+//
+// MULTINOMIAL(number1,[number2],...)
+func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg {
+ val, num, denom := 0.0, 0.0, 1.0
+ var err error
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ if token.String == "" {
+ continue
+ }
+ if val, err = strconv.ParseFloat(token.String, 64); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ case ArgNumber:
+ val = token.Number
+ }
+ num += val
+ denom *= fact(val)
+ }
+ return newNumberFormulaArg(fact(num) / denom)
+}
+
+// MUNIT function returns the unit matrix for a specified dimension. The
+// syntax of the function is:
+//
+// MUNIT(dimension)
+func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument")
+ }
+ dimension := argsList.Back().Value.(formulaArg).ToNumber()
+ if dimension.Type == ArgError || dimension.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, dimension.Error)
+ }
+ matrix := make([][]formulaArg, 0, int(dimension.Number))
+ for i := 0; i < int(dimension.Number); i++ {
+ row := make([]formulaArg, int(dimension.Number))
+ for j := 0; j < int(dimension.Number); j++ {
+ if i == j {
+ row[j] = newNumberFormulaArg(1.0)
+ } else {
+ row[j] = newNumberFormulaArg(0.0)
+ }
+ }
+ matrix = append(matrix, row)
+ }
+ return newMatrixFormulaArg(matrix)
+}
+
+// ODD function ounds a supplied number away from zero (i.e. rounds a positive
+// number up and a negative number down), to the next odd number. The syntax
+// of the function is:
+//
+// ODD(number)
+func (fn *formulaFuncs) ODD(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ODD requires 1 numeric argument")
+ }
+ number := argsList.Back().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if number.Number == 0 {
+ return newNumberFormulaArg(1)
+ }
+ sign := math.Signbit(number.Number)
+ m, frac := math.Modf((number.Number - 1) / 2)
+ val := m*2 + 1
+ if frac != 0 {
+ if !sign {
+ val += 2
+ } else {
+ val -= 2
+ }
+ }
+ return newNumberFormulaArg(val)
+}
+
+// PI function returns the value of the mathematical constant π (pi), accurate
+// to 15 digits (14 decimal places). The syntax of the function is:
+//
+// PI()
+func (fn *formulaFuncs) PI(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PI accepts no arguments")
+ }
+ return newNumberFormulaArg(math.Pi)
+}
+
+// POWER function calculates a given number, raised to a supplied power.
+// The syntax of the function is:
+//
+// POWER(number,power)
+func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "POWER requires 2 numeric arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type == ArgError {
+ return x
+ }
+ y := argsList.Back().Value.(formulaArg).ToNumber()
+ if y.Type == ArgError {
+ return y
+ }
+ if x.Number == 0 && y.Number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if x.Number == 0 && y.Number < 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(math.Pow(x.Number, y.Number))
+}
+
+// PRODUCT function returns the product (multiplication) of a supplied set of
+// numerical values. The syntax of the function is:
+//
+// PRODUCT(number1,[number2],...)
+func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg {
+ product := 1.0
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ num := token.ToNumber()
+ if num.Type != ArgNumber {
+ return num
+ }
+ product = product * num.Number
+ case ArgNumber:
+ product = product * token.Number
+ case ArgMatrix:
+ for _, row := range token.Matrix {
+ for _, cell := range row {
+ if cell.Type == ArgNumber {
+ product *= cell.Number
+ }
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(product)
+}
+
+// QUOTIENT function returns the integer portion of a division between two
+// supplied numbers. The syntax of the function is:
+//
+// QUOTIENT(numerator,denominator)
+func (fn *formulaFuncs) QUOTIENT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "QUOTIENT requires 2 numeric arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type == ArgError {
+ return x
+ }
+ y := argsList.Back().Value.(formulaArg).ToNumber()
+ if y.Type == ArgError {
+ return y
+ }
+ if y.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(math.Trunc(x.Number / y.Number))
+}
+
+// RADIANS function converts radians into degrees. The syntax of the function is:
+//
+// RADIANS(angle)
+func (fn *formulaFuncs) RADIANS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RADIANS requires 1 numeric argument")
+ }
+ angle := argsList.Front().Value.(formulaArg).ToNumber()
+ if angle.Type == ArgError {
+ return angle
+ }
+ return newNumberFormulaArg(math.Pi / 180.0 * angle.Number)
+}
+
+// RAND function generates a random real number between 0 and 1. The syntax of
+// the function is:
+//
+// RAND()
+func (fn *formulaFuncs) RAND(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RAND accepts no arguments")
+ }
+ return newNumberFormulaArg(rand.New(rand.NewSource(time.Now().UnixNano())).Float64())
+}
+
+// RANDBETWEEN function generates a random integer between two supplied
+// integers. The syntax of the function is:
+//
+// RANDBETWEEN(bottom,top)
+func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RANDBETWEEN requires 2 numeric arguments")
+ }
+ bottom := argsList.Front().Value.(formulaArg).ToNumber()
+ if bottom.Type == ArgError {
+ return bottom
+ }
+ top := argsList.Back().Value.(formulaArg).ToNumber()
+ if top.Type == ArgError {
+ return top
+ }
+ if top.Number < bottom.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ num := rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(int64(top.Number - bottom.Number + 1))
+ return newNumberFormulaArg(float64(num + int64(bottom.Number)))
+}
+
+// romanNumerals defined a numeral system that originated in ancient Rome and
+// remained the usual way of writing numbers throughout Europe well into the
+// Late Middle Ages.
+type romanNumerals struct {
+ n float64
+ s string
+}
+
+var romanTable = [][]romanNumerals{
+ {
+ {1000, "M"},
+ {900, "CM"},
+ {500, "D"},
+ {400, "CD"},
+ {100, "C"},
+ {90, "XC"},
+ {50, "L"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+ },
+ {
+ {1000, "M"},
+ {950, "LM"},
+ {900, "CM"},
+ {500, "D"},
+ {450, "LD"},
+ {400, "CD"},
+ {100, "C"},
+ {95, "VC"},
+ {90, "XC"},
+ {50, "L"},
+ {45, "VL"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+ },
+ {
+ {1000, "M"},
+ {990, "XM"},
+ {950, "LM"},
+ {900, "CM"},
+ {500, "D"},
+ {490, "XD"},
+ {450, "LD"},
+ {400, "CD"},
+ {100, "C"},
+ {99, "IC"},
+ {90, "XC"},
+ {50, "L"},
+ {45, "VL"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+ },
+ {
+ {1000, "M"},
+ {995, "VM"},
+ {990, "XM"},
+ {950, "LM"},
+ {900, "CM"},
+ {500, "D"},
+ {495, "VD"},
+ {490, "XD"},
+ {450, "LD"},
+ {400, "CD"},
+ {100, "C"},
+ {99, "IC"},
+ {90, "XC"},
+ {50, "L"},
+ {45, "VL"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+ },
+ {
+ {1000, "M"},
+ {999, "IM"},
+ {995, "VM"},
+ {990, "XM"},
+ {950, "LM"},
+ {900, "CM"},
+ {500, "D"},
+ {499, "ID"},
+ {495, "VD"},
+ {490, "XD"},
+ {450, "LD"},
+ {400, "CD"},
+ {100, "C"},
+ {99, "IC"},
+ {90, "XC"},
+ {50, "L"},
+ {45, "VL"},
+ {40, "XL"},
+ {10, "X"},
+ {9, "IX"},
+ {5, "V"},
+ {4, "IV"},
+ {1, "I"},
+ },
+}
+
+// ROMAN function converts an arabic number to Roman. I.e. for a supplied
+// integer, the function returns a text string depicting the roman numeral
+// form of the number. The syntax of the function is:
+//
+// ROMAN(number,[form])
+func (fn *formulaFuncs) ROMAN(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROMAN requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROMAN allows at most 2 arguments")
+ }
+ var form int
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if argsList.Len() > 1 {
+ f := argsList.Back().Value.(formulaArg).ToNumber()
+ if f.Type == ArgError {
+ return f
+ }
+ form = int(f.Number)
+ if form < 0 {
+ form = 0
+ } else if form > 4 {
+ form = 4
+ }
+ }
+ decimalTable := romanTable[0]
+ switch form {
+ case 1:
+ decimalTable = romanTable[1]
+ case 2:
+ decimalTable = romanTable[2]
+ case 3:
+ decimalTable = romanTable[3]
+ case 4:
+ decimalTable = romanTable[4]
+ }
+ val := math.Trunc(number.Number)
+ buf := bytes.Buffer{}
+ for _, r := range decimalTable {
+ for val >= r.n {
+ buf.WriteString(r.s)
+ val -= r.n
+ }
+ }
+ return newStringFormulaArg(buf.String())
+}
+
+type roundMode byte
+
+const (
+ closest roundMode = iota
+ down
+ up
+)
+
+// round rounds a supplied number up or down.
+func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 {
+ var significance float64
+ if digits > 0 {
+ significance = math.Pow(1/10.0, digits)
+ } else {
+ significance = math.Pow(10.0, -digits)
+ }
+ val, res := math.Modf(number / significance)
+ switch mode {
+ case closest:
+ const eps = 0.499999999
+ if res >= eps {
+ val++
+ } else if res <= -eps {
+ val--
+ }
+ case down:
+ case up:
+ if res > 0 {
+ val++
+ } else if res < 0 {
+ val--
+ }
+ }
+ return val * significance
+}
+
+// ROUND function rounds a supplied number up or down, to a specified number
+// of decimal places. The syntax of the function is:
+//
+// ROUND(number,num_digits)
+func (fn *formulaFuncs) ROUND(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROUND requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ digits := argsList.Back().Value.(formulaArg).ToNumber()
+ if digits.Type == ArgError {
+ return digits
+ }
+ return newNumberFormulaArg(fn.round(number.Number, digits.Number, closest))
+}
+
+// ROUNDDOWN function rounds a supplied number down towards zero, to a
+// specified number of decimal places. The syntax of the function is:
+//
+// ROUNDDOWN(number,num_digits)
+func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROUNDDOWN requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ digits := argsList.Back().Value.(formulaArg).ToNumber()
+ if digits.Type == ArgError {
+ return digits
+ }
+ return newNumberFormulaArg(fn.round(number.Number, digits.Number, down))
+}
+
+// ROUNDUP function rounds a supplied number up, away from zero, to a
+// specified number of decimal places. The syntax of the function is:
+//
+// ROUNDUP(number,num_digits)
+func (fn *formulaFuncs) ROUNDUP(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROUNDUP requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ digits := argsList.Back().Value.(formulaArg).ToNumber()
+ if digits.Type == ArgError {
+ return digits
+ }
+ return newNumberFormulaArg(fn.round(number.Number, digits.Number, up))
+}
+
+// SEC function calculates the secant of a given angle. The syntax of the
+// function is:
+//
+// SEC(number)
+func (fn *formulaFuncs) SEC(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SEC requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Cos(number.Number))
+}
+
+// SECH function calculates the hyperbolic secant (sech) of a supplied angle.
+// The syntax of the function is:
+//
+// SECH(number)
+func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SECH requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(1 / math.Cosh(number.Number))
+}
+
+// SERIESSUM function returns the sum of a power series. The syntax of the
+// function is:
+//
+// SERIESSUM(x,n,m,coefficients)
+func (fn *formulaFuncs) SERIESSUM(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SERIESSUM requires 4 arguments")
+ }
+ var x, n, m formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if n = argsList.Front().Next().Value.(formulaArg).ToNumber(); n.Type != ArgNumber {
+ return n
+ }
+ if m = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); m.Type != ArgNumber {
+ return m
+ }
+ var result, i float64
+ for _, coefficient := range argsList.Back().Value.(formulaArg).ToList() {
+ if coefficient.Value() == "" {
+ continue
+ }
+ num := coefficient.ToNumber()
+ if num.Type != ArgNumber {
+ return num
+ }
+ result += num.Number * math.Pow(x.Number, n.Number+(m.Number*i))
+ i++
+ }
+ return newNumberFormulaArg(result)
+}
+
+// SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied
+// number. I.e. if the number is positive, the Sign function returns +1, if
+// the number is negative, the function returns -1 and if the number is 0
+// (zero), the function returns 0. The syntax of the function is:
+//
+// SIGN(number)
+func (fn *formulaFuncs) SIGN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SIGN requires 1 numeric argument")
+ }
+ val := argsList.Front().Value.(formulaArg).ToNumber()
+ if val.Type == ArgError {
+ return val
+ }
+ if val.Number < 0 {
+ return newNumberFormulaArg(-1)
+ }
+ if val.Number > 0 {
+ return newNumberFormulaArg(1)
+ }
+ return newNumberFormulaArg(0)
+}
+
+// SIN function calculates the sine of a given angle. The syntax of the
+// function is:
+//
+// SIN(number)
+func (fn *formulaFuncs) SIN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SIN requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Sin(number.Number))
+}
+
+// SINH function calculates the hyperbolic sine (sinh) of a supplied number.
+// The syntax of the function is:
+//
+// SINH(number)
+func (fn *formulaFuncs) SINH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SINH requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Sinh(number.Number))
+}
+
+// SQRT function calculates the positive square root of a supplied number. The
+// syntax of the function is:
+//
+// SQRT(number)
+func (fn *formulaFuncs) SQRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SQRT requires 1 numeric argument")
+ }
+ value := argsList.Front().Value.(formulaArg).ToNumber()
+ if value.Type == ArgError {
+ return value
+ }
+ if value.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(math.Sqrt(value.Number))
+}
+
+// SQRTPI function returns the square root of a supplied number multiplied by
+// the mathematical constant, π. The syntax of the function is:
+//
+// SQRTPI(number)
+func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SQRTPI requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Sqrt(number.Number * math.Pi))
+}
+
+// STDEV function calculates the sample standard deviation of a supplied set
+// of values. The syntax of the function is:
+//
+// STDEV(number1,[number2],...)
+func (fn *formulaFuncs) STDEV(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "STDEV requires at least 1 argument")
+ }
+ return fn.stdev(false, argsList)
+}
+
+// STDEVdotS function calculates the sample standard deviation of a supplied
+// set of values. The syntax of the function is:
+//
+// STDEV.S(number1,[number2],...)
+func (fn *formulaFuncs) STDEVdotS(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "STDEV.S requires at least 1 argument")
+ }
+ return fn.stdev(false, argsList)
+}
+
+// STDEVA function estimates standard deviation based on a sample. The
+// standard deviation is a measure of how widely values are dispersed from
+// the average value (the mean). The syntax of the function is:
+//
+// STDEVA(number1,[number2],...)
+func (fn *formulaFuncs) STDEVA(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "STDEVA requires at least 1 argument")
+ }
+ return fn.stdev(true, argsList)
+}
+
+// calcStdevPow is part of the implementation stdev.
+func calcStdevPow(result, count float64, n, m formulaArg) (float64, float64) {
+ if result == -1 {
+ result = math.Pow(n.Number-m.Number, 2)
+ } else {
+ result += math.Pow(n.Number-m.Number, 2)
+ }
+ count++
+ return result, count
+}
+
+// calcStdev is part of the implementation stdev.
+func calcStdev(stdeva bool, result, count float64, mean, token formulaArg) (float64, float64) {
+ for _, row := range token.ToList() {
+ if row.Type == ArgNumber || row.Type == ArgString {
+ if !stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") {
+ continue
+ } else if stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") {
+ num := row.ToBool()
+ if num.Type == ArgNumber {
+ result, count = calcStdevPow(result, count, num, mean)
+ continue
+ }
+ } else {
+ num := row.ToNumber()
+ if num.Type == ArgNumber {
+ result, count = calcStdevPow(result, count, num, mean)
+ }
+ }
+ }
+ }
+ return result, count
+}
+
+// stdev is an implementation of the formula functions STDEV and STDEVA.
+func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg {
+ count, result := -1.0, -1.0
+ var mean formulaArg
+ if stdeva {
+ mean = fn.AVERAGEA(argsList)
+ } else {
+ mean = fn.AVERAGE(argsList)
+ }
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString, ArgNumber:
+ if !stdeva && (token.Value() == "TRUE" || token.Value() == "FALSE") {
+ continue
+ } else if stdeva && (token.Value() == "TRUE" || token.Value() == "FALSE") {
+ num := token.ToBool()
+ if num.Type == ArgNumber {
+ result, count = calcStdevPow(result, count, num, mean)
+ continue
+ }
+ } else {
+ num := token.ToNumber()
+ if num.Type == ArgNumber {
+ result, count = calcStdevPow(result, count, num, mean)
+ }
+ }
+ case ArgList, ArgMatrix:
+ result, count = calcStdev(stdeva, result, count, mean, token)
+ }
+ }
+ if count > 0 && result >= 0 {
+ return newNumberFormulaArg(math.Sqrt(result / count))
+ }
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+}
+
+// POISSONdotDIST function calculates the Poisson Probability Mass Function or
+// the Cumulative Poisson Probability Function for a supplied set of
+// parameters. The syntax of the function is:
+//
+// POISSON.DIST(x,mean,cumulative)
+func (fn *formulaFuncs) POISSONdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "POISSON.DIST requires 3 arguments")
+ }
+ return fn.POISSON(argsList)
+}
+
+// POISSON function calculates the Poisson Probability Mass Function or the
+// Cumulative Poisson Probability Function for a supplied set of parameters.
+// The syntax of the function is:
+//
+// POISSON(x,mean,cumulative)
+func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "POISSON requires 3 arguments")
+ }
+ var x, mean, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if x.Number < 0 || mean.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if cumulative.Number == 1 {
+ summer := 0.0
+ floor := math.Floor(x.Number)
+ for i := 0; i <= int(floor); i++ {
+ summer += math.Pow(mean.Number, float64(i)) / fact(float64(i))
+ }
+ return newNumberFormulaArg(math.Exp(0-mean.Number) * summer)
+ }
+ return newNumberFormulaArg(math.Exp(0-mean.Number) * math.Pow(mean.Number, x.Number) / fact(x.Number))
+}
+
+// prepareProbArgs checking and prepare arguments for the formula function
+// PROB.
+func prepareProbArgs(argsList *list.List) []formulaArg {
+ if argsList.Len() < 3 {
+ return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, "PROB requires at least 3 arguments")}
+ }
+ if argsList.Len() > 4 {
+ return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, "PROB requires at most 4 arguments")}
+ }
+ var lower, upper formulaArg
+ xRange := argsList.Front().Value.(formulaArg)
+ probRange := argsList.Front().Next().Value.(formulaArg)
+ if lower = argsList.Front().Next().Next().Value.(formulaArg); lower.Type != ArgNumber {
+ return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)}
+ }
+ upper = lower
+ if argsList.Len() == 4 {
+ if upper = argsList.Back().Value.(formulaArg); upper.Type != ArgNumber {
+ return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)}
+ }
+ }
+ nR1, nR2 := len(xRange.Matrix), len(probRange.Matrix)
+ if nR1 == 0 || nR2 == 0 {
+ return []formulaArg{newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)}
+ }
+ if nR1 != nR2 {
+ return []formulaArg{newErrorFormulaArg(formulaErrorNA, formulaErrorNA)}
+ }
+ nC1, nC2 := len(xRange.Matrix[0]), len(probRange.Matrix[0])
+ if nC1 != nC2 {
+ return []formulaArg{newErrorFormulaArg(formulaErrorNA, formulaErrorNA)}
+ }
+ return []formulaArg{xRange, probRange, lower, upper}
+}
+
+// PROB function calculates the probability associated with a given range. The
+// syntax of the function is:
+//
+// PROB(x_range,prob_range,lower_limit,[upper_limit])
+func (fn *formulaFuncs) PROB(argsList *list.List) formulaArg {
+ args := prepareProbArgs(argsList)
+ if len(args) == 1 {
+ return args[0]
+ }
+ xRange, probRange, lower, upper := args[0], args[1], args[2], args[3]
+ var sum, res, fP, fW float64
+ var stop bool
+ for r := 0; r < len(xRange.Matrix) && !stop; r++ {
+ for c := 0; c < len(xRange.Matrix[0]) && !stop; c++ {
+ p := probRange.Matrix[r][c]
+ x := xRange.Matrix[r][c]
+ if p.Type == ArgNumber && x.Type == ArgNumber {
+ if fP, fW = p.Number, x.Number; fP < 0 || fP > 1 {
+ stop = true
+ continue
+ }
+ if sum += fP; fW >= lower.Number && fW <= upper.Number {
+ res += fP
+ }
+ continue
+ }
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ if stop || math.Abs(sum-1) > 1.0e-7 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(res)
+}
+
+// SUBTOTAL function performs a specified calculation (e.g. the sum, product,
+// average, etc.) for a supplied set of values. The syntax of the function is:
+//
+// SUBTOTAL(function_num,ref1,[ref2],...)
+func (fn *formulaFuncs) SUBTOTAL(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL requires at least 2 arguments")
+ }
+ var fnNum formulaArg
+ if fnNum = argsList.Front().Value.(formulaArg).ToNumber(); fnNum.Type != ArgNumber {
+ return fnNum
+ }
+ subFn, ok := map[int]func(argsList *list.List) formulaArg{
+ 1: fn.AVERAGE, 101: fn.AVERAGE,
+ 2: fn.COUNT, 102: fn.COUNT,
+ 3: fn.COUNTA, 103: fn.COUNTA,
+ 4: fn.MAX, 104: fn.MAX,
+ 5: fn.MIN, 105: fn.MIN,
+ 6: fn.PRODUCT, 106: fn.PRODUCT,
+ 7: fn.STDEV, 107: fn.STDEV,
+ 8: fn.STDEVP, 108: fn.STDEVP,
+ 9: fn.SUM, 109: fn.SUM,
+ 10: fn.VAR, 110: fn.VAR,
+ 11: fn.VARP, 111: fn.VARP,
+ }[int(fnNum.Number)]
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUBTOTAL has invalid function_num")
+ }
+ subArgList := list.New().Init()
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ subArgList.PushBack(arg.Value.(formulaArg))
+ }
+ return subFn(subArgList)
+}
+
+// SUM function adds together a supplied set of numbers and returns the sum of
+// these values. The syntax of the function is:
+//
+// SUM(number1,[number2],...)
+func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg {
+ var sum float64
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgError:
+ return token
+ case ArgString:
+ if num := token.ToNumber(); num.Type == ArgNumber {
+ sum += num.Number
+ }
+ case ArgNumber:
+ sum += token.Number
+ case ArgMatrix:
+ for _, row := range token.Matrix {
+ for _, value := range row {
+ if num := value.ToNumber(); num.Type == ArgNumber {
+ sum += num.Number
+ }
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(sum)
+}
+
+// SUMIF function finds the values in a supplied array, that satisfy a given
+// criteria, and returns the sum of the corresponding values in a second
+// supplied array. The syntax of the function is:
+//
+// SUMIF(range,criteria,[sum_range])
+func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 arguments")
+ }
+ criteria := formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg))
+ rangeMtx := argsList.Front().Value.(formulaArg).Matrix
+ var sumRange [][]formulaArg
+ if argsList.Len() == 3 {
+ sumRange = argsList.Back().Value.(formulaArg).Matrix
+ }
+ var sum float64
+ var arg formulaArg
+ for rowIdx, row := range rangeMtx {
+ for colIdx, cell := range row {
+ arg = cell
+ if arg.Type == ArgEmpty {
+ continue
+ }
+ if ok, _ := formulaCriteriaEval(arg, criteria); ok {
+ if argsList.Len() == 3 {
+ if len(sumRange) > rowIdx && len(sumRange[rowIdx]) > colIdx {
+ arg = sumRange[rowIdx][colIdx]
+ }
+ }
+ if arg.Type == ArgNumber {
+ sum += arg.Number
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(sum)
+}
+
+// SUMIFS function finds values in one or more supplied arrays, that satisfy a
+// set of criteria, and returns the sum of the corresponding values in a
+// further supplied array. The syntax of the function is:
+//
+// SUMIFS(sum_range,criteria_range1,criteria1,[criteria_range2,criteria2],...)
+func (fn *formulaFuncs) SUMIFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUMIFS requires at least 3 arguments")
+ }
+ if argsList.Len()%2 != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var args []formulaArg
+ sum, sumRange := 0.0, argsList.Front().Value.(formulaArg).Matrix
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ for _, ref := range formulaIfsMatch(args) {
+ if ref.Row >= len(sumRange) || ref.Col >= len(sumRange[ref.Row]) {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if num := sumRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber {
+ sum += num.Number
+ }
+ }
+ return newNumberFormulaArg(sum)
+}
+
+// sumproduct is an implementation of the formula function SUMPRODUCT.
+func (fn *formulaFuncs) sumproduct(argsList *list.List) formulaArg {
+ var (
+ argType ArgType
+ n int
+ res []float64
+ sum float64
+ )
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ if argType == ArgUnknown {
+ argType = token.Type
+ }
+ if token.Type != argType {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ switch token.Type {
+ case ArgString, ArgNumber:
+ if num := token.ToNumber(); num.Type == ArgNumber {
+ sum = fn.PRODUCT(argsList).Number
+ continue
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ case ArgMatrix:
+ args := token.ToList()
+ if res == nil {
+ n = len(args)
+ res = make([]float64, n)
+ for i := range res {
+ res[i] = 1.0
+ }
+ }
+ if len(args) != n {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ for i, value := range args {
+ num := value.ToNumber()
+ if num.Type != ArgNumber && value.Value() != "" {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ res[i] = res[i] * num.Number
+ }
+ }
+ }
+ for _, r := range res {
+ sum += r
+ }
+ return newNumberFormulaArg(sum)
+}
+
+// SUMPRODUCT function returns the sum of the products of the corresponding
+// values in a set of supplied arrays. The syntax of the function is:
+//
+// SUMPRODUCT(array1,[array2],[array3],...)
+func (fn *formulaFuncs) SUMPRODUCT(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUMPRODUCT requires at least 1 argument")
+ }
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ if token := arg.Value.(formulaArg); token.Type == ArgError {
+ return token
+ }
+ }
+ return fn.sumproduct(argsList)
+}
+
+// SUMSQ function returns the sum of squares of a supplied set of values. The
+// syntax of the function is:
+//
+// SUMSQ(number1,[number2],...)
+func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg {
+ var val, sq float64
+ var err error
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ if token.String == "" {
+ continue
+ }
+ if val, err = strconv.ParseFloat(token.String, 64); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ sq += val * val
+ case ArgNumber:
+ sq += token.Number * token.Number
+ case ArgMatrix:
+ for _, row := range token.Matrix {
+ for _, value := range row {
+ if value.Value() == "" {
+ continue
+ }
+ if val, err = strconv.ParseFloat(value.Value(), 64); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ sq += val * val
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(sq)
+}
+
+// sumx is an implementation of the formula functions SUMX2MY2, SUMX2PY2 and
+// SUMXMY2.
+func (fn *formulaFuncs) sumx(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name))
+ }
+ array1 := argsList.Front().Value.(formulaArg)
+ array2 := argsList.Back().Value.(formulaArg)
+ left, right := array1.ToList(), array2.ToList()
+ n := len(left)
+ if n != len(right) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ result := 0.0
+ for i := 0; i < n; i++ {
+ if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 {
+ switch name {
+ case "SUMX2MY2":
+ result += lhs.Number*lhs.Number - rhs.Number*rhs.Number
+ case "SUMX2PY2":
+ result += lhs.Number*lhs.Number + rhs.Number*rhs.Number
+ default:
+ result += (lhs.Number - rhs.Number) * (lhs.Number - rhs.Number)
+ }
+ }
+ }
+ return newNumberFormulaArg(result)
+}
+
+// SUMX2MY2 function returns the sum of the differences of squares of two
+// supplied sets of values. The syntax of the function is:
+//
+// SUMX2MY2(array_x,array_y)
+func (fn *formulaFuncs) SUMX2MY2(argsList *list.List) formulaArg {
+ return fn.sumx("SUMX2MY2", argsList)
+}
+
+// SUMX2PY2 function returns the sum of the sum of squares of two supplied sets
+// of values. The syntax of the function is:
+//
+// SUMX2PY2(array_x,array_y)
+func (fn *formulaFuncs) SUMX2PY2(argsList *list.List) formulaArg {
+ return fn.sumx("SUMX2PY2", argsList)
+}
+
+// SUMXMY2 function returns the sum of the squares of differences between
+// corresponding values in two supplied arrays. The syntax of the function
+// is:
+//
+// SUMXMY2(array_x,array_y)
+func (fn *formulaFuncs) SUMXMY2(argsList *list.List) formulaArg {
+ return fn.sumx("SUMXMY2", argsList)
+}
+
+// TAN function calculates the tangent of a given angle. The syntax of the
+// function is:
+//
+// TAN(number)
+func (fn *formulaFuncs) TAN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TAN requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Tan(number.Number))
+}
+
+// TANH function calculates the hyperbolic tangent (tanh) of a supplied
+// number. The syntax of the function is:
+//
+// TANH(number)
+func (fn *formulaFuncs) TANH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TANH requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ return newNumberFormulaArg(math.Tanh(number.Number))
+}
+
+// TRUNC function truncates a supplied number to a specified number of decimal
+// places. The syntax of the function is:
+//
+// TRUNC(number,[number_digits])
+func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TRUNC requires at least 1 argument")
+ }
+ var digits, adjust, rtrim float64
+ var err error
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type == ArgError {
+ return number
+ }
+ if argsList.Len() > 1 {
+ d := argsList.Back().Value.(formulaArg).ToNumber()
+ if d.Type == ArgError {
+ return d
+ }
+ digits = d.Number
+ digits = math.Floor(digits)
+ }
+ adjust = math.Pow(10, digits)
+ x := int((math.Abs(number.Number) - math.Abs(float64(int(number.Number)))) * adjust)
+ if x != 0 {
+ if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ }
+ if (digits > 0) && (rtrim < adjust/10) {
+ return newNumberFormulaArg(number.Number)
+ }
+ return newNumberFormulaArg(float64(int(number.Number*adjust)) / adjust)
+}
+
+// Statistical Functions
+
+// AVEDEV function calculates the average deviation of a supplied set of
+// values. The syntax of the function is:
+//
+// AVEDEV(number1,[number2],...)
+func (fn *formulaFuncs) AVEDEV(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AVEDEV requires at least 1 argument")
+ }
+ average := fn.AVERAGE(argsList)
+ if average.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ result, count := 0.0, 0.0
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ num := arg.Value.(formulaArg).ToNumber()
+ if num.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ result += math.Abs(num.Number - average.Number)
+ count++
+ }
+ return newNumberFormulaArg(result / count)
+}
+
+// AVERAGE function returns the arithmetic mean of a list of supplied numbers.
+// The syntax of the function is:
+//
+// AVERAGE(number1,[number2],...)
+func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg {
+ var args []formulaArg
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ count, sum := fn.countSum(false, args)
+ if count == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(sum / count)
+}
+
+// AVERAGEA function returns the arithmetic mean of a list of supplied numbers
+// with text cell and zero values. The syntax of the function is:
+//
+// AVERAGEA(number1,[number2],...)
+func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg {
+ var args []formulaArg
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ count, sum := fn.countSum(true, args)
+ if count == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(sum / count)
+}
+
+// AVERAGEIF function finds the values in a supplied array that satisfy a
+// specified criteria, and returns the average (i.e. the statistical mean) of
+// the corresponding values in a second supplied array. The syntax of the
+// function is:
+//
+// AVERAGEIF(range,criteria,[average_range])
+func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIF requires at least 2 arguments")
+ }
+ var (
+ criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg))
+ rangeMtx = argsList.Front().Value.(formulaArg).Matrix
+ cellRange [][]formulaArg
+ args []formulaArg
+ val float64
+ err error
+ ok bool
+ )
+ if argsList.Len() == 3 {
+ cellRange = argsList.Back().Value.(formulaArg).Matrix
+ }
+ for rowIdx, row := range rangeMtx {
+ for colIdx, col := range row {
+ fromVal := col.Value()
+ if fromVal == "" {
+ continue
+ }
+ if col.Type == ArgString && criteria.Condition.Type != ArgString {
+ continue
+ }
+ ok, _ = formulaCriteriaEval(col, criteria)
+ if ok {
+ if argsList.Len() == 3 {
+ if len(cellRange) > rowIdx && len(cellRange[rowIdx]) > colIdx {
+ fromVal = cellRange[rowIdx][colIdx].Value()
+ }
+ }
+ if val, err = strconv.ParseFloat(fromVal, 64); err != nil {
+ continue
+ }
+ args = append(args, newNumberFormulaArg(val))
+ }
+ }
+ }
+ count, sum := fn.countSum(false, args)
+ if count == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(sum / count)
+}
+
+// AVERAGEIFS function finds entries in one or more arrays, that satisfy a set
+// of supplied criteria, and returns the average (i.e. the statistical mean)
+// of the corresponding values in a further supplied array. The syntax of the
+// function is:
+//
+// AVERAGEIFS(average_range,criteria_range1,criteria1,[criteria_range2,criteria2],...)
+func (fn *formulaFuncs) AVERAGEIFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AVERAGEIFS requires at least 3 arguments")
+ }
+ if argsList.Len()%2 != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var args []formulaArg
+ sum, sumRange := 0.0, argsList.Front().Value.(formulaArg).Matrix
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ count := 0.0
+ for _, ref := range formulaIfsMatch(args) {
+ if num := sumRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber {
+ sum += num.Number
+ count++
+ }
+ }
+ if count == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, "AVERAGEIF divide by zero")
+ }
+ return newNumberFormulaArg(sum / count)
+}
+
+// getBetaHelperContFrac continued fractions for the beta function.
+func getBetaHelperContFrac(fX, fA, fB float64) float64 {
+ var a1, b1, a2, b2, fnorm, cfnew, cf, rm float64
+ a1, b1, b2 = 1, 1, 1-(fA+fB)/(fA+1)*fX
+ if b2 == 0 {
+ a2, fnorm, cf = 0, 1, 1
+ } else {
+ a2, fnorm = 1, 1/b2
+ cf = a2 * fnorm
+ }
+ cfnew, rm = 1, 1
+ fMaxIter, fMachEps := 50000.0, 2.22045e-016
+ bfinished := false
+ for rm < fMaxIter && !bfinished {
+ apl2m := fA + 2*rm
+ d2m := rm * (fB - rm) * fX / ((apl2m - 1) * apl2m)
+ d2m1 := -(fA + rm) * (fA + fB + rm) * fX / (apl2m * (apl2m + 1))
+ a1 = (a2 + d2m*a1) * fnorm
+ b1 = (b2 + d2m*b1) * fnorm
+ a2 = a1 + d2m1*a2*fnorm
+ b2 = b1 + d2m1*b2*fnorm
+ if b2 != 0 {
+ fnorm = 1 / b2
+ cfnew = a2 * fnorm
+ bfinished = math.Abs(cf-cfnew) < math.Abs(cf)*fMachEps
+ }
+ cf = cfnew
+ rm++
+ }
+ return cf
+}
+
+// getLanczosSum uses a variant of the Lanczos sum with a rational function.
+func getLanczosSum(fZ float64) float64 {
+ num := []float64{
+ 23531376880.41075968857200767445163675473,
+ 42919803642.64909876895789904700198885093,
+ 35711959237.35566804944018545154716670596,
+ 17921034426.03720969991975575445893111267,
+ 6039542586.35202800506429164430729792107,
+ 1439720407.311721673663223072794912393972,
+ 248874557.8620541565114603864132294232163,
+ 31426415.58540019438061423162831820536287,
+ 2876370.628935372441225409051620849613599,
+ 186056.2653952234950402949897160456992822,
+ 8071.672002365816210638002902272250613822,
+ 210.8242777515793458725097339207133627117,
+ 2.506628274631000270164908177133837338626,
+ }
+ denom := []float64{
+ 0,
+ 39916800,
+ 120543840,
+ 150917976,
+ 105258076,
+ 45995730,
+ 13339535,
+ 2637558,
+ 357423,
+ 32670,
+ 1925,
+ 66,
+ 1,
+ }
+ var sumNum, sumDenom, zInv float64
+ if fZ <= 1 {
+ sumNum = num[12]
+ sumDenom = denom[12]
+ for i := 11; i >= 0; i-- {
+ sumNum *= fZ
+ sumNum += num[i]
+ sumDenom *= fZ
+ sumDenom += denom[i]
+ }
+ } else {
+ zInv = 1 / fZ
+ sumNum = num[0]
+ sumDenom = denom[0]
+ for i := 1; i <= 12; i++ {
+ sumNum *= zInv
+ sumNum += num[i]
+ sumDenom *= zInv
+ sumDenom += denom[i]
+ }
+ }
+ return sumNum / sumDenom
+}
+
+// getBeta return beta distribution.
+func getBeta(fAlpha, fBeta float64) float64 {
+ var fA, fB float64
+ if fAlpha > fBeta {
+ fA = fAlpha
+ fB = fBeta
+ } else {
+ fA = fBeta
+ fB = fAlpha
+ }
+ const maxGammaArgument = 171.624376956302
+ if fA+fB < maxGammaArgument {
+ return math.Gamma(fA) / math.Gamma(fA+fB) * math.Gamma(fB)
+ }
+ fg := 6.024680040776729583740234375
+ fgm := fg - 0.5
+ fLanczos := getLanczosSum(fA)
+ fLanczos /= getLanczosSum(fA + fB)
+ fLanczos *= getLanczosSum(fB)
+ fABgm := fA + fB + fgm
+ fLanczos *= math.Sqrt((fABgm / (fA + fgm)) / (fB + fgm))
+ fTempA := fB / (fA + fgm)
+ fTempB := fA / (fB + fgm)
+ fResult := math.Exp(-fA*math.Log1p(fTempA) - fB*math.Log1p(fTempB) - fgm)
+ fResult *= fLanczos
+ return fResult
+}
+
+// getBetaDistPDF is an implementation for the Beta probability density
+// function.
+func getBetaDistPDF(fX, fA, fB float64) float64 {
+ if fX <= 0 || fX >= 1 {
+ return 0
+ }
+ fLogDblMax, fLogDblMin := math.Log(1.79769e+308), math.Log(2.22507e-308)
+ fLogY := math.Log(0.5 - fX + 0.5)
+ if fX < 0.1 {
+ fLogY = math.Log1p(-fX)
+ }
+ fLogX := math.Log(fX)
+ fAm1LogX := (fA - 1) * fLogX
+ fBm1LogY := (fB - 1) * fLogY
+ fLogBeta := getLogBeta(fA, fB)
+ if fAm1LogX < fLogDblMax && fAm1LogX > fLogDblMin && fBm1LogY < fLogDblMax &&
+ fBm1LogY > fLogDblMin && fLogBeta < fLogDblMax && fLogBeta > fLogDblMin &&
+ fAm1LogX+fBm1LogY < fLogDblMax && fAm1LogX+fBm1LogY > fLogDblMin {
+ return math.Pow(fX, fA-1) * math.Pow(0.5-fX+0.5, fB-1) / getBeta(fA, fB)
+ }
+ return math.Exp(fAm1LogX + fBm1LogY - fLogBeta)
+}
+
+// getLogBeta return beta with logarithm.
+func getLogBeta(fAlpha, fBeta float64) float64 {
+ var fA, fB float64
+ if fAlpha > fBeta {
+ fA, fB = fAlpha, fBeta
+ } else {
+ fA, fB = fBeta, fAlpha
+ }
+ fg := 6.024680040776729583740234375
+ fgm := fg - 0.5
+ fLanczos := getLanczosSum(fA)
+ fLanczos /= getLanczosSum(fA + fB)
+ fLanczos *= getLanczosSum(fB)
+ fLogLanczos := math.Log(fLanczos)
+ fABgm := fA + fB + fgm
+ fLogLanczos += 0.5 * (math.Log(fABgm) - math.Log(fA+fgm) - math.Log(fB+fgm))
+ fTempA := fB / (fA + fgm)
+ fTempB := fA / (fB + fgm)
+ fResult := -fA*math.Log1p(fTempA) - fB*math.Log1p(fTempB) - fgm
+ fResult += fLogLanczos
+ return fResult
+}
+
+// getBetaDist is an implementation for the beta distribution function.
+func getBetaDist(fXin, fAlpha, fBeta float64) float64 {
+ if fXin <= 0 {
+ return 0
+ }
+ if fXin >= 1 {
+ return 1
+ }
+ if fBeta == 1 {
+ return math.Pow(fXin, fAlpha)
+ }
+ if fAlpha == 1 {
+ return -math.Expm1(fBeta * math.Log1p(-fXin))
+ }
+ var fResult float64
+ fY, flnY := (0.5-fXin)+0.5, math.Log1p(-fXin)
+ fX, flnX := fXin, math.Log(fXin)
+ fA, fB := fAlpha, fBeta
+ bReflect := fXin > fAlpha/(fAlpha+fBeta)
+ if bReflect {
+ fA = fBeta
+ fB = fAlpha
+ fX = fY
+ fY = fXin
+ flnX = flnY
+ flnY = math.Log(fXin)
+ }
+ fResult = getBetaHelperContFrac(fX, fA, fB) / fA
+ fP, fQ := fA/(fA+fB), fB/(fA+fB)
+ var fTemp float64
+ if fA > 1 && fB > 1 && fP < 0.97 && fQ < 0.97 {
+ fTemp = getBetaDistPDF(fX, fA, fB) * fX * fY
+ } else {
+ fTemp = math.Exp(fA*flnX + fB*flnY - getLogBeta(fA, fB))
+ }
+ fResult *= fTemp
+ if bReflect {
+ fResult = 0.5 - fResult + 0.5
+ }
+ return fResult
+}
+
+// prepareBETAdotDISTArgs checking and prepare arguments for the formula
+// function BETA.DIST.
+func (fn *formulaFuncs) prepareBETAdotDISTArgs(argsList *list.List) formulaArg {
+ if argsList.Len() < 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BETA.DIST requires at least 4 arguments")
+ }
+ if argsList.Len() > 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BETA.DIST requires at most 6 arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ alpha := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if alpha.Type != ArgNumber {
+ return alpha
+ }
+ beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if beta.Type != ArgNumber {
+ return beta
+ }
+ if alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ cumulative := argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool()
+ if cumulative.Type != ArgNumber {
+ return cumulative
+ }
+ a, b := newNumberFormulaArg(0), newNumberFormulaArg(1)
+ if argsList.Len() > 4 {
+ if a = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); a.Type != ArgNumber {
+ return a
+ }
+ }
+ if argsList.Len() == 6 {
+ if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber {
+ return b
+ }
+ }
+ return newListFormulaArg([]formulaArg{x, alpha, beta, cumulative, a, b})
+}
+
+// BETAdotDIST function calculates the cumulative beta distribution function
+// or the probability density function of the Beta distribution, for a
+// supplied set of parameters. The syntax of the function is:
+//
+// BETA.DIST(x,alpha,beta,cumulative,[A],[B])
+func (fn *formulaFuncs) BETAdotDIST(argsList *list.List) formulaArg {
+ args := fn.prepareBETAdotDISTArgs(argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ x, alpha, beta, cumulative, a, b := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5]
+ if x.Number < a.Number || x.Number > b.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if a.Number == b.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ scale := b.Number - a.Number
+ x.Number = (x.Number - a.Number) / scale
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(getBetaDist(x.Number, alpha.Number, beta.Number))
+ }
+ return newNumberFormulaArg(getBetaDistPDF(x.Number, alpha.Number, beta.Number) / scale)
+}
+
+// BETADIST function calculates the cumulative beta probability density
+// function for a supplied set of parameters. The syntax of the function is:
+//
+// BETADIST(x,alpha,beta,[A],[B])
+func (fn *formulaFuncs) BETADIST(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BETADIST requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BETADIST requires at most 5 arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ alpha := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if alpha.Type != ArgNumber {
+ return alpha
+ }
+ beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if beta.Type != ArgNumber {
+ return beta
+ }
+ if alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ a, b := newNumberFormulaArg(0), newNumberFormulaArg(1)
+ if argsList.Len() > 3 {
+ if a = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); a.Type != ArgNumber {
+ return a
+ }
+ }
+ if argsList.Len() == 5 {
+ if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber {
+ return b
+ }
+ }
+ if x.Number < a.Number || x.Number > b.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if a.Number == b.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(getBetaDist((x.Number-a.Number)/(b.Number-a.Number), alpha.Number, beta.Number))
+}
+
+// d1mach returns double precision real machine constants.
+func d1mach(i int) float64 {
+ arr := []float64{
+ 2.2250738585072014e-308,
+ 1.7976931348623158e+308,
+ 1.1102230246251565e-16,
+ 2.2204460492503131e-16,
+ 0.301029995663981195,
+ }
+ if i > len(arr) {
+ return 0
+ }
+ return arr[i-1]
+}
+
+// chebyshevInit determines the number of terms for the double precision
+// orthogonal series "dos" needed to insure the error is no larger
+// than "eta". Ordinarily eta will be chosen to be one-tenth machine
+// precision.
+func chebyshevInit(nos int, eta float64, dos []float64) int {
+ i, e := 0, 0.0
+ if nos < 1 {
+ return 0
+ }
+ for ii := 1; ii <= nos; ii++ {
+ i = nos - ii
+ e += math.Abs(dos[i])
+ if e > eta {
+ return i
+ }
+ }
+ return i
+}
+
+// chebyshevEval evaluates the n-term Chebyshev series "a" at "x".
+func chebyshevEval(n int, x float64, a []float64) float64 {
+ if n < 1 || n > 1000 || x < -1.1 || x > 1.1 {
+ return math.NaN()
+ }
+ twox, b0, b1, b2 := x*2, 0.0, 0.0, 0.0
+ for i := 1; i <= n; i++ {
+ b2 = b1
+ b1 = b0
+ b0 = twox*b1 - b2 + a[n-i]
+ }
+ return (b0 - b2) * 0.5
+}
+
+// lgammacor is an implementation for the log(gamma) correction.
+func lgammacor(x float64) float64 {
+ algmcs := []float64{
+ 0.1666389480451863247205729650822, -0.1384948176067563840732986059135e-4,
+ 0.9810825646924729426157171547487e-8, -0.1809129475572494194263306266719e-10,
+ 0.6221098041892605227126015543416e-13, -0.3399615005417721944303330599666e-15,
+ 0.2683181998482698748957538846666e-17, -0.2868042435334643284144622399999e-19,
+ 0.3962837061046434803679306666666e-21, -0.6831888753985766870111999999999e-23,
+ 0.1429227355942498147573333333333e-24, -0.3547598158101070547199999999999e-26,
+ 0.1025680058010470912000000000000e-27, -0.3401102254316748799999999999999e-29,
+ 0.1276642195630062933333333333333e-30,
+ }
+ nalgm := chebyshevInit(15, d1mach(3), algmcs)
+ xbig := 1.0 / math.Sqrt(d1mach(3))
+ xmax := math.Exp(math.Min(math.Log(d1mach(2)/12.0), -math.Log(12.0*d1mach(1))))
+ if x < 10.0 {
+ return math.NaN()
+ } else if x >= xmax {
+ return 4.930380657631324e-32
+ } else if x < xbig {
+ tmp := 10.0 / x
+ return chebyshevEval(nalgm, tmp*tmp*2.0-1.0, algmcs) / x
+ }
+ return 1.0 / (x * 12.0)
+}
+
+// logrelerr compute the relative error logarithm.
+func logrelerr(x float64) float64 {
+ alnrcs := []float64{
+ 0.10378693562743769800686267719098e+1, -0.13364301504908918098766041553133,
+ 0.19408249135520563357926199374750e-1, -0.30107551127535777690376537776592e-2,
+ 0.48694614797154850090456366509137e-3, -0.81054881893175356066809943008622e-4,
+ 0.13778847799559524782938251496059e-4, -0.23802210894358970251369992914935e-5,
+ 0.41640416213865183476391859901989e-6, -0.73595828378075994984266837031998e-7,
+ 0.13117611876241674949152294345011e-7, -0.23546709317742425136696092330175e-8,
+ 0.42522773276034997775638052962567e-9, -0.77190894134840796826108107493300e-10,
+ 0.14075746481359069909215356472191e-10, -0.25769072058024680627537078627584e-11,
+ 0.47342406666294421849154395005938e-12, -0.87249012674742641745301263292675e-13,
+ 0.16124614902740551465739833119115e-13, -0.29875652015665773006710792416815e-14,
+ 0.55480701209082887983041321697279e-15, -0.10324619158271569595141333961932e-15,
+ 0.19250239203049851177878503244868e-16, -0.35955073465265150011189707844266e-17,
+ 0.67264542537876857892194574226773e-18, -0.12602624168735219252082425637546e-18,
+ 0.23644884408606210044916158955519e-19, -0.44419377050807936898878389179733e-20,
+ 0.83546594464034259016241293994666e-21, -0.15731559416479562574899253521066e-21,
+ 0.29653128740247422686154369706666e-22, -0.55949583481815947292156013226666e-23,
+ 0.10566354268835681048187284138666e-23, -0.19972483680670204548314999466666e-24,
+ 0.37782977818839361421049855999999e-25, -0.71531586889081740345038165333333e-26,
+ 0.13552488463674213646502024533333e-26, -0.25694673048487567430079829333333e-27,
+ 0.48747756066216949076459519999999e-28, -0.92542112530849715321132373333333e-29,
+ 0.17578597841760239233269760000000e-29, -0.33410026677731010351377066666666e-30,
+ 0.63533936180236187354180266666666e-31,
+ }
+ nlnrel := chebyshevInit(43, 0.1*d1mach(3), alnrcs)
+ if x <= -1 {
+ return math.NaN()
+ }
+ if math.Abs(x) <= 0.375 {
+ return x * (1.0 - x*chebyshevEval(nlnrel, x/0.375, alnrcs))
+ }
+ return math.Log(x + 1.0)
+}
+
+// logBeta is an implementation for the log of the beta distribution
+// function.
+func logBeta(a, b float64) float64 {
+ corr, p, q := 0.0, a, a
+ if b < p {
+ p = b
+ }
+ if b > q {
+ q = b
+ }
+ if p < 0 {
+ return math.NaN()
+ }
+ if p == 0 {
+ return math.MaxFloat64
+ }
+ if p >= 10.0 {
+ corr = lgammacor(p) + lgammacor(q) - lgammacor(p+q)
+ f1 := q * logrelerr(-p/(p+q))
+ return math.Log(q)*-0.5 + 0.918938533204672741780329736406 + corr + (p-0.5)*math.Log(p/(p+q)) + math.Nextafter(f1, f1)
+ }
+ if q >= 10 {
+ corr = lgammacor(q) - lgammacor(p+q)
+ val, _ := math.Lgamma(p)
+ return val + corr + p - p*math.Log(p+q) + (q-0.5)*logrelerr(-p/(p+q))
+ }
+ return math.Log(math.Gamma(p) * (math.Gamma(q) / math.Gamma(p+q)))
+}
+
+// pbetaRaw is a part of pbeta for the beta distribution.
+func pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y float64) float64 {
+ if q > 1.0 {
+ xb := p*math.Log(y) + q*math.Log(1.0-y) - logBeta(p, q) - math.Log(q)
+ ib := int(math.Max(xb/alnsml, 0.0))
+ term := math.Exp(xb - float64(ib)*alnsml)
+ c := 1.0 / (1.0 - y)
+ p1 := q * c / (p + q - 1.0)
+ finsum := 0.0
+ n := int(q)
+ if q == float64(n) {
+ n = n - 1
+ }
+ for i := 1; i <= n; i++ {
+ if p1 <= 1 && term/eps <= finsum {
+ break
+ }
+ xi := float64(i)
+ term = (q - xi + 1.0) * c * term / (p + q - xi)
+ if term > 1.0 {
+ ib = ib - 1
+ term = term * sml
+ }
+ if ib == 0 {
+ finsum = finsum + term
+ }
+ }
+ ans = ans + finsum
+ }
+ if y != x || p != pin {
+ ans = 1.0 - ans
+ }
+ ans = math.Max(math.Min(ans, 1.0), 0.0)
+ return ans
+}
+
+// pbeta returns distribution function of the beta distribution.
+func pbeta(x, pin, qin float64) (ans float64) {
+ eps := d1mach(3)
+ alneps := math.Log(eps)
+ sml := d1mach(1)
+ alnsml := math.Log(sml)
+ y := x
+ p := pin
+ q := qin
+ if p/(p+q) < x {
+ y = 1.0 - y
+ p = qin
+ q = pin
+ }
+ if (p+q)*y/(p+1.0) < eps {
+ xb := p*math.Log(math.Max(y, sml)) - math.Log(p) - logBeta(p, q)
+ if xb > alnsml && y != 0.0 {
+ ans = math.Exp(xb)
+ }
+ if y != x || p != pin {
+ ans = 1.0 - ans
+ }
+ } else {
+ ps := q - math.Floor(q)
+ if ps == 0.0 {
+ ps = 1.0
+ }
+ xb := p*math.Log(y) - logBeta(ps, p) - math.Log(p)
+ if xb >= alnsml {
+ ans = math.Exp(xb)
+ term := ans * p
+ if ps != 1.0 {
+ n := int(math.Max(alneps/math.Log(y), 4.0))
+ for i := 1; i <= n; i++ {
+ xi := float64(i)
+ term = term * (xi - ps) * y / xi
+ ans = ans + term/(p+xi)
+ }
+ }
+ }
+ ans = pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y)
+ }
+ return ans
+}
+
+// betainvProbIterator is a part of betainv for the inverse of the beta
+// function.
+func betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logBeta, maxCumulative, prob1, prob2 float64) float64 {
+ var i, j, prev, prop4 float64
+ j = 1
+ for prob := 0; prob < 1000; prob++ {
+ prop3 := pbeta(beta3, alpha1, beta1)
+ prop3 = (prop3 - prob1) * math.Exp(logBeta+prob2*math.Log(beta3)+beta2*math.Log(1.0-beta3))
+ if prop3*prop4 <= 0 {
+ prev = math.Max(math.Abs(j), maxCumulative)
+ }
+ h := 1.0
+ for iteratorCount := 0; iteratorCount < 1000; iteratorCount++ {
+ j = h * prop3
+ if math.Abs(j) < prev {
+ i = beta3 - j
+ if i >= 0 && i <= 1.0 {
+ if prev <= alpha3 {
+ return beta3
+ }
+ if math.Abs(prop3) <= alpha3 {
+ return beta3
+ }
+ if i != 0 && i != 1.0 {
+ break
+ }
+ }
+ }
+ h /= 3.0
+ }
+ if i == beta3 {
+ return beta3
+ }
+ beta3, prop4 = i, prop3
+ }
+ return beta3
+}
+
+// calcBetainv is an implementation for the quantile of the beta
+// distribution.
+func calcBetainv(probability, alpha, beta, lower, upper float64) float64 {
+ minCumulative, maxCumulative := 1.0e-300, 3.0e-308
+ lowerBound, upperBound := maxCumulative, 1.0-2.22e-16
+ needSwap := false
+ var alpha1, alpha2, beta1, beta2, beta3, prob1, x, y float64
+ if probability <= 0.5 {
+ prob1, alpha1, beta1 = probability, alpha, beta
+ } else {
+ prob1, alpha1, beta1, needSwap = 1.0-probability, beta, alpha, true
+ }
+ logBetaNum := logBeta(alpha, beta)
+ prob2 := math.Sqrt(-math.Log(prob1 * prob1))
+ prob3 := prob2 - (prob2*0.27061+2.3075)/(prob2*(prob2*0.04481+0.99229)+1)
+ if alpha1 > 1 && beta1 > 1 {
+ alpha2, beta2, prob2 = 1/(alpha1+alpha1-1), 1/(beta1+beta1-1), (prob3*prob3-3)/6
+ x = 2 / (alpha2 + beta2)
+ y = prob3*math.Sqrt(x+prob2)/x - (beta2-alpha2)*(prob2+5/6.0-2/(x*3))
+ beta3 = alpha1 / (alpha1 + beta1*math.Exp(y+y))
+ } else {
+ beta2, prob2 = 1/(beta1*9), beta1+beta1
+ beta2 = prob2 * math.Pow(1-beta2+prob3*math.Sqrt(beta2), 3)
+ if beta2 <= 0 {
+ beta3 = 1 - math.Exp((math.Log((1-prob1)*beta1)+logBetaNum)/beta1)
+ } else {
+ beta2 = (prob2 + alpha1*4 - 2) / beta2
+ if beta2 <= 1 {
+ beta3 = math.Exp((logBetaNum + math.Log(alpha1*prob1)) / alpha1)
+ } else {
+ beta3 = 1 - 2/(beta2+1)
+ }
+ }
+ }
+ beta2, prob2 = 1-beta1, 1-alpha1
+ if beta3 < lowerBound {
+ beta3 = lowerBound
+ } else if beta3 > upperBound {
+ beta3 = upperBound
+ }
+ alpha3 := math.Max(minCumulative, math.Pow(10.0, -13.0-2.5/(alpha1*alpha1)-0.5/(prob1*prob1)))
+ beta3 = betainvProbIterator(alpha1, alpha3, beta1, beta2, beta3, logBetaNum, maxCumulative, prob1, prob2)
+ if needSwap {
+ beta3 = 1.0 - beta3
+ }
+ return (upper-lower)*beta3 + lower
+}
+
+// betainv is an implementation of the formula functions BETAINV and
+// BETA.INV.
+func (fn *formulaFuncs) betainv(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 3 arguments", name))
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at most 5 arguments", name))
+ }
+ probability := argsList.Front().Value.(formulaArg).ToNumber()
+ if probability.Type != ArgNumber {
+ return probability
+ }
+ if probability.Number <= 0 || probability.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ alpha := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if alpha.Type != ArgNumber {
+ return alpha
+ }
+ beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if beta.Type != ArgNumber {
+ return beta
+ }
+ if alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ a, b := newNumberFormulaArg(0), newNumberFormulaArg(1)
+ if argsList.Len() > 3 {
+ if a = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); a.Type != ArgNumber {
+ return a
+ }
+ }
+ if argsList.Len() == 5 {
+ if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber {
+ return b
+ }
+ }
+ if a.Number == b.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(calcBetainv(probability.Number, alpha.Number, beta.Number, a.Number, b.Number))
+}
+
+// BETAINV function uses an iterative procedure to calculate the inverse of
+// the cumulative beta probability density function for a supplied
+// probability. The syntax of the function is:
+//
+// BETAINV(probability,alpha,beta,[A],[B])
+func (fn *formulaFuncs) BETAINV(argsList *list.List) formulaArg {
+ return fn.betainv("BETAINV", argsList)
+}
+
+// BETAdotINV function uses an iterative procedure to calculate the inverse of
+// the cumulative beta probability density function for a supplied
+// probability. The syntax of the function is:
+//
+// BETA.INV(probability,alpha,beta,[A],[B])
+func (fn *formulaFuncs) BETAdotINV(argsList *list.List) formulaArg {
+ return fn.betainv("BETA.INV", argsList)
+}
+
+// incompleteGamma is an implementation of the incomplete gamma function.
+func incompleteGamma(a, x float64) float64 {
+ maxVal := 32
+ summer := 0.0
+ for n := 0; n <= maxVal; n++ {
+ divisor := a
+ for i := 1; i <= n; i++ {
+ divisor *= a + float64(i)
+ }
+ summer += math.Pow(x, float64(n)) / divisor
+ }
+ return math.Pow(x, a) * math.Exp(0-x) * summer
+}
+
+// binomCoeff implement binomial coefficient calculation.
+func binomCoeff(n, k float64) float64 {
+ return fact(n) / (fact(k) * fact(n-k))
+}
+
+// binomdist implement binomial distribution calculation.
+func binomdist(x, n, p float64) float64 {
+ return binomCoeff(n, x) * math.Pow(p, x) * math.Pow(1-p, n-x)
+}
+
+// BINOMdotDIST function returns the Binomial Distribution probability for a
+// given number of successes from a specified number of trials. The syntax of
+// the function is:
+//
+// BINOM.DIST(number_s,trials,probability_s,cumulative)
+func (fn *formulaFuncs) BINOMdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST requires 4 arguments")
+ }
+ return fn.BINOMDIST(argsList)
+}
+
+// BINOMDIST function returns the Binomial Distribution probability of a
+// specified number of successes out of a specified number of trials. The
+// syntax of the function is:
+//
+// BINOMDIST(number_s,trials,probability_s,cumulative)
+func (fn *formulaFuncs) BINOMDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BINOMDIST requires 4 arguments")
+ }
+ var s, trials, probability, cumulative formulaArg
+ if s = argsList.Front().Value.(formulaArg).ToNumber(); s.Type != ArgNumber {
+ return s
+ }
+ if trials = argsList.Front().Next().Value.(formulaArg).ToNumber(); trials.Type != ArgNumber {
+ return trials
+ }
+ if s.Number < 0 || s.Number > trials.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if probability = argsList.Back().Prev().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+
+ if probability.Number < 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if cumulative.Number == 1 {
+ bm := 0.0
+ for i := 0; i <= int(s.Number); i++ {
+ bm += binomdist(float64(i), trials.Number, probability.Number)
+ }
+ return newNumberFormulaArg(bm)
+ }
+ return newNumberFormulaArg(binomdist(s.Number, trials.Number, probability.Number))
+}
+
+// BINOMdotDISTdotRANGE function returns the Binomial Distribution probability
+// for the number of successes from a specified number of trials falling into
+// a specified range.
+//
+// BINOM.DIST.RANGE(trials,probability_s,number_s,[number_s2])
+func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST.RANGE requires at least 3 arguments")
+ }
+ if argsList.Len() > 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BINOM.DIST.RANGE requires at most 4 arguments")
+ }
+ trials := argsList.Front().Value.(formulaArg).ToNumber()
+ if trials.Type != ArgNumber {
+ return trials
+ }
+ probability := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if probability.Type != ArgNumber {
+ return probability
+ }
+ if probability.Number < 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ num1 := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if num1.Type != ArgNumber {
+ return num1
+ }
+ if num1.Number < 0 || num1.Number > trials.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ num2 := num1
+ if argsList.Len() > 3 {
+ if num2 = argsList.Back().Value.(formulaArg).ToNumber(); num2.Type != ArgNumber {
+ return num2
+ }
+ }
+ if num2.Number < 0 || num2.Number > trials.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ sum := 0.0
+ for i := num1.Number; i <= num2.Number; i++ {
+ sum += binomdist(i, trials.Number, probability.Number)
+ }
+ return newNumberFormulaArg(sum)
+}
+
+// binominv implement inverse of the binomial distribution calculation.
+func binominv(n, p, alpha float64) float64 {
+ q, i, sum, maxVal := 1-p, 0.0, 0.0, 0.0
+ n = math.Floor(n)
+ if q > p {
+ factor := math.Pow(q, n)
+ sum = factor
+ for i = 0; i < n && sum < alpha; i++ {
+ factor *= (n - i) / (i + 1) * p / q
+ sum += factor
+ }
+ return i
+ }
+ factor := math.Pow(p, n)
+ sum, maxVal = 1-factor, n
+ for i = 0; i < maxVal && sum >= alpha; i++ {
+ factor *= (n - i) / (i + 1) * q / p
+ sum -= factor
+ }
+ return n - i
+}
+
+// BINOMdotINV function returns the inverse of the Cumulative Binomial
+// Distribution. The syntax of the function is:
+//
+// BINOM.INV(trials,probability_s,alpha)
+func (fn *formulaFuncs) BINOMdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "BINOM.INV requires 3 numeric arguments")
+ }
+ trials := argsList.Front().Value.(formulaArg).ToNumber()
+ if trials.Type != ArgNumber {
+ return trials
+ }
+ if trials.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ probability := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if probability.Type != ArgNumber {
+ return probability
+ }
+ if probability.Number <= 0 || probability.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ alpha := argsList.Back().Value.(formulaArg).ToNumber()
+ if alpha.Type != ArgNumber {
+ return alpha
+ }
+ if alpha.Number <= 0 || alpha.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(binominv(trials.Number, probability.Number, alpha.Number))
+}
+
+// CHIDIST function calculates the right-tailed probability of the chi-square
+// distribution. The syntax of the function is:
+//
+// CHIDIST(x,degrees_freedom)
+func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHIDIST requires 2 numeric arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ degrees := argsList.Back().Value.(formulaArg).ToNumber()
+ if degrees.Type != ArgNumber {
+ return degrees
+ }
+ logSqrtPi, sqrtPi := math.Log(math.Sqrt(math.Pi)), 1/math.Sqrt(math.Pi)
+ var e, s, z, c, y float64
+ a, x1, even := x.Number/2, x.Number, int(degrees.Number)%2 == 0
+ if degrees.Number > 1 {
+ y = math.Exp(-a)
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg(-math.Sqrt(x1)))
+ o := fn.NORMSDIST(args)
+ s = 2 * o.Number
+ if even {
+ s = y
+ }
+ if degrees.Number > 2 {
+ x1 = (degrees.Number - 1) / 2
+ z = 0.5
+ if even {
+ z = 1
+ }
+ if a > 20 {
+ e = logSqrtPi
+ if even {
+ e = 0
+ }
+ c = math.Log(a)
+ for z <= x1 {
+ e = math.Log(z) + e
+ s += math.Exp(c*z - a - e)
+ z++
+ }
+ return newNumberFormulaArg(s)
+ }
+ e = sqrtPi / math.Sqrt(a)
+ if even {
+ e = 1
+ }
+ c = 0
+ for z <= x1 {
+ e = e * (a / z)
+ c = c + e
+ z++
+ }
+ return newNumberFormulaArg(c*y + s)
+ }
+ return newNumberFormulaArg(s)
+}
+
+// CHIINV function calculates the inverse of the right-tailed probability of
+// the Chi-Square Distribution. The syntax of the function is:
+//
+// CHIINV(probability,deg_freedom)
+func (fn *formulaFuncs) CHIINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHIINV requires 2 numeric arguments")
+ }
+ probability := argsList.Front().Value.(formulaArg).ToNumber()
+ if probability.Type != ArgNumber {
+ return probability
+ }
+ if probability.Number <= 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ deg := argsList.Back().Value.(formulaArg).ToNumber()
+ if deg.Type != ArgNumber {
+ return deg
+ }
+ if deg.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(gammainv(1-probability.Number, 0.5*deg.Number, 2.0))
+}
+
+// CHITEST function uses the chi-square test to calculate the probability that
+// the differences between two supplied data sets (of observed and expected
+// frequencies), are likely to be simply due to sampling error, or if they are
+// likely to be real. The syntax of the function is:
+//
+// CHITEST(actual_range,expected_range)
+func (fn *formulaFuncs) CHITEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHITEST requires 2 arguments")
+ }
+ actual, expected := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg)
+ actualList, expectedList := actual.ToList(), expected.ToList()
+ rows := len(actual.Matrix)
+ if rows == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ columns := len(actualList) / rows
+ if len(actualList) != len(expectedList) || len(actualList) == 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var result float64
+ var degrees int
+ for i := 0; i < len(actualList); i++ {
+ a, e := actualList[i].ToNumber(), expectedList[i].ToNumber()
+ if a.Type == ArgNumber && e.Type == ArgNumber {
+ if e.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ if e.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ result += (a.Number - e.Number) * (a.Number - e.Number) / e.Number
+ }
+ }
+ if rows == 1 {
+ degrees = columns - 1
+ } else if columns == 1 {
+ degrees = rows - 1
+ } else {
+ degrees = (columns - 1) * (rows - 1)
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg(result))
+ args.PushBack(newNumberFormulaArg(float64(degrees)))
+ return fn.CHIDIST(args)
+}
+
+// getGammaSeries calculates a power-series of the gamma function.
+func getGammaSeries(fA, fX float64) float64 {
+ var (
+ fHalfMachEps = 2.22045e-016 / 2
+ fDenomfactor = fA
+ fSummand = 1 / fA
+ fSum = fSummand
+ nCount = 1
+ )
+ for fSummand/fSum > fHalfMachEps && nCount <= 10000 {
+ fDenomfactor = fDenomfactor + 1
+ fSummand = fSummand * fX / fDenomfactor
+ fSum = fSum + fSummand
+ nCount = nCount + 1
+ }
+ return fSum
+}
+
+// getGammaContFraction returns continued fraction with odd items of the gamma
+// function.
+func getGammaContFraction(fA, fX float64) float64 {
+ var (
+ fBigInv = 2.22045e-016
+ fHalfMachEps = fBigInv / 2
+ fBig = 1 / fBigInv
+ fCount = 0.0
+ fY = 1 - fA
+ fDenom = fX + 2 - fA
+ fPkm1 = fX + 1
+ fPkm2 = 1.0
+ fQkm1 = fDenom * fX
+ fQkm2 = fX
+ fApprox = fPkm1 / fQkm1
+ bFinished = false
+ )
+ for !bFinished && fCount < 10000 {
+ fCount = fCount + 1
+ fY = fY + 1
+ fDenom = fDenom + 2
+ var (
+ fNum = fY * fCount
+ f1 = fPkm1 * fDenom
+ f2 = fPkm2 * fNum
+ fPk = math.Nextafter(f1, f1) - math.Nextafter(f2, f2)
+ f3 = fQkm1 * fDenom
+ f4 = fQkm2 * fNum
+ fQk = math.Nextafter(f3, f3) - math.Nextafter(f4, f4)
+ )
+ if fQk != 0 {
+ fR := fPk / fQk
+ bFinished = math.Abs((fApprox-fR)/fR) <= fHalfMachEps
+ fApprox = fR
+ }
+ fPkm2, fPkm1, fQkm2, fQkm1 = fPkm1, fPk, fQkm1, fQk
+ if math.Abs(fPk) > fBig {
+ // reduce a fraction does not change the value
+ fPkm2 = fPkm2 * fBigInv
+ fPkm1 = fPkm1 * fBigInv
+ fQkm2 = fQkm2 * fBigInv
+ fQkm1 = fQkm1 * fBigInv
+ }
+ }
+ return fApprox
+}
+
+// getLogGammaHelper is a part of implementation of the function getLogGamma.
+func getLogGammaHelper(fZ float64) float64 {
+ _fg := 6.024680040776729583740234375
+ zgHelp := fZ + _fg - 0.5
+ return math.Log(getLanczosSum(fZ)) + (fZ-0.5)*math.Log(zgHelp) - zgHelp
+}
+
+// getGammaHelper is a part of implementation of the function getLogGamma.
+func getGammaHelper(fZ float64) float64 {
+ var (
+ gamma = getLanczosSum(fZ)
+ fg = 6.024680040776729583740234375
+ zgHelp = fZ + fg - 0.5
+ // avoid intermediate overflow
+ halfpower = math.Pow(zgHelp, fZ/2-0.25)
+ )
+ gamma *= halfpower
+ gamma /= math.Exp(zgHelp)
+ gamma *= halfpower
+ if fZ <= 20 && fZ == math.Floor(fZ) {
+ gamma = math.Round(gamma)
+ }
+ return gamma
+}
+
+// getLogGamma calculates the natural logarithm of the gamma function.
+func getLogGamma(fZ float64) float64 {
+ fMaxGammaArgument := 171.624376956302
+ if fZ >= fMaxGammaArgument {
+ return getLogGammaHelper(fZ)
+ }
+ if fZ >= 1.0 {
+ return math.Log(getGammaHelper(fZ))
+ }
+ if fZ >= 0.5 {
+ return math.Log(getGammaHelper(fZ+1) / fZ)
+ }
+ return getLogGammaHelper(fZ+2) - math.Log(fZ+1) - math.Log(fZ)
+}
+
+// getLowRegIGamma returns lower regularized incomplete gamma function.
+func getLowRegIGamma(fA, fX float64) float64 {
+ lnFactor := fA*math.Log(fX) - fX - getLogGamma(fA)
+ factor := math.Exp(lnFactor)
+ if fX > fA+1 {
+ return 1 - factor*getGammaContFraction(fA, fX)
+ }
+ return factor * getGammaSeries(fA, fX)
+}
+
+// getChiSqDistCDF returns left tail for the Chi-Square distribution.
+func getChiSqDistCDF(fX, fDF float64) float64 {
+ if fX <= 0 {
+ return 0
+ }
+ return getLowRegIGamma(fDF/2, fX/2)
+}
+
+// getChiSqDistPDF calculates the probability density function for the
+// Chi-Square distribution.
+func getChiSqDistPDF(fX, fDF float64) float64 {
+ if fDF*fX > 1391000 {
+ return math.Exp((0.5*fDF-1)*math.Log(fX*0.5) - 0.5*fX - math.Log(2) - getLogGamma(0.5*fDF))
+ }
+ var fCount, fValue float64
+ if math.Mod(fDF, 2) < 0.5 {
+ fValue = 0.5
+ fCount = 2
+ } else {
+ fValue = 1 / math.Sqrt(fX*2*math.Pi)
+ fCount = 1
+ }
+ for fCount < fDF {
+ fValue *= fX / fCount
+ fCount += 2
+ }
+ if fX >= 1425 {
+ fValue = math.Exp(math.Log(fValue) - fX/2)
+ } else {
+ fValue *= math.Exp(-fX / 2)
+ }
+ return fValue
+}
+
+// CHISQdotDIST function calculates the Probability Density Function or the
+// Cumulative Distribution Function for the Chi-Square Distribution. The
+// syntax of the function is:
+//
+// CHISQ.DIST(x,degrees_freedom,cumulative)
+func (fn *formulaFuncs) CHISQdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST requires 3 arguments")
+ }
+ var x, degrees, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if x.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ maxDeg := math.Pow10(10)
+ if degrees.Number < 1 || degrees.Number >= maxDeg {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(getChiSqDistCDF(x.Number, degrees.Number))
+ }
+ return newNumberFormulaArg(getChiSqDistPDF(x.Number, degrees.Number))
+}
+
+// CHISQdotDISTdotRT function calculates the right-tailed probability of the
+// Chi-Square Distribution. The syntax of the function is:
+//
+// CHISQ.DIST.RT(x,degrees_freedom)
+func (fn *formulaFuncs) CHISQdotDISTdotRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.DIST.RT requires 2 numeric arguments")
+ }
+ return fn.CHIDIST(argsList)
+}
+
+// CHISQdotTEST function performs the chi-square test on two supplied data sets
+// (of observed and expected frequencies), and returns the probability that
+// the differences between the sets are simply due to sampling error. The
+// syntax of the function is:
+//
+// CHISQ.TEST(actual_range,expected_range)
+func (fn *formulaFuncs) CHISQdotTEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.TEST requires 2 arguments")
+ }
+ return fn.CHITEST(argsList)
+}
+
+// hasChangeOfSign check if the sign has been changed.
+func hasChangeOfSign(u, w float64) bool {
+ return (u < 0 && w > 0) || (u > 0 && w < 0)
+}
+
+// calcInverseIterator directly maps the required parameters for inverse
+// distribution functions.
+type calcInverseIterator struct {
+ name string
+ fp, fDF, nT float64
+}
+
+// callBack implements the callback function for the inverse iterator.
+func (iterator *calcInverseIterator) callBack(x float64) float64 {
+ if iterator.name == "CHISQ.INV" {
+ return iterator.fp - getChiSqDistCDF(x, iterator.fDF)
+ }
+ return iterator.fp - getTDist(x, iterator.fDF, iterator.nT)
+}
+
+// inverseQuadraticInterpolation inverse quadratic interpolation with
+// additional brackets.
+func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, fBy float64) float64 {
+ fYEps := 1.0e-307
+ fXEps := 2.22045e-016
+ fPx, fPy, fQx, fQy, fRx, fRy := fAx, fAy, fBx, fBy, fAx, fAy
+ fSx := 0.5 * (fAx + fBx)
+ bHasToInterpolate := true
+ nCount := 0
+ for nCount < 500 && math.Abs(fRy) > fYEps && (fBx-fAx) > math.Max(math.Abs(fAx), math.Abs(fBx))*fXEps {
+ if bHasToInterpolate {
+ if fPy != fQy && fQy != fRy && fRy != fPy {
+ fSx = fPx*fRy*fQy/(fRy-fPy)/(fQy-fPy) + fRx*fQy*fPy/(fQy-fRy)/(fPy-fRy) +
+ fQx*fPy*fRy/(fPy-fQy)/(fRy-fQy)
+ bHasToInterpolate = (fAx < fSx) && (fSx < fBx)
+ } else {
+ bHasToInterpolate = false
+ }
+ }
+ if !bHasToInterpolate {
+ fSx = 0.5 * (fAx + fBx)
+ fQx, fQy = fBx, fBy
+ bHasToInterpolate = true
+ }
+ fPx, fQx, fRx, fPy, fQy = fQx, fRx, fSx, fQy, fRy
+ fRy = iterator.callBack(fSx)
+ if hasChangeOfSign(fAy, fRy) {
+ fBx, fBy = fRx, fRy
+ } else {
+ fAx, fAy = fRx, fRy
+ }
+ bHasToInterpolate = bHasToInterpolate && (math.Abs(fRy)*2 <= math.Abs(fQy))
+ nCount++
+ }
+ return fRx
+}
+
+// calcIterateInverse function calculates the iteration for inverse
+// distributions.
+func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 {
+ fAy, fBy := iterator.callBack(fAx), iterator.callBack(fBx)
+ var fTemp float64
+ var nCount int
+ for nCount = 0; nCount < 1000 && !hasChangeOfSign(fAy, fBy); nCount++ {
+ if math.Abs(fAy) <= math.Abs(fBy) {
+ fTemp = fAx
+ fAx += 2 * (fAx - fBx)
+ if fAx < 0 {
+ fAx = 0
+ }
+ fBx = fTemp
+ fBy = fAy
+ fAy = iterator.callBack(fAx)
+ } else {
+ fTemp = fBx
+ fBx += 2 * (fBx - fAx)
+ fAx = fTemp
+ fAy = fBy
+ fBy = iterator.callBack(fBx)
+ }
+ }
+ if fAy == 0 || fBy == 0 {
+ return 0
+ }
+ return inverseQuadraticInterpolation(iterator, fAx, fAy, fBx, fBy)
+}
+
+// CHISQdotINV function calculates the inverse of the left-tailed probability
+// of the Chi-Square Distribution. The syntax of the function is:
+//
+// CHISQ.INV(probability,degrees_freedom)
+func (fn *formulaFuncs) CHISQdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV requires 2 numeric arguments")
+ }
+ var probability, degrees formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if probability.Number < 0 || probability.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if degrees.Number < 1 || degrees.Number > math.Pow10(10) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{
+ name: "CHISQ.INV",
+ fp: probability.Number,
+ fDF: degrees.Number,
+ }, degrees.Number/2, degrees.Number))
+}
+
+// CHISQdotINVdotRT function calculates the inverse of the right-tailed
+// probability of the Chi-Square Distribution. The syntax of the function is:
+//
+// CHISQ.INV.RT(probability,degrees_freedom)
+func (fn *formulaFuncs) CHISQdotINVdotRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHISQ.INV.RT requires 2 numeric arguments")
+ }
+ return fn.CHIINV(argsList)
+}
+
+// confidence is an implementation of the formula functions CONFIDENCE and
+// CONFIDENCE.NORM.
+func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 numeric arguments", name))
+ }
+ alpha := argsList.Front().Value.(formulaArg).ToNumber()
+ if alpha.Type != ArgNumber {
+ return alpha
+ }
+ if alpha.Number <= 0 || alpha.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ stdDev := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if stdDev.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ size := argsList.Back().Value.(formulaArg).ToNumber()
+ if size.Type != ArgNumber {
+ return size
+ }
+ if size.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New()
+ args.Init()
+ args.PushBack(newNumberFormulaArg(alpha.Number / 2))
+ args.PushBack(newNumberFormulaArg(0))
+ args.PushBack(newNumberFormulaArg(1))
+ return newNumberFormulaArg(-fn.NORMINV(args).Number * (stdDev.Number / math.Sqrt(size.Number)))
+}
+
+// CONFIDENCE function uses a Normal Distribution to calculate a confidence
+// value that can be used to construct the Confidence Interval for a
+// population mean, for a supplied probability and sample size. It is assumed
+// that the standard deviation of the population is known. The syntax of the
+// function is:
+//
+// CONFIDENCE(alpha,standard_dev,size)
+func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg {
+ return fn.confidence("CONFIDENCE", argsList)
+}
+
+// CONFIDENCEdotNORM function uses a Normal Distribution to calculate a
+// confidence value that can be used to construct the confidence interval for
+// a population mean, for a supplied probability and sample size. It is
+// assumed that the standard deviation of the population is known. The syntax
+// of the function is:
+//
+// CONFIDENCE.NORM(alpha,standard_dev,size)
+func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg {
+ return fn.confidence("CONFIDENCE.NORM", argsList)
+}
+
+// CONFIDENCEdotT function uses a Student's T-Distribution to calculate a
+// confidence value that can be used to construct the confidence interval for
+// a population mean, for a supplied probablity and supplied sample size. It
+// is assumed that the standard deviation of the population is known. The
+// syntax of the function is:
+//
+// CONFIDENCE.T(alpha,standard_dev,size)
+func (fn *formulaFuncs) CONFIDENCEdotT(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CONFIDENCE.T requires 3 arguments")
+ }
+ var alpha, standardDev, size formulaArg
+ if alpha = argsList.Front().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber {
+ return alpha
+ }
+ if standardDev = argsList.Front().Next().Value.(formulaArg).ToNumber(); standardDev.Type != ArgNumber {
+ return standardDev
+ }
+ if size = argsList.Back().Value.(formulaArg).ToNumber(); size.Type != ArgNumber {
+ return size
+ }
+ if alpha.Number <= 0 || alpha.Number >= 1 || standardDev.Number <= 0 || size.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if size.Number == 1 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(standardDev.Number * calcIterateInverse(calcInverseIterator{
+ name: "CONFIDENCE.T",
+ fp: alpha.Number,
+ fDF: size.Number - 1,
+ nT: 2,
+ }, size.Number/2, size.Number) / math.Sqrt(size.Number))
+}
+
+// covar is an implementation of the formula functions COVAR, COVARIANCE.P and
+// COVARIANCE.S.
+func (fn *formulaFuncs) covar(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name))
+ }
+ array1 := argsList.Front().Value.(formulaArg)
+ array2 := argsList.Back().Value.(formulaArg)
+ left, right := array1.ToList(), array2.ToList()
+ n := len(left)
+ if n != len(right) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ l1, l2 := list.New(), list.New()
+ l1.PushBack(array1)
+ l2.PushBack(array2)
+ result, skip := 0.0, 0
+ mean1, mean2 := fn.AVERAGE(l1), fn.AVERAGE(l2)
+ for i := 0; i < n; i++ {
+ arg1 := left[i].ToNumber()
+ arg2 := right[i].ToNumber()
+ if arg1.Type == ArgError || arg2.Type == ArgError {
+ skip++
+ continue
+ }
+ result += (arg1.Number - mean1.Number) * (arg2.Number - mean2.Number)
+ }
+ if name == "COVARIANCE.S" {
+ return newNumberFormulaArg(result / float64(n-skip-1))
+ }
+ return newNumberFormulaArg(result / float64(n-skip))
+}
+
+// COVAR function calculates the covariance of two supplied sets of values. The
+// syntax of the function is:
+//
+// COVAR(array1,array2)
+func (fn *formulaFuncs) COVAR(argsList *list.List) formulaArg {
+ return fn.covar("COVAR", argsList)
+}
+
+// COVARIANCEdotP function calculates the population covariance of two supplied
+// sets of values. The syntax of the function is:
+//
+// COVARIANCE.P(array1,array2)
+func (fn *formulaFuncs) COVARIANCEdotP(argsList *list.List) formulaArg {
+ return fn.covar("COVARIANCE.P", argsList)
+}
+
+// COVARIANCEdotS function calculates the sample covariance of two supplied
+// sets of values. The syntax of the function is:
+//
+// COVARIANCE.S(array1,array2)
+func (fn *formulaFuncs) COVARIANCEdotS(argsList *list.List) formulaArg {
+ return fn.covar("COVARIANCE.S", argsList)
+}
+
+// calcStringCountSum is part of the implementation countSum.
+func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) {
+ if countText && num.Type == ArgError && arg.String != "" {
+ count++
+ }
+ if num.Type == ArgNumber {
+ sum += num.Number
+ count++
+ }
+ return count, sum
+}
+
+// countSum get count and sum for a formula arguments array.
+func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum float64) {
+ for _, arg := range args {
+ switch arg.Type {
+ case ArgNumber:
+ if countText || !arg.Boolean {
+ sum += arg.Number
+ count++
+ }
+ case ArgString:
+ if !countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") {
+ continue
+ } else if countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") {
+ num := arg.ToBool()
+ if num.Type == ArgNumber {
+ count++
+ sum += num.Number
+ continue
+ }
+ }
+ num := arg.ToNumber()
+ count, sum = calcStringCountSum(countText, count, sum, num, arg)
+ case ArgList, ArgMatrix:
+ cnt, summary := fn.countSum(countText, arg.ToList())
+ sum += summary
+ count += cnt
+ }
+ }
+ return
+}
+
+// CORREL function calculates the Pearson Product-Moment Correlation
+// Coefficient for two sets of values. The syntax of the function is:
+//
+// CORREL(array1,array2)
+func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CORREL requires 2 arguments")
+ }
+ array1 := argsList.Front().Value.(formulaArg)
+ array2 := argsList.Back().Value.(formulaArg)
+ left, right := array1.ToList(), array2.ToList()
+ n := len(left)
+ if n != len(right) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ l1, l2, l3 := list.New(), list.New(), list.New()
+ for i := 0; i < n; i++ {
+ if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 {
+ l1.PushBack(lhs)
+ l2.PushBack(rhs)
+ }
+ }
+ stdev1, stdev2 := fn.STDEV(l1), fn.STDEV(l2)
+ if stdev1.Number == 0 || stdev2.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ mean1, mean2, skip := fn.AVERAGE(l1), fn.AVERAGE(l2), 0
+ for i := 0; i < n; i++ {
+ lhs, rhs := left[i].ToNumber(), right[i].ToNumber()
+ if lhs.Number == 0 || rhs.Number == 0 {
+ skip++
+ continue
+ }
+ l3.PushBack(newNumberFormulaArg((lhs.Number - mean1.Number) * (rhs.Number - mean2.Number)))
+ }
+ return newNumberFormulaArg(fn.SUM(l3).Number / float64(n-skip-1) / stdev1.Number / stdev2.Number)
+}
+
+// COUNT function returns the count of numeric values in a supplied set of
+// cells or values. This count includes both numbers and dates. The syntax of
+// the function is:
+//
+// COUNT(value1,[value2],...)
+func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg {
+ var count int
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ if num := arg.ToNumber(); num.Type == ArgNumber {
+ count++
+ }
+ case ArgNumber:
+ count++
+ case ArgMatrix:
+ for _, row := range arg.Matrix {
+ for _, cell := range row {
+ if cell.Type == ArgNumber {
+ count++
+ }
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(float64(count))
+}
+
+// COUNTA function returns the number of non-blanks within a supplied set of
+// cells or values. The syntax of the function is:
+//
+// COUNTA(value1,[value2],...)
+func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg {
+ var count int
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ if arg.String != "" {
+ count++
+ }
+ case ArgNumber:
+ count++
+ case ArgMatrix:
+ for _, row := range arg.ToList() {
+ switch row.Type {
+ case ArgString:
+ if row.String != "" {
+ count++
+ }
+ case ArgNumber:
+ count++
+ }
+ }
+ }
+ }
+ return newNumberFormulaArg(float64(count))
+}
+
+// COUNTBLANK function returns the number of blank cells in a supplied range.
+// The syntax of the function is:
+//
+// COUNTBLANK(range)
+func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument")
+ }
+ var count float64
+ for _, cell := range argsList.Front().Value.(formulaArg).ToList() {
+ if cell.Type == ArgEmpty {
+ count++
+ }
+ }
+ return newNumberFormulaArg(count)
+}
+
+// COUNTIF function returns the number of cells within a supplied range, that
+// satisfy a given criteria. The syntax of the function is:
+//
+// COUNTIF(range,criteria)
+func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments")
+ }
+ var (
+ criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg))
+ count float64
+ )
+ for _, cell := range argsList.Front().Value.(formulaArg).ToList() {
+ if cell.Type == ArgString && criteria.Condition.Type != ArgString {
+ continue
+ }
+ if ok, _ := formulaCriteriaEval(cell, criteria); ok {
+ count++
+ }
+ }
+ return newNumberFormulaArg(count)
+}
+
+// formulaIfsMatch function returns cells reference array which match criteria.
+func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) {
+ for i := 0; i < len(args)-1; i += 2 {
+ var match []cellRef
+ matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1])
+ if i == 0 {
+ for rowIdx, row := range matrix {
+ for colIdx, col := range row {
+ if ok, _ := formulaCriteriaEval(col, criteria); ok {
+ match = append(match, cellRef{Col: colIdx, Row: rowIdx})
+ }
+ }
+ }
+ } else {
+ match = []cellRef{}
+ for _, ref := range cellRefs {
+ value := matrix[ref.Row][ref.Col]
+ if ok, _ := formulaCriteriaEval(value, criteria); ok {
+ match = append(match, ref)
+ }
+ }
+ }
+ cellRefs = match[:]
+ }
+ return
+}
+
+// COUNTIFS function returns the number of rows within a table, that satisfy a
+// set of given criteria. The syntax of the function is:
+//
+// COUNTIFS(criteria_range1,criteria1,[criteria_range2,criteria2],...)
+func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COUNTIFS requires at least 2 arguments")
+ }
+ if argsList.Len()%2 != 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var args []formulaArg
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ return newNumberFormulaArg(float64(len(formulaIfsMatch(args))))
+}
+
+// CRITBINOM function returns the inverse of the Cumulative Binomial
+// Distribution. I.e. for a specific number of independent trials, the
+// function returns the smallest value (number of successes) for which the
+// cumulative binomial distribution is greater than or equal to a specified
+// value. The syntax of the function is:
+//
+// CRITBINOM(trials,probability_s,alpha)
+func (fn *formulaFuncs) CRITBINOM(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CRITBINOM requires 3 numeric arguments")
+ }
+ return fn.BINOMdotINV(argsList)
+}
+
+// DEVSQ function calculates the sum of the squared deviations from the sample
+// mean. The syntax of the function is:
+//
+// DEVSQ(number1,[number2],...)
+func (fn *formulaFuncs) DEVSQ(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DEVSQ requires at least 1 numeric argument")
+ }
+ avg, count, result := fn.AVERAGE(argsList), -1, 0.0
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ for _, cell := range arg.Value.(formulaArg).ToList() {
+ if cell.Type != ArgNumber {
+ continue
+ }
+ count++
+ if count == 0 {
+ result = math.Pow(cell.Number-avg.Number, 2)
+ continue
+ }
+ result += math.Pow(cell.Number-avg.Number, 2)
+ }
+ }
+ if count == -1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(result)
+}
+
+// FISHER function calculates the Fisher Transformation for a supplied value.
+// The syntax of the function is:
+//
+// FISHER(x)
+func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ arg := token.ToNumber()
+ if arg.Type == ArgNumber {
+ if arg.Number <= -1 || arg.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(0.5 * math.Log((1+arg.Number)/(1-arg.Number)))
+ }
+ case ArgNumber:
+ if token.Number <= -1 || token.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(0.5 * math.Log((1+token.Number)/(1-token.Number)))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument")
+}
+
+// FISHERINV function calculates the inverse of the Fisher Transformation and
+// returns a value between -1 and +1. The syntax of the function is:
+//
+// FISHERINV(y)
+func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgString:
+ arg := token.ToNumber()
+ if arg.Type == ArgNumber {
+ return newNumberFormulaArg((math.Exp(2*arg.Number) - 1) / (math.Exp(2*arg.Number) + 1))
+ }
+ case ArgNumber:
+ return newNumberFormulaArg((math.Exp(2*token.Number) - 1) / (math.Exp(2*token.Number) + 1))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument")
+}
+
+// FORECAST function predicts a future point on a linear trend line fitted to a
+// supplied set of x- and y- values. The syntax of the function is:
+//
+// FORECAST(x,known_y's,known_x's)
+func (fn *formulaFuncs) FORECAST(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("FORECAST", 3, argsList)
+}
+
+// FORECASTdotLINEAR function predicts a future point on a linear trend line
+// fitted to a supplied set of x- and y- values. The syntax of the function is:
+//
+// FORECAST.LINEAR(x,known_y's,known_x's)
+func (fn *formulaFuncs) FORECASTdotLINEAR(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("FORECAST.LINEAR", 3, argsList)
+}
+
+// matrixToSortedColumnList convert matrix formula arguments to a ascending
+// order list by column.
+func matrixToSortedColumnList(arg formulaArg) formulaArg {
+ var (
+ mtx []formulaArg
+ cols = len(arg.Matrix[0])
+ )
+ for colIdx := 0; colIdx < cols; colIdx++ {
+ for _, row := range arg.Matrix {
+ cell := row[colIdx]
+ if cell.Type == ArgError {
+ return cell
+ }
+ if cell.Type == ArgNumber {
+ mtx = append(mtx, cell)
+ }
+ }
+ }
+ argsList := newListFormulaArg(mtx)
+ sort.Slice(argsList.List, func(i, j int) bool {
+ return argsList.List[i].Number < argsList.List[j].Number
+ })
+ return argsList
+}
+
+// FREQUENCY function to count how many children fall into different age
+// ranges. The syntax of the function is:
+//
+// FREQUENCY(data_array,bins_array)
+func (fn *formulaFuncs) FREQUENCY(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FREQUENCY requires 2 arguments")
+ }
+ data, bins := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg)
+ if len(data.Matrix) == 0 {
+ data.Matrix = [][]formulaArg{{data}}
+ }
+ if len(bins.Matrix) == 0 {
+ bins.Matrix = [][]formulaArg{{bins}}
+ }
+ var (
+ dataMtx, binsMtx formulaArg
+ c [][]formulaArg
+ i, j int
+ )
+ if dataMtx = matrixToSortedColumnList(data); dataMtx.Type != ArgList {
+ return dataMtx
+ }
+ if binsMtx = matrixToSortedColumnList(bins); binsMtx.Type != ArgList {
+ return binsMtx
+ }
+ for row := 0; row < len(binsMtx.List)+1; row++ {
+ var rows []formulaArg
+ for col := 0; col < 1; col++ {
+ rows = append(rows, newNumberFormulaArg(0))
+ }
+ c = append(c, rows)
+ }
+ for j = 0; j < len(binsMtx.List); j++ {
+ n := 0.0
+ for i < len(dataMtx.List) && dataMtx.List[i].Number <= binsMtx.List[j].Number {
+ n++
+ i++
+ }
+ c[j] = []formulaArg{newNumberFormulaArg(n)}
+ }
+ c[j] = []formulaArg{newNumberFormulaArg(float64(len(dataMtx.List) - i))}
+ if len(c) > 2 {
+ c[1], c[2] = c[2], c[1]
+ }
+ return newMatrixFormulaArg(c)
+}
+
+// GAMMA function returns the value of the Gamma Function, Γ(n), for a
+// specified number, n. The syntax of the function is:
+//
+// GAMMA(number)
+func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument")
+ }
+ if number.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(math.Gamma(number.Number))
+}
+
+// GAMMAdotDIST function returns the Gamma Distribution, which is frequently
+// used to provide probabilities for values that may have a skewed
+// distribution, such as queuing analysis.
+//
+// GAMMA.DIST(x,alpha,beta,cumulative)
+func (fn *formulaFuncs) GAMMAdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMA.DIST requires 4 arguments")
+ }
+ return fn.GAMMADIST(argsList)
+}
+
+// GAMMADIST function returns the Gamma Distribution, which is frequently used
+// to provide probabilities for values that may have a skewed distribution,
+// such as queuing analysis.
+//
+// GAMMADIST(x,alpha,beta,cumulative)
+func (fn *formulaFuncs) GAMMADIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMADIST requires 4 arguments")
+ }
+ var x, alpha, beta, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if x.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if alpha = argsList.Front().Next().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber {
+ return alpha
+ }
+ if beta = argsList.Back().Prev().Value.(formulaArg).ToNumber(); beta.Type != ArgNumber {
+ return beta
+ }
+ if alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(incompleteGamma(alpha.Number, x.Number/beta.Number) / math.Gamma(alpha.Number))
+ }
+ return newNumberFormulaArg((1 / (math.Pow(beta.Number, alpha.Number) * math.Gamma(alpha.Number))) * math.Pow(x.Number, alpha.Number-1) * math.Exp(0-(x.Number/beta.Number)))
+}
+
+// gammainv returns the inverse of the Gamma distribution for the specified
+// value.
+func gammainv(probability, alpha, beta float64) float64 {
+ xLo, xHi := 0.0, alpha*beta*5
+ dx, x, xNew, result := 1024.0, 1.0, 1.0, 0.0
+ for i := 0; math.Abs(dx) > 8.88e-016 && i <= 256; i++ {
+ result = incompleteGamma(alpha, x/beta) / math.Gamma(alpha)
+ e := result - probability
+ if e == 0 {
+ dx = 0
+ } else if e < 0 {
+ xLo = x
+ } else {
+ xHi = x
+ }
+ pdf := (1 / (math.Pow(beta, alpha) * math.Gamma(alpha))) * math.Pow(x, alpha-1) * math.Exp(0-(x/beta))
+ if pdf != 0 {
+ dx = e / pdf
+ xNew = x - dx
+ }
+ if xNew < xLo || xNew > xHi || pdf == 0 {
+ xNew = (xLo + xHi) / 2
+ dx = xNew - x
+ }
+ x = xNew
+ }
+ return x
+}
+
+// GAMMAdotINV function returns the inverse of the Gamma Cumulative
+// Distribution. The syntax of the function is:
+//
+// GAMMA.INV(probability,alpha,beta)
+func (fn *formulaFuncs) GAMMAdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMA.INV requires 3 arguments")
+ }
+ return fn.GAMMAINV(argsList)
+}
+
+// GAMMAINV function returns the inverse of the Gamma Cumulative Distribution.
+// The syntax of the function is:
+//
+// GAMMAINV(probability,alpha,beta)
+func (fn *formulaFuncs) GAMMAINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMAINV requires 3 arguments")
+ }
+ var probability, alpha, beta formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if probability.Number < 0 || probability.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if alpha = argsList.Front().Next().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber {
+ return alpha
+ }
+ if beta = argsList.Back().Value.(formulaArg).ToNumber(); beta.Type != ArgNumber {
+ return beta
+ }
+ if alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(gammainv(probability.Number, alpha.Number, beta.Number))
+}
+
+// GAMMALN function returns the natural logarithm of the Gamma Function, Γ
+// (n). The syntax of the function is:
+//
+// GAMMALN(x)
+func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument")
+ }
+ if x.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(math.Log(math.Gamma(x.Number)))
+}
+
+// GAMMALNdotPRECISE function returns the natural logarithm of the Gamma
+// Function, Γ(n). The syntax of the function is:
+//
+// GAMMALN.PRECISE(x)
+func (fn *formulaFuncs) GAMMALNdotPRECISE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN.PRECISE requires 1 numeric argument")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ if x.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(getLogGamma(x.Number))
+}
+
+// GAUSS function returns the probability that a member of a standard normal
+// population will fall between the mean and a specified number of standard
+// deviations from the mean. The syntax of the function is:
+//
+// GAUSS(z)
+func (fn *formulaFuncs) GAUSS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GAUSS requires 1 numeric argument")
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 0})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1})
+ args.PushBack(newBoolFormulaArg(true))
+ normdist := fn.NORMDIST(args)
+ if normdist.Type != ArgNumber {
+ return normdist
+ }
+ return newNumberFormulaArg(normdist.Number - 0.5)
+}
+
+// GEOMEAN function calculates the geometric mean of a supplied set of values.
+// The syntax of the function is:
+//
+// GEOMEAN(number1,[number2],...)
+func (fn *formulaFuncs) GEOMEAN(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "GEOMEAN requires at least 1 numeric argument")
+ }
+ product := fn.PRODUCT(argsList)
+ if product.Type != ArgNumber {
+ return product
+ }
+ count := fn.COUNT(argsList)
+ minVal := fn.MIN(argsList)
+ if product.Number > 0 && minVal.Number > 0 {
+ return newNumberFormulaArg(math.Pow(product.Number, 1/count.Number))
+ }
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+}
+
+// getNewMatrix create matrix by given columns and rows.
+func getNewMatrix(c, r int) (matrix [][]float64) {
+ for i := 0; i < c; i++ {
+ for j := 0; j < r; j++ {
+ for x := len(matrix); x <= i; x++ {
+ matrix = append(matrix, []float64{})
+ }
+ for y := len(matrix[i]); y <= j; y++ {
+ matrix[i] = append(matrix[i], 0)
+ }
+ matrix[i][j] = 0
+ }
+ }
+ return
+}
+
+// approxSub subtract two values, if signs are identical and the values are
+// equal, will be returns 0 instead of calculating the subtraction.
+func approxSub(a, b float64) float64 {
+ if ((a < 0 && b < 0) || (a > 0 && b > 0)) && math.Abs(a-b) < 2.22045e-016 {
+ return 0
+ }
+ return a - b
+}
+
+// matrixClone return a copy of all elements of the original matrix.
+func matrixClone(matrix [][]float64) (cloneMatrix [][]float64) {
+ for i := 0; i < len(matrix); i++ {
+ for j := 0; j < len(matrix[i]); j++ {
+ for x := len(cloneMatrix); x <= i; x++ {
+ cloneMatrix = append(cloneMatrix, []float64{})
+ }
+ for k := len(cloneMatrix[i]); k <= j; k++ {
+ cloneMatrix[i] = append(cloneMatrix[i], 0)
+ }
+ cloneMatrix[i][j] = matrix[i][j]
+ }
+ }
+ return
+}
+
+// trendGrowthMatrixInfo defined matrix checking result.
+type trendGrowthMatrixInfo struct {
+ trendType, nCX, nCY, nRX, nRY, M, N int
+ mtxX, mtxY [][]float64
+}
+
+// prepareTrendGrowthMtxX is a part of implementation of the trend growth prepare.
+func prepareTrendGrowthMtxX(mtxX [][]float64) [][]float64 {
+ var mtx [][]float64
+ for i := 0; i < len(mtxX); i++ {
+ for j := 0; j < len(mtxX[i]); j++ {
+ if mtxX[i][j] == 0 {
+ return nil
+ }
+ for x := len(mtx); x <= j; x++ {
+ mtx = append(mtx, []float64{})
+ }
+ for y := len(mtx[j]); y <= i; y++ {
+ mtx[j] = append(mtx[j], 0)
+ }
+ mtx[j][i] = mtxX[i][j]
+ }
+ }
+ return mtx
+}
+
+// prepareTrendGrowthMtxY is a part of implementation of the trend growth prepare.
+func prepareTrendGrowthMtxY(bLOG bool, mtxY [][]float64) [][]float64 {
+ var mtx [][]float64
+ for i := 0; i < len(mtxY); i++ {
+ for j := 0; j < len(mtxY[i]); j++ {
+ if mtxY[i][j] == 0 {
+ return nil
+ }
+ for x := len(mtx); x <= j; x++ {
+ mtx = append(mtx, []float64{})
+ }
+ for y := len(mtx[j]); y <= i; y++ {
+ mtx[j] = append(mtx[j], 0)
+ }
+ mtx[j][i] = mtxY[i][j]
+ }
+ }
+ if bLOG {
+ var pNewY [][]float64
+ for i := 0; i < len(mtxY); i++ {
+ for j := 0; j < len(mtxY[i]); j++ {
+ fVal := mtxY[i][j]
+ if fVal <= 0 {
+ return nil
+ }
+ for x := len(pNewY); x <= j; x++ {
+ pNewY = append(pNewY, []float64{})
+ }
+ for y := len(pNewY[j]); y <= i; y++ {
+ pNewY[j] = append(pNewY[j], 0)
+ }
+ pNewY[j][i] = math.Log(fVal)
+ }
+ }
+ mtx = pNewY
+ }
+ return mtx
+}
+
+// prepareTrendGrowth check and return the result.
+func prepareTrendGrowth(bLOG bool, mtxX, mtxY [][]float64) (*trendGrowthMatrixInfo, formulaArg) {
+ var nCX, nRX, M, N, trendType int
+ nRY, nCY := len(mtxY), len(mtxY[0])
+ cntY := nCY * nRY
+ newY := prepareTrendGrowthMtxY(bLOG, mtxY)
+ if newY == nil {
+ return nil, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ var newX [][]float64
+ if len(mtxX) != 0 {
+ nRX, nCX = len(mtxX), len(mtxX[0])
+ if newX = prepareTrendGrowthMtxX(mtxX); newX == nil {
+ return nil, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if nCX == nCY && nRX == nRY {
+ trendType, M, N = 1, 1, cntY // simple regression
+ } else if nCY != 1 && nRY != 1 {
+ return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ } else if nCY == 1 {
+ if nRX != nRY {
+ return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ }
+ trendType, M, N = 2, nCX, nRY
+ } else if nCX != nCY {
+ return nil, newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ } else {
+ trendType, M, N = 3, nRX, nCY
+ }
+ } else {
+ newX = getNewMatrix(nCY, nRY)
+ nCX, nRX = nCY, nRY
+ num := 1.0
+ for i := 0; i < nRY; i++ {
+ for j := 0; j < nCY; j++ {
+ newX[j][i] = num
+ num++
+ }
+ }
+ trendType, M, N = 1, 1, cntY
+ }
+ return &trendGrowthMatrixInfo{
+ trendType: trendType,
+ nCX: nCX,
+ nCY: nCY,
+ nRX: nRX,
+ nRY: nRY,
+ M: M,
+ N: N,
+ mtxX: newX,
+ mtxY: newY,
+ }, newEmptyFormulaArg()
+}
+
+// calcPosition calculate position for matrix by given index.
+func calcPosition(mtx [][]float64, idx int) (row, col int) {
+ rowSize := len(mtx[0])
+ col = idx
+ if rowSize > 1 {
+ col = idx / rowSize
+ }
+ row = idx - col*rowSize
+ return
+}
+
+// getDouble returns float64 data type value in the matrix by given index.
+func getDouble(mtx [][]float64, idx int) float64 {
+ row, col := calcPosition(mtx, idx)
+ return mtx[col][row]
+}
+
+// putDouble set a float64 data type value in the matrix by given index.
+func putDouble(mtx [][]float64, idx int, val float64) {
+ row, col := calcPosition(mtx, idx)
+ mtx[col][row] = val
+}
+
+// calcMeanOverAll returns mean of the given matrix by over all element.
+func calcMeanOverAll(mtx [][]float64, n int) float64 {
+ var sum float64
+ for i := 0; i < len(mtx); i++ {
+ for j := 0; j < len(mtx[i]); j++ {
+ sum += mtx[i][j]
+ }
+ }
+ return sum / float64(n)
+}
+
+// calcSumProduct returns uses the matrices as vectors of length M over all
+// element.
+func calcSumProduct(mtxA, mtxB [][]float64, m int) float64 {
+ sum := 0.0
+ for i := 0; i < m; i++ {
+ sum += getDouble(mtxA, i) * getDouble(mtxB, i)
+ }
+ return sum
+}
+
+// calcColumnMeans calculates means of the columns of matrix.
+func calcColumnMeans(mtxX, mtxRes [][]float64, c, r int) {
+ for i := 0; i < c; i++ {
+ var sum float64
+ for k := 0; k < r; k++ {
+ sum += mtxX[i][k]
+ }
+ putDouble(mtxRes, i, sum/float64(r))
+ }
+}
+
+// calcColumnsDelta calculates subtract of the columns of matrix.
+func calcColumnsDelta(mtx, columnMeans [][]float64, c, r int) {
+ for i := 0; i < c; i++ {
+ for k := 0; k < r; k++ {
+ mtx[i][k] = approxSub(mtx[i][k], getDouble(columnMeans, i))
+ }
+ }
+}
+
+// calcSign returns sign by given value, no mathematical signum, but used to
+// switch between adding and subtracting.
+func calcSign(val float64) float64 {
+ if val > 0 {
+ return 1
+ }
+ return -1
+}
+
+// calcColsMaximumNorm is a special version for use within QR
+// decomposition. Maximum norm of column index c starting in row index r;
+// matrix A has count n rows.
+func calcColsMaximumNorm(mtxA [][]float64, c, r, n int) float64 {
+ var norm float64
+ for row := r; row < n; row++ {
+ if norm < math.Abs(mtxA[c][row]) {
+ norm = math.Abs(mtxA[c][row])
+ }
+ }
+ return norm
+}
+
+// calcFastMult returns multiply n x m matrix A with m x l matrix B to n x l matrix R.
+func calcFastMult(mtxA, mtxB, mtxR [][]float64, n, m, l int) {
+ var sum float64
+ for row := 0; row < n; row++ {
+ for col := 0; col < l; col++ {
+ sum = 0.0
+ for k := 0; k < m; k++ {
+ sum += mtxA[k][row] * mtxB[col][k]
+ }
+ mtxR[col][row] = sum
+ }
+ }
+}
+
+// calcRowsEuclideanNorm is a special version for use within QR
+// decomposition. Euclidean norm of column index c starting in row index r;
+// matrix a has count n rows.
+func calcRowsEuclideanNorm(mtxA [][]float64, c, r, n int) float64 {
+ var norm float64
+ for row := r; row < n; row++ {
+ norm += mtxA[c][row] * mtxA[c][row]
+ }
+ return math.Sqrt(norm)
+}
+
+// calcRowsSumProduct is a special version for use within QR decomposition.
+// starting in row index r;
+// a and b are indices of columns, matrices A and B have count n rows.
+func calcRowsSumProduct(mtxA [][]float64, a int, mtxB [][]float64, b, r, n int) float64 {
+ var result float64
+ for row := r; row < n; row++ {
+ result += mtxA[a][row] * mtxB[b][row]
+ }
+ return result
+}
+
+// calcSolveWithUpperRightTriangle solve for X in R*X=S using back substitution.
+func calcSolveWithUpperRightTriangle(mtxA [][]float64, vecR []float64, mtxS [][]float64, k int, bIsTransposed bool) {
+ var row int
+ for rowp1 := k; rowp1 > 0; rowp1-- {
+ row = rowp1 - 1
+ sum := getDouble(mtxS, row)
+ for col := rowp1; col < k; col++ {
+ if bIsTransposed {
+ sum -= mtxA[row][col] * getDouble(mtxS, col)
+ } else {
+ sum -= mtxA[col][row] * getDouble(mtxS, col)
+ }
+ }
+ putDouble(mtxS, row, sum/vecR[row])
+ }
+}
+
+// calcRowQRDecomposition calculates a QR decomposition with Householder
+// reflection.
+func calcRowQRDecomposition(mtxA [][]float64, vecR []float64, k, n int) bool {
+ for col := 0; col < k; col++ {
+ scale := calcColsMaximumNorm(mtxA, col, col, n)
+ if scale == 0 {
+ return false
+ }
+ for row := col; row < n; row++ {
+ mtxA[col][row] = mtxA[col][row] / scale
+ }
+ euclid := calcRowsEuclideanNorm(mtxA, col, col, n)
+ factor := 1.0 / euclid / (euclid + math.Abs(mtxA[col][col]))
+ signum := calcSign(mtxA[col][col])
+ mtxA[col][col] = mtxA[col][col] + signum*euclid
+ vecR[col] = -signum * scale * euclid
+ // apply Householder transformation to A
+ for c := col + 1; c < k; c++ {
+ sum := calcRowsSumProduct(mtxA, col, mtxA, c, col, n)
+ for row := col; row < n; row++ {
+ mtxA[c][row] = mtxA[c][row] - sum*factor*mtxA[col][row]
+ }
+ }
+ }
+ return true
+}
+
+// calcApplyColsHouseholderTransformation transposed matrices A and Y.
+func calcApplyColsHouseholderTransformation(mtxA [][]float64, r int, mtxY [][]float64, n int) {
+ denominator := calcColsSumProduct(mtxA, r, mtxA, r, r, n)
+ numerator := calcColsSumProduct(mtxA, r, mtxY, 0, r, n)
+ factor := 2 * (numerator / denominator)
+ for col := r; col < n; col++ {
+ putDouble(mtxY, col, getDouble(mtxY, col)-factor*mtxA[col][r])
+ }
+}
+
+// calcRowMeans calculates means of the rows of matrix.
+func calcRowMeans(mtxX, mtxRes [][]float64, c, r int) {
+ for k := 0; k < r; k++ {
+ var fSum float64
+ for i := 0; i < c; i++ {
+ fSum += mtxX[i][k]
+ }
+ mtxRes[k][0] = fSum / float64(c)
+ }
+}
+
+// calcRowsDelta calculates subtract of the rows of matrix.
+func calcRowsDelta(mtx, rowMeans [][]float64, c, r int) {
+ for k := 0; k < r; k++ {
+ for i := 0; i < c; i++ {
+ mtx[i][k] = approxSub(mtx[i][k], rowMeans[k][0])
+ }
+ }
+}
+
+// calcColumnMaximumNorm returns maximum norm of row index R starting in col
+// index C; matrix A has count N columns.
+func calcColumnMaximumNorm(mtxA [][]float64, r, c, n int) float64 {
+ var norm float64
+ for col := c; col < n; col++ {
+ if norm < math.Abs(mtxA[col][r]) {
+ norm = math.Abs(mtxA[col][r])
+ }
+ }
+ return norm
+}
+
+// calcColsEuclideanNorm returns euclidean norm of row index R starting in
+// column index C; matrix A has count N columns.
+func calcColsEuclideanNorm(mtxA [][]float64, r, c, n int) float64 {
+ var norm float64
+ for col := c; col < n; col++ {
+ norm += (mtxA[col][r]) * (mtxA[col][r])
+ }
+ return math.Sqrt(norm)
+}
+
+// calcColsSumProduct returns sum product for given matrix.
+func calcColsSumProduct(mtxA [][]float64, a int, mtxB [][]float64, b, c, n int) float64 {
+ var result float64
+ for col := c; col < n; col++ {
+ result += mtxA[col][a] * mtxB[col][b]
+ }
+ return result
+}
+
+// calcColQRDecomposition same with transposed matrix A, N is count of
+// columns, k count of rows.
+func calcColQRDecomposition(mtxA [][]float64, vecR []float64, k, n int) bool {
+ var sum float64
+ for row := 0; row < k; row++ {
+ // calculate vector u of the householder transformation
+ scale := calcColumnMaximumNorm(mtxA, row, row, n)
+ if scale == 0 {
+ return false
+ }
+ for col := row; col < n; col++ {
+ mtxA[col][row] = mtxA[col][row] / scale
+ }
+ euclid := calcColsEuclideanNorm(mtxA, row, row, n)
+ factor := 1 / euclid / (euclid + math.Abs(mtxA[row][row]))
+ signum := calcSign(mtxA[row][row])
+ mtxA[row][row] = mtxA[row][row] + signum*euclid
+ vecR[row] = -signum * scale * euclid
+ // apply Householder transformation to A
+ for r := row + 1; r < k; r++ {
+ sum = calcColsSumProduct(mtxA, row, mtxA, r, row, n)
+ for col := row; col < n; col++ {
+ mtxA[col][r] = mtxA[col][r] - sum*factor*mtxA[col][row]
+ }
+ }
+ }
+ return true
+}
+
+// calcApplyRowsHouseholderTransformation applies a Householder transformation to a
+// column vector Y with is given as Nx1 Matrix. The vector u, from which the
+// Householder transformation is built, is the column part in matrix A, with
+// column index c, starting with row index c. A is the result of the QR
+// decomposition as obtained from calcRowQRDecomposition.
+func calcApplyRowsHouseholderTransformation(mtxA [][]float64, c int, mtxY [][]float64, n int) {
+ denominator := calcRowsSumProduct(mtxA, c, mtxA, c, c, n)
+ numerator := calcRowsSumProduct(mtxA, c, mtxY, 0, c, n)
+ factor := 2 * (numerator / denominator)
+ for row := c; row < n; row++ {
+ putDouble(mtxY, row, getDouble(mtxY, row)-factor*mtxA[c][row])
+ }
+}
+
+// calcTrendGrowthSimpleRegression calculate simple regression for the calcTrendGrowth.
+func calcTrendGrowthSimpleRegression(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, N int) {
+ var meanX float64
+ if bConstant {
+ meanX = calcMeanOverAll(mtxX, N)
+ for i := 0; i < len(mtxX); i++ {
+ for j := 0; j < len(mtxX[i]); j++ {
+ mtxX[i][j] = approxSub(mtxX[i][j], meanX)
+ }
+ }
+ }
+ sumXY := calcSumProduct(mtxX, mtxY, N)
+ sumX2 := calcSumProduct(mtxX, mtxX, N)
+ slope := sumXY / sumX2
+ var help float64
+ var intercept float64
+ if bConstant {
+ intercept = meanY - slope*meanX
+ for i := 0; i < len(mtxRes); i++ {
+ for j := 0; j < len(mtxRes[i]); j++ {
+ help = newX[i][j]*slope + intercept
+ if bGrowth {
+ mtxRes[i][j] = math.Exp(help)
+ } else {
+ mtxRes[i][j] = help
+ }
+ }
+ }
+ } else {
+ for i := 0; i < len(mtxRes); i++ {
+ for j := 0; j < len(mtxRes[i]); j++ {
+ help = newX[i][j] * slope
+ if bGrowth {
+ mtxRes[i][j] = math.Exp(help)
+ } else {
+ mtxRes[i][j] = help
+ }
+ }
+ }
+ }
+}
+
+// calcTrendGrowthMultipleRegressionPart1 calculate multiple regression for the
+// calcTrendGrowth.
+func calcTrendGrowthMultipleRegressionPart1(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, RXN, K, N int) {
+ vecR := make([]float64, N) // for QR decomposition
+ means := getNewMatrix(K, 1) // mean of each column
+ slopes := getNewMatrix(1, K) // from b1 to bK
+ if len(means) == 0 || len(slopes) == 0 {
+ return
+ }
+ if bConstant {
+ calcColumnMeans(mtxX, means, K, N)
+ calcColumnsDelta(mtxX, means, K, N)
+ }
+ if !calcRowQRDecomposition(mtxX, vecR, K, N) {
+ return
+ }
+ // Later on we will divide by elements of vecR, so make sure that they aren't zero.
+ bIsSingular := false
+ for row := 0; row < K && !bIsSingular; row++ {
+ bIsSingular = bIsSingular || vecR[row] == 0
+ }
+ if bIsSingular {
+ return
+ }
+ for col := 0; col < K; col++ {
+ calcApplyRowsHouseholderTransformation(mtxX, col, mtxY, N)
+ }
+ for col := 0; col < K; col++ {
+ putDouble(slopes, col, getDouble(mtxY, col))
+ }
+ calcSolveWithUpperRightTriangle(mtxX, vecR, slopes, K, false)
+ // Fill result matrix
+ calcFastMult(newX, slopes, mtxRes, RXN, K, 1)
+ if bConstant {
+ intercept := meanY - calcSumProduct(means, slopes, K)
+ for row := 0; row < RXN; row++ {
+ mtxRes[0][row] = mtxRes[0][row] + intercept
+ }
+ }
+ if bGrowth {
+ for i := 0; i < RXN; i++ {
+ putDouble(mtxRes, i, math.Exp(getDouble(mtxRes, i)))
+ }
+ }
+}
+
+// calcTrendGrowthMultipleRegressionPart2 calculate multiple regression for the
+// calcTrendGrowth.
+func calcTrendGrowthMultipleRegressionPart2(bConstant, bGrowth bool, mtxY, mtxX, newX, mtxRes [][]float64, meanY float64, nCXN, K, N int) {
+ vecR := make([]float64, N) // for QR decomposition
+ means := getNewMatrix(K, 1) // mean of each row
+ slopes := getNewMatrix(K, 1) // row from b1 to bK
+ if len(means) == 0 || len(slopes) == 0 {
+ return
+ }
+ if bConstant {
+ calcRowMeans(mtxX, means, N, K)
+ calcRowsDelta(mtxX, means, N, K)
+ }
+ if !calcColQRDecomposition(mtxX, vecR, K, N) {
+ return
+ }
+ // later on we will divide by elements of vecR, so make sure that they aren't zero
+ bIsSingular := false
+ for row := 0; row < K && !bIsSingular; row++ {
+ bIsSingular = bIsSingular || vecR[row] == 0
+ }
+ if bIsSingular {
+ return
+ }
+ for row := 0; row < K; row++ {
+ calcApplyColsHouseholderTransformation(mtxX, row, mtxY, N)
+ }
+ for col := 0; col < K; col++ {
+ putDouble(slopes, col, getDouble(mtxY, col))
+ }
+ calcSolveWithUpperRightTriangle(mtxX, vecR, slopes, K, true)
+ // fill result matrix
+ calcFastMult(slopes, newX, mtxRes, 1, K, nCXN)
+ if bConstant {
+ fIntercept := meanY - calcSumProduct(means, slopes, K)
+ for col := 0; col < nCXN; col++ {
+ mtxRes[col][0] = mtxRes[col][0] + fIntercept
+ }
+ }
+ if bGrowth {
+ for i := 0; i < nCXN; i++ {
+ putDouble(mtxRes, i, math.Exp(getDouble(mtxRes, i)))
+ }
+ }
+}
+
+// calcTrendGrowthRegression is a part of implementation of the calcTrendGrowth.
+func calcTrendGrowthRegression(bConstant, bGrowth bool, trendType, nCXN, nRXN, K, N int, mtxY, mtxX, newX, mtxRes [][]float64) {
+ if len(mtxRes) == 0 {
+ return
+ }
+ var meanY float64
+ if bConstant {
+ copyX, copyY := matrixClone(mtxX), matrixClone(mtxY)
+ mtxX, mtxY = copyX, copyY
+ meanY = calcMeanOverAll(mtxY, N)
+ for i := 0; i < len(mtxY); i++ {
+ for j := 0; j < len(mtxY[i]); j++ {
+ mtxY[i][j] = approxSub(mtxY[i][j], meanY)
+ }
+ }
+ }
+ switch trendType {
+ case 1:
+ calcTrendGrowthSimpleRegression(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, N)
+ case 2:
+ calcTrendGrowthMultipleRegressionPart1(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nRXN, K, N)
+ default:
+ calcTrendGrowthMultipleRegressionPart2(bConstant, bGrowth, mtxY, mtxX, newX, mtxRes, meanY, nCXN, K, N)
+ }
+}
+
+// calcTrendGrowth returns values along a predicted exponential trend.
+func calcTrendGrowth(mtxY, mtxX, newX [][]float64, bConstant, bGrowth bool) ([][]float64, formulaArg) {
+ getMatrixParams, errArg := prepareTrendGrowth(bGrowth, mtxX, mtxY)
+ if errArg.Type != ArgEmpty {
+ return nil, errArg
+ }
+ trendType := getMatrixParams.trendType
+ nCX := getMatrixParams.nCX
+ nRX := getMatrixParams.nRX
+ K := getMatrixParams.M
+ N := getMatrixParams.N
+ mtxX = getMatrixParams.mtxX
+ mtxY = getMatrixParams.mtxY
+ // checking if data samples are enough
+ if (bConstant && (N < K+1)) || (!bConstant && (N < K)) || (N < 1) || (K < 1) {
+ return nil, errArg
+ }
+ // set the default newX if necessary
+ nCXN, nRXN := nCX, nRX
+ if len(newX) == 0 {
+ newX = matrixClone(mtxX) // mtxX will be changed to X-meanX
+ } else {
+ nRXN, nCXN = len(newX[0]), len(newX)
+ if (trendType == 2 && K != nCXN) || (trendType == 3 && K != nRXN) {
+ return nil, errArg
+ }
+ }
+ var mtxRes [][]float64
+ switch trendType {
+ case 1:
+ mtxRes = getNewMatrix(nCXN, nRXN)
+ case 2:
+ mtxRes = getNewMatrix(1, nRXN)
+ default:
+ mtxRes = getNewMatrix(nCXN, 1)
+ }
+ calcTrendGrowthRegression(bConstant, bGrowth, trendType, nCXN, nRXN, K, N, mtxY, mtxX, newX, mtxRes)
+ return mtxRes, errArg
+}
+
+// trendGrowth is an implementation of the formula functions GROWTH and TREND.
+func (fn *formulaFuncs) trendGrowth(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ if argsList.Len() > 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 4 arguments", name))
+ }
+ var knowY, knowX, newX [][]float64
+ var errArg formulaArg
+ constArg := newBoolFormulaArg(true)
+ knowY, errArg = newNumberMatrix(argsList.Front().Value.(formulaArg), false)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ if argsList.Len() > 1 {
+ knowX, errArg = newNumberMatrix(argsList.Front().Next().Value.(formulaArg), false)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ }
+ if argsList.Len() > 2 {
+ newX, errArg = newNumberMatrix(argsList.Front().Next().Next().Value.(formulaArg), false)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ }
+ if argsList.Len() > 3 {
+ if constArg = argsList.Back().Value.(formulaArg).ToBool(); constArg.Type != ArgNumber {
+ return constArg
+ }
+ }
+ var mtxNewX [][]float64
+ for i := 0; i < len(newX); i++ {
+ for j := 0; j < len(newX[i]); j++ {
+ for x := len(mtxNewX); x <= j; x++ {
+ mtxNewX = append(mtxNewX, []float64{})
+ }
+ for k := len(mtxNewX[j]); k <= i; k++ {
+ mtxNewX[j] = append(mtxNewX[j], 0)
+ }
+ mtxNewX[j][i] = newX[i][j]
+ }
+ }
+ mtx, errArg := calcTrendGrowth(knowY, knowX, mtxNewX, constArg.Number == 1, name == "GROWTH")
+ if errArg.Type != ArgEmpty {
+ return errArg
+ }
+ return newMatrixFormulaArg(newFormulaArgMatrix(mtx))
+}
+
+// GROWTH function calculates the exponential growth curve through a given set
+// of y-values and (optionally), one or more sets of x-values. The function
+// then extends the curve to calculate additional y-values for a further
+// supplied set of new x-values. The syntax of the function is:
+//
+// GROWTH(known_y's,[known_x's],[new_x's],[const])
+func (fn *formulaFuncs) GROWTH(argsList *list.List) formulaArg {
+ return fn.trendGrowth("GROWTH", argsList)
+}
+
+// HARMEAN function calculates the harmonic mean of a supplied set of values.
+// The syntax of the function is:
+//
+// HARMEAN(number1,[number2],...)
+func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument")
+ }
+ if minVal := fn.MIN(argsList); minVal.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ number, val, cnt := 0.0, 0.0, 0.0
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ num := arg.ToNumber()
+ if num.Type != ArgNumber {
+ continue
+ }
+ number = num.Number
+ case ArgNumber:
+ number = arg.Number
+ }
+ if number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ val += 1 / number
+ cnt++
+ }
+ return newNumberFormulaArg(1 / (val / cnt))
+}
+
+// checkHYPGEOMDISTArgs checking arguments for the formula function HYPGEOMDIST
+// and HYPGEOM.DIST.
+func checkHYPGEOMDISTArgs(sampleS, numberSample, populationS, numberPop formulaArg) bool {
+ return sampleS.Number < 0 ||
+ sampleS.Number > math.Min(numberSample.Number, populationS.Number) ||
+ sampleS.Number < math.Max(0, numberSample.Number-numberPop.Number+populationS.Number) ||
+ numberSample.Number <= 0 ||
+ numberSample.Number > numberPop.Number ||
+ populationS.Number <= 0 ||
+ populationS.Number > numberPop.Number ||
+ numberPop.Number <= 0
+}
+
+// prepareHYPGEOMDISTArgs prepare arguments for the formula function
+// HYPGEOMDIST and HYPGEOM.DIST.
+func (fn *formulaFuncs) prepareHYPGEOMDISTArgs(name string, argsList *list.List) formulaArg {
+ if name == "HYPGEOMDIST" && argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HYPGEOMDIST requires 4 numeric arguments")
+ }
+ if name == "HYPGEOM.DIST" && argsList.Len() != 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HYPGEOM.DIST requires 5 arguments")
+ }
+ var sampleS, numberSample, populationS, numberPop, cumulative formulaArg
+ if sampleS = argsList.Front().Value.(formulaArg).ToNumber(); sampleS.Type != ArgNumber {
+ return sampleS
+ }
+ if numberSample = argsList.Front().Next().Value.(formulaArg).ToNumber(); numberSample.Type != ArgNumber {
+ return numberSample
+ }
+ if populationS = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); populationS.Type != ArgNumber {
+ return populationS
+ }
+ if numberPop = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); numberPop.Type != ArgNumber {
+ return numberPop
+ }
+ if checkHYPGEOMDISTArgs(sampleS, numberSample, populationS, numberPop) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if name == "HYPGEOM.DIST" {
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber {
+ return cumulative
+ }
+ }
+ return newListFormulaArg([]formulaArg{sampleS, numberSample, populationS, numberPop, cumulative})
+}
+
+// HYPGEOMdotDIST function returns the value of the hypergeometric distribution
+// for a specified number of successes from a population sample. The function
+// can calculate the cumulative distribution or the probability density
+// function. The syntax of the function is:
+//
+// HYPGEOM.DIST(sample_s,number_sample,population_s,number_pop,cumulative)
+func (fn *formulaFuncs) HYPGEOMdotDIST(argsList *list.List) formulaArg {
+ args := fn.prepareHYPGEOMDISTArgs("HYPGEOM.DIST", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ sampleS, numberSample, populationS, numberPop, cumulative := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4]
+ if cumulative.Number == 1 {
+ var res float64
+ for i := 0; i <= int(sampleS.Number); i++ {
+ res += binomCoeff(populationS.Number, float64(i)) *
+ binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-float64(i)) /
+ binomCoeff(numberPop.Number, numberSample.Number)
+ }
+ return newNumberFormulaArg(res)
+ }
+ return newNumberFormulaArg(binomCoeff(populationS.Number, sampleS.Number) *
+ binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-sampleS.Number) /
+ binomCoeff(numberPop.Number, numberSample.Number))
+}
+
+// HYPGEOMDIST function returns the value of the hypergeometric distribution
+// for a given number of successes from a sample of a population. The syntax
+// of the function is:
+//
+// HYPGEOMDIST(sample_s,number_sample,population_s,number_pop)
+func (fn *formulaFuncs) HYPGEOMDIST(argsList *list.List) formulaArg {
+ args := fn.prepareHYPGEOMDISTArgs("HYPGEOMDIST", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ sampleS, numberSample, populationS, numberPop := args.List[0], args.List[1], args.List[2], args.List[3]
+ return newNumberFormulaArg(binomCoeff(populationS.Number, sampleS.Number) *
+ binomCoeff(numberPop.Number-populationS.Number, numberSample.Number-sampleS.Number) /
+ binomCoeff(numberPop.Number, numberSample.Number))
+}
+
+// INTERCEPT function calculates the intercept (the value at the intersection
+// of the y axis) of the linear regression line through a supplied set of x-
+// and y- values. The syntax of the function is:
+//
+// INTERCEPT(known_y's,known_x's)
+func (fn *formulaFuncs) INTERCEPT(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("INTERCEPT", 2, argsList)
+}
+
+// KURT function calculates the kurtosis of a supplied set of values. The
+// syntax of the function is:
+//
+// KURT(number1,[number2],...)
+func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "KURT requires at least 1 argument")
+ }
+ mean, stdev := fn.AVERAGE(argsList), fn.STDEV(argsList)
+ if stdev.Number > 0 {
+ count, summer := 0.0, 0.0
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgString, ArgNumber:
+ num := token.ToNumber()
+ if num.Type == ArgError {
+ continue
+ }
+ summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4)
+ count++
+ case ArgList, ArgMatrix:
+ for _, row := range token.ToList() {
+ if row.Type == ArgNumber || row.Type == ArgString {
+ num := row.ToNumber()
+ if num.Type == ArgError {
+ continue
+ }
+ summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4)
+ count++
+ }
+ }
+ }
+ }
+ if count > 3 {
+ return newNumberFormulaArg(summer*(count*(count+1)/((count-1)*(count-2)*(count-3))) - (3 * math.Pow(count-1, 2) / ((count - 2) * (count - 3))))
+ }
+ }
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+}
+
+// EXPONdotDIST function returns the value of the exponential distribution for
+// a give value of x. The user can specify whether the probability density
+// function or the cumulative distribution function is used. The syntax of the
+// Expondist function is:
+//
+// EXPON.DIST(x,lambda,cumulative)
+func (fn *formulaFuncs) EXPONdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EXPON.DIST requires 3 arguments")
+ }
+ return fn.EXPONDIST(argsList)
+}
+
+// EXPONDIST function returns the value of the exponential distribution for a
+// give value of x. The user can specify whether the probability density
+// function or the cumulative distribution function is used. The syntax of the
+// Expondist function is:
+//
+// EXPONDIST(x,lambda,cumulative)
+func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EXPONDIST requires 3 arguments")
+ }
+ var x, lambda, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if lambda = argsList.Front().Next().Value.(formulaArg).ToNumber(); lambda.Type != ArgNumber {
+ return lambda
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if x.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if lambda.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(1 - math.Exp(-lambda.Number*x.Number))
+ }
+ return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number))
+}
+
+// FdotDIST function calculates the Probability Density Function or the
+// Cumulative Distribution Function for the F Distribution. This function is
+// frequently used to measure the degree of diversity between two data
+// sets. The syntax of the function is:
+//
+// F.DIST(x,deg_freedom1,deg_freedom2,cumulative)
+func (fn *formulaFuncs) FdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "F.DIST requires 4 arguments")
+ }
+ var x, deg1, deg2, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if deg1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); deg1.Type != ArgNumber {
+ return deg1
+ }
+ if deg2 = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); deg2.Type != ArgNumber {
+ return deg2
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if x.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ maxDeg := math.Pow10(10)
+ if deg1.Number < 1 || deg1.Number >= maxDeg {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if deg2.Number < 1 || deg2.Number >= maxDeg {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(1 - getBetaDist(deg2.Number/(deg2.Number+deg1.Number*x.Number), deg2.Number/2, deg1.Number/2))
+ }
+ return newNumberFormulaArg(math.Gamma((deg2.Number+deg1.Number)/2) / (math.Gamma(deg1.Number/2) * math.Gamma(deg2.Number/2)) * math.Pow(deg1.Number/deg2.Number, deg1.Number/2) * (math.Pow(x.Number, (deg1.Number-2)/2) / math.Pow(1+(deg1.Number/deg2.Number)*x.Number, (deg1.Number+deg2.Number)/2)))
+}
+
+// FDIST function calculates the (right-tailed) F Probability Distribution,
+// which measures the degree of diversity between two data sets. The syntax
+// of the function is:
+//
+// FDIST(x,deg_freedom1,deg_freedom2)
+func (fn *formulaFuncs) FDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FDIST requires 3 arguments")
+ }
+ var x, deg1, deg2 formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if deg1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); deg1.Type != ArgNumber {
+ return deg1
+ }
+ if deg2 = argsList.Back().Value.(formulaArg).ToNumber(); deg2.Type != ArgNumber {
+ return deg2
+ }
+ if x.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ maxDeg := math.Pow10(10)
+ if deg1.Number < 1 || deg1.Number >= maxDeg {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if deg2.Number < 1 || deg2.Number >= maxDeg {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg(deg1.Number * x.Number / (deg1.Number*x.Number + deg2.Number)))
+ args.PushBack(newNumberFormulaArg(0.5 * deg1.Number))
+ args.PushBack(newNumberFormulaArg(0.5 * deg2.Number))
+ args.PushBack(newNumberFormulaArg(0))
+ args.PushBack(newNumberFormulaArg(1))
+ return newNumberFormulaArg(1 - fn.BETADIST(args).Number)
+}
+
+// FdotDISTdotRT function calculates the (right-tailed) F Probability
+// Distribution, which measures the degree of diversity between two data sets.
+// The syntax of the function is:
+//
+// F.DIST.RT(x,deg_freedom1,deg_freedom2)
+func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "F.DIST.RT requires 3 arguments")
+ }
+ return fn.FDIST(argsList)
+}
+
+// prepareFinvArgs checking and prepare arguments for the formula functions
+// F.INV, F.INV.RT and FINV.
+func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name))
+ }
+ var probability, d1, d2 formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if d1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); d1.Type != ArgNumber {
+ return d1
+ }
+ if d2 = argsList.Back().Value.(formulaArg).ToNumber(); d2.Type != ArgNumber {
+ return d2
+ }
+ if probability.Number <= 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if d1.Number < 1 || d1.Number >= math.Pow10(10) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if d2.Number < 1 || d2.Number >= math.Pow10(10) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newListFormulaArg([]formulaArg{probability, d1, d2})
+}
+
+// FdotINV function calculates the inverse of the Cumulative F Distribution
+// for a supplied probability. The syntax of the F.Inv function is:
+//
+// F.INV(probability,deg_freedom1,deg_freedom2)
+func (fn *formulaFuncs) FdotINV(argsList *list.List) formulaArg {
+ args := fn.prepareFinvArgs("F.INV", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ probability, d1, d2 := args.List[0], args.List[1], args.List[2]
+ return newNumberFormulaArg((1/calcBetainv(1-probability.Number, d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number))
+}
+
+// FdotINVdotRT function calculates the inverse of the (right-tailed) F
+// Probability Distribution for a supplied probability. The syntax of the
+// function is:
+//
+// F.INV.RT(probability,deg_freedom1,deg_freedom2)
+func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg {
+ args := fn.prepareFinvArgs("F.INV.RT", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ probability, d1, d2 := args.List[0], args.List[1], args.List[2]
+ return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number))
+}
+
+// FINV function calculates the inverse of the (right-tailed) F Probability
+// Distribution for a supplied probability. The syntax of the function is:
+//
+// FINV(probability,deg_freedom1,deg_freedom2)
+func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg {
+ args := fn.prepareFinvArgs("FINV", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ probability, d1, d2 := args.List[0], args.List[1], args.List[2]
+ return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number))
+}
+
+// FdotTEST function returns the F-Test for two supplied arrays. I.e. the
+// function returns the two-tailed probability that the variances in the two
+// supplied arrays are not significantly different. The syntax of the Ftest
+// function is:
+//
+// F.TEST(array1,array2)
+func (fn *formulaFuncs) FdotTEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "F.TEST requires 2 arguments")
+ }
+ array1 := argsList.Front().Value.(formulaArg)
+ array2 := argsList.Back().Value.(formulaArg)
+ left, right := array1.ToList(), array2.ToList()
+ collectMatrix := func(args []formulaArg) (n, accu float64) {
+ var p, sum float64
+ for _, arg := range args {
+ if num := arg.ToNumber(); num.Type == ArgNumber {
+ x := num.Number - p
+ y := x / (n + 1)
+ p += y
+ accu += n * x * y
+ n++
+ sum += num.Number
+ }
+ }
+ return
+ }
+ nums, accu := collectMatrix(left)
+ f3 := nums - 1
+ if nums == 1 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ f1 := accu / (nums - 1)
+ if f1 == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ nums, accu = collectMatrix(right)
+ f4 := nums - 1
+ if nums == 1 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ f2 := accu / (nums - 1)
+ if f2 == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg(f1 / f2))
+ args.PushBack(newNumberFormulaArg(f3))
+ args.PushBack(newNumberFormulaArg(f4))
+ probability := (1 - fn.FDIST(args).Number) * 2
+ if probability > 1 {
+ probability = 2 - probability
+ }
+ return newNumberFormulaArg(probability)
+}
+
+// FTEST function returns the F-Test for two supplied arrays. I.e. the function
+// returns the two-tailed probability that the variances in the two supplied
+// arrays are not significantly different. The syntax of the Ftest function
+// is:
+//
+// FTEST(array1,array2)
+func (fn *formulaFuncs) FTEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FTEST requires 2 arguments")
+ }
+ return fn.FdotTEST(argsList)
+}
+
+// LOGINV function calculates the inverse of the Cumulative Log-Normal
+// Distribution Function of x, for a supplied probability. The syntax of the
+// function is:
+//
+// LOGINV(probability,mean,standard_dev)
+func (fn *formulaFuncs) LOGINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOGINV requires 3 arguments")
+ }
+ var probability, mean, stdDev formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if stdDev = argsList.Back().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if probability.Number <= 0 || probability.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if stdDev.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New()
+ args.PushBack(probability)
+ args.PushBack(newNumberFormulaArg(0))
+ args.PushBack(newNumberFormulaArg(1))
+ norminv := fn.NORMINV(args)
+ return newNumberFormulaArg(math.Exp(mean.Number + stdDev.Number*norminv.Number))
+}
+
+// LOGNORMdotINV function calculates the inverse of the Cumulative Log-Normal
+// Distribution Function of x, for a supplied probability. The syntax of the
+// function is:
+//
+// LOGNORM.INV(probability,mean,standard_dev)
+func (fn *formulaFuncs) LOGNORMdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.INV requires 3 arguments")
+ }
+ return fn.LOGINV(argsList)
+}
+
+// LOGNORMdotDIST function calculates the Log-Normal Probability Density
+// Function or the Cumulative Log-Normal Distribution Function for a supplied
+// value of x. The syntax of the function is:
+//
+// LOGNORM.DIST(x,mean,standard_dev,cumulative)
+func (fn *formulaFuncs) LOGNORMdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOGNORM.DIST requires 4 arguments")
+ }
+ var x, mean, stdDev, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if stdDev = argsList.Back().Prev().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if x.Number <= 0 || stdDev.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 1 {
+ args := list.New()
+ args.PushBack(newNumberFormulaArg((math.Log(x.Number) - mean.Number) / stdDev.Number))
+ args.PushBack(newNumberFormulaArg(0))
+ args.PushBack(newNumberFormulaArg(1))
+ args.PushBack(cumulative)
+ return fn.NORMDIST(args)
+ }
+ return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number * x.Number)) *
+ math.Exp(0-(math.Pow(math.Log(x.Number)-mean.Number, 2)/(2*math.Pow(stdDev.Number, 2)))))
+}
+
+// LOGNORMDIST function calculates the Cumulative Log-Normal Distribution
+// Function at a supplied value of x. The syntax of the function is:
+//
+// LOGNORMDIST(x,mean,standard_dev)
+func (fn *formulaFuncs) LOGNORMDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOGNORMDIST requires 3 arguments")
+ }
+ var x, mean, stdDev formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if stdDev = argsList.Back().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if x.Number <= 0 || stdDev.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg((math.Log(x.Number) - mean.Number) / stdDev.Number))
+ return fn.NORMSDIST(args)
+}
+
+// MODE function returns the statistical mode (the most frequently occurring
+// value) of a list of supplied numbers. If there are 2 or more most
+// frequently occurring values in the supplied data, the function returns the
+// lowest of these values The syntax of the function is:
+//
+// MODE(number1,[number2],...)
+func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MODE requires at least 1 argument")
+ }
+ var values []float64
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ cells := arg.Value.(formulaArg)
+ if cells.Type != ArgMatrix && cells.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ for _, cell := range cells.ToList() {
+ if cell.Type == ArgNumber {
+ values = append(values, cell.Number)
+ }
+ }
+ }
+ sort.Float64s(values)
+ cnt := len(values)
+ var count, modeCnt int
+ var mode float64
+ for i := 0; i < cnt; i++ {
+ count = 0
+ for j := 0; j < cnt; j++ {
+ if j != i && values[j] == values[i] {
+ count++
+ }
+ }
+ if count > modeCnt {
+ modeCnt = count
+ mode = values[i]
+ }
+ }
+ if modeCnt == 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(mode)
+}
+
+// MODEdotMULT function returns a vertical array of the statistical modes
+// (the most frequently occurring values) within a list of supplied numbers.
+// The syntax of the function is:
+//
+// MODE.MULT(number1,[number2],...)
+func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MODE.MULT requires at least 1 argument")
+ }
+ var values []float64
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ cells := arg.Value.(formulaArg)
+ if cells.Type != ArgMatrix && cells.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ for _, cell := range cells.ToList() {
+ if cell.Type == ArgNumber {
+ values = append(values, cell.Number)
+ }
+ }
+ }
+ sort.Float64s(values)
+ cnt := len(values)
+ var count, modeCnt int
+ var mtx [][]formulaArg
+ for i := 0; i < cnt; i++ {
+ count = 0
+ for j := i + 1; j < cnt; j++ {
+ if values[i] == values[j] {
+ count++
+ }
+ }
+ if count > modeCnt {
+ modeCnt = count
+ mtx = [][]formulaArg{}
+ mtx = append(mtx, []formulaArg{newNumberFormulaArg(values[i])})
+ } else if count == modeCnt {
+ mtx = append(mtx, []formulaArg{newNumberFormulaArg(values[i])})
+ }
+ }
+ if modeCnt == 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newMatrixFormulaArg(mtx)
+}
+
+// MODEdotSNGL function returns the statistical mode (the most frequently
+// occurring value) within a list of supplied numbers. If there are 2 or more
+// most frequently occurring values in the supplied data, the function returns
+// the lowest of these values. The syntax of the function is:
+//
+// MODE.SNGL(number1,[number2],...)
+func (fn *formulaFuncs) MODEdotSNGL(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MODE.SNGL requires at least 1 argument")
+ }
+ return fn.MODE(argsList)
+}
+
+// NEGBINOMdotDIST function calculates the probability mass function or the
+// cumulative distribution function for the Negative Binomial Distribution.
+// This gives the probability that there will be a given number of failures
+// before a required number of successes is achieved. The syntax of the
+// function is:
+//
+// NEGBINOM.DIST(number_f,number_s,probability_s,cumulative)
+func (fn *formulaFuncs) NEGBINOMdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOM.DIST requires 4 arguments")
+ }
+ var f, s, probability, cumulative formulaArg
+ if f = argsList.Front().Value.(formulaArg).ToNumber(); f.Type != ArgNumber {
+ return f
+ }
+ if s = argsList.Front().Next().Value.(formulaArg).ToNumber(); s.Type != ArgNumber {
+ return s
+ }
+ if probability = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber {
+ return cumulative
+ }
+ if f.Number < 0 || s.Number < 1 || probability.Number < 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(1 - getBetaDist(1-probability.Number, f.Number+1, s.Number))
+ }
+ return newNumberFormulaArg(binomCoeff(f.Number+s.Number-1, s.Number-1) * math.Pow(probability.Number, s.Number) * math.Pow(1-probability.Number, f.Number))
+}
+
+// NEGBINOMDIST function calculates the Negative Binomial Distribution for a
+// given set of parameters. This gives the probability that there will be a
+// specified number of failures before a required number of successes is
+// achieved. The syntax of the function is:
+//
+// NEGBINOMDIST(number_f,number_s,probability_s)
+func (fn *formulaFuncs) NEGBINOMDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NEGBINOMDIST requires 3 arguments")
+ }
+ var f, s, probability formulaArg
+ if f = argsList.Front().Value.(formulaArg).ToNumber(); f.Type != ArgNumber {
+ return f
+ }
+ if s = argsList.Front().Next().Value.(formulaArg).ToNumber(); s.Type != ArgNumber {
+ return s
+ }
+ if probability = argsList.Back().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if f.Number < 0 || s.Number < 1 || probability.Number < 0 || probability.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(binomCoeff(f.Number+s.Number-1, s.Number-1) * math.Pow(probability.Number, s.Number) * math.Pow(1-probability.Number, f.Number))
+}
+
+// NORMdotDIST function calculates the Normal Probability Density Function or
+// the Cumulative Normal Distribution. Function for a supplied set of
+// parameters. The syntax of the function is:
+//
+// NORM.DIST(x,mean,standard_dev,cumulative)
+func (fn *formulaFuncs) NORMdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORM.DIST requires 4 arguments")
+ }
+ return fn.NORMDIST(argsList)
+}
+
+// NORMDIST function calculates the Normal Probability Density Function or the
+// Cumulative Normal Distribution. Function for a supplied set of parameters.
+// The syntax of the function is:
+//
+// NORMDIST(x,mean,standard_dev,cumulative)
+func (fn *formulaFuncs) NORMDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORMDIST requires 4 arguments")
+ }
+ var x, mean, stdDev, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if stdDev = argsList.Back().Prev().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError {
+ return cumulative
+ }
+ if stdDev.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if cumulative.Number == 1 {
+ return newNumberFormulaArg(0.5 * (1 + math.Erf((x.Number-mean.Number)/(stdDev.Number*math.Sqrt(2)))))
+ }
+ return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number)) * math.Exp(0-(math.Pow(x.Number-mean.Number, 2)/(2*(stdDev.Number*stdDev.Number)))))
+}
+
+// NORMdotINV function calculates the inverse of the Cumulative Normal
+// Distribution Function for a supplied value of x, and a supplied
+// distribution mean & standard deviation. The syntax of the function is:
+//
+// NORM.INV(probability,mean,standard_dev)
+func (fn *formulaFuncs) NORMdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORM.INV requires 3 arguments")
+ }
+ return fn.NORMINV(argsList)
+}
+
+// NORMINV function calculates the inverse of the Cumulative Normal
+// Distribution Function for a supplied value of x, and a supplied
+// distribution mean & standard deviation. The syntax of the function is:
+//
+// NORMINV(probability,mean,standard_dev)
+func (fn *formulaFuncs) NORMINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORMINV requires 3 arguments")
+ }
+ var prob, mean, stdDev formulaArg
+ if prob = argsList.Front().Value.(formulaArg).ToNumber(); prob.Type != ArgNumber {
+ return prob
+ }
+ if mean = argsList.Front().Next().Value.(formulaArg).ToNumber(); mean.Type != ArgNumber {
+ return mean
+ }
+ if stdDev = argsList.Back().Value.(formulaArg).ToNumber(); stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if prob.Number < 0 || prob.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if stdDev.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ inv, err := norminv(prob.Number)
+ if err != nil {
+ return newErrorFormulaArg(err.Error(), err.Error())
+ }
+ return newNumberFormulaArg(inv*stdDev.Number + mean.Number)
+}
+
+// NORMdotSdotDIST function calculates the Standard Normal Cumulative
+// Distribution Function for a supplied value. The syntax of the function
+// is:
+//
+// NORM.S.DIST(z)
+func (fn *formulaFuncs) NORMdotSdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORM.S.DIST requires 2 numeric arguments")
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 0})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1})
+ args.PushBack(argsList.Back().Value.(formulaArg))
+ return fn.NORMDIST(args)
+}
+
+// NORMSDIST function calculates the Standard Normal Cumulative Distribution
+// Function for a supplied value. The syntax of the function is:
+//
+// NORMSDIST(z)
+func (fn *formulaFuncs) NORMSDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORMSDIST requires 1 numeric argument")
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 0})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1, Boolean: true})
+ return fn.NORMDIST(args)
+}
+
+// NORMSINV function calculates the inverse of the Standard Normal Cumulative
+// Distribution Function for a supplied probability value. The syntax of the
+// function is:
+//
+// NORMSINV(probability)
+func (fn *formulaFuncs) NORMSINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORMSINV requires 1 numeric argument")
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 0})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1})
+ return fn.NORMINV(args)
+}
+
+// NORMdotSdotINV function calculates the inverse of the Standard Normal
+// Cumulative Distribution Function for a supplied probability value. The
+// syntax of the function is:
+//
+// NORM.S.INV(probability)
+func (fn *formulaFuncs) NORMdotSdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NORM.S.INV requires 1 numeric argument")
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 0})
+ args.PushBack(formulaArg{Type: ArgNumber, Number: 1})
+ return fn.NORMINV(args)
+}
+
+// norminv returns the inverse of the normal cumulative distribution for the
+// specified value.
+func norminv(p float64) (float64, error) {
+ a := map[int]float64{
+ 1: -3.969683028665376e+01, 2: 2.209460984245205e+02, 3: -2.759285104469687e+02,
+ 4: 1.383577518672690e+02, 5: -3.066479806614716e+01, 6: 2.506628277459239e+00,
+ }
+ b := map[int]float64{
+ 1: -5.447609879822406e+01, 2: 1.615858368580409e+02, 3: -1.556989798598866e+02,
+ 4: 6.680131188771972e+01, 5: -1.328068155288572e+01,
+ }
+ c := map[int]float64{
+ 1: -7.784894002430293e-03, 2: -3.223964580411365e-01, 3: -2.400758277161838e+00,
+ 4: -2.549732539343734e+00, 5: 4.374664141464968e+00, 6: 2.938163982698783e+00,
+ }
+ d := map[int]float64{
+ 1: 7.784695709041462e-03, 2: 3.224671290700398e-01, 3: 2.445134137142996e+00,
+ 4: 3.754408661907416e+00,
+ }
+ pLow := 0.02425 // Use lower region approx. below this
+ pHigh := 1 - pLow // Use upper region approx. above this
+ if 0 < p && p < pLow {
+ // Rational approximation for lower region.
+ q := math.Sqrt(-2 * math.Log(p))
+ return (((((c[1]*q+c[2])*q+c[3])*q+c[4])*q+c[5])*q + c[6]) /
+ ((((d[1]*q+d[2])*q+d[3])*q+d[4])*q + 1), nil
+ } else if pLow <= p && p <= pHigh {
+ // Rational approximation for central region.
+ q := p - 0.5
+ r := q * q
+ f1 := ((((a[1]*r+a[2])*r+a[3])*r+a[4])*r + a[5]) * r
+ f2 := (b[1]*r + b[2]) * r
+ f3 := ((math.Nextafter(f2, f2)+b[3])*r + b[4]) * r
+ f4 := (math.Nextafter(f3, f3) + b[5]) * r
+ return (math.Nextafter(f1, f1) + a[6]) * q /
+ (math.Nextafter(f4, f4) + 1), nil
+ } else if pHigh < p && p < 1 {
+ // Rational approximation for upper region.
+ q := math.Sqrt(-2 * math.Log(1-p))
+ return -(((((c[1]*q+c[2])*q+c[3])*q+c[4])*q+c[5])*q + c[6]) /
+ ((((d[1]*q+d[2])*q+d[3])*q+d[4])*q + 1), nil
+ }
+ return 0, errors.New(formulaErrorNUM)
+}
+
+// kth is an implementation of the formula functions LARGE and SMALL.
+func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name))
+ }
+ array := argsList.Front().Value.(formulaArg).ToList()
+ argK := argsList.Back().Value.(formulaArg).ToNumber()
+ if argK.Type != ArgNumber {
+ return argK
+ }
+ k := int(argK.Number)
+ if k < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, "k should be > 0")
+ }
+ var data []float64
+ for _, arg := range array {
+ if arg.Type == ArgNumber {
+ data = append(data, arg.Number)
+ }
+ }
+ if len(data) < k {
+ return newErrorFormulaArg(formulaErrorNUM, "k should be <= length of array")
+ }
+ sort.Float64s(data)
+ if name == "LARGE" {
+ return newNumberFormulaArg(data[len(data)-k])
+ }
+ return newNumberFormulaArg(data[k-1])
+}
+
+// LARGE function returns the k'th largest value from an array of numeric
+// values. The syntax of the function is:
+//
+// LARGE(array,k)
+func (fn *formulaFuncs) LARGE(argsList *list.List) formulaArg {
+ return fn.kth("LARGE", argsList)
+}
+
+// MAX function returns the largest value from a supplied set of numeric
+// values. The syntax of the function is:
+//
+// MAX(number1,[number2],...)
+func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MAX requires at least 1 argument")
+ }
+ return fn.maxValue(false, argsList)
+}
+
+// MAXA function returns the largest value from a supplied set of numeric
+// values, while counting text and the logical value FALSE as the value 0 and
+// counting the logical value TRUE as the value 1. The syntax of the function
+// is:
+//
+// MAXA(number1,[number2],...)
+func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MAXA requires at least 1 argument")
+ }
+ return fn.maxValue(true, argsList)
+}
+
+// MAXIFS function returns the maximum value from a subset of values that are
+// specified according to one or more criteria. The syntax of the function
+// is:
+//
+// MAXIFS(max_range,criteria_range1,criteria1,[criteria_range2,criteria2],...)
+func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MAXIFS requires at least 3 arguments")
+ }
+ if argsList.Len()%2 != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var args []formulaArg
+ maxVal, maxRange := -math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ for _, ref := range formulaIfsMatch(args) {
+ if num := maxRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && maxVal < num.Number {
+ maxVal = num.Number
+ }
+ }
+ if maxVal == -math.MaxFloat64 {
+ maxVal = 0
+ }
+ return newNumberFormulaArg(maxVal)
+}
+
+// calcListMatrixMax is part of the implementation max.
+func calcListMatrixMax(maxa bool, maxVal float64, arg formulaArg) float64 {
+ for _, cell := range arg.ToList() {
+ if cell.Type == ArgNumber && cell.Number > maxVal {
+ if maxa && cell.Boolean || !cell.Boolean {
+ maxVal = cell.Number
+ }
+ }
+ }
+ return maxVal
+}
+
+// maxValue is an implementation of the formula functions MAX and MAXA.
+func (fn *formulaFuncs) maxValue(maxa bool, argsList *list.List) formulaArg {
+ maxVal := -math.MaxFloat64
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ if !maxa && (arg.Value() == "TRUE" || arg.Value() == "FALSE") {
+ continue
+ } else {
+ num := arg.ToBool()
+ if num.Type == ArgNumber && num.Number > maxVal {
+ maxVal = num.Number
+ continue
+ }
+ }
+ num := arg.ToNumber()
+ if num.Type != ArgError && num.Number > maxVal {
+ maxVal = num.Number
+ }
+ case ArgNumber:
+ if arg.Number > maxVal {
+ maxVal = arg.Number
+ }
+ case ArgList, ArgMatrix:
+ maxVal = calcListMatrixMax(maxa, maxVal, arg)
+ case ArgError:
+ return arg
+ }
+ }
+ if maxVal == -math.MaxFloat64 {
+ maxVal = 0
+ }
+ return newNumberFormulaArg(maxVal)
+}
+
+// MEDIAN function returns the statistical median (the middle value) of a list
+// of supplied numbers. The syntax of the function is:
+//
+// MEDIAN(number1,[number2],...)
+func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument")
+ }
+ var values []float64
+ var median float64
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ value := arg.ToNumber()
+ if value.Type != ArgNumber {
+ return value
+ }
+ values = append(values, value.Number)
+ case ArgNumber:
+ values = append(values, arg.Number)
+ case ArgMatrix:
+ for _, row := range arg.Matrix {
+ for _, cell := range row {
+ if cell.Type == ArgNumber {
+ values = append(values, cell.Number)
+ }
+ }
+ }
+ }
+ }
+ if len(values) == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ sort.Float64s(values)
+ if len(values)%2 == 0 {
+ median = (values[len(values)/2-1] + values[len(values)/2]) / 2
+ } else {
+ median = values[len(values)/2]
+ }
+ return newNumberFormulaArg(median)
+}
+
+// MIN function returns the smallest value from a supplied set of numeric
+// values. The syntax of the function is:
+//
+// MIN(number1,[number2],...)
+func (fn *formulaFuncs) MIN(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MIN requires at least 1 argument")
+ }
+ return fn.minValue(false, argsList)
+}
+
+// MINA function returns the smallest value from a supplied set of numeric
+// values, while counting text and the logical value FALSE as the value 0 and
+// counting the logical value TRUE as the value 1. The syntax of the function
+// is:
+//
+// MINA(number1,[number2],...)
+func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MINA requires at least 1 argument")
+ }
+ return fn.minValue(true, argsList)
+}
+
+// MINIFS function returns the minimum value from a subset of values that are
+// specified according to one or more criteria. The syntax of the function
+// is:
+//
+// MINIFS(min_range,criteria_range1,criteria1,[criteria_range2,criteria2],...)
+func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MINIFS requires at least 3 arguments")
+ }
+ if argsList.Len()%2 != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var args []formulaArg
+ minVal, minRange := math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ args = append(args, arg.Value.(formulaArg))
+ }
+ for _, ref := range formulaIfsMatch(args) {
+ if num := minRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && minVal > num.Number {
+ minVal = num.Number
+ }
+ }
+ if minVal == math.MaxFloat64 {
+ minVal = 0
+ }
+ return newNumberFormulaArg(minVal)
+}
+
+// calcListMatrixMin is part of the implementation min.
+func calcListMatrixMin(mina bool, minVal float64, arg formulaArg) float64 {
+ for _, cell := range arg.ToList() {
+ if cell.Type == ArgNumber && cell.Number < minVal {
+ if mina && cell.Boolean || !cell.Boolean {
+ minVal = cell.Number
+ }
+ }
+ }
+ return minVal
+}
+
+// minValue is an implementation of the formula functions MIN and MINA.
+func (fn *formulaFuncs) minValue(mina bool, argsList *list.List) formulaArg {
+ minVal := math.MaxFloat64
+ for token := argsList.Front(); token != nil; token = token.Next() {
+ arg := token.Value.(formulaArg)
+ switch arg.Type {
+ case ArgString:
+ if !mina && (arg.Value() == "TRUE" || arg.Value() == "FALSE") {
+ continue
+ } else {
+ num := arg.ToBool()
+ if num.Type == ArgNumber && num.Number < minVal {
+ minVal = num.Number
+ continue
+ }
+ }
+ num := arg.ToNumber()
+ if num.Type != ArgError && num.Number < minVal {
+ minVal = num.Number
+ }
+ case ArgNumber:
+ if arg.Number < minVal {
+ minVal = arg.Number
+ }
+ case ArgList, ArgMatrix:
+ minVal = calcListMatrixMin(mina, minVal, arg)
+ case ArgError:
+ return arg
+ }
+ }
+ if minVal == math.MaxFloat64 {
+ minVal = 0
+ }
+ return newNumberFormulaArg(minVal)
+}
+
+// pearsonProduct is an implementation of the formula functions FORECAST,
+// FORECAST.LINEAR, INTERCEPT, PEARSON, RSQ and SLOPE.
+func (fn *formulaFuncs) pearsonProduct(name string, n int, argsList *list.List) formulaArg {
+ if argsList.Len() != n {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires %d arguments", name, n))
+ }
+ var fx formulaArg
+ array1 := argsList.Back().Value.(formulaArg).ToList()
+ array2 := argsList.Front().Value.(formulaArg).ToList()
+ if name == "PEARSON" || name == "RSQ" {
+ array1, array2 = array2, array1
+ }
+ if n == 3 {
+ if fx = argsList.Front().Value.(formulaArg).ToNumber(); fx.Type != ArgNumber {
+ return fx
+ }
+ array2 = argsList.Front().Next().Value.(formulaArg).ToList()
+ }
+ if len(array1) != len(array2) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var sum, deltaX, deltaY, x, y, length float64
+ for i := 0; i < len(array1); i++ {
+ num1, num2 := array1[i], array2[i]
+ if !(num1.Type == ArgNumber && num2.Type == ArgNumber) {
+ continue
+ }
+ x += num1.Number
+ y += num2.Number
+ length++
+ }
+ x /= length
+ y /= length
+ for i := 0; i < len(array1); i++ {
+ num1, num2 := array1[i], array2[i]
+ if !(num1.Type == ArgNumber && num2.Type == ArgNumber) {
+ continue
+ }
+ sum += (num1.Number - x) * (num2.Number - y)
+ deltaX += (num1.Number - x) * (num1.Number - x)
+ deltaY += (num2.Number - y) * (num2.Number - y)
+ }
+ if sum*deltaX*deltaY == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(map[string]float64{
+ "FORECAST": y + sum/deltaX*(fx.Number-x),
+ "FORECAST.LINEAR": y + sum/deltaX*(fx.Number-x),
+ "INTERCEPT": y - sum/deltaX*x,
+ "PEARSON": sum / math.Sqrt(deltaX*deltaY),
+ "RSQ": math.Pow(sum/math.Sqrt(deltaX*deltaY), 2),
+ "SLOPE": sum / deltaX,
+ }[name])
+}
+
+// PEARSON function calculates the Pearson Product-Moment Correlation
+// Coefficient for two sets of values. The syntax of the function is:
+//
+// PEARSON(array1,array2)
+func (fn *formulaFuncs) PEARSON(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("PEARSON", 2, argsList)
+}
+
+// PERCENTILEdotEXC function returns the k'th percentile (i.e. the value below
+// which k% of the data values fall) for a supplied range of values and a
+// supplied k (between 0 & 1 exclusive).The syntax of the function is:
+//
+// PERCENTILE.EXC(array,k)
+func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.EXC requires 2 arguments")
+ }
+ array := argsList.Front().Value.(formulaArg).ToList()
+ k := argsList.Back().Value.(formulaArg).ToNumber()
+ if k.Type != ArgNumber {
+ return k
+ }
+ if k.Number <= 0 || k.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ var numbers []float64
+ for _, arg := range array {
+ if arg.Type == ArgError {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if arg.Type == ArgNumber {
+ numbers = append(numbers, arg.Number)
+ }
+ }
+ cnt := len(numbers)
+ sort.Float64s(numbers)
+ idx := k.Number * (float64(cnt) + 1)
+ base := math.Floor(idx)
+ next := base - 1
+ proportion := math.Nextafter(idx, idx) - base
+ return newNumberFormulaArg(numbers[int(next)] + ((numbers[int(base)] - numbers[int(next)]) * proportion))
+}
+
+// PERCENTILEdotINC function returns the k'th percentile (i.e. the value below
+// which k% of the data values fall) for a supplied range of values and a
+// supplied k. The syntax of the function is:
+//
+// PERCENTILE.INC(array,k)
+func (fn *formulaFuncs) PERCENTILEdotINC(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.INC requires 2 arguments")
+ }
+ return fn.PERCENTILE(argsList)
+}
+
+// PERCENTILE function returns the k'th percentile (i.e. the value below which
+// k% of the data values fall) for a supplied range of values and a supplied
+// k. The syntax of the function is:
+//
+// PERCENTILE(array,k)
+func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE requires 2 arguments")
+ }
+ array := argsList.Front().Value.(formulaArg).ToList()
+ k := argsList.Back().Value.(formulaArg).ToNumber()
+ if k.Type != ArgNumber {
+ return k
+ }
+ if k.Number < 0 || k.Number > 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var numbers []float64
+ for _, arg := range array {
+ if arg.Type == ArgError {
+ return arg
+ }
+ if arg.Type == ArgNumber {
+ numbers = append(numbers, arg.Number)
+ }
+ }
+ cnt := len(numbers)
+ sort.Float64s(numbers)
+ idx := k.Number * (float64(cnt) - 1)
+ base := math.Floor(idx)
+ if idx == base {
+ return newNumberFormulaArg(numbers[int(idx)])
+ }
+ next := base + 1
+ proportion := math.Nextafter(idx, idx) - base
+ return newNumberFormulaArg(numbers[int(base)] + ((numbers[int(next)] - numbers[int(base)]) * proportion))
+}
+
+// percentrank is an implementation of the formula functions PERCENTRANK and
+// PERCENTRANK.INC.
+func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 2 && argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 or 3 arguments", name))
+ }
+ array := argsList.Front().Value.(formulaArg).ToList()
+ x := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ var numbers []float64
+ for _, arg := range array {
+ if arg.Type == ArgError {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if arg.Type == ArgNumber {
+ numbers = append(numbers, arg.Number)
+ }
+ }
+ cnt := len(numbers)
+ sort.Float64s(numbers)
+ if x.Number < numbers[0] || x.Number > numbers[cnt-1] {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ pos, significance := float64(inFloat64Slice(numbers, x.Number)), newNumberFormulaArg(3)
+ if argsList.Len() == 3 {
+ if significance = argsList.Back().Value.(formulaArg).ToNumber(); significance.Type != ArgNumber {
+ return significance
+ }
+ if significance.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s arguments significance should be > 1", name))
+ }
+ }
+ if pos == -1 {
+ pos = 0
+ cmp := numbers[0]
+ for cmp < x.Number {
+ pos++
+ cmp = numbers[int(pos)]
+ }
+ pos--
+ pos += (x.Number - numbers[int(pos)]) / (cmp - numbers[int(pos)])
+ }
+ pow := math.Pow(10, significance.Number)
+ digit := pow * pos / (float64(cnt) - 1)
+ if name == "PERCENTRANK.EXC" {
+ digit = pow * (pos + 1) / (float64(cnt) + 1)
+ }
+ return newNumberFormulaArg(math.Floor(digit) / pow)
+}
+
+// PERCENTRANKdotEXC function calculates the relative position, between 0 and
+// 1 (exclusive), of a specified value within a supplied array. The syntax of
+// the function is:
+//
+// PERCENTRANK.EXC(array,x,[significance])
+func (fn *formulaFuncs) PERCENTRANKdotEXC(argsList *list.List) formulaArg {
+ return fn.percentrank("PERCENTRANK.EXC", argsList)
+}
+
+// PERCENTRANKdotINC function calculates the relative position, between 0 and
+// 1 (inclusive), of a specified value within a supplied array.The syntax of
+// the function is:
+//
+// PERCENTRANK.INC(array,x,[significance])
+func (fn *formulaFuncs) PERCENTRANKdotINC(argsList *list.List) formulaArg {
+ return fn.percentrank("PERCENTRANK.INC", argsList)
+}
+
+// PERCENTRANK function calculates the relative position of a specified value,
+// within a set of values, as a percentage. The syntax of the function is:
+//
+// PERCENTRANK(array,x,[significance])
+func (fn *formulaFuncs) PERCENTRANK(argsList *list.List) formulaArg {
+ return fn.percentrank("PERCENTRANK", argsList)
+}
+
+// PERMUT function calculates the number of permutations of a specified number
+// of objects from a set of objects. The syntax of the function is:
+//
+// PERMUT(number,number_chosen)
+func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PERMUT requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ chosen := argsList.Back().Value.(formulaArg).ToNumber()
+ if number.Type != ArgNumber {
+ return number
+ }
+ if chosen.Type != ArgNumber {
+ return chosen
+ }
+ if number.Number < chosen.Number {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number)))
+}
+
+// PERMUTATIONA function calculates the number of permutations, with
+// repetitions, of a specified number of objects from a set. The syntax of
+// the function is:
+//
+// PERMUTATIONA(number,number_chosen)
+func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PERMUTATIONA requires 2 numeric arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ chosen := argsList.Back().Value.(formulaArg).ToNumber()
+ if number.Type != ArgNumber {
+ return number
+ }
+ if chosen.Type != ArgNumber {
+ return chosen
+ }
+ num, numChosen := math.Floor(number.Number), math.Floor(chosen.Number)
+ if num < 0 || numChosen < 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(math.Pow(num, numChosen))
+}
+
+// PHI function returns the value of the density function for a standard normal
+// distribution for a supplied number. The syntax of the function is:
+//
+// PHI(x)
+func (fn *formulaFuncs) PHI(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PHI requires 1 argument")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ return newNumberFormulaArg(0.39894228040143268 * math.Exp(-(x.Number*x.Number)/2))
+}
+
+// QUARTILE function returns a requested quartile of a supplied range of
+// values. The syntax of the function is:
+//
+// QUARTILE(array,quart)
+func (fn *formulaFuncs) QUARTILE(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE requires 2 arguments")
+ }
+ quart := argsList.Back().Value.(formulaArg).ToNumber()
+ if quart.Type != ArgNumber {
+ return quart
+ }
+ if quart.Number < 0 || quart.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(newNumberFormulaArg(quart.Number / 4))
+ return fn.PERCENTILE(args)
+}
+
+// QUARTILEdotEXC function returns a requested quartile of a supplied range of
+// values, based on a percentile range of 0 to 1 exclusive. The syntax of the
+// function is:
+//
+// QUARTILE.EXC(array,quart)
+func (fn *formulaFuncs) QUARTILEdotEXC(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.EXC requires 2 arguments")
+ }
+ quart := argsList.Back().Value.(formulaArg).ToNumber()
+ if quart.Type != ArgNumber {
+ return quart
+ }
+ if quart.Number <= 0 || quart.Number >= 4 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(newNumberFormulaArg(quart.Number / 4))
+ return fn.PERCENTILEdotEXC(args)
+}
+
+// QUARTILEdotINC function returns a requested quartile of a supplied range of
+// values. The syntax of the function is:
+//
+// QUARTILE.INC(array,quart)
+func (fn *formulaFuncs) QUARTILEdotINC(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.INC requires 2 arguments")
+ }
+ return fn.QUARTILE(argsList)
+}
+
+// rank is an implementation of the formula functions RANK and RANK.EQ.
+func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at most 3 arguments", name))
+ }
+ num := argsList.Front().Value.(formulaArg).ToNumber()
+ if num.Type != ArgNumber {
+ return num
+ }
+ var arr []float64
+ for _, arg := range argsList.Front().Next().Value.(formulaArg).ToList() {
+ if arg.Type == ArgNumber {
+ arr = append(arr, arg.Number)
+ }
+ }
+ sort.Float64s(arr)
+ order := newNumberFormulaArg(0)
+ if argsList.Len() == 3 {
+ if order = argsList.Back().Value.(formulaArg).ToNumber(); order.Type != ArgNumber {
+ return order
+ }
+ }
+ if order.Number == 0 {
+ sort.Sort(sort.Reverse(sort.Float64Slice(arr)))
+ }
+ if idx := inFloat64Slice(arr, num.Number); idx != -1 {
+ return newNumberFormulaArg(float64(idx + 1))
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// RANKdotEQ function returns the statistical rank of a given value, within a
+// supplied array of values. If there are duplicate values in the list, these
+// are given the same rank. The syntax of the function is:
+//
+// RANK.EQ(number,ref,[order])
+func (fn *formulaFuncs) RANKdotEQ(argsList *list.List) formulaArg {
+ return fn.rank("RANK.EQ", argsList)
+}
+
+// RANK function returns the statistical rank of a given value, within a
+// supplied array of values. If there are duplicate values in the list, these
+// are given the same rank. The syntax of the function is:
+//
+// RANK(number,ref,[order])
+func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg {
+ return fn.rank("RANK", argsList)
+}
+
+// RSQ function calculates the square of the Pearson Product-Moment Correlation
+// Coefficient for two supplied sets of values. The syntax of the function
+// is:
+//
+// RSQ(known_y's,known_x's)
+func (fn *formulaFuncs) RSQ(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("RSQ", 2, argsList)
+}
+
+// skew is an implementation of the formula functions SKEW and SKEW.P.
+func (fn *formulaFuncs) skew(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ mean := fn.AVERAGE(argsList)
+ var stdDev formulaArg
+ var count, summer float64
+ if name == "SKEW" {
+ stdDev = fn.STDEV(argsList)
+ } else {
+ stdDev = fn.STDEVP(argsList)
+ }
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgNumber, ArgString:
+ num := token.ToNumber()
+ if num.Type == ArgError {
+ return num
+ }
+ summer += math.Pow((num.Number-mean.Number)/stdDev.Number, 3)
+ count++
+ case ArgList, ArgMatrix:
+ for _, cell := range token.ToList() {
+ if cell.Type != ArgNumber {
+ continue
+ }
+ summer += math.Pow((cell.Number-mean.Number)/stdDev.Number, 3)
+ count++
+ }
+ }
+ }
+ if count > 2 {
+ if name == "SKEW" {
+ return newNumberFormulaArg(summer * (count / ((count - 1) * (count - 2))))
+ }
+ return newNumberFormulaArg(summer / count)
+ }
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+}
+
+// SKEW function calculates the skewness of the distribution of a supplied set
+// of values. The syntax of the function is:
+//
+// SKEW(number1,[number2],...)
+func (fn *formulaFuncs) SKEW(argsList *list.List) formulaArg {
+ return fn.skew("SKEW", argsList)
+}
+
+// SKEWdotP function calculates the skewness of the distribution of a supplied
+// set of values. The syntax of the function is:
+//
+// SKEW.P(number1,[number2],...)
+func (fn *formulaFuncs) SKEWdotP(argsList *list.List) formulaArg {
+ return fn.skew("SKEW.P", argsList)
+}
+
+// SLOPE returns the slope of the linear regression line through data points in
+// known_y's and known_x's. The slope is the vertical distance divided by the
+// horizontal distance between any two points on the line, which is the rate
+// of change along the regression line. The syntax of the function is:
+//
+// SLOPE(known_y's,known_x's)
+func (fn *formulaFuncs) SLOPE(argsList *list.List) formulaArg {
+ return fn.pearsonProduct("SLOPE", 2, argsList)
+}
+
+// SMALL function returns the k'th smallest value from an array of numeric
+// values. The syntax of the function is:
+//
+// SMALL(array,k)
+func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg {
+ return fn.kth("SMALL", argsList)
+}
+
+// STANDARDIZE function returns a normalized value of a distribution that is
+// characterized by a supplied mean and standard deviation. The syntax of the
+// function is:
+//
+// STANDARDIZE(x,mean,standard_dev)
+func (fn *formulaFuncs) STANDARDIZE(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "STANDARDIZE requires 3 arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ if x.Type != ArgNumber {
+ return x
+ }
+ mean := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if mean.Type != ArgNumber {
+ return mean
+ }
+ stdDev := argsList.Back().Value.(formulaArg).ToNumber()
+ if stdDev.Type != ArgNumber {
+ return stdDev
+ }
+ if stdDev.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg((x.Number - mean.Number) / stdDev.Number)
+}
+
+// stdevp is an implementation of the formula functions STDEVP, STDEV.P and
+// STDEVPA.
+func (fn *formulaFuncs) stdevp(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ fnName := "VARP"
+ if name == "STDEVPA" {
+ fnName = "VARPA"
+ }
+ varp := fn.vars(fnName, argsList)
+ if varp.Type != ArgNumber {
+ return varp
+ }
+ return newNumberFormulaArg(math.Sqrt(varp.Number))
+}
+
+// STDEVP function calculates the standard deviation of a supplied set of
+// values. The syntax of the function is:
+//
+// STDEVP(number1,[number2],...)
+func (fn *formulaFuncs) STDEVP(argsList *list.List) formulaArg {
+ return fn.stdevp("STDEVP", argsList)
+}
+
+// STDEVdotP function calculates the standard deviation of a supplied set of
+// values.
+//
+// STDEV.P( number1, [number2], ... )
+func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg {
+ return fn.stdevp("STDEV.P", argsList)
+}
+
+// STDEVPA function calculates the standard deviation of a supplied set of
+// values. The syntax of the function is:
+//
+// STDEVPA(number1,[number2],...)
+func (fn *formulaFuncs) STDEVPA(argsList *list.List) formulaArg {
+ return fn.stdevp("STDEVPA", argsList)
+}
+
+// STEYX function calculates the standard error for the line of best fit,
+// through a supplied set of x- and y- values. The syntax of the function is:
+//
+// STEYX(known_y's,known_x's)
+func (fn *formulaFuncs) STEYX(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "STEYX requires 2 arguments")
+ }
+ array1 := argsList.Back().Value.(formulaArg).ToList()
+ array2 := argsList.Front().Value.(formulaArg).ToList()
+ if len(array1) != len(array2) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var count, sumX, sumY, squareX, squareY, sigmaXY float64
+ for i := 0; i < len(array1); i++ {
+ num1, num2 := array1[i], array2[i]
+ if !(num1.Type == ArgNumber && num2.Type == ArgNumber) {
+ continue
+ }
+ sumX += num1.Number
+ sumY += num2.Number
+ squareX += num1.Number * num1.Number
+ squareY += num2.Number * num2.Number
+ sigmaXY += num1.Number * num2.Number
+ count++
+ }
+ if count < 3 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ dx, dy := sumX/count, sumY/count
+ sigma1 := squareY - 2*dy*sumY + count*dy*dy
+ sigma2 := sigmaXY - dy*sumX - sumY*dx + count*dy*dx
+ sigma3 := squareX - 2*dx*sumX + count*dx*dx
+ return newNumberFormulaArg(math.Sqrt((sigma1 - (sigma2*sigma2)/sigma3) / (count - 2)))
+}
+
+// getTDist is an implementation for the beta distribution probability density
+// function.
+func getTDist(T, fDF, nType float64) float64 {
+ var res float64
+ switch nType {
+ case 1:
+ res = 0.5 * getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5)
+ case 2:
+ res = getBetaDist(fDF/(fDF+T*T), fDF/2, 0.5)
+ case 3:
+ res = math.Pow(1+(T*T/fDF), -(fDF+1)/2) / (math.Sqrt(fDF) * getBeta(0.5, fDF/2.0))
+ case 4:
+ X := fDF / (T*T + fDF)
+ R := 0.5 * getBetaDist(X, 0.5*fDF, 0.5)
+ res = 1 - R
+ if T < 0 {
+ res = R
+ }
+ }
+ return res
+}
+
+// TdotDIST function calculates the one-tailed Student's T Distribution, which
+// is a continuous probability distribution that is frequently used for
+// testing hypotheses on small sample data sets. The syntax of the function
+// is:
+//
+// T.DIST(x,degrees_freedom,cumulative)
+func (fn *formulaFuncs) TdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.DIST requires 3 arguments")
+ }
+ var x, degrees, cumulative formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type != ArgNumber {
+ return cumulative
+ }
+ if cumulative.Number == 1 && degrees.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if cumulative.Number == 0 {
+ if degrees.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if degrees.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 3))
+ }
+ return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 4))
+}
+
+// TdotDISTdot2T function calculates the two-tailed Student's T Distribution,
+// which is a continuous probability distribution that is frequently used for
+// testing hypotheses on small sample data sets. The syntax of the function
+// is:
+//
+// T.DIST.2T(x,degrees_freedom)
+func (fn *formulaFuncs) TdotDISTdot2T(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.2T requires 2 arguments")
+ }
+ var x, degrees formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if x.Number < 0 || degrees.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(getTDist(x.Number, degrees.Number, 2))
+}
+
+// TdotDISTdotRT function calculates the right-tailed Student's T Distribution,
+// which is a continuous probability distribution that is frequently used for
+// testing hypotheses on small sample data sets. The syntax of the function
+// is:
+//
+// T.DIST.RT(x,degrees_freedom)
+func (fn *formulaFuncs) TdotDISTdotRT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.DIST.RT requires 2 arguments")
+ }
+ var x, degrees formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if degrees.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ v := getTDist(x.Number, degrees.Number, 1)
+ if x.Number < 0 {
+ v = 1 - v
+ }
+ return newNumberFormulaArg(v)
+}
+
+// TDIST function calculates the Student's T Distribution, which is a
+// continuous probability distribution that is frequently used for testing
+// hypotheses on small sample data sets. The syntax of the function is:
+//
+// TDIST(x,degrees_freedom,tails)
+func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TDIST requires 3 arguments")
+ }
+ var x, degrees, tails formulaArg
+ if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber {
+ return x
+ }
+ if degrees = argsList.Front().Next().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if tails = argsList.Back().Value.(formulaArg).ToNumber(); tails.Type != ArgNumber {
+ return tails
+ }
+ if x.Number < 0 || degrees.Number < 1 || (tails.Number != 1 && tails.Number != 2) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(getTDist(x.Number, degrees.Number, tails.Number))
+}
+
+// TdotINV function calculates the left-tailed inverse of the Student's T
+// Distribution, which is a continuous probability distribution that is
+// frequently used for testing hypotheses on small sample data sets. The
+// syntax of the function is:
+//
+// T.INV(probability,degrees_freedom)
+func (fn *formulaFuncs) TdotINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.INV requires 2 arguments")
+ }
+ var probability, degrees formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if probability.Number <= 0 || probability.Number >= 1 || degrees.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if probability.Number < 0.5 {
+ return newNumberFormulaArg(-calcIterateInverse(calcInverseIterator{
+ name: "T.INV",
+ fp: 1 - probability.Number,
+ fDF: degrees.Number,
+ nT: 4,
+ }, degrees.Number/2, degrees.Number))
+ }
+ return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{
+ name: "T.INV",
+ fp: probability.Number,
+ fDF: degrees.Number,
+ nT: 4,
+ }, degrees.Number/2, degrees.Number))
+}
+
+// TdotINVdot2T function calculates the inverse of the two-tailed Student's T
+// Distribution, which is a continuous probability distribution that is
+// frequently used for testing hypotheses on small sample data sets. The
+// syntax of the function is:
+//
+// T.INV.2T(probability,degrees_freedom)
+func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.INV.2T requires 2 arguments")
+ }
+ var probability, degrees formulaArg
+ if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber {
+ return probability
+ }
+ if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber {
+ return degrees
+ }
+ if probability.Number <= 0 || probability.Number > 1 || degrees.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{
+ name: "T.INV.2T",
+ fp: probability.Number,
+ fDF: degrees.Number,
+ nT: 2,
+ }, degrees.Number/2, degrees.Number))
+}
+
+// TINV function calculates the inverse of the two-tailed Student's T
+// Distribution, which is a continuous probability distribution that is
+// frequently used for testing hypotheses on small sample data sets. The
+// syntax of the function is:
+//
+// TINV(probability,degrees_freedom)
+func (fn *formulaFuncs) TINV(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TINV requires 2 arguments")
+ }
+ return fn.TdotINVdot2T(argsList)
+}
+
+// TREND function calculates the linear trend line through a given set of
+// y-values and (optionally), a given set of x-values. The function then
+// extends the linear trendline to calculate additional y-values for a further
+// supplied set of new x-values. The syntax of the function is:
+//
+// TREND(known_y's,[known_x's],[new_x's],[const])
+func (fn *formulaFuncs) TREND(argsList *list.List) formulaArg {
+ return fn.trendGrowth("TREND", argsList)
+}
+
+// tTest calculates the probability associated with the Student's T Test.
+func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float64, float64, bool) {
+ var cnt1, cnt2, sum1, sumSqr1, sum2, sumSqr2 float64
+ var fVal formulaArg
+ for i := 0; i < c1; i++ {
+ for j := 0; j < r1; j++ {
+ if fVal = mtx1[i][j]; fVal.Type == ArgNumber {
+ sum1 += fVal.Number
+ sumSqr1 += fVal.Number * fVal.Number
+ cnt1++
+ }
+ }
+ }
+ for i := 0; i < c2; i++ {
+ for j := 0; j < r2; j++ {
+ if fVal = mtx2[i][j]; fVal.Type == ArgNumber {
+ sum2 += fVal.Number
+ sumSqr2 += fVal.Number * fVal.Number
+ cnt2++
+ }
+ }
+ }
+ if cnt1 < 2.0 || cnt2 < 2.0 {
+ return 0, 0, false
+ }
+ if bTemplin {
+ fS1 := (sumSqr1 - sum1*sum1/cnt1) / (cnt1 - 1) / cnt1
+ fS2 := (sumSqr2 - sum2*sum2/cnt2) / (cnt2 - 1) / cnt2
+ if fS1+fS2 == 0 {
+ return 0, 0, false
+ }
+ c := fS1 / (fS1 + fS2)
+ return math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt(fS1+fS2), 1 / (c*c/(cnt1-1) + (1-c)*(1-c)/(cnt2-1)), true
+ }
+ fS1 := (sumSqr1 - sum1*sum1/cnt1) / (cnt1 - 1)
+ fS2 := (sumSqr2 - sum2*sum2/cnt2) / (cnt2 - 1)
+ return math.Abs(sum1/cnt1-sum2/cnt2) / math.Sqrt((cnt1-1)*fS1+(cnt2-1)*fS2) * math.Sqrt(cnt1*cnt2*(cnt1+cnt2-2)/(cnt1+cnt2)), cnt1 + cnt2 - 2, true
+}
+
+// tTest is an implementation of the formula function TTEST.
+func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) formulaArg {
+ var fT, fF float64
+ c1, c2, r1, r2, ok := len(mtx1), len(mtx2), len(mtx1[0]), len(mtx2[0]), true
+ if fTyp == 1 {
+ if c1 != c2 || r1 != r2 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ var cnt, sum1, sum2, sumSqrD float64
+ var fVal1, fVal2 formulaArg
+ for i := 0; i < c1; i++ {
+ for j := 0; j < r1; j++ {
+ fVal1, fVal2 = mtx1[i][j], mtx2[i][j]
+ if fVal1.Type != ArgNumber || fVal2.Type != ArgNumber {
+ continue
+ }
+ sum1 += fVal1.Number
+ sum2 += fVal2.Number
+ sumSqrD += (fVal1.Number - fVal2.Number) * (fVal1.Number - fVal2.Number)
+ cnt++
+ }
+ }
+ if cnt < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ sumD := sum1 - sum2
+ divider := cnt*sumSqrD - sumD*sumD
+ if divider == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ fT = math.Abs(sumD) * math.Sqrt((cnt-1)/divider)
+ fF = cnt - 1
+ } else if fTyp == 2 {
+ fT, fF, ok = tTest(false, mtx1, mtx2, c1, c2, r1, r2)
+ } else {
+ fT, fF, ok = tTest(true, mtx1, mtx2, c1, c2, r1, r2)
+ }
+ if !ok {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(getTDist(fT, fF, fTails))
+}
+
+// TTEST function calculates the probability associated with the Student's T
+// Test, which is commonly used for identifying whether two data sets are
+// likely to have come from the same two underlying populations with the same
+// mean. The syntax of the function is:
+//
+// TTEST(array1,array2,tails,type)
+func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TTEST requires 4 arguments")
+ }
+ var array1, array2, tails, typeArg formulaArg
+ array1 = argsList.Front().Value.(formulaArg)
+ array2 = argsList.Front().Next().Value.(formulaArg)
+ if tails = argsList.Front().Next().Next().Value.(formulaArg); tails.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if typeArg = argsList.Back().Value.(formulaArg); typeArg.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if len(array1.Matrix) == 0 || len(array2.Matrix) == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if tails.Number != 1 && tails.Number != 2 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if typeArg.Number != 1 && typeArg.Number != 2 && typeArg.Number != 3 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return fn.tTest(array1.Matrix, array2.Matrix, tails.Number, typeArg.Number)
+}
+
+// TdotTEST function calculates the probability associated with the Student's T
+// Test, which is commonly used for identifying whether two data sets are
+// likely to have come from the same two underlying populations with the same
+// mean. The syntax of the function is:
+//
+// T.TEST(array1,array2,tails,type)
+func (fn *formulaFuncs) TdotTEST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T.TEST requires 4 arguments")
+ }
+ return fn.TTEST(argsList)
+}
+
+// TRIMMEAN function calculates the trimmed mean (or truncated mean) of a
+// supplied set of values. The syntax of the function is:
+//
+// TRIMMEAN(array,percent)
+func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TRIMMEAN requires 2 arguments")
+ }
+ percent := argsList.Back().Value.(formulaArg).ToNumber()
+ if percent.Type != ArgNumber {
+ return percent
+ }
+ if percent.Number < 0 || percent.Number >= 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ var arr []float64
+ arrArg := argsList.Front().Value.(formulaArg).ToList()
+ for _, cell := range arrArg {
+ if cell.Type != ArgNumber {
+ continue
+ }
+ arr = append(arr, cell.Number)
+ }
+ discard := math.Floor(float64(len(arr)) * percent.Number / 2)
+ sort.Float64s(arr)
+ for i := 0; i < int(discard); i++ {
+ if len(arr) > 0 {
+ arr = arr[1:]
+ }
+ if len(arr) > 0 {
+ arr = arr[:len(arr)-1]
+ }
+ }
+
+ args := list.New().Init()
+ for _, ele := range arr {
+ args.PushBack(newNumberFormulaArg(ele))
+ }
+ return fn.AVERAGE(args)
+}
+
+// vars is an implementation of the formula functions VAR, VARA, VARP, VAR.P
+// VAR.S and VARPA.
+func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ summerA, summerB, count := 0.0, 0.0, 0.0
+ minimum := 0.0
+ if name == "VAR" || name == "VAR.S" || name == "VARA" {
+ minimum = 1.0
+ }
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ for _, token := range arg.Value.(formulaArg).ToList() {
+ if token.Value() == "" {
+ continue
+ }
+ num := token.ToNumber()
+ if token.Value() != "TRUE" && num.Type == ArgNumber {
+ summerA += num.Number * num.Number
+ summerB += num.Number
+ count++
+ continue
+ }
+ num = token.ToBool()
+ if num.Type == ArgNumber {
+ summerA += num.Number * num.Number
+ summerB += num.Number
+ count++
+ continue
+ }
+ if name == "VARA" || name == "VARPA" {
+ count++
+ }
+ }
+ }
+ if count > minimum {
+ summerA *= count
+ summerB *= summerB
+ return newNumberFormulaArg((summerA - summerB) / (count * (count - minimum)))
+ }
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+}
+
+// VAR function returns the sample variance of a supplied set of values. The
+// syntax of the function is:
+//
+// VAR(number1,[number2],...)
+func (fn *formulaFuncs) VAR(argsList *list.List) formulaArg {
+ return fn.vars("VAR", argsList)
+}
+
+// VARA function calculates the sample variance of a supplied set of values.
+// The syntax of the function is:
+//
+// VARA(number1,[number2],...)
+func (fn *formulaFuncs) VARA(argsList *list.List) formulaArg {
+ return fn.vars("VARA", argsList)
+}
+
+// VARP function returns the Variance of a given set of values. The syntax of
+// the function is:
+//
+// VARP(number1,[number2],...)
+func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg {
+ return fn.vars("VARP", argsList)
+}
+
+// VARdotP function returns the Variance of a given set of values. The syntax
+// of the function is:
+//
+// VAR.P(number1,[number2],...)
+func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg {
+ return fn.vars("VAR.P", argsList)
+}
+
+// VARdotS function calculates the sample variance of a supplied set of
+// values. The syntax of the function is:
+//
+// VAR.S(number1,[number2],...)
+func (fn *formulaFuncs) VARdotS(argsList *list.List) formulaArg {
+ return fn.vars("VAR.S", argsList)
+}
+
+// VARPA function returns the Variance of a given set of values. The syntax of
+// the function is:
+//
+// VARPA(number1,[number2],...)
+func (fn *formulaFuncs) VARPA(argsList *list.List) formulaArg {
+ return fn.vars("VARPA", argsList)
+}
+
+// WEIBULL function calculates the Weibull Probability Density Function or the
+// Weibull Cumulative Distribution Function for a supplied set of parameters.
+// The syntax of the function is:
+//
+// WEIBULL(x,alpha,beta,cumulative)
+func (fn *formulaFuncs) WEIBULL(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEIBULL requires 4 arguments")
+ }
+ x := argsList.Front().Value.(formulaArg).ToNumber()
+ alpha := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ beta := argsList.Back().Prev().Value.(formulaArg).ToNumber()
+ if alpha.Type == ArgNumber && beta.Type == ArgNumber && x.Type == ArgNumber {
+ if alpha.Number < 0 || alpha.Number <= 0 || beta.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ cumulative := argsList.Back().Value.(formulaArg).ToBool()
+ if cumulative.Boolean && cumulative.Number == 1 {
+ return newNumberFormulaArg(1 - math.Exp(0-math.Pow(x.Number/beta.Number, alpha.Number)))
+ }
+ return newNumberFormulaArg((alpha.Number / math.Pow(beta.Number, alpha.Number)) *
+ math.Pow(x.Number, alpha.Number-1) * math.Exp(0-math.Pow(x.Number/beta.Number, alpha.Number)))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+}
+
+// WEIBULLdotDIST function calculates the Weibull Probability Density Function
+// or the Weibull Cumulative Distribution Function for a supplied set of
+// parameters. The syntax of the function is:
+//
+// WEIBULL.DIST(x,alpha,beta,cumulative)
+func (fn *formulaFuncs) WEIBULLdotDIST(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEIBULL.DIST requires 4 arguments")
+ }
+ return fn.WEIBULL(argsList)
+}
+
+// ZdotTEST function calculates the one-tailed probability value of the
+// Z-Test. The syntax of the function is:
+//
+// Z.TEST(array,x,[sigma])
+func (fn *formulaFuncs) ZdotTEST(argsList *list.List) formulaArg {
+ argsLen := argsList.Len()
+ if argsLen < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "Z.TEST requires at least 2 arguments")
+ }
+ if argsLen > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "Z.TEST accepts at most 3 arguments")
+ }
+ return fn.ZTEST(argsList)
+}
+
+// ZTEST function calculates the one-tailed probability value of the Z-Test.
+// The syntax of the function is:
+//
+// ZTEST(array,x,[sigma])
+func (fn *formulaFuncs) ZTEST(argsList *list.List) formulaArg {
+ argsLen := argsList.Len()
+ if argsLen < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ZTEST requires at least 2 arguments")
+ }
+ if argsLen > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ZTEST accepts at most 3 arguments")
+ }
+ arrArg, arrArgs := argsList.Front().Value.(formulaArg), list.New()
+ arrArgs.PushBack(arrArg)
+ arr := fn.AVERAGE(arrArgs)
+ if arr.Type == ArgError {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ x := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if x.Type == ArgError {
+ return x
+ }
+ sigma := argsList.Back().Value.(formulaArg).ToNumber()
+ if sigma.Type == ArgError {
+ return sigma
+ }
+ if argsLen != 3 {
+ sigma = fn.STDEV(arrArgs).ToNumber()
+ }
+ normsdistArg := list.New()
+ div := sigma.Number / math.Sqrt(float64(len(arrArg.ToList())))
+ if div == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
+ }
+ normsdistArg.PushBack(newNumberFormulaArg((arr.Number - x.Number) / div))
+ return newNumberFormulaArg(1 - fn.NORMSDIST(normsdistArg).Number)
+}
+
+// Information Functions
+
+// ERRORdotTYPE function receives an error value and returns an integer, that
+// tells you the type of the supplied error. The syntax of the function is:
+//
+// ERROR.TYPE(error_val)
+func (fn *formulaFuncs) ERRORdotTYPE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ERROR.TYPE requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ if token.Type == ArgError {
+ for i, errType := range []string{
+ formulaErrorNULL, formulaErrorDIV, formulaErrorVALUE, formulaErrorREF,
+ formulaErrorNAME, formulaErrorNUM, formulaErrorNA,
+ } {
+ if errType == token.String {
+ return newNumberFormulaArg(float64(i) + 1)
+ }
+ }
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// ISBLANK function tests if a specified cell is blank (empty) and if so,
+// returns TRUE; Otherwise the function returns FALSE. The syntax of the
+// function is:
+//
+// ISBLANK(value)
+func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISBLANK requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgUnknown, ArgEmpty:
+ return newBoolFormulaArg(true)
+ default:
+ return newBoolFormulaArg(false)
+ }
+}
+
+// ISERR function tests if an initial supplied expression (or value) returns
+// any Excel Error, except the #N/A error. If so, the function returns the
+// logical value TRUE; If the supplied value is not an error or is the #N/A
+// error, the ISERR function returns FALSE. The syntax of the function is:
+//
+// ISERR(value)
+func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISERR requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ result := false
+ if token.Type == ArgError {
+ for _, errType := range []string{
+ formulaErrorDIV, formulaErrorNAME, formulaErrorNUM,
+ formulaErrorVALUE, formulaErrorREF, formulaErrorNULL,
+ formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA,
+ } {
+ if errType == token.String {
+ result = true
+ }
+ }
+ }
+ return newBoolFormulaArg(result)
+}
+
+// ISERROR function tests if an initial supplied expression (or value) returns
+// an Excel Error, and if so, returns the logical value TRUE; Otherwise the
+// function returns FALSE. The syntax of the function is:
+//
+// ISERROR(value)
+func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISERROR requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ result := false
+ if token.Type == ArgError {
+ for _, errType := range []string{
+ formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM,
+ formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL,
+ formulaErrorCALC, formulaErrorGETTINGDATA,
+ } {
+ if errType == token.String {
+ result = true
+ }
+ }
+ }
+ return newBoolFormulaArg(result)
+}
+
+// ISEVEN function tests if a supplied number (or numeric expression)
+// evaluates to an even number, and if so, returns TRUE; Otherwise, the
+// function returns FALSE. The syntax of the function is:
+//
+// ISEVEN(value)
+func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISEVEN requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgEmpty:
+ return newBoolFormulaArg(true)
+ case ArgNumber, ArgString:
+ num := token.ToNumber()
+ if num.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if num.Number == 1 {
+ return newBoolFormulaArg(false)
+ }
+ return newBoolFormulaArg(num.Number == num.Number/2*2)
+ default:
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+}
+
+// ISFORMULA function tests if a specified cell contains a formula, and if so,
+// returns TRUE; Otherwise, the function returns FALSE. The syntax of the
+// function is:
+//
+// ISFORMULA(reference)
+func (fn *formulaFuncs) ISFORMULA(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISFORMULA requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if arg.cellRefs != nil && arg.cellRefs.Len() == 1 {
+ ref := arg.cellRefs.Front().Value.(cellRef)
+ cell, _ := CoordinatesToCellName(ref.Col, ref.Row)
+ if formula, _ := fn.f.GetCellFormula(ref.Sheet, cell); len(formula) > 0 {
+ return newBoolFormulaArg(true)
+ }
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISLOGICAL function tests if a supplied value (or expression) returns a
+// logical value (i.e. evaluates to True or False). If so, the function
+// returns TRUE; Otherwise, it returns FALSE. The syntax of the function is:
+//
+// ISLOGICAL(value)
+func (fn *formulaFuncs) ISLOGICAL(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISLOGICAL requires 1 argument")
+ }
+ val := argsList.Front().Value.(formulaArg).Value()
+ if strings.EqualFold("TRUE", val) || strings.EqualFold("FALSE", val) {
+ return newBoolFormulaArg(true)
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISNA function tests if an initial supplied expression (or value) returns
+// the Excel #N/A Error, and if so, returns TRUE; Otherwise the function
+// returns FALSE. The syntax of the function is:
+//
+// ISNA(value)
+func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISNA requires 1 argument")
+ }
+ if token := argsList.Front().Value.(formulaArg); token.Type == ArgError && token.String == formulaErrorNA {
+ return newBoolFormulaArg(true)
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISNONTEXT function tests if a supplied value is text. If not, the
+// function returns TRUE; If the supplied value is text, the function returns
+// FALSE. The syntax of the function is:
+//
+// ISNONTEXT(value)
+func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISNONTEXT requires 1 argument")
+ }
+ if argsList.Front().Value.(formulaArg).Type == ArgString {
+ return newBoolFormulaArg(false)
+ }
+ return newBoolFormulaArg(true)
+}
+
+// ISNUMBER function tests if a supplied value is a number. If so,
+// the function returns TRUE; Otherwise it returns FALSE. The syntax of the
+// function is:
+//
+// ISNUMBER(value)
+func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if arg.Type == ArgMatrix {
+ var mtx [][]formulaArg
+ for _, row := range arg.Matrix {
+ var array []formulaArg
+ for _, val := range row {
+ if val.Type == ArgNumber {
+ array = append(array, newBoolFormulaArg(true))
+ }
+ array = append(array, newBoolFormulaArg(false))
+ }
+ mtx = append(mtx, array)
+ }
+ return newMatrixFormulaArg(mtx)
+ }
+ if arg.Type == ArgNumber {
+ return newBoolFormulaArg(true)
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISODD function tests if a supplied number (or numeric expression) evaluates
+// to an odd number, and if so, returns TRUE; Otherwise, the function returns
+// FALSE. The syntax of the function is:
+//
+// ISODD(value)
+func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISODD requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if int(arg.Number) != int(arg.Number)/2*2 {
+ return newBoolFormulaArg(true)
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISREF function tests if a supplied value is a reference. If so, the
+// function returns TRUE; Otherwise it returns FALSE. The syntax of the
+// function is:
+//
+// ISREF(value)
+func (fn *formulaFuncs) ISREF(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISREF requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if arg.cellRanges != nil && arg.cellRanges.Len() > 0 || arg.cellRefs != nil && arg.cellRefs.Len() > 0 {
+ return newBoolFormulaArg(true)
+ }
+ return newBoolFormulaArg(false)
+}
+
+// ISTEXT function tests if a supplied value is text, and if so, returns TRUE;
+// Otherwise, the function returns FALSE. The syntax of the function is:
+//
+// ISTEXT(value)
+func (fn *formulaFuncs) ISTEXT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISTEXT requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ if token.ToNumber().Type != ArgError {
+ return newBoolFormulaArg(false)
+ }
+ return newBoolFormulaArg(token.Type == ArgString)
+}
+
+// N function converts data into a numeric value. The syntax of the function
+// is:
+//
+// N(value)
+func (fn *formulaFuncs) N(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "N requires 1 argument")
+ }
+ token, num := argsList.Front().Value.(formulaArg), 0.0
+ if token.Type == ArgError {
+ return token
+ }
+ if arg := token.ToNumber(); arg.Type == ArgNumber {
+ num = arg.Number
+ }
+ if token.Value() == "TRUE" {
+ num = 1
+ }
+ return newNumberFormulaArg(num)
+}
+
+// NA function returns the Excel #N/A error. This error message has the
+// meaning 'value not available' and is produced when an Excel Formula is
+// unable to find a value that it needs. The syntax of the function is:
+//
+// NA()
+func (fn *formulaFuncs) NA(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NA accepts no arguments")
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// SHEET function returns the Sheet number for a specified reference. The
+// syntax of the function is:
+//
+// SHEET([value])
+func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg {
+ if argsList.Len() > 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts at most 1 argument")
+ }
+ if argsList.Len() == 0 {
+ idx, _ := fn.f.GetSheetIndex(fn.sheet)
+ return newNumberFormulaArg(float64(idx + 1))
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if sheetIdx, _ := fn.f.GetSheetIndex(arg.Value()); sheetIdx != -1 {
+ return newNumberFormulaArg(float64(sheetIdx + 1))
+ }
+ if arg.cellRanges != nil && arg.cellRanges.Len() > 0 {
+ if sheetIdx, _ := fn.f.GetSheetIndex(arg.cellRanges.Front().Value.(cellRange).From.Sheet); sheetIdx != -1 {
+ return newNumberFormulaArg(float64(sheetIdx + 1))
+ }
+ }
+ if arg.cellRefs != nil && arg.cellRefs.Len() > 0 {
+ if sheetIdx, _ := fn.f.GetSheetIndex(arg.cellRefs.Front().Value.(cellRef).Sheet); sheetIdx != -1 {
+ return newNumberFormulaArg(float64(sheetIdx + 1))
+ }
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// SHEETS function returns the number of sheets in a supplied reference. The
+// result includes sheets that are Visible, Hidden or Very Hidden. The syntax
+// of the function is:
+//
+// SHEETS([reference])
+func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg {
+ if argsList.Len() > 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SHEETS accepts at most 1 argument")
+ }
+ if argsList.Len() == 0 {
+ return newNumberFormulaArg(float64(len(fn.f.GetSheetList())))
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ sheetMap := map[string]struct{}{}
+ if arg.cellRanges != nil && arg.cellRanges.Len() > 0 {
+ for rng := arg.cellRanges.Front(); rng != nil; rng = rng.Next() {
+ sheetMap[rng.Value.(cellRange).From.Sheet] = struct{}{}
+ }
+ }
+ if arg.cellRefs != nil && arg.cellRefs.Len() > 0 {
+ for ref := arg.cellRefs.Front(); ref != nil; ref = ref.Next() {
+ sheetMap[ref.Value.(cellRef).Sheet] = struct{}{}
+ }
+ }
+ if len(sheetMap) > 0 {
+ return newNumberFormulaArg(float64(len(sheetMap)))
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// TYPE function returns an integer that represents the value's data type. The
+// syntax of the function is:
+//
+// TYPE(value)
+func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TYPE requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgError:
+ return newNumberFormulaArg(16)
+ case ArgMatrix:
+ return newNumberFormulaArg(64)
+ case ArgNumber, ArgEmpty:
+ if token.Boolean {
+ return newNumberFormulaArg(4)
+ }
+ return newNumberFormulaArg(1)
+ default:
+ return newNumberFormulaArg(2)
+ }
+}
+
+// T function tests if a supplied value is text and if so, returns the
+// supplied text; Otherwise, the function returns an empty text string. The
+// syntax of the function is:
+//
+// T(value)
+func (fn *formulaFuncs) T(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "T requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ if token.Type == ArgError {
+ return token
+ }
+ if token.Type == ArgNumber {
+ return newStringFormulaArg("")
+ }
+ return newStringFormulaArg(token.Value())
+}
+
+// Logical Functions
+
+// AND function tests a number of supplied conditions and returns TRUE or
+// FALSE. The syntax of the function is:
+//
+// AND(logical_test1,[logical_test2],...)
+func (fn *formulaFuncs) AND(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AND requires at least 1 argument")
+ }
+ if argsList.Len() > 30 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AND accepts at most 30 arguments")
+ }
+ and := true
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgUnknown:
+ continue
+ case ArgString:
+ if token.String == "TRUE" {
+ continue
+ }
+ if token.String == "FALSE" {
+ return newStringFormulaArg(token.String)
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ case ArgNumber:
+ and = and && token.Number != 0
+ case ArgMatrix:
+ // TODO
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ return newBoolFormulaArg(and)
+}
+
+// FALSE function returns the logical value FALSE. The syntax of the
+// function is:
+//
+// FALSE()
+func (fn *formulaFuncs) FALSE(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FALSE takes no arguments")
+ }
+ return newBoolFormulaArg(false)
+}
+
+// IFERROR function receives two values (or expressions) and tests if the
+// first of these evaluates to an error. The syntax of the function is:
+//
+// IFERROR(value,value_if_error)
+func (fn *formulaFuncs) IFERROR(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IFERROR requires 2 arguments")
+ }
+ value := argsList.Front().Value.(formulaArg)
+ if value.Type != ArgError {
+ if value.Type == ArgEmpty {
+ return newNumberFormulaArg(0)
+ }
+ return value
+ }
+ return argsList.Back().Value.(formulaArg)
+}
+
+// IFNA function tests if an initial supplied value (or expression) evaluates
+// to the Excel #N/A error. If so, the function returns a second supplied
+// value; Otherwise the function returns the first supplied value. The syntax
+// of the function is:
+//
+// IFNA(value,value_if_na)
+func (fn *formulaFuncs) IFNA(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IFNA requires 2 arguments")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if arg.Type == ArgError && arg.String == formulaErrorNA {
+ return argsList.Back().Value.(formulaArg)
+ }
+ return arg
+}
+
+// IFS function tests a number of supplied conditions and returns the result
+// corresponding to the first condition that evaluates to TRUE. If none of
+// the supplied conditions evaluate to TRUE, the function returns the #N/A
+// error.
+//
+// IFS(logical_test1,value_if_true1,[logical_test2,value_if_true2],...)
+func (fn *formulaFuncs) IFS(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IFS requires at least 2 arguments")
+ }
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ if arg.Value.(formulaArg).ToBool().Number == 1 {
+ return arg.Next().Value.(formulaArg)
+ }
+ arg = arg.Next()
+ }
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+}
+
+// NOT function returns the opposite to a supplied logical value. The syntax
+// of the function is:
+//
+// NOT(logical)
+func (fn *formulaFuncs) NOT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NOT requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ switch token.Type {
+ case ArgString, ArgList:
+ if strings.ToUpper(token.String) == "TRUE" {
+ return newBoolFormulaArg(false)
+ }
+ if strings.ToUpper(token.String) == "FALSE" {
+ return newBoolFormulaArg(true)
+ }
+ case ArgNumber:
+ return newBoolFormulaArg(!(token.Number != 0))
+ case ArgError:
+ return token
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, "NOT expects 1 boolean or numeric argument")
+}
+
+// OR function tests a number of supplied conditions and returns either TRUE
+// or FALSE. The syntax of the function is:
+//
+// OR(logical_test1,[logical_test2],...)
+func (fn *formulaFuncs) OR(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OR requires at least 1 argument")
+ }
+ if argsList.Len() > 30 {
+ return newErrorFormulaArg(formulaErrorVALUE, "OR accepts at most 30 arguments")
+ }
+ var or bool
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgUnknown:
+ continue
+ case ArgString:
+ if token.String == "FALSE" {
+ continue
+ }
+ if token.String == "TRUE" {
+ or = true
+ continue
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ case ArgNumber:
+ if or = token.Number != 0; or {
+ return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or)))
+ }
+ case ArgMatrix:
+ args := list.New()
+ for _, arg := range token.ToList() {
+ args.PushBack(arg)
+ }
+ return fn.OR(args)
+ }
+ }
+ return newBoolFormulaArg(or)
+}
+
+// SWITCH function compares a number of supplied values to a supplied test
+// expression and returns a result corresponding to the first value that
+// matches the test expression. A default value can be supplied, to be
+// returned if none of the supplied values match the test expression. The
+// syntax of the function is:
+//
+// SWITCH(expression,value1,result1,[value2,result2],[value3,result3],...,[default])
+func (fn *formulaFuncs) SWITCH(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SWITCH requires at least 3 arguments")
+ }
+ target := argsList.Front().Value.(formulaArg)
+ argCount := argsList.Len() - 1
+ switchCount := int(math.Floor(float64(argCount) / 2))
+ hasDefaultClause := argCount%2 != 0
+ result := newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ if hasDefaultClause {
+ result = argsList.Back().Value.(formulaArg)
+ }
+ if switchCount > 0 {
+ arg := argsList.Front()
+ for i := 0; i < switchCount; i++ {
+ arg = arg.Next()
+ if target.Value() == arg.Value.(formulaArg).Value() {
+ result = arg.Next().Value.(formulaArg)
+ break
+ }
+ arg = arg.Next()
+ }
+ }
+ return result
+}
+
+// TRUE function returns the logical value TRUE. The syntax of the function
+// is:
+//
+// TRUE()
+func (fn *formulaFuncs) TRUE(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TRUE takes no arguments")
+ }
+ return newBoolFormulaArg(true)
+}
+
+// calcXor checking if numeric cell exists and count it by given arguments
+// sequence for the formula function XOR.
+func calcXor(argsList *list.List) formulaArg {
+ count, ok := 0, false
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ token := arg.Value.(formulaArg)
+ switch token.Type {
+ case ArgError:
+ return token
+ case ArgNumber:
+ ok = true
+ if token.Number != 0 {
+ count++
+ }
+ case ArgMatrix:
+ for _, value := range token.ToList() {
+ if num := value.ToNumber(); num.Type == ArgNumber {
+ ok = true
+ if num.Number != 0 {
+ count++
+ }
+ }
+ }
+ }
+ }
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newBoolFormulaArg(count%2 != 0)
+}
+
+// XOR function returns the Exclusive Or logical operation for one or more
+// supplied conditions. I.e. the Xor function returns TRUE if an odd number
+// of the supplied conditions evaluate to TRUE, and FALSE otherwise. The
+// syntax of the function is:
+//
+// XOR(logical_test1,[logical_test2],...)
+func (fn *formulaFuncs) XOR(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XOR requires at least 1 argument")
+ }
+ return calcXor(argsList)
+}
+
+// Date and Time Functions
+
+// DATE returns a date, from a user-supplied year, month and day. The syntax
+// of the function is:
+//
+// DATE(year,month,day)
+func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments")
+ }
+ year := argsList.Front().Value.(formulaArg).ToNumber()
+ month := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ day := argsList.Back().Value.(formulaArg).ToNumber()
+ if year.Type != ArgNumber || month.Type != ArgNumber || day.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments")
+ }
+ d := makeDate(int(year.Number), time.Month(month.Number), int(day.Number))
+ return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), d) + 1)
+}
+
+// calcDateDif is an implementation of the formula function DATEDIF,
+// calculation difference between two dates.
+func calcDateDif(unit string, diff float64, seq []int, startArg, endArg formulaArg) float64 {
+ ey, sy, em, sm, ed, sd := seq[0], seq[1], seq[2], seq[3], seq[4], seq[5]
+ switch unit {
+ case "d":
+ diff = endArg.Number - startArg.Number
+ case "md":
+ smMD := em
+ if ed < sd {
+ smMD--
+ }
+ diff = endArg.Number - daysBetween(excelMinTime1900.Unix(), makeDate(ey, time.Month(smMD), sd)) - 1
+ case "ym":
+ diff = float64(em - sm)
+ if ed < sd {
+ diff--
+ }
+ if diff < 0 {
+ diff += 12
+ }
+ case "yd":
+ syYD := sy
+ if em < sm || (em == sm && ed < sd) {
+ syYD++
+ }
+ s := daysBetween(excelMinTime1900.Unix(), makeDate(syYD, time.Month(em), ed))
+ e := daysBetween(excelMinTime1900.Unix(), makeDate(sy, time.Month(sm), sd))
+ diff = s - e
+ }
+ return diff
+}
+
+// DATEDIF function calculates the number of days, months, or years between
+// two dates. The syntax of the function is:
+//
+// DATEDIF(start_date,end_date,unit)
+func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF requires 3 number arguments")
+ }
+ startArg, endArg := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if startArg.Type != ArgNumber || endArg.Type != ArgNumber {
+ return startArg
+ }
+ if startArg.Number > endArg.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "start_date > end_date")
+ }
+ if startArg.Number == endArg.Number {
+ return newNumberFormulaArg(0)
+ }
+ unit := strings.ToLower(argsList.Back().Value.(formulaArg).Value())
+ startDate, endDate := timeFromExcelTime(startArg.Number, false), timeFromExcelTime(endArg.Number, false)
+ sy, smm, sd := startDate.Date()
+ ey, emm, ed := endDate.Date()
+ sm, em, diff := int(smm), int(emm), 0.0
+ switch unit {
+ case "y":
+ diff = float64(ey - sy)
+ if em < sm || (em == sm && ed < sd) {
+ diff--
+ }
+ case "m":
+ yDiff := ey - sy
+ mDiff := em - sm
+ if ed < sd {
+ mDiff--
+ }
+ if mDiff < 0 {
+ yDiff--
+ mDiff += 12
+ }
+ diff = float64(yDiff*12 + mDiff)
+ case "d", "md", "ym", "yd":
+ diff = calcDateDif(unit, diff, []int{ey, sy, em, sm, ed, sd}, startArg, endArg)
+ default:
+ return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF has invalid unit")
+ }
+ return newNumberFormulaArg(diff)
+}
+
+// isDateOnlyFmt check if the given string matches date-only format regular expressions.
+func isDateOnlyFmt(dateString string) bool {
+ for _, df := range dateOnlyFormats {
+ subMatch := df.FindStringSubmatch(dateString)
+ if len(subMatch) > 1 {
+ return true
+ }
+ }
+ return false
+}
+
+// isTimeOnlyFmt check if the given string matches time-only format regular expressions.
+func isTimeOnlyFmt(timeString string) bool {
+ for _, tf := range timeFormats {
+ subMatch := tf.FindStringSubmatch(timeString)
+ if len(subMatch) > 1 {
+ return true
+ }
+ }
+ return false
+}
+
+// strToTimePatternHandler1 parse and convert the given string in pattern
+// hh to the time.
+func strToTimePatternHandler1(subMatch []string) (h, m int, s float64, err error) {
+ h, err = strconv.Atoi(subMatch[0])
+ return
+}
+
+// strToTimePatternHandler2 parse and convert the given string in pattern
+// hh:mm to the time.
+func strToTimePatternHandler2(subMatch []string) (h, m int, s float64, err error) {
+ if h, err = strconv.Atoi(subMatch[0]); err != nil {
+ return
+ }
+ m, err = strconv.Atoi(subMatch[2])
+ return
+}
+
+// strToTimePatternHandler3 parse and convert the given string in pattern
+// mm:ss to the time.
+func strToTimePatternHandler3(subMatch []string) (h, m int, s float64, err error) {
+ if m, err = strconv.Atoi(subMatch[0]); err != nil {
+ return
+ }
+ s, err = strconv.ParseFloat(subMatch[2], 64)
+ return
+}
+
+// strToTimePatternHandler4 parse and convert the given string in pattern
+// hh:mm:ss to the time.
+func strToTimePatternHandler4(subMatch []string) (h, m int, s float64, err error) {
+ if h, err = strconv.Atoi(subMatch[0]); err != nil {
+ return
+ }
+ if m, err = strconv.Atoi(subMatch[2]); err != nil {
+ return
+ }
+ s, err = strconv.ParseFloat(subMatch[4], 64)
+ return
+}
+
+// strToTime parse and convert the given string to the time.
+func strToTime(str string) (int, int, float64, bool, bool, formulaArg) {
+ var subMatch []string
+ pattern := ""
+ for key, tf := range timeFormats {
+ subMatch = tf.FindStringSubmatch(str)
+ if len(subMatch) > 1 {
+ pattern = key
+ break
+ }
+ }
+ if pattern == "" {
+ return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ dateIsEmpty := subMatch[1] == ""
+ subMatch = subMatch[49:]
+ var (
+ l = len(subMatch)
+ last = subMatch[l-1]
+ am = last == "am"
+ pm = last == "pm"
+ hours, minutes int
+ seconds float64
+ err error
+ )
+ if handler, ok := map[string]func(match []string) (int, int, float64, error){
+ "hh": strToTimePatternHandler1,
+ "hh:mm": strToTimePatternHandler2,
+ "mm:ss": strToTimePatternHandler3,
+ "hh:mm:ss": strToTimePatternHandler4,
+ }[pattern]; ok {
+ if hours, minutes, seconds, err = handler(subMatch); err != nil {
+ return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ if minutes >= 60 {
+ return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if am || pm {
+ if hours > 12 || seconds >= 60 {
+ return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ } else if hours == 12 {
+ hours = 0
+ }
+ } else if hours >= 24 || seconds >= 10000 {
+ return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return hours, minutes, seconds, pm, dateIsEmpty, newEmptyFormulaArg()
+}
+
+// strToDatePatternHandler1 parse and convert the given string in pattern
+// mm/dd/yy to the date.
+func strToDatePatternHandler1(subMatch []string) (int, int, int, bool, error) {
+ var year, month, day int
+ var err error
+ if month, err = strconv.Atoi(subMatch[1]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ if day, err = strconv.Atoi(subMatch[3]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ if year, err = strconv.Atoi(subMatch[5]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ if year < 0 || year > 9999 || (year > 99 && year < 1900) {
+ return 0, 0, 0, false, ErrParameterInvalid
+ }
+ return formatYear(year), month, day, subMatch[8] == "", err
+}
+
+// strToDatePatternHandler2 parse and convert the given string in pattern mm
+// dd, yy to the date.
+func strToDatePatternHandler2(subMatch []string) (int, int, int, bool, error) {
+ var year, month, day int
+ var err error
+ month = month2num[subMatch[1]]
+ if day, err = strconv.Atoi(subMatch[14]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ if year, err = strconv.Atoi(subMatch[16]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ if year < 0 || year > 9999 || (year > 99 && year < 1900) {
+ return 0, 0, 0, false, ErrParameterInvalid
+ }
+ return formatYear(year), month, day, subMatch[19] == "", err
+}
+
+// strToDatePatternHandler3 parse and convert the given string in pattern
+// yy-mm-dd to the date.
+func strToDatePatternHandler3(subMatch []string) (int, int, int, bool, error) {
+ var year, month, day int
+ v1, err := strconv.Atoi(subMatch[1])
+ if err != nil {
+ return 0, 0, 0, false, err
+ }
+ v2, err := strconv.Atoi(subMatch[3])
+ if err != nil {
+ return 0, 0, 0, false, err
+ }
+ v3, err := strconv.Atoi(subMatch[5])
+ if err != nil {
+ return 0, 0, 0, false, err
+ }
+ if v1 >= 1900 && v1 < 10000 {
+ year = v1
+ month = v2
+ day = v3
+ } else if v1 > 0 && v1 < 13 {
+ month = v1
+ day = v2
+ year = v3
+ } else {
+ return 0, 0, 0, false, ErrParameterInvalid
+ }
+ return year, month, day, subMatch[8] == "", err
+}
+
+// strToDatePatternHandler4 parse and convert the given string in pattern
+// yy-mmStr-dd, yy to the date.
+func strToDatePatternHandler4(subMatch []string) (int, int, int, bool, error) {
+ var year, month, day int
+ var err error
+ if year, err = strconv.Atoi(subMatch[16]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ month = month2num[subMatch[3]]
+ if day, err = strconv.Atoi(subMatch[1]); err != nil {
+ return 0, 0, 0, false, err
+ }
+ return formatYear(year), month, day, subMatch[19] == "", err
+}
+
+// strToDate parse and convert the given string to the date.
+func strToDate(str string) (int, int, int, bool, formulaArg) {
+ var subMatch []string
+ pattern := ""
+ for key, df := range dateFormats {
+ subMatch = df.FindStringSubmatch(str)
+ if len(subMatch) > 1 {
+ pattern = key
+ break
+ }
+ }
+ if pattern == "" {
+ return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ var (
+ timeIsEmpty bool
+ year, month, day int
+ err error
+ )
+ if handler, ok := map[string]func(match []string) (int, int, int, bool, error){
+ "mm/dd/yy": strToDatePatternHandler1,
+ "mm dd, yy": strToDatePatternHandler2,
+ "yy-mm-dd": strToDatePatternHandler3,
+ "yy-mmStr-dd": strToDatePatternHandler4,
+ }[pattern]; ok {
+ if year, month, day, timeIsEmpty, err = handler(subMatch); err != nil {
+ return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ if !validateDate(year, month, day) {
+ return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return year, month, day, timeIsEmpty, newEmptyFormulaArg()
+}
+
+// DATEVALUE function converts a text representation of a date into an Excel
+// date. For example, the function converts a text string representing a
+// date, into the serial number that represents the date in Excels' date-time
+// code. The syntax of the function is:
+//
+// DATEVALUE(date_text)
+func (fn *formulaFuncs) DATEVALUE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DATEVALUE requires 1 argument")
+ }
+ dateText := argsList.Front().Value.(formulaArg).Value()
+ if !isDateOnlyFmt(dateText) {
+ if _, _, _, _, _, err := strToTime(dateText); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateText)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(y, time.Month(m), d)) + 1)
+}
+
+// DAY function returns the day of a date, represented by a serial number. The
+// day is given as an integer ranging from 1 to 31. The syntax of the
+// function is:
+//
+// DAY(serial_number)
+func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DAY requires exactly 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ num := arg.ToNumber()
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(arg.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ _, _, day, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(float64(day))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "DAY only accepts positive argument")
+ }
+ if num.Number <= 60 {
+ return newNumberFormulaArg(math.Mod(num.Number, 31.0))
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Day()))
+}
+
+// DAYS function returns the number of days between two supplied dates. The
+// syntax of the function is:
+//
+// DAYS(end_date,start_date)
+func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DAYS requires 2 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ end, start := args.List[0], args.List[1]
+ return newNumberFormulaArg(end.Number - start.Number)
+}
+
+// DAYS360 function returns the number of days between 2 dates, based on a
+// 360-day year (12 x 30 months). The syntax of the function is:
+//
+// DAYS360(start_date,end_date,[method])
+func (fn *formulaFuncs) DAYS360(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DAYS360 requires at least 2 arguments")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DAYS360 requires at most 3 arguments")
+ }
+ startDate := toExcelDateArg(argsList.Front().Value.(formulaArg))
+ if startDate.Type != ArgNumber {
+ return startDate
+ }
+ endDate := toExcelDateArg(argsList.Front().Next().Value.(formulaArg))
+ if endDate.Type != ArgNumber {
+ return endDate
+ }
+ start, end := timeFromExcelTime(startDate.Number, false), timeFromExcelTime(endDate.Number, false)
+ sy, sm, sd, ey, em, ed := start.Year(), int(start.Month()), start.Day(), end.Year(), int(end.Month()), end.Day()
+ method := newBoolFormulaArg(false)
+ if argsList.Len() > 2 {
+ if method = argsList.Back().Value.(formulaArg).ToBool(); method.Type != ArgNumber {
+ return method
+ }
+ }
+ if method.Number == 1 {
+ if sd == 31 {
+ sd--
+ }
+ if ed == 31 {
+ ed--
+ }
+ } else {
+ if getDaysInMonth(sy, sm) == sd {
+ sd = 30
+ }
+ if ed > 30 {
+ if sd < 30 {
+ em++
+ ed = 1
+ } else {
+ ed = 30
+ }
+ }
+ }
+ return newNumberFormulaArg(float64(360*(ey-sy) + 30*(em-sm) + (ed - sd)))
+}
+
+// ISOWEEKNUM function returns the ISO week number of a supplied date. The
+// syntax of the function is:
+//
+// ISOWEEKNUM(date)
+func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISOWEEKNUM requires 1 argument")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ weekNum := 0
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(date.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ _, weekNum = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC).ISOWeek()
+ } else {
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ _, weekNum = timeFromExcelTime(num.Number, false).ISOWeek()
+ }
+ return newNumberFormulaArg(float64(weekNum))
+}
+
+// EDATE function returns a date that is a specified number of months before or
+// after a supplied start date. The syntax of function is:
+//
+// EDATE(start_date,months)
+func (fn *formulaFuncs) EDATE(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EDATE requires 2 arguments")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ var dateTime time.Time
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(date.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ dateTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location())
+ } else {
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ dateTime = timeFromExcelTime(num.Number, false)
+ }
+ month := argsList.Back().Value.(formulaArg).ToNumber()
+ if month.Type != ArgNumber {
+ return month
+ }
+ y, d := dateTime.Year(), dateTime.Day()
+ m := int(dateTime.Month()) + int(month.Number)
+ if month.Number < 0 {
+ y -= int(math.Ceil(-1 * float64(m) / 12))
+ }
+ if month.Number > 11 {
+ y += int(math.Floor(float64(m) / 12))
+ }
+ if m = m % 12; m < 0 {
+ m += 12
+ }
+ if d > 28 {
+ if days := getDaysInMonth(y, m); d > days {
+ d = days
+ }
+ }
+ result, _ := timeToExcelTime(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC), false)
+ return newNumberFormulaArg(result)
+}
+
+// EOMONTH function returns the last day of the month, that is a specified
+// number of months before or after an initial supplied start date. The syntax
+// of the function is:
+//
+// EOMONTH(start_date,months)
+func (fn *formulaFuncs) EOMONTH(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EOMONTH requires 2 arguments")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ var dateTime time.Time
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(date.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ dateTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location())
+ } else {
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ dateTime = timeFromExcelTime(num.Number, false)
+ }
+ months := argsList.Back().Value.(formulaArg).ToNumber()
+ if months.Type != ArgNumber {
+ return months
+ }
+ y, m := dateTime.Year(), int(dateTime.Month())+int(months.Number)-1
+ if m < 0 {
+ y -= int(math.Ceil(-1 * float64(m) / 12))
+ }
+ if m > 11 {
+ y += int(math.Floor(float64(m) / 12))
+ }
+ if m = m % 12; m < 0 {
+ m += 12
+ }
+ result, _ := timeToExcelTime(time.Date(y, time.Month(m+1), getDaysInMonth(y, m+1), 0, 0, 0, 0, time.UTC), false)
+ return newNumberFormulaArg(result)
+}
+
+// HOUR function returns an integer representing the hour component of a
+// supplied Excel time. The syntax of the function is:
+//
+// HOUR(serial_number)
+func (fn *formulaFuncs) HOUR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HOUR requires exactly 1 argument")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ if num.Type != ArgNumber {
+ timeString := strings.ToLower(date.Value())
+ if !isTimeOnlyFmt(timeString) {
+ _, _, _, _, err := strToDate(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ }
+ h, _, _, pm, _, err := strToTime(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ if pm {
+ h += 12
+ }
+ return newNumberFormulaArg(float64(h))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "HOUR only accepts positive argument")
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Hour()))
+}
+
+// MINUTE function returns an integer representing the minute component of a
+// supplied Excel time. The syntax of the function is:
+//
+// MINUTE(serial_number)
+func (fn *formulaFuncs) MINUTE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MINUTE requires exactly 1 argument")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ if num.Type != ArgNumber {
+ timeString := strings.ToLower(date.Value())
+ if !isTimeOnlyFmt(timeString) {
+ _, _, _, _, err := strToDate(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ }
+ _, m, _, _, _, err := strToTime(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(float64(m))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "MINUTE only accepts positive argument")
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Minute()))
+}
+
+// MONTH function returns the month of a date represented by a serial number.
+// The month is given as an integer, ranging from 1 (January) to 12
+// (December). The syntax of the function is:
+//
+// MONTH(serial_number)
+func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MONTH requires exactly 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ num := arg.ToNumber()
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(arg.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ _, month, _, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(float64(month))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "MONTH only accepts positive argument")
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Month()))
+}
+
+// genWeekendMask generate weekend mask of a series of seven 0's and 1's which
+// represent the seven weekdays, starting from Monday.
+func genWeekendMask(weekend int) []byte {
+ if masks, ok := map[int][]int{
+ 1: {5, 6}, 2: {6, 0}, 3: {0, 1}, 4: {1, 2}, 5: {2, 3}, 6: {3, 4}, 7: {4, 5},
+ 11: {6}, 12: {0}, 13: {1}, 14: {2}, 15: {3}, 16: {4}, 17: {5},
+ }[weekend]; ok {
+ mask := make([]byte, 7)
+ for _, idx := range masks {
+ mask[idx] = 1
+ }
+ return mask
+ }
+ return nil
+}
+
+// isWorkday check if the date is workday.
+func isWorkday(weekendMask []byte, date float64) bool {
+ dateTime := timeFromExcelTime(date, false)
+ weekday := dateTime.Weekday()
+ if weekday == time.Sunday {
+ weekday = 7
+ }
+ return weekendMask[weekday-1] == 0
+}
+
+// prepareWorkday returns weekend mask and workdays pre week by given days
+// counted as weekend.
+func prepareWorkday(weekend formulaArg) ([]byte, int) {
+ weekendArg := weekend.ToNumber()
+ if weekendArg.Type != ArgNumber {
+ return nil, 0
+ }
+ var weekendMask []byte
+ var workdaysPerWeek int
+ if len(weekend.Value()) == 7 {
+ // possible string values for the weekend argument
+ for _, mask := range weekend.Value() {
+ if mask != '0' && mask != '1' {
+ return nil, 0
+ }
+ weekendMask = append(weekendMask, byte(mask)-48)
+ }
+ } else {
+ weekendMask = genWeekendMask(int(weekendArg.Number))
+ }
+ for _, mask := range weekendMask {
+ if mask == 0 {
+ workdaysPerWeek++
+ }
+ }
+ return weekendMask, workdaysPerWeek
+}
+
+// toExcelDateArg function converts a text representation of a time, into an
+// Excel date time number formula argument.
+func toExcelDateArg(arg formulaArg) formulaArg {
+ num := arg.ToNumber()
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(arg.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ num.Number, _ = timeToExcelTime(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC), false)
+ return newNumberFormulaArg(num.Number)
+ }
+ if arg.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return num
+}
+
+// prepareHolidays function converts array type formula arguments to into an
+// Excel date time number formula arguments list.
+func prepareHolidays(args formulaArg) []int {
+ var holidays []int
+ for _, arg := range args.ToList() {
+ num := toExcelDateArg(arg)
+ if num.Type != ArgNumber {
+ continue
+ }
+ holidays = append(holidays, int(math.Ceil(num.Number)))
+ }
+ return holidays
+}
+
+// workdayIntl is an implementation of the formula function WORKDAY.INTL.
+func workdayIntl(endDate, sign int, holidays []int, weekendMask []byte, startDate float64) int {
+ for i := 0; i < len(holidays); i++ {
+ holiday := holidays[i]
+ if sign > 0 {
+ if holiday > endDate {
+ break
+ }
+ } else {
+ if holiday < endDate {
+ break
+ }
+ }
+ if sign > 0 {
+ if holiday > int(math.Ceil(startDate)) {
+ if isWorkday(weekendMask, float64(holiday)) {
+ endDate += sign
+ for !isWorkday(weekendMask, float64(endDate)) {
+ endDate += sign
+ }
+ }
+ }
+ } else {
+ if holiday < int(math.Ceil(startDate)) {
+ if isWorkday(weekendMask, float64(holiday)) {
+ endDate += sign
+ for !isWorkday(weekendMask, float64(endDate)) {
+ endDate += sign
+ }
+ }
+ }
+ }
+ }
+ return endDate
+}
+
+// NETWORKDAYS function calculates the number of work days between two supplied
+// dates (including the start and end date). The calculation includes all
+// weekdays (Mon - Fri), excluding a supplied list of holidays. The syntax of
+// the function is:
+//
+// NETWORKDAYS(start_date,end_date,[holidays])
+func (fn *formulaFuncs) NETWORKDAYS(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at least 2 arguments")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS requires at most 3 arguments")
+ }
+ args := list.New()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(argsList.Front().Next().Value.(formulaArg))
+ args.PushBack(newNumberFormulaArg(1))
+ if argsList.Len() == 3 {
+ args.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.NETWORKDAYSdotINTL(args)
+}
+
+// NETWORKDAYSdotINTL function calculates the number of whole work days between
+// two supplied dates, excluding weekends and holidays. The function allows
+// the user to specify which days are counted as weekends and holidays. The
+// syntax of the function is:
+//
+// NETWORKDAYS.INTL(start_date,end_date,[weekend],[holidays])
+func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at least 2 arguments")
+ }
+ if argsList.Len() > 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NETWORKDAYS.INTL requires at most 4 arguments")
+ }
+ startDate := toExcelDateArg(argsList.Front().Value.(formulaArg))
+ if startDate.Type != ArgNumber {
+ return startDate
+ }
+ endDate := toExcelDateArg(argsList.Front().Next().Value.(formulaArg))
+ if endDate.Type != ArgNumber {
+ return endDate
+ }
+ weekend := newNumberFormulaArg(1)
+ if argsList.Len() > 2 {
+ weekend = argsList.Front().Next().Next().Value.(formulaArg)
+ }
+ var holidays []int
+ if argsList.Len() == 4 {
+ holidays = prepareHolidays(argsList.Back().Value.(formulaArg))
+ sort.Ints(holidays)
+ }
+ weekendMask, workdaysPerWeek := prepareWorkday(weekend)
+ if workdaysPerWeek == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ sign := 1
+ if startDate.Number > endDate.Number {
+ sign = -1
+ temp := startDate.Number
+ startDate.Number = endDate.Number
+ endDate.Number = temp
+ }
+ offset := endDate.Number - startDate.Number
+ count := int(math.Floor(offset/7) * float64(workdaysPerWeek))
+ daysMod := int(offset) % 7
+ for daysMod >= 0 {
+ if isWorkday(weekendMask, endDate.Number-float64(daysMod)) {
+ count++
+ }
+ daysMod--
+ }
+ for i := 0; i < len(holidays); i++ {
+ holiday := float64(holidays[i])
+ if isWorkday(weekendMask, holiday) && holiday >= startDate.Number && holiday <= endDate.Number {
+ count--
+ }
+ }
+ return newNumberFormulaArg(float64(sign * count))
+}
+
+// WORKDAY function returns a date that is a supplied number of working days
+// (excluding weekends and holidays) ahead of a given start date. The syntax
+// of the function is:
+//
+// WORKDAY(start_date,days,[holidays])
+func (fn *formulaFuncs) WORKDAY(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at least 2 arguments")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY requires at most 3 arguments")
+ }
+ args := list.New()
+ args.PushBack(argsList.Front().Value.(formulaArg))
+ args.PushBack(argsList.Front().Next().Value.(formulaArg))
+ args.PushBack(newNumberFormulaArg(1))
+ if argsList.Len() == 3 {
+ args.PushBack(argsList.Back().Value.(formulaArg))
+ }
+ return fn.WORKDAYdotINTL(args)
+}
+
+// WORKDAYdotINTL function returns a date that is a supplied number of working
+// days (excluding weekends and holidays) ahead of a given start date. The
+// function allows the user to specify which days of the week are counted as
+// weekends. The syntax of the function is:
+//
+// WORKDAY.INTL(start_date,days,[weekend],[holidays])
+func (fn *formulaFuncs) WORKDAYdotINTL(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at least 2 arguments")
+ }
+ if argsList.Len() > 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WORKDAY.INTL requires at most 4 arguments")
+ }
+ startDate := toExcelDateArg(argsList.Front().Value.(formulaArg))
+ if startDate.Type != ArgNumber {
+ return startDate
+ }
+ days := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if days.Type != ArgNumber {
+ return days
+ }
+ weekend := newNumberFormulaArg(1)
+ if argsList.Len() > 2 {
+ weekend = argsList.Front().Next().Next().Value.(formulaArg)
+ }
+ var holidays []int
+ if argsList.Len() == 4 {
+ holidays = prepareHolidays(argsList.Back().Value.(formulaArg))
+ sort.Ints(holidays)
+ }
+ if days.Number == 0 {
+ return newNumberFormulaArg(math.Ceil(startDate.Number))
+ }
+ weekendMask, workdaysPerWeek := prepareWorkday(weekend)
+ if workdaysPerWeek == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ sign := 1
+ if days.Number < 0 {
+ sign = -1
+ }
+ offset := int(days.Number) / workdaysPerWeek
+ daysMod := int(days.Number) % workdaysPerWeek
+ endDate := int(math.Ceil(startDate.Number)) + offset*7
+ if daysMod == 0 {
+ for !isWorkday(weekendMask, float64(endDate)) {
+ endDate -= sign
+ }
+ } else {
+ for daysMod != 0 {
+ endDate += sign
+ if isWorkday(weekendMask, float64(endDate)) {
+ if daysMod < 0 {
+ daysMod++
+ continue
+ }
+ daysMod--
+ }
+ }
+ }
+ return newNumberFormulaArg(float64(workdayIntl(endDate, sign, holidays, weekendMask, startDate.Number)))
+}
+
+// YEAR function returns an integer representing the year of a supplied date.
+// The syntax of the function is:
+//
+// YEAR(serial_number)
+func (fn *formulaFuncs) YEAR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "YEAR requires exactly 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ num := arg.ToNumber()
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(arg.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ year, _, _, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(float64(year))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YEAR only accepts positive argument")
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Year()))
+}
+
+// yearFracBasisCond is an implementation of the yearFracBasis1.
+func yearFracBasisCond(sy, sm, sd, ey, em, ed int) bool {
+ return (isLeapYear(sy) && (sm < 2 || (sm == 2 && sd <= 29))) || (isLeapYear(ey) && (em > 2 || (em == 2 && ed == 29)))
+}
+
+// yearFracBasis0 function returns the fraction of a year that between two
+// supplied dates in US (NASD) 30/360 type of day.
+func yearFracBasis0(startDate, endDate float64) (dayDiff, daysInYear float64) {
+ startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false)
+ sy, smM, sd := startTime.Date()
+ ey, emM, ed := endTime.Date()
+ sm, em := int(smM), int(emM)
+ if sd == 31 {
+ sd--
+ }
+ if sd == 30 && ed == 31 {
+ ed--
+ } else if leap := isLeapYear(sy); sm == 2 && ((leap && sd == 29) || (!leap && sd == 28)) {
+ sd = 30
+ if leap := isLeapYear(ey); em == 2 && ((leap && ed == 29) || (!leap && ed == 28)) {
+ ed = 30
+ }
+ }
+ dayDiff = float64((ey-sy)*360 + (em-sm)*30 + (ed - sd))
+ daysInYear = 360
+ return
+}
+
+// yearFracBasis1 function returns the fraction of a year that between two
+// supplied dates in actual type of day.
+func yearFracBasis1(startDate, endDate float64) (dayDiff, daysInYear float64) {
+ startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false)
+ sy, smM, sd := startTime.Date()
+ ey, emM, ed := endTime.Date()
+ sm, em := int(smM), int(emM)
+ dayDiff = endDate - startDate
+ isYearDifferent := sy != ey
+ if isYearDifferent && (ey != sy+1 || sm < em || (sm == em && sd < ed)) {
+ dayCount := 0
+ for y := sy; y <= ey; y++ {
+ dayCount += getYearDays(y, 1)
+ }
+ daysInYear = float64(dayCount) / float64(ey-sy+1)
+ } else {
+ if !isYearDifferent && isLeapYear(sy) {
+ daysInYear = 366
+ } else {
+ if isYearDifferent && yearFracBasisCond(sy, sm, sd, ey, em, ed) {
+ daysInYear = 366
+ } else {
+ daysInYear = 365
+ }
+ }
+ }
+ return
+}
+
+// yearFracBasis4 function returns the fraction of a year that between two
+// supplied dates in European 30/360 type of day.
+func yearFracBasis4(startDate, endDate float64) (dayDiff, daysInYear float64) {
+ startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false)
+ sy, smM, sd := startTime.Date()
+ ey, emM, ed := endTime.Date()
+ sm, em := int(smM), int(emM)
+ if sd == 31 {
+ sd--
+ }
+ if ed == 31 {
+ ed--
+ }
+ dayDiff = float64((ey-sy)*360 + (em-sm)*30 + (ed - sd))
+ daysInYear = 360
+ return
+}
+
+// yearFrac is an implementation of the formula function YEARFRAC.
+func yearFrac(startDate, endDate float64, basis int) formulaArg {
+ startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false)
+ if startTime == endTime {
+ return newNumberFormulaArg(0)
+ }
+ var dayDiff, daysInYear float64
+ switch basis {
+ case 0:
+ dayDiff, daysInYear = yearFracBasis0(startDate, endDate)
+ case 1:
+ dayDiff, daysInYear = yearFracBasis1(startDate, endDate)
+ case 2:
+ dayDiff = endDate - startDate
+ daysInYear = 360
+ case 3:
+ dayDiff = endDate - startDate
+ daysInYear = 365
+ case 4:
+ dayDiff, daysInYear = yearFracBasis4(startDate, endDate)
+ default:
+ return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
+ }
+ return newNumberFormulaArg(dayDiff / daysInYear)
+}
+
+// getYearDays return days of the year with specifying the type of day count
+// basis to be used.
+func getYearDays(year, basis int) int {
+ switch basis {
+ case 1:
+ if isLeapYear(year) {
+ return 366
+ }
+ return 365
+ case 3:
+ return 365
+ default:
+ return 360
+ }
+}
+
+// YEARFRAC function returns the fraction of a year that is represented by the
+// number of whole days between two supplied dates. The syntax of the
+// function is:
+//
+// YEARFRAC(start_date,end_date,[basis])
+func (fn *formulaFuncs) YEARFRAC(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 && argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "YEARFRAC requires 3 or 4 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ start, end := args.List[0], args.List[1]
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 3 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return basis
+ }
+ }
+ return yearFrac(start.Number, end.Number, int(basis.Number))
+}
+
+// NOW function returns the current date and time. The function receives no
+// arguments and therefore. The syntax of the function is:
+//
+// NOW()
+func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NOW accepts no arguments")
+ }
+ now := time.Now()
+ _, offset := now.Zone()
+ return newNumberFormulaArg(25569.0 + float64(now.Unix()+int64(offset))/86400)
+}
+
+// SECOND function returns an integer representing the second component of a
+// supplied Excel time. The syntax of the function is:
+//
+// SECOND(serial_number)
+func (fn *formulaFuncs) SECOND(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SECOND requires exactly 1 argument")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ num := date.ToNumber()
+ if num.Type != ArgNumber {
+ timeString := strings.ToLower(date.Value())
+ if !isTimeOnlyFmt(timeString) {
+ _, _, _, _, err := strToDate(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ }
+ _, _, s, _, _, err := strToTime(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ return newNumberFormulaArg(float64(int(s) % 60))
+ }
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "SECOND only accepts positive argument")
+ }
+ return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Second()))
+}
+
+// TIME function accepts three integer arguments representing hours, minutes
+// and seconds, and returns an Excel time. I.e. the function returns the
+// decimal value that represents the time in Excel. The syntax of the
+// function is:
+//
+// TIME(hour,minute,second)
+func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TIME requires 3 number arguments")
+ }
+ h := argsList.Front().Value.(formulaArg).ToNumber()
+ m := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ s := argsList.Back().Value.(formulaArg).ToNumber()
+ if h.Type != ArgNumber || m.Type != ArgNumber || s.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "TIME requires 3 number arguments")
+ }
+ t := (h.Number*3600 + m.Number*60 + s.Number) / 86400
+ if t < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(t)
+}
+
+// TIMEVALUE function converts a text representation of a time, into an Excel
+// time. The syntax of the function is:
+//
+// TIMEVALUE(time_text)
+func (fn *formulaFuncs) TIMEVALUE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TIMEVALUE requires exactly 1 argument")
+ }
+ date := argsList.Front().Value.(formulaArg)
+ timeString := strings.ToLower(date.Value())
+ if !isTimeOnlyFmt(timeString) {
+ _, _, _, _, err := strToDate(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ }
+ h, m, s, pm, _, err := strToTime(timeString)
+ if err.Type == ArgError {
+ return err
+ }
+ if pm {
+ h += 12
+ }
+ args := list.New()
+ args.PushBack(newNumberFormulaArg(float64(h)))
+ args.PushBack(newNumberFormulaArg(float64(m)))
+ args.PushBack(newNumberFormulaArg(s))
+ return fn.TIME(args)
+}
+
+// TODAY function returns the current date. The function has no arguments and
+// therefore. The syntax of the function is:
+//
+// TODAY()
+func (fn *formulaFuncs) TODAY(argsList *list.List) formulaArg {
+ if argsList.Len() != 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TODAY accepts no arguments")
+ }
+ now := time.Now()
+ _, offset := now.Zone()
+ return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), now.Unix()+int64(offset)) + 1)
+}
+
+// makeDate return date as a Unix time, the number of seconds elapsed since
+// January 1, 1970 UTC.
+func makeDate(y int, m time.Month, d int) int64 {
+ if y == 1900 && int(m) <= 2 {
+ d--
+ }
+ date := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
+ return date.Unix()
+}
+
+// daysBetween return time interval of the given start timestamp and end
+// timestamp.
+func daysBetween(startDate, endDate int64) float64 {
+ return float64(int(0.5 + float64((endDate-startDate)/86400)))
+}
+
+// WEEKDAY function returns an integer representing the day of the week for a
+// supplied date. The syntax of the function is:
+//
+// WEEKDAY(serial_number,[return_type])
+func (fn *formulaFuncs) WEEKDAY(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY allows at most 2 arguments")
+ }
+ sn := argsList.Front().Value.(formulaArg)
+ num := sn.ToNumber()
+ weekday, returnType := 0, 1
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(sn.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ weekday = int(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location()).Weekday())
+ } else {
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ weekday = int(timeFromExcelTime(num.Number, false).Weekday())
+ }
+ if argsList.Len() == 2 {
+ returnTypeArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if returnTypeArg.Type != ArgNumber {
+ return returnTypeArg
+ }
+ returnType = int(returnTypeArg.Number)
+ }
+ if returnType == 2 {
+ returnType = 11
+ }
+ weekday++
+ if returnType == 1 {
+ return newNumberFormulaArg(float64(weekday))
+ }
+ if returnType == 3 {
+ return newNumberFormulaArg(float64((weekday + 6 - 1) % 7))
+ }
+ if returnType >= 11 && returnType <= 17 {
+ return newNumberFormulaArg(float64((weekday+6-(returnType-10))%7 + 1))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+}
+
+// weeknum is an implementation of the formula function WEEKNUM.
+func (fn *formulaFuncs) weeknum(snTime time.Time, returnType int) formulaArg {
+ days := snTime.YearDay()
+ weekMod, weekNum := days%7, math.Ceil(float64(days)/7)
+ if weekMod == 0 {
+ weekMod = 7
+ }
+ year := snTime.Year()
+ firstWeekday := int(time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC).Weekday())
+ var offset int
+ switch returnType {
+ case 1, 17:
+ offset = 0
+ case 2, 11, 21:
+ offset = 1
+ case 12, 13, 14, 15, 16:
+ offset = returnType - 10
+ default:
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ padding := offset + 7 - firstWeekday
+ if padding > 7 {
+ padding -= 7
+ }
+ if weekMod > padding {
+ weekNum++
+ }
+ if returnType == 21 && (firstWeekday == 0 || firstWeekday > 4) {
+ if weekNum--; weekNum < 1 {
+ if weekNum = 52; int(time.Date(year-1, time.January, 1, 0, 0, 0, 0, time.UTC).Weekday()) < 4 {
+ weekNum++
+ }
+ }
+ }
+ return newNumberFormulaArg(weekNum)
+}
+
+// WEEKNUM function returns an integer representing the week number (from 1 to
+// 53) of the year. The syntax of the function is:
+//
+// WEEKNUM(serial_number,[return_type])
+func (fn *formulaFuncs) WEEKNUM(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEEKNUM requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "WEEKNUM allows at most 2 arguments")
+ }
+ sn := argsList.Front().Value.(formulaArg)
+ num, returnType := sn.ToNumber(), 1
+ var snTime time.Time
+ if num.Type != ArgNumber {
+ dateString := strings.ToLower(sn.Value())
+ if !isDateOnlyFmt(dateString) {
+ if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
+ return err
+ }
+ }
+ y, m, d, _, err := strToDate(dateString)
+ if err.Type == ArgError {
+ return err
+ }
+ snTime = time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location())
+ } else {
+ if num.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ snTime = timeFromExcelTime(num.Number, false)
+ }
+ if argsList.Len() == 2 {
+ returnTypeArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if returnTypeArg.Type != ArgNumber {
+ return returnTypeArg
+ }
+ returnType = int(returnTypeArg.Number)
+ }
+ return fn.weeknum(snTime, returnType)
+}
+
+// Text Functions
+
+// prepareToText checking and prepare arguments for the formula functions
+// ARRAYTOTEXT and VALUETOTEXT.
+func prepareToText(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name))
+ }
+ format := newNumberFormulaArg(0)
+ if argsList.Len() == 2 {
+ if format = argsList.Back().Value.(formulaArg).ToNumber(); format.Type != ArgNumber {
+ return format
+ }
+ }
+ if format.Number != 0 && format.Number != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return format
+}
+
+// ARRAYTOTEXT function returns an array of text values from any specified
+// range. It passes text values unchanged, and converts non-text values to
+// text. The syntax of the function is:
+//
+// ARRAYTOTEXT(array,[format])
+func (fn *formulaFuncs) ARRAYTOTEXT(argsList *list.List) formulaArg {
+ var mtx [][]string
+ format := prepareToText("ARRAYTOTEXT", argsList)
+ if format.Type != ArgNumber {
+ return format
+ }
+ for _, rows := range argsList.Front().Value.(formulaArg).Matrix {
+ var row []string
+ for _, cell := range rows {
+ if num := cell.ToNumber(); num.Type != ArgNumber && format.Number == 1 {
+ row = append(row, fmt.Sprintf("\"%s\"", cell.Value()))
+ continue
+ }
+ row = append(row, cell.Value())
+ }
+ mtx = append(mtx, row)
+ }
+ var text []string
+ for _, row := range mtx {
+ if format.Number == 1 {
+ text = append(text, strings.Join(row, ","))
+ continue
+ }
+ text = append(text, strings.Join(row, ", "))
+ }
+ if format.Number == 1 {
+ return newStringFormulaArg(fmt.Sprintf("{%s}", strings.Join(text, ";")))
+ }
+ return newStringFormulaArg(strings.Join(text, ", "))
+}
+
+// CHAR function returns the character relating to a supplied character set
+// number (from 1 to 255). The syntax of the function is:
+//
+// CHAR(number)
+func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHAR requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg).ToNumber()
+ if arg.Type != ArgNumber {
+ return arg
+ }
+ num := int(arg.Number)
+ if num < 0 || num > MaxFieldLength {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newStringFormulaArg(fmt.Sprintf("%c", num))
+}
+
+// CLEAN removes all non-printable characters from a supplied text string. The
+// syntax of the function is:
+//
+// CLEAN(text)
+func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CLEAN requires 1 argument")
+ }
+ b := bytes.Buffer{}
+ for _, c := range argsList.Front().Value.(formulaArg).Value() {
+ if c > 31 {
+ b.WriteRune(c)
+ }
+ }
+ return newStringFormulaArg(b.String())
+}
+
+// CODE function converts the first character of a supplied text string into
+// the associated numeric character set code used by your computer. The
+// syntax of the function is:
+//
+// CODE(text)
+func (fn *formulaFuncs) CODE(argsList *list.List) formulaArg {
+ return fn.code("CODE", argsList)
+}
+
+// code is an implementation of the formula functions CODE and UNICODE.
+func (fn *formulaFuncs) code(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 1 argument", name))
+ }
+ text := argsList.Front().Value.(formulaArg).Value()
+ if len(text) == 0 {
+ if name == "CODE" {
+ return newNumberFormulaArg(0)
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newNumberFormulaArg(float64(text[0]))
+}
+
+// CONCAT function joins together a series of supplied text strings into one
+// combined text string.
+//
+// CONCAT(text1,[text2],...)
+func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg {
+ return fn.concat(argsList)
+}
+
+// CONCATENATE function joins together a series of supplied text strings into
+// one combined text string.
+//
+// CONCATENATE(text1,[text2],...)
+func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg {
+ return fn.concat(argsList)
+}
+
+// concat is an implementation of the formula functions CONCAT and
+// CONCATENATE.
+func (fn *formulaFuncs) concat(argsList *list.List) formulaArg {
+ var buf bytes.Buffer
+ for arg := argsList.Front(); arg != nil; arg = arg.Next() {
+ for _, cell := range arg.Value.(formulaArg).ToList() {
+ if cell.Type == ArgError {
+ return cell
+ }
+ buf.WriteString(cell.Value())
+ }
+ }
+ return newStringFormulaArg(buf.String())
+}
+
+// DBCS converts half-width (single-byte) letters within a character string to
+// full-width (double-byte) characters. The syntax of the function is:
+//
+// DBCS(text)
+func (fn *formulaFuncs) DBCS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DBCS requires 1 argument")
+ }
+ arg := argsList.Front().Value.(formulaArg)
+ if arg.Type == ArgError {
+ return arg
+ }
+ if fn.f.options.CultureInfo == CultureNameJaJP ||
+ fn.f.options.CultureInfo == CultureNameZhCN ||
+ fn.f.options.CultureInfo == CultureNameZhTW {
+ var chars []string
+ for _, r := range arg.Value() {
+ code := r
+ if code == 32 {
+ code = 12288
+ } else {
+ code += 65248
+ }
+ if (code < 32 || code > 126) && r != 165 && code < 65381 {
+ chars = append(chars, string(code))
+ } else {
+ chars = append(chars, string(r))
+ }
+ }
+ return newStringFormulaArg(strings.Join(chars, ""))
+ }
+ return arg
+}
+
+// EXACT function tests if two supplied text strings or values are exactly
+// equal and if so, returns TRUE; Otherwise, the function returns FALSE. The
+// function is case-sensitive. The syntax of the function is:
+//
+// EXACT(text1,text2)
+func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EXACT requires 2 arguments")
+ }
+ text1 := argsList.Front().Value.(formulaArg).Value()
+ text2 := argsList.Back().Value.(formulaArg).Value()
+ return newBoolFormulaArg(text1 == text2)
+}
+
+// FIXED function rounds a supplied number to a specified number of decimal
+// places and then converts this into text. The syntax of the function is:
+//
+// FIXED(number,[decimals],[no_commas])
+func (fn *formulaFuncs) FIXED(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FIXED requires at least 1 argument")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FIXED allows at most 3 arguments")
+ }
+ numArg := argsList.Front().Value.(formulaArg).ToNumber()
+ if numArg.Type != ArgNumber {
+ return numArg
+ }
+ precision, decimals, noCommas := 0, 0, false
+ s := strings.Split(argsList.Front().Value.(formulaArg).Value(), ".")
+ if argsList.Len() == 1 && len(s) == 2 {
+ precision = len(s[1])
+ decimals = len(s[1])
+ }
+ if argsList.Len() >= 2 {
+ decimalsArg := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if decimalsArg.Type != ArgNumber {
+ return decimalsArg
+ }
+ decimals = int(decimalsArg.Number)
+ }
+ if argsList.Len() == 3 {
+ noCommasArg := argsList.Back().Value.(formulaArg).ToBool()
+ if noCommasArg.Type == ArgError {
+ return noCommasArg
+ }
+ noCommas = noCommasArg.Boolean
+ }
+ n := math.Pow(10, float64(decimals))
+ r := numArg.Number * n
+ fixed := float64(int(r+math.Copysign(0.5, r))) / n
+ if decimals > 0 {
+ precision = decimals
+ }
+ if noCommas {
+ return newStringFormulaArg(fmt.Sprintf(fmt.Sprintf("%%.%df", precision), fixed))
+ }
+ p := message.NewPrinter(language.English)
+ return newStringFormulaArg(p.Sprintf(fmt.Sprintf("%%.%df", precision), fixed))
+}
+
+// FIND function returns the position of a specified character or sub-string
+// within a supplied text string. The function is case-sensitive. The syntax
+// of the function is:
+//
+// FIND(find_text,within_text,[start_num])
+func (fn *formulaFuncs) FIND(argsList *list.List) formulaArg {
+ return fn.find("FIND", argsList)
+}
+
+// FINDB counts each double-byte character as 2 when you have enabled the
+// editing of a language that supports DBCS and then set it as the default
+// language. Otherwise, FINDB counts each character as 1. The syntax of the
+// function is:
+//
+// FINDB(find_text,within_text,[start_num])
+func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg {
+ return fn.find("FINDB", argsList)
+}
+
+// prepareFindArgs checking and prepare arguments for the formula functions
+// FIND, FINDB, SEARCH and SEARCHB.
+func (fn *formulaFuncs) prepareFindArgs(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name))
+ }
+ startNum := 1
+ if argsList.Len() == 3 {
+ numArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if numArg.Type != ArgNumber {
+ return numArg
+ }
+ if numArg.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ startNum = int(numArg.Number)
+ }
+ return newListFormulaArg([]formulaArg{newNumberFormulaArg(float64(startNum))})
+}
+
+// find is an implementation of the formula functions FIND, FINDB, SEARCH and
+// SEARCHB.
+func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
+ args := fn.prepareFindArgs(name, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ findTextArg := argsList.Front().Value.(formulaArg)
+ withinText := argsList.Front().Next().Value.(formulaArg).Value()
+ startNum := int(args.List[0].Number)
+ dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB"
+ find := func(findText string) formulaArg {
+ if findText == "" {
+ return newNumberFormulaArg(float64(startNum))
+ }
+ if search {
+ findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText)
+ }
+ offset, ok := matchPattern(findText, withinText, dbcs, startNum)
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ result := offset
+ if dbcs {
+ var pre int
+ for idx := range withinText {
+ if pre > offset {
+ break
+ }
+ if idx-pre > 1 {
+ result++
+ }
+ pre = idx
+ }
+ }
+ return newNumberFormulaArg(float64(result))
+ }
+ if findTextArg.Type == ArgMatrix {
+ var mtx [][]formulaArg
+ for _, row := range findTextArg.Matrix {
+ var array []formulaArg
+ for _, findText := range row {
+ array = append(array, find(findText.Value()))
+ }
+ mtx = append(mtx, array)
+ }
+ return newMatrixFormulaArg(mtx)
+ }
+ return find(findTextArg.Value())
+}
+
+// LEFT function returns a specified number of characters from the start of a
+// supplied text string. The syntax of the function is:
+//
+// LEFT(text,[num_chars])
+func (fn *formulaFuncs) LEFT(argsList *list.List) formulaArg {
+ return fn.leftRight("LEFT", argsList)
+}
+
+// LEFTB returns the first character or characters in a text string, based on
+// the number of bytes you specify. The syntax of the function is:
+//
+// LEFTB(text,[num_bytes])
+func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg {
+ return fn.leftRight("LEFTB", argsList)
+}
+
+// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT,
+// RIGHTB.
+func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name))
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name))
+ }
+ text, numChars := argsList.Front().Value.(formulaArg).Value(), 1
+ if argsList.Len() == 2 {
+ numArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if numArg.Type != ArgNumber {
+ return numArg
+ }
+ if numArg.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ numChars = int(numArg.Number)
+ }
+ if name == "LEFTB" || name == "RIGHTB" {
+ if len(text) > numChars {
+ if name == "LEFTB" {
+ return newStringFormulaArg(text[:numChars])
+ }
+ // RIGHTB
+ return newStringFormulaArg(text[len(text)-numChars:])
+ }
+ return newStringFormulaArg(text)
+ }
+ // LEFT/RIGHT
+ if utf8.RuneCountInString(text) > numChars {
+ if name == "LEFT" {
+ return newStringFormulaArg(string([]rune(text)[:numChars]))
+ }
+ // RIGHT
+ return newStringFormulaArg(string([]rune(text)[utf8.RuneCountInString(text)-numChars:]))
+ }
+ return newStringFormulaArg(text)
+}
+
+// LEN returns the length of a supplied text string. The syntax of the
+// function is:
+//
+// LEN(text)
+func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument")
+ }
+ return newNumberFormulaArg(float64(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).Value())))
+}
+
+// LENB returns the number of bytes used to represent the characters in a text
+// string. LENB counts 2 bytes per character only when a DBCS language is set
+// as the default language. Otherwise LENB behaves the same as LEN, counting
+// 1 byte per character. The syntax of the function is:
+//
+// LENB(text)
+func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LENB requires 1 string argument")
+ }
+ result := 0
+ for _, r := range argsList.Front().Value.(formulaArg).Value() {
+ b := utf8.RuneLen(r)
+ if b == 1 {
+ result++
+ } else if b > 1 {
+ result += 2
+ }
+ }
+ return newNumberFormulaArg(float64(result))
+}
+
+// LOWER converts all characters in a supplied text string to lower case. The
+// syntax of the function is:
+//
+// LOWER(text)
+func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "LOWER requires 1 argument")
+ }
+ return newStringFormulaArg(strings.ToLower(argsList.Front().Value.(formulaArg).Value()))
+}
+
+// MID function returns a specified number of characters from the middle of a
+// supplied text string. The syntax of the function is:
+//
+// MID(text,start_num,num_chars)
+func (fn *formulaFuncs) MID(argsList *list.List) formulaArg {
+ return fn.mid("MID", argsList)
+}
+
+// MIDB returns a specific number of characters from a text string, starting
+// at the position you specify, based on the number of bytes you specify. The
+// syntax of the function is:
+//
+// MID(text,start_num,num_chars)
+func (fn *formulaFuncs) MIDB(argsList *list.List) formulaArg {
+ return fn.mid("MIDB", argsList)
+}
+
+// mid is an implementation of the formula functions MID and MIDB.
+func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name))
+ }
+ text := argsList.Front().Value.(formulaArg).Value()
+ startNumArg, numCharsArg := argsList.Front().Next().Value.(formulaArg).ToNumber(), argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if startNumArg.Type != ArgNumber {
+ return startNumArg
+ }
+ if numCharsArg.Type != ArgNumber {
+ return numCharsArg
+ }
+ startNum := int(startNumArg.Number)
+ if startNum < 1 || numCharsArg.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if name == "MIDB" {
+ var result string
+ var cnt, offset int
+ for _, char := range text {
+ offset++
+ var dbcs bool
+ if utf8.RuneLen(char) > 1 {
+ dbcs = true
+ offset++
+ }
+ if cnt == int(numCharsArg.Number) {
+ break
+ }
+ if offset+1 > startNum {
+ if dbcs {
+ if cnt+2 > int(numCharsArg.Number) {
+ result += string(char)[:1]
+ break
+ }
+ result += string(char)
+ cnt += 2
+ } else {
+ result += string(char)
+ cnt++
+ }
+ }
+ }
+ return newStringFormulaArg(result)
+ }
+ // MID
+ textLen := utf8.RuneCountInString(text)
+ if startNum > textLen {
+ return newStringFormulaArg("")
+ }
+ startNum--
+ endNum := startNum + int(numCharsArg.Number)
+ if endNum > textLen+1 {
+ return newStringFormulaArg(string([]rune(text)[startNum:]))
+ }
+ return newStringFormulaArg(string([]rune(text)[startNum:endNum]))
+}
+
+// PROPER converts all characters in a supplied text string to proper case
+// (i.e. all letters that do not immediately follow another letter are set to
+// upper case and all other characters are lower case). The syntax of the
+// function is:
+//
+// PROPER(text)
+func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PROPER requires 1 argument")
+ }
+ buf := bytes.Buffer{}
+ isLetter := false
+ for _, char := range argsList.Front().Value.(formulaArg).Value() {
+ if !isLetter && unicode.IsLetter(char) {
+ buf.WriteRune(unicode.ToUpper(char))
+ } else {
+ buf.WriteRune(unicode.ToLower(char))
+ }
+ isLetter = unicode.IsLetter(char)
+ }
+ return newStringFormulaArg(buf.String())
+}
+
+// REPLACE function replaces all or part of a text string with another string.
+// The syntax of the function is:
+//
+// REPLACE(old_text,start_num,num_chars,new_text)
+func (fn *formulaFuncs) REPLACE(argsList *list.List) formulaArg {
+ return fn.replace("REPLACE", argsList)
+}
+
+// REPLACEB replaces part of a text string, based on the number of bytes you
+// specify, with a different text string.
+//
+// REPLACEB(old_text,start_num,num_chars,new_text)
+func (fn *formulaFuncs) REPLACEB(argsList *list.List) formulaArg {
+ return fn.replace("REPLACEB", argsList)
+}
+
+// replace is an implementation of the formula functions REPLACE and REPLACEB.
+// TODO: support DBCS include Japanese, Chinese (Simplified), Chinese
+// (Traditional), and Korean.
+func (fn *formulaFuncs) replace(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 4 arguments", name))
+ }
+ sourceText, targetText := argsList.Front().Value.(formulaArg).Value(), argsList.Back().Value.(formulaArg).Value()
+ startNumArg, numCharsArg := argsList.Front().Next().Value.(formulaArg).ToNumber(), argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if startNumArg.Type != ArgNumber {
+ return startNumArg
+ }
+ if numCharsArg.Type != ArgNumber {
+ return numCharsArg
+ }
+ sourceTextLen, startIdx := len(sourceText), int(startNumArg.Number)
+ if startIdx > sourceTextLen {
+ startIdx = sourceTextLen + 1
+ }
+ endIdx := startIdx + int(numCharsArg.Number)
+ if endIdx > sourceTextLen {
+ endIdx = sourceTextLen + 1
+ }
+ if startIdx < 1 || endIdx < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ result := sourceText[:startIdx-1] + targetText + sourceText[endIdx-1:]
+ return newStringFormulaArg(result)
+}
+
+// REPT function returns a supplied text string, repeated a specified number
+// of times. The syntax of the function is:
+//
+// REPT(text,number_times)
+func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "REPT requires 2 arguments")
+ }
+ text := argsList.Front().Value.(formulaArg)
+ if text.Type != ArgString {
+ return newErrorFormulaArg(formulaErrorVALUE, "REPT requires first argument to be a string")
+ }
+ times := argsList.Back().Value.(formulaArg).ToNumber()
+ if times.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "REPT requires second argument to be a number")
+ }
+ if times.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "REPT requires second argument to be >= 0")
+ }
+ if times.Number == 0 {
+ return newStringFormulaArg("")
+ }
+ buf := bytes.Buffer{}
+ for i := 0; i < int(times.Number); i++ {
+ buf.WriteString(text.Value())
+ }
+ return newStringFormulaArg(buf.String())
+}
+
+// RIGHT function returns a specified number of characters from the end of a
+// supplied text string. The syntax of the function is:
+//
+// RIGHT(text,[num_chars])
+func (fn *formulaFuncs) RIGHT(argsList *list.List) formulaArg {
+ return fn.leftRight("RIGHT", argsList)
+}
+
+// RIGHTB returns the last character or characters in a text string, based on
+// the number of bytes you specify. The syntax of the function is:
+//
+// RIGHTB(text,[num_bytes])
+func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg {
+ return fn.leftRight("RIGHTB", argsList)
+}
+
+// SEARCH function returns the position of a specified character or sub-string
+// within a supplied text string. The syntax of the function is:
+//
+// SEARCH(search_text,within_text,[start_num])
+func (fn *formulaFuncs) SEARCH(argsList *list.List) formulaArg {
+ return fn.find("SEARCH", argsList)
+}
+
+// SEARCHB functions locate one text string within a second text string, and
+// return the number of the starting position of the first text string from the
+// first character of the second text string. The syntax of the function is:
+//
+// SEARCHB(search_text,within_text,[start_num])
+func (fn *formulaFuncs) SEARCHB(argsList *list.List) formulaArg {
+ return fn.find("SEARCHB", argsList)
+}
+
+// SUBSTITUTE function replaces one or more instances of a given text string,
+// within an original text string. The syntax of the function is:
+//
+// SUBSTITUTE(text,old_text,new_text,[instance_num])
+func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 && argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SUBSTITUTE requires 3 or 4 arguments")
+ }
+ text, sourceText := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)
+ targetText, instanceNum := argsList.Front().Next().Next().Value.(formulaArg), 0
+ if argsList.Len() == 3 {
+ return newStringFormulaArg(strings.ReplaceAll(text.Value(), sourceText.Value(), targetText.Value()))
+ }
+ instanceNumArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if instanceNumArg.Type != ArgNumber {
+ return instanceNumArg
+ }
+ instanceNum = int(instanceNumArg.Number)
+ if instanceNum < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "instance_num should be > 0")
+ }
+ str, sourceTextLen, count, chars, pos := text.Value(), len(sourceText.Value()), instanceNum, 0, -1
+ for {
+ count--
+ index := strings.Index(str, sourceText.Value())
+ if index == -1 {
+ pos = -1
+ break
+ } else {
+ pos = index + chars
+ if count == 0 {
+ break
+ }
+ idx := sourceTextLen + index
+ chars += idx
+ str = str[idx:]
+ }
+ }
+ if pos == -1 {
+ return newStringFormulaArg(text.Value())
+ }
+ pre, post := text.Value()[:pos], text.Value()[pos+sourceTextLen:]
+ return newStringFormulaArg(pre + targetText.Value() + post)
+}
+
+// TEXT function converts a supplied numeric value into text, in a
+// user-specified format. The syntax of the function is:
+//
+// TEXT(value,format_text)
+func (fn *formulaFuncs) TEXT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TEXT requires 2 arguments")
+ }
+ value, fmtText := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg)
+ if value.Type == ArgError {
+ return value
+ }
+ if fmtText.Type == ArgError {
+ return fmtText
+ }
+ cellType := CellTypeNumber
+ if num := value.ToNumber(); num.Type != ArgNumber {
+ cellType = CellTypeSharedString
+ }
+ return newStringFormulaArg(format(value.Value(), fmtText.Value(), false, cellType, nil))
+}
+
+// prepareTextAfterBefore checking and prepare arguments for the formula
+// functions TEXTAFTER and TEXTBEFORE.
+func (fn *formulaFuncs) prepareTextAfterBefore(name string, argsList *list.List) formulaArg {
+ argsLen := argsList.Len()
+ if argsLen < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
+ }
+ if argsLen > 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s accepts at most 6 arguments", name))
+ }
+ text, delimiter := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg)
+ instanceNum, matchMode, matchEnd, ifNotFound := newNumberFormulaArg(1), newBoolFormulaArg(false), newBoolFormulaArg(false), newEmptyFormulaArg()
+ if argsLen > 2 {
+ instanceNum = argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if instanceNum.Type != ArgNumber {
+ return instanceNum
+ }
+ }
+ if argsLen > 3 {
+ matchMode = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool()
+ if matchMode.Type != ArgNumber {
+ return matchMode
+ }
+ if matchMode.Number == 1 {
+ text, delimiter = newStringFormulaArg(strings.ToLower(text.Value())), newStringFormulaArg(strings.ToLower(delimiter.Value()))
+ }
+ }
+ if argsLen > 4 {
+ matchEnd = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToBool()
+ if matchEnd.Type != ArgNumber {
+ return matchEnd
+ }
+ }
+ if argsLen > 5 {
+ ifNotFound = argsList.Back().Value.(formulaArg)
+ }
+ if text.Value() == "" {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ lenArgsList := list.New().Init()
+ lenArgsList.PushBack(text)
+ textLen := fn.LEN(lenArgsList)
+ if instanceNum.Number == 0 || instanceNum.Number > textLen.Number {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ reverseSearch, startPos := instanceNum.Number < 0, 0.0
+ if reverseSearch {
+ startPos = textLen.Number
+ }
+ return newListFormulaArg([]formulaArg{
+ text, delimiter, instanceNum, matchMode, matchEnd, ifNotFound,
+ textLen, newBoolFormulaArg(reverseSearch), newNumberFormulaArg(startPos),
+ })
+}
+
+// textAfterBeforeSearch is an implementation of the formula functions TEXTAFTER
+// and TEXTBEFORE.
+func textAfterBeforeSearch(text string, delimiter []string, startPos int, reverseSearch bool) (int, string) {
+ idx := -1
+ var modifiedDelimiter string
+ for i := 0; i < len(delimiter); i++ {
+ nextDelimiter := delimiter[i]
+ nextIdx := strings.Index(text[startPos:], nextDelimiter)
+ if nextIdx != -1 {
+ nextIdx += startPos
+ }
+ if reverseSearch {
+ nextIdx = strings.LastIndex(text[:startPos], nextDelimiter)
+ }
+ if idx == -1 || (((nextIdx < idx && !reverseSearch) || (nextIdx > idx && reverseSearch)) && idx != -1) {
+ idx = nextIdx
+ modifiedDelimiter = nextDelimiter
+ }
+ }
+ return idx, modifiedDelimiter
+}
+
+// textAfterBeforeResult is an implementation of the formula functions TEXTAFTER
+// and TEXTBEFORE.
+func textAfterBeforeResult(name, modifiedDelimiter string, text []rune, foundIdx, repeatZero, textLen int, matchEndActive, matchEnd, reverseSearch bool) formulaArg {
+ if name == "TEXTAFTER" {
+ endPos := len(modifiedDelimiter)
+ if (repeatZero > 1 || matchEndActive) && matchEnd && reverseSearch {
+ endPos = 0
+ }
+ if foundIdx+endPos >= textLen {
+ return newEmptyFormulaArg()
+ }
+ return newStringFormulaArg(string(text[foundIdx+endPos : textLen]))
+ }
+ return newStringFormulaArg(string(text[:foundIdx]))
+}
+
+// textAfterBefore is an implementation of the formula functions TEXTAFTER and
+// TEXTBEFORE.
+func (fn *formulaFuncs) textAfterBefore(name string, argsList *list.List) formulaArg {
+ args := fn.prepareTextAfterBefore(name, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ var (
+ text = []rune(argsList.Front().Value.(formulaArg).Value())
+ modifiedText = args.List[0].Value()
+ delimiter = []string{args.List[1].Value()}
+ instanceNum = args.List[2].Number
+ matchEnd = args.List[4].Number == 1
+ ifNotFound = args.List[5]
+ textLen = args.List[6]
+ reverseSearch = args.List[7].Number == 1
+ foundIdx = -1
+ repeatZero, startPos int
+ matchEndActive bool
+ modifiedDelimiter string
+ )
+ if reverseSearch {
+ startPos = int(args.List[8].Number)
+ }
+ for i := 0; i < int(math.Abs(instanceNum)); i++ {
+ foundIdx, modifiedDelimiter = textAfterBeforeSearch(modifiedText, delimiter, startPos, reverseSearch)
+ if foundIdx == 0 {
+ repeatZero++
+ }
+ if foundIdx == -1 {
+ if matchEnd && i == int(math.Abs(instanceNum))-1 {
+ if foundIdx = int(textLen.Number); reverseSearch {
+ foundIdx = 0
+ }
+ matchEndActive = true
+ }
+ break
+ }
+ if startPos = foundIdx + len(modifiedDelimiter); reverseSearch {
+ startPos = foundIdx - len(modifiedDelimiter)
+ }
+ }
+ if foundIdx == -1 {
+ return ifNotFound
+ }
+ return textAfterBeforeResult(name, modifiedDelimiter, text, foundIdx, repeatZero, int(textLen.Number), matchEndActive, matchEnd, reverseSearch)
+}
+
+// TEXTAFTER function returns the text that occurs after a given substring or
+// delimiter. The syntax of the function is:
+//
+// TEXTAFTER(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found])
+func (fn *formulaFuncs) TEXTAFTER(argsList *list.List) formulaArg {
+ return fn.textAfterBefore("TEXTAFTER", argsList)
+}
+
+// TEXTBEFORE function returns text that occurs before a given character or
+// string. The syntax of the function is:
+//
+// TEXTBEFORE(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found])
+func (fn *formulaFuncs) TEXTBEFORE(argsList *list.List) formulaArg {
+ return fn.textAfterBefore("TEXTBEFORE", argsList)
+}
+
+// TEXTJOIN function joins together a series of supplied text strings into one
+// combined text string. The user can specify a delimiter to add between the
+// individual text items, if required. The syntax of the function is:
+//
+// TEXTJOIN([delimiter],[ignore_empty],text1,[text2],...)
+func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN requires at least 3 arguments")
+ }
+ if argsList.Len() > 252 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN accepts at most 252 arguments")
+ }
+ delimiter := argsList.Front().Value.(formulaArg)
+ ignoreEmpty := argsList.Front().Next().Value.(formulaArg)
+ if ignoreEmpty.Type != ArgNumber || !ignoreEmpty.Boolean {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ args, ok := textJoin(argsList.Front().Next().Next(), []string{}, ignoreEmpty.Number != 0)
+ if ok.Type != ArgNumber {
+ return ok
+ }
+ result := strings.Join(args, delimiter.Value())
+ if len(result) > TotalCellChars {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("TEXTJOIN function exceeds %d characters", TotalCellChars))
+ }
+ return newStringFormulaArg(result)
+}
+
+// textJoin is an implementation of the formula function TEXTJOIN.
+func textJoin(arg *list.Element, arr []string, ignoreEmpty bool) ([]string, formulaArg) {
+ for arg.Next(); arg != nil; arg = arg.Next() {
+ switch arg.Value.(formulaArg).Type {
+ case ArgError:
+ return arr, arg.Value.(formulaArg)
+ case ArgString, ArgEmpty:
+ val := arg.Value.(formulaArg).Value()
+ if val != "" || !ignoreEmpty {
+ arr = append(arr, val)
+ }
+ case ArgNumber:
+ arr = append(arr, arg.Value.(formulaArg).Value())
+ case ArgMatrix:
+ for _, row := range arg.Value.(formulaArg).Matrix {
+ argList := list.New().Init()
+ for _, ele := range row {
+ argList.PushBack(ele)
+ }
+ if argList.Len() > 0 {
+ args, _ := textJoin(argList.Front(), []string{}, ignoreEmpty)
+ arr = append(arr, args...)
+ }
+ }
+ }
+ }
+ return arr, newBoolFormulaArg(true)
+}
+
+// TRIM removes extra spaces (i.e. all spaces except for single spaces between
+// words or characters) from a supplied text string. The syntax of the
+// function is:
+//
+// TRIM(text)
+func (fn *formulaFuncs) TRIM(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TRIM requires 1 argument")
+ }
+ return newStringFormulaArg(strings.TrimSpace(argsList.Front().Value.(formulaArg).Value()))
+}
+
+// UNICHAR returns the Unicode character that is referenced by the given
+// numeric value. The syntax of the function is:
+//
+// UNICHAR(number)
+func (fn *formulaFuncs) UNICHAR(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "UNICHAR requires 1 argument")
+ }
+ numArg := argsList.Front().Value.(formulaArg).ToNumber()
+ if numArg.Type != ArgNumber {
+ return numArg
+ }
+ if numArg.Number <= 0 || numArg.Number > 55295 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newStringFormulaArg(string(rune(numArg.Number)))
+}
+
+// UNICODE function returns the code point for the first character of a
+// supplied text string. The syntax of the function is:
+//
+// UNICODE(text)
+func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg {
+ return fn.code("UNICODE", argsList)
+}
+
+// UPPER converts all characters in a supplied text string to upper case. The
+// syntax of the function is:
+//
+// UPPER(text)
+func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "UPPER requires 1 argument")
+ }
+ return newStringFormulaArg(strings.ToUpper(argsList.Front().Value.(formulaArg).Value()))
+}
+
+// VALUE function converts a text string into a numeric value. The syntax of
+// the function is:
+//
+// VALUE(text)
+func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "VALUE requires 1 argument")
+ }
+ text := strings.ReplaceAll(argsList.Front().Value.(formulaArg).Value(), ",", "")
+ percent := 1.0
+ if strings.HasSuffix(text, "%") {
+ percent, text = 0.01, strings.TrimSuffix(text, "%")
+ }
+ decimal := big.Float{}
+ if _, ok := decimal.SetString(text); ok {
+ value, _ := decimal.Float64()
+ return newNumberFormulaArg(value * percent)
+ }
+ dateValue, timeValue, errTime := 0.0, 0.0, false
+ if !isDateOnlyFmt(text) {
+ h, m, s, _, _, err := strToTime(text)
+ errTime = err.Type == ArgError
+ if !errTime {
+ timeValue = (float64(h)*3600 + float64(m)*60 + s) / 86400
+ }
+ }
+ y, m, d, _, err := strToDate(text)
+ errDate := err.Type == ArgError
+ if !errDate {
+ dateValue = daysBetween(excelMinTime1900.Unix(), makeDate(y, time.Month(m), d)) + 1
+ }
+ if errTime && errDate {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newNumberFormulaArg(dateValue + timeValue)
+}
+
+// VALUETOTEXT function returns text from any specified value. It passes text
+// values unchanged, and converts non-text values to text.
+//
+// VALUETOTEXT(value,[format])
+func (fn *formulaFuncs) VALUETOTEXT(argsList *list.List) formulaArg {
+ format := prepareToText("VALUETOTEXT", argsList)
+ if format.Type != ArgNumber {
+ return format
+ }
+ cell := argsList.Front().Value.(formulaArg)
+ if num := cell.ToNumber(); num.Type != ArgNumber && format.Number == 1 {
+ return newStringFormulaArg(fmt.Sprintf("\"%s\"", cell.Value()))
+ }
+ return newStringFormulaArg(cell.Value())
+}
+
+// Conditional Functions
+
+// IF function tests a supplied condition and returns one result if the
+// condition evaluates to TRUE, and another result if the condition evaluates
+// to FALSE. The syntax of the function is:
+//
+// IF(logical_test,value_if_true,value_if_false)
+func (fn *formulaFuncs) IF(argsList *list.List) formulaArg {
+ if argsList.Len() == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IF requires at least 1 argument")
+ }
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IF accepts at most 3 arguments")
+ }
+ token := argsList.Front().Value.(formulaArg)
+ var (
+ cond bool
+ err error
+ result formulaArg
+ )
+ switch token.Type {
+ case ArgString:
+ if cond, err = strconv.ParseBool(token.Value()); err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ case ArgNumber:
+ cond = token.Number == 1
+ }
+
+ if argsList.Len() == 1 {
+ return newBoolFormulaArg(cond)
+ }
+ if cond {
+ value := argsList.Front().Next().Value.(formulaArg)
+ switch value.Type {
+ case ArgNumber:
+ result = value.ToNumber()
+ default:
+ result = newStringFormulaArg(value.Value())
+ }
+ return result
+ }
+ if argsList.Len() == 3 {
+ value := argsList.Back().Value.(formulaArg)
+ switch value.Type {
+ case ArgNumber:
+ result = value.ToNumber()
+ default:
+ result = newStringFormulaArg(value.Value())
+ }
+ }
+ return result
+}
+
+// Lookup and Reference Functions
+
+// ADDRESS function takes a row and a column number and returns a cell
+// reference as a text string. The syntax of the function is:
+//
+// ADDRESS(row_num,column_num,[abs_num],[a1],[sheet_text])
+func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ADDRESS requires at least 2 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ADDRESS requires at most 5 arguments")
+ }
+ rowNum := argsList.Front().Value.(formulaArg).ToNumber()
+ if rowNum.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if rowNum.Number > TotalRows {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ colNum := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if colNum.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ absNum := newNumberFormulaArg(1)
+ if argsList.Len() >= 3 {
+ absNum = argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if absNum.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ if absNum.Number < 1 || absNum.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ a1 := newBoolFormulaArg(true)
+ if argsList.Len() >= 4 {
+ a1 = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool()
+ if a1.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ var sheetText string
+ if argsList.Len() == 5 {
+ sheetText = fmt.Sprintf("%s!", argsList.Back().Value.(formulaArg).Value())
+ }
+ formatter := addressFmtMaps[fmt.Sprintf("%d_%s", int(absNum.Number), a1.Value())]
+ addr, err := formatter(int(colNum.Number), int(rowNum.Number))
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newStringFormulaArg(fmt.Sprintf("%s%s", sheetText, addr))
+}
+
+// ANCHORARRAY function returns the entire spilled range for the dynamic array
+// in cell. The syntax of the function is:
+//
+// ANCHORARRAY(cell)
+func (fn *formulaFuncs) ANCHORARRAY(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ANCHORARRAY requires 1 numeric argument")
+ }
+ ws, err := fn.f.workSheetReader(fn.sheet)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ ref := argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef)
+ cell := ws.SheetData.Row[ref.Row-1].C[ref.Col-1]
+ if cell.F == nil {
+ return newEmptyFormulaArg()
+ }
+ coordinates, err := rangeRefToCoordinates(cell.F.Ref)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ _ = sortCoordinates(coordinates)
+ var mtx [][]formulaArg
+ for c := coordinates[0]; c <= coordinates[2]; c++ {
+ var row []formulaArg
+ for r := coordinates[1]; r <= coordinates[3]; r++ {
+ cellName, _ := CoordinatesToCellName(c, r)
+ result, err := fn.f.CalcCellValue(ref.Sheet, cellName, Options{RawCellValue: true})
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, err.Error())
+ }
+ arg := newStringFormulaArg(result)
+ if num := arg.ToNumber(); num.Type == ArgNumber {
+ arg = num
+ }
+ row = append(row, arg)
+ }
+ mtx = append(mtx, row)
+ }
+ return newMatrixFormulaArg(mtx)
+}
+
+// CHOOSE function returns a value from an array, that corresponds to a
+// supplied index number (position). The syntax of the function is:
+//
+// CHOOSE(index_num,value1,[value2],...)
+func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires 2 arguments")
+ }
+ idx, err := strconv.Atoi(argsList.Front().Value.(formulaArg).Value())
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires first argument of type number")
+ }
+ if argsList.Len() <= idx {
+ return newErrorFormulaArg(formulaErrorVALUE, "index_num should be <= to the number of values")
+ }
+ arg := argsList.Front()
+ for i := 0; i < idx; i++ {
+ arg = arg.Next()
+ }
+ return arg.Value.(formulaArg)
+}
+
+// matchPatternToRegExp convert find text pattern to regular expression.
+func matchPatternToRegExp(findText string, dbcs bool) (string, bool) {
+ var (
+ exp string
+ wildCard bool
+ mark = "."
+ )
+ if dbcs {
+ mark = "(?:(?:[\\x00-\\x0081])|(?:[\\xFF61-\\xFFA0])|(?:[\\xF8F1-\\xF8F4])|[0-9A-Za-z])"
+ }
+ for _, char := range findText {
+ if strings.ContainsAny(string(char), ".+$^[](){}|/") {
+ exp += fmt.Sprintf("\\%s", string(char))
+ continue
+ }
+ if char == '?' {
+ wildCard = true
+ exp += mark
+ continue
+ }
+ if char == '*' {
+ wildCard = true
+ exp += ".*"
+ continue
+ }
+ exp += string(char)
+ }
+ return fmt.Sprintf("^%s", exp), wildCard
+}
+
+// matchPattern finds whether the text matches or satisfies the pattern
+// string. The pattern supports '*' and '?' wildcards in the pattern string.
+func matchPattern(findText, withinText string, dbcs bool, startNum int) (int, bool) {
+ exp, wildCard := matchPatternToRegExp(findText, dbcs)
+ offset := 1
+ for idx := range withinText {
+ if offset < startNum {
+ offset++
+ continue
+ }
+ if wildCard {
+ if ok, _ := regexp.MatchString(exp, withinText[idx:]); ok {
+ break
+ }
+ }
+ if strings.Index(withinText[idx:], findText) == 0 {
+ break
+ }
+ offset++
+ }
+ return offset, utf8.RuneCountInString(withinText) != offset-1
+}
+
+// compareFormulaArg compares the left-hand sides and the right-hand sides'
+// formula arguments by given conditions such as case-sensitive, if exact
+// match, and make compare result as formula criteria condition type.
+func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte {
+ if lhs.Type != rhs.Type {
+ return criteriaNe
+ }
+ switch lhs.Type {
+ case ArgNumber:
+ if lhs.Number == rhs.Number {
+ return criteriaEq
+ }
+ if lhs.Number < rhs.Number {
+ return criteriaL
+ }
+ return criteriaG
+ case ArgString:
+ ls, rs := lhs.Value(), rhs.Value()
+ if !caseSensitive {
+ ls, rs = strings.ToLower(ls), strings.ToLower(rs)
+ }
+ if matchMode.Number == matchModeWildcard {
+ if _, ok := matchPattern(rs, ls, false, 0); ok {
+ return criteriaEq
+ }
+ }
+ return map[int]byte{1: criteriaG, -1: criteriaL, 0: criteriaEq}[strings.Compare(ls, rs)]
+ case ArgEmpty:
+ return criteriaEq
+ case ArgList:
+ return compareFormulaArgList(lhs, rhs, matchMode, caseSensitive)
+ case ArgMatrix:
+ return compareFormulaArgMatrix(lhs, rhs, matchMode, caseSensitive)
+ default:
+ return criteriaErr
+ }
+}
+
+// compareFormulaArgList compares the left-hand sides and the right-hand sides
+// list type formula arguments.
+func compareFormulaArgList(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte {
+ if len(lhs.List) < len(rhs.List) {
+ return criteriaL
+ }
+ if len(lhs.List) > len(rhs.List) {
+ return criteriaG
+ }
+ for arg := range lhs.List {
+ criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], matchMode, caseSensitive)
+ if criteria != criteriaEq {
+ return criteria
+ }
+ }
+ return criteriaEq
+}
+
+// compareFormulaArgMatrix compares the left-hand sides and the right-hand sides'
+// matrix type formula arguments.
+func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte {
+ if len(lhs.Matrix) < len(rhs.Matrix) {
+ return criteriaL
+ }
+ if len(lhs.Matrix) > len(rhs.Matrix) {
+ return criteriaG
+ }
+ for i := range lhs.Matrix {
+ left, right := lhs.Matrix[i], rhs.Matrix[i]
+ if len(left) < len(right) {
+ return criteriaL
+ }
+ if len(left) > len(right) {
+ return criteriaG
+ }
+ for arg := range left {
+ criteria := compareFormulaArg(left[arg], right[arg], matchMode, caseSensitive)
+ if criteria != criteriaEq {
+ return criteria
+ }
+ }
+ }
+ return criteriaEq
+}
+
+// COLUMN function returns the first column number within a supplied reference
+// or the number of the current column. The syntax of the function is:
+//
+// COLUMN([reference])
+func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg {
+ if argsList.Len() > 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COLUMN requires at most 1 argument")
+ }
+ if argsList.Len() == 1 {
+ if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 {
+ return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRanges.Front().Value.(cellRange).From.Col))
+ }
+ if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 {
+ return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef).Col))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, "invalid reference")
+ }
+ col, _, _ := CellNameToCoordinates(fn.cell)
+ return newNumberFormulaArg(float64(col))
+}
+
+// calcColsRowsMinMax calculation min and max value for given formula arguments
+// sequence of the formula functions COLUMNS and ROWS.
+func calcColsRowsMinMax(cols bool, argsList *list.List) (minVal, maxVal int) {
+ getVal := func(cols bool, cell cellRef) int {
+ if cols {
+ return cell.Col
+ }
+ return cell.Row
+ }
+ if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 {
+ crs := argsList.Front().Value.(formulaArg).cellRanges
+ for cr := crs.Front(); cr != nil; cr = cr.Next() {
+ if minVal == 0 {
+ minVal = getVal(cols, cr.Value.(cellRange).From)
+ }
+ if maxVal < getVal(cols, cr.Value.(cellRange).To) {
+ maxVal = getVal(cols, cr.Value.(cellRange).To)
+ }
+ }
+ }
+ if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 {
+ cr := argsList.Front().Value.(formulaArg).cellRefs
+ for refs := cr.Front(); refs != nil; refs = refs.Next() {
+ if minVal == 0 {
+ minVal = getVal(cols, refs.Value.(cellRef))
+ }
+ if maxVal < getVal(cols, refs.Value.(cellRef)) {
+ maxVal = getVal(cols, refs.Value.(cellRef))
+ }
+ }
+ }
+ return
+}
+
+// COLUMNS function receives an Excel range and returns the number of columns
+// that are contained within the range. The syntax of the function is:
+//
+// COLUMNS(array)
+func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument")
+ }
+ minVal, maxVal := calcColsRowsMinMax(true, argsList)
+ if maxVal == MaxColumns {
+ return newNumberFormulaArg(float64(MaxColumns))
+ }
+ result := maxVal - minVal + 1
+ if maxVal == minVal {
+ if minVal == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "invalid reference")
+ }
+ return newNumberFormulaArg(float64(1))
+ }
+ return newNumberFormulaArg(float64(result))
+}
+
+// FORMULATEXT function returns a formula as a text string. The syntax of the
+// function is:
+//
+// FORMULATEXT(reference)
+func (fn *formulaFuncs) FORMULATEXT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FORMULATEXT requires 1 argument")
+ }
+ refs := argsList.Front().Value.(formulaArg).cellRefs
+ col, row := 0, 0
+ if refs != nil && refs.Len() > 0 {
+ col, row = refs.Front().Value.(cellRef).Col, refs.Front().Value.(cellRef).Row
+ }
+ ranges := argsList.Front().Value.(formulaArg).cellRanges
+ if ranges != nil && ranges.Len() > 0 {
+ col, row = ranges.Front().Value.(cellRange).From.Col, ranges.Front().Value.(cellRange).From.Row
+ }
+ cell, err := CoordinatesToCellName(col, row)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ formula, _ := fn.f.GetCellFormula(fn.sheet, cell)
+ return newStringFormulaArg(formula)
+}
+
+// checkHVLookupArgs checking arguments, prepare extract mode, lookup value,
+// and data for the formula functions HLOOKUP and VLOOKUP.
+func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, tableArray, matchMode, errArg formulaArg) {
+ unit := map[string]string{
+ "HLOOKUP": "row",
+ "VLOOKUP": "col",
+ }[name]
+ if argsList.Len() < 3 {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 3 arguments", name))
+ return
+ }
+ if argsList.Len() > 4 {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at most 4 arguments", name))
+ return
+ }
+ lookupValue = argsList.Front().Value.(formulaArg)
+ tableArray = argsList.Front().Next().Value.(formulaArg)
+ if tableArray.Type != ArgMatrix {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires second argument of table array", name))
+ return
+ }
+ arg := argsList.Front().Next().Next().Value.(formulaArg)
+ if arg.Type != ArgNumber || arg.Boolean {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit))
+ return
+ }
+ idx, matchMode = int(arg.Number)-1, newNumberFormulaArg(matchModeMaxLess)
+ if argsList.Len() == 4 {
+ rangeLookup := argsList.Back().Value.(formulaArg).ToBool()
+ if rangeLookup.Type == ArgError {
+ errArg = rangeLookup
+ return
+ }
+ if rangeLookup.Number == 0 {
+ matchMode = newNumberFormulaArg(matchModeWildcard)
+ }
+ }
+ return
+}
+
+// HLOOKUP function 'looks up' a given value in the top row of a data array
+// (or table), and returns the corresponding value from another row of the
+// array. The syntax of the function is:
+//
+// HLOOKUP(lookup_value,table_array,row_index_num,[range_lookup])
+func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg {
+ rowIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("HLOOKUP", argsList)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ var matchIdx int
+ var wasExact bool
+ if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows {
+ matchIdx, wasExact = lookupLinearSearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear))
+ } else {
+ matchIdx, wasExact = lookupBinarySearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary))
+ }
+ if matchIdx == -1 {
+ return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found")
+ }
+ if rowIdx < 0 || rowIdx >= len(tableArray.Matrix) {
+ return newErrorFormulaArg(formulaErrorNA, "HLOOKUP has invalid row index")
+ }
+ row := tableArray.Matrix[rowIdx]
+ if wasExact || matchMode.Number == matchModeWildcard {
+ return row[matchIdx]
+ }
+ return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found")
+}
+
+// HYPERLINK function creates a hyperlink to a specified location. The syntax
+// of the function is:
+//
+// HYPERLINK(link_location,[friendly_name])
+func (fn *formulaFuncs) HYPERLINK(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HYPERLINK requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "HYPERLINK allows at most 2 arguments")
+ }
+ return newStringFormulaArg(argsList.Back().Value.(formulaArg).Value())
+}
+
+// calcMatch returns the position of the value by given match type, criteria
+// and lookup array for the formula function MATCH.
+func calcMatch(matchType int, criteria *formulaCriteria, lookupArray []formulaArg) formulaArg {
+ idx := -1
+ switch matchType {
+ case 0:
+ for i, arg := range lookupArray {
+ if ok, _ := formulaCriteriaEval(arg, criteria); ok {
+ return newNumberFormulaArg(float64(i + 1))
+ }
+ }
+ case -1:
+ for i, arg := range lookupArray {
+ if ok, _ := formulaCriteriaEval(arg, &formulaCriteria{
+ Type: criteriaGe, Condition: criteria.Condition,
+ }); ok {
+ idx = i
+ continue
+ }
+ if criteria.Condition.Type == ArgNumber {
+ break
+ }
+ }
+ case 1:
+ for i, arg := range lookupArray {
+ if ok, _ := formulaCriteriaEval(arg, &formulaCriteria{
+ Type: criteriaLe, Condition: criteria.Condition,
+ }); ok {
+ idx = i
+ continue
+ }
+ if criteria.Condition.Type == ArgNumber {
+ break
+ }
+ }
+ }
+ if idx == -1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ return newNumberFormulaArg(float64(idx + 1))
+}
+
+// MATCH function looks up a value in an array, and returns the position of
+// the value within the array. The user can specify that the function should
+// only return a result if an exact match is found, or that the function
+// should return the position of the closest match (above or below), if an
+// exact match is not found. The syntax of the Match function is:
+//
+// MATCH(lookup_value,lookup_array,[match_type])
+func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 && argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MATCH requires 1 or 2 arguments")
+ }
+ var (
+ matchType = 1
+ lookupArray []formulaArg
+ lookupArrayArg = argsList.Front().Next().Value.(formulaArg)
+ lookupArrayErr = "MATCH arguments lookup_array should be one-dimensional array"
+ )
+ if argsList.Len() == 3 {
+ matchTypeArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if matchTypeArg.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, "MATCH requires numeric match_type argument")
+ }
+ if matchTypeArg.Number == -1 || matchTypeArg.Number == 0 {
+ matchType = int(matchTypeArg.Number)
+ }
+ }
+ switch lookupArrayArg.Type {
+ case ArgMatrix:
+ if len(lookupArrayArg.Matrix) != 1 && len(lookupArrayArg.Matrix[0]) != 1 {
+ return newErrorFormulaArg(formulaErrorNA, lookupArrayErr)
+ }
+ lookupArray = lookupArrayArg.ToList()
+ default:
+ return newErrorFormulaArg(formulaErrorNA, lookupArrayErr)
+ }
+ return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg)), lookupArray)
+}
+
+// TRANSPOSE function 'transposes' an array of cells (i.e. the function copies
+// a horizontal range of cells into a vertical range and vice versa). The
+// syntax of the function is:
+//
+// TRANSPOSE(array)
+func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TRANSPOSE requires 1 argument")
+ }
+ args := argsList.Back().Value.(formulaArg).ToList()
+ rmin, rmax := calcColsRowsMinMax(false, argsList)
+ cmin, cmax := calcColsRowsMinMax(true, argsList)
+ cols, rows := cmax-cmin+1, rmax-rmin+1
+ src := make([][]formulaArg, 0)
+ for i := 0; i < len(args); i += cols {
+ src = append(src, args[i:i+cols])
+ }
+ mtx := make([][]formulaArg, cols)
+ for r, row := range src {
+ colIdx := r % rows
+ for c, cell := range row {
+ rowIdx := c % cols
+ if len(mtx[rowIdx]) == 0 {
+ mtx[rowIdx] = make([]formulaArg, rows)
+ }
+ mtx[rowIdx][colIdx] = cell
+ }
+ }
+ return newMatrixFormulaArg(mtx)
+}
+
+// lookupLinearSearch sequentially checks each look value of the lookup array until
+// a match is found or the whole list has been searched.
+func lookupLinearSearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (int, bool) {
+ var tableArray []formulaArg
+ if vertical {
+ for _, row := range lookupArray.Matrix {
+ tableArray = append(tableArray, row[0])
+ }
+ } else {
+ tableArray = lookupArray.Matrix[0]
+ }
+ matchIdx, wasExact := -1, false
+start:
+ for i, cell := range tableArray {
+ lhs := cell
+ if lookupValue.Type == ArgNumber {
+ if lhs = cell.ToNumber(); lhs.Type == ArgError {
+ lhs = cell
+ }
+ } else if lookupValue.Type == ArgMatrix {
+ lhs = lookupArray
+ } else if lookupArray.Type == ArgString {
+ lhs = newStringFormulaArg(cell.Value())
+ }
+ if compareFormulaArg(lhs, lookupValue, matchMode, false) == criteriaEq {
+ matchIdx = i
+ wasExact = true
+ if searchMode.Number == searchModeLinear {
+ break start
+ }
+ }
+ if matchMode.Number == matchModeMinGreater || matchMode.Number == matchModeMaxLess {
+ matchIdx = int(calcMatch(int(matchMode.Number), formulaCriteriaParser(lookupValue), tableArray).Number)
+ continue
+ }
+ }
+ return matchIdx, wasExact
+}
+
+// VLOOKUP function 'looks up' a given value in the left-hand column of a
+// data array (or table), and returns the corresponding value from another
+// column of the array. The syntax of the function is:
+//
+// VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup])
+func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg {
+ colIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("VLOOKUP", argsList)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ var matchIdx int
+ var wasExact bool
+ if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows {
+ matchIdx, wasExact = lookupLinearSearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear))
+ } else {
+ matchIdx, wasExact = lookupBinarySearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary))
+ }
+ if matchIdx == -1 {
+ return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
+ }
+ mtx := tableArray.Matrix[matchIdx]
+ if colIdx < 0 || colIdx >= len(mtx) {
+ return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index")
+ }
+ if wasExact || matchMode.Number == matchModeWildcard {
+ return mtx[colIdx]
+ }
+ return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
+}
+
+// lookupBinarySearch finds the position of a target value when range lookup
+// is TRUE, if the data of table array can't guarantee be sorted, it will
+// return wrong result.
+func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (matchIdx int, wasExact bool) {
+ var tableArray []formulaArg
+ if vertical {
+ for _, row := range lookupArray.Matrix {
+ tableArray = append(tableArray, row[0])
+ }
+ } else {
+ tableArray = lookupArray.Matrix[0]
+ }
+ low, high, lastMatchIdx := 0, len(tableArray)-1, -1
+ count := high
+ for low <= high {
+ mid := low + (high-low)/2
+ cell := tableArray[mid]
+ lhs := cell
+ if lookupValue.Type == ArgNumber {
+ if lhs = cell.ToNumber(); lhs.Type == ArgError {
+ lhs = cell
+ }
+ } else if lookupValue.Type == ArgMatrix && vertical {
+ lhs = lookupArray
+ } else if lookupValue.Type == ArgString {
+ lhs = newStringFormulaArg(cell.Value())
+ }
+ result := compareFormulaArg(lhs, lookupValue, matchMode, false)
+ if result == criteriaEq {
+ matchIdx, wasExact = mid, true
+ if searchMode.Number == searchModeDescBinary {
+ matchIdx = count - matchIdx
+ }
+ return
+ } else if result == criteriaG {
+ high = mid - 1
+ } else if result == criteriaL {
+ matchIdx = mid
+ if cell.Type != ArgEmpty {
+ lastMatchIdx = matchIdx
+ }
+ low = mid + 1
+ } else {
+ return -1, false
+ }
+ }
+ matchIdx, wasExact = lastMatchIdx, true
+ return
+}
+
+// checkLookupArgs checking arguments, prepare lookup value, and data for the
+// formula function LOOKUP.
+func checkLookupArgs(argsList *list.List) (arrayForm bool, lookupValue, lookupVector, errArg formulaArg) {
+ if argsList.Len() < 2 {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at least 2 arguments")
+ return
+ }
+ if argsList.Len() > 3 {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments")
+ return
+ }
+ lookupValue = newStringFormulaArg(argsList.Front().Value.(formulaArg).Value())
+ lookupVector = argsList.Front().Next().Value.(formulaArg)
+ if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array")
+ return
+ }
+ arrayForm = lookupVector.Type == ArgMatrix
+ if arrayForm && len(lookupVector.Matrix) == 0 {
+ errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires not empty range as second argument")
+ }
+ return
+}
+
+// iterateLookupArgs iterate arguments to extract columns and calculate match
+// index for the formula function LOOKUP.
+func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, bool) {
+ cols, matchIdx, ok := lookupCol(lookupVector, 0), -1, false
+ for idx, col := range cols {
+ lhs := lookupValue
+ switch col.Type {
+ case ArgNumber:
+ lhs = lhs.ToNumber()
+ if !col.Boolean {
+ if lhs.Type == ArgError {
+ lhs = lookupValue
+ }
+ }
+ }
+ compare := compareFormulaArg(lhs, col, newNumberFormulaArg(matchModeMaxLess), false)
+ // Find exact match
+ if compare == criteriaEq {
+ matchIdx = idx
+ break
+ }
+ // Find the nearest match if lookup value is more than or equal to the first value in lookup vector
+ if idx == 0 {
+ ok = compare == criteriaG
+ } else if ok && compare == criteriaL && matchIdx == -1 {
+ matchIdx = idx - 1
+ }
+ }
+ return cols, matchIdx, ok
+}
+
+// index is an implementation of the formula function INDEX.
+func (fn *formulaFuncs) index(array formulaArg, rowIdx, colIdx int) formulaArg {
+ var cells []formulaArg
+ if array.Type == ArgMatrix {
+ cellMatrix := array.Matrix
+ if rowIdx < -1 || rowIdx >= len(cellMatrix) {
+ return newErrorFormulaArg(formulaErrorREF, "INDEX row_num out of range")
+ }
+ if rowIdx == -1 {
+ if colIdx >= len(cellMatrix[0]) {
+ return newErrorFormulaArg(formulaErrorREF, "INDEX col_num out of range")
+ }
+ var column [][]formulaArg
+ for _, cells = range cellMatrix {
+ column = append(column, []formulaArg{cells[colIdx]})
+ }
+ return newMatrixFormulaArg(column)
+ }
+ cells = cellMatrix[rowIdx]
+ }
+ if colIdx < -1 || colIdx >= len(cells) {
+ return newErrorFormulaArg(formulaErrorREF, "INDEX col_num out of range")
+ }
+ return newListFormulaArg(cells)
+}
+
+// validateMatchMode check the number of match mode if be equal to 0, 1, -1 or
+// 2.
+func validateMatchMode(mode float64) bool {
+ return mode == matchModeExact || mode == matchModeMinGreater || mode == matchModeMaxLess || mode == matchModeWildcard
+}
+
+// validateSearchMode check the number of search mode if be equal to 1, -1, 2
+// or -2.
+func validateSearchMode(mode float64) bool {
+ return mode == searchModeLinear || mode == searchModeReverseLinear || mode == searchModeAscBinary || mode == searchModeDescBinary
+}
+
+// prepareXlookupArgs checking and prepare arguments for the formula function
+// XLOOKUP.
+func (fn *formulaFuncs) prepareXlookupArgs(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP requires at least 3 arguments")
+ }
+ if argsList.Len() > 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP allows at most 6 arguments")
+ }
+ lookupValue := argsList.Front().Value.(formulaArg)
+ lookupArray := argsList.Front().Next().Value.(formulaArg)
+ returnArray := argsList.Front().Next().Next().Value.(formulaArg)
+ ifNotFond := newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ matchMode, searchMode := newNumberFormulaArg(matchModeExact), newNumberFormulaArg(searchModeLinear)
+ if argsList.Len() > 3 {
+ ifNotFond = argsList.Front().Next().Next().Next().Value.(formulaArg)
+ }
+ if argsList.Len() > 4 {
+ if matchMode = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); matchMode.Type != ArgNumber {
+ return matchMode
+ }
+ }
+ if argsList.Len() > 5 {
+ if searchMode = argsList.Back().Value.(formulaArg).ToNumber(); searchMode.Type != ArgNumber {
+ return searchMode
+ }
+ }
+ if lookupArray.Type != ArgMatrix || returnArray.Type != ArgMatrix {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if !validateMatchMode(matchMode.Number) || !validateSearchMode(searchMode.Number) {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return newListFormulaArg([]formulaArg{lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode})
+}
+
+// xlookup is an implementation of the formula function XLOOKUP.
+func (fn *formulaFuncs) xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx int,
+ condition1, condition2, condition3, condition4 bool, returnArray formulaArg,
+) formulaArg {
+ var result [][]formulaArg
+ for rowIdx, row := range returnArray.Matrix {
+ for colIdx, cell := range row {
+ if condition1 {
+ if condition2 {
+ result = append(result, []formulaArg{cell})
+ continue
+ }
+ if returnArrayRows > 1 && returnArrayCols > 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ if condition3 {
+ if returnArrayCols != lookupCols {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if colIdx == matchIdx {
+ result = append(result, []formulaArg{cell})
+ continue
+ }
+ }
+ if condition4 {
+ if returnArrayRows != lookupRows {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if rowIdx == matchIdx {
+ if len(result) == 0 {
+ result = append(result, []formulaArg{cell})
+ continue
+ }
+ result[0] = append(result[0], cell)
+ }
+ }
+ }
+ }
+ array := newMatrixFormulaArg(result)
+ cells := array.ToList()
+ if len(cells) == 1 {
+ return cells[0]
+ }
+ return array
+}
+
+// XLOOKUP function searches a range or an array, and then returns the item
+// corresponding to the first match it finds. If no match exists, then
+// XLOOKUP can return the closest (approximate) match. The syntax of the
+// function is:
+//
+// XLOOKUP(lookup_value,lookup_array,return_array,[if_not_found],[match_mode],[search_mode])
+func (fn *formulaFuncs) XLOOKUP(argsList *list.List) formulaArg {
+ args := fn.prepareXlookupArgs(argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5]
+ lookupRows, lookupCols := len(lookupArray.Matrix), 0
+ if lookupRows > 0 {
+ lookupCols = len(lookupArray.Matrix[0])
+ }
+ if lookupRows != 1 && lookupCols != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ verticalLookup := lookupRows >= lookupCols
+ var matchIdx int
+ switch searchMode.Number {
+ case searchModeLinear, searchModeReverseLinear:
+ matchIdx, _ = lookupLinearSearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode)
+ default:
+ matchIdx, _ = lookupBinarySearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode)
+ }
+ if matchIdx == -1 {
+ return ifNotFond
+ }
+ returnArrayRows, returnArrayCols := len(returnArray.Matrix), len(returnArray.Matrix[0])
+ condition1 := lookupRows == 1 && lookupCols == 1
+ condition2 := returnArrayRows == 1 || returnArrayCols == 1
+ condition3 := lookupRows == 1 && lookupCols > 1
+ condition4 := lookupRows > 1 && lookupCols == 1
+ return fn.xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx, condition1, condition2, condition3, condition4, returnArray)
+}
+
+// INDEX function returns a reference to a cell that lies in a specified row
+// and column of a range of cells. The syntax of the function is:
+//
+// INDEX(array,row_num,[col_num])
+func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 || argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "INDEX requires 2 or 3 arguments")
+ }
+ array := argsList.Front().Value.(formulaArg)
+ if array.Type != ArgMatrix && array.Type != ArgList {
+ array = newMatrixFormulaArg([][]formulaArg{{array}})
+ }
+ rowArg := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if rowArg.Type != ArgNumber {
+ return rowArg
+ }
+ rowIdx, colIdx := int(rowArg.Number)-1, -1
+ if argsList.Len() == 3 {
+ colArg := argsList.Back().Value.(formulaArg).ToNumber()
+ if colArg.Type != ArgNumber {
+ return colArg
+ }
+ colIdx = int(colArg.Number) - 1
+ }
+ if rowIdx == -1 && colIdx == -1 {
+ if len(array.ToList()) != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return array.ToList()[0]
+ }
+ cells := fn.index(array, rowIdx, colIdx)
+ if cells.Type != ArgList {
+ return cells
+ }
+ if colIdx == -1 {
+ return newMatrixFormulaArg([][]formulaArg{cells.List})
+ }
+ return cells.List[colIdx]
+}
+
+// INDIRECT function converts a text string into a cell reference. The syntax
+// of the Indirect function is:
+//
+// INDIRECT(ref_text,[a1])
+func (fn *formulaFuncs) INDIRECT(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 && argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "INDIRECT requires 1 or 2 arguments")
+ }
+ refText := argsList.Front().Value.(formulaArg).Value()
+ a1 := newBoolFormulaArg(true)
+ if argsList.Len() == 2 {
+ if a1 = argsList.Back().Value.(formulaArg).ToBool(); a1.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ R1C1ToA1 := func(ref string) (cell string, err error) {
+ parts := strings.Split(strings.TrimLeft(ref, "R"), "C")
+ if len(parts) != 2 {
+ return
+ }
+ row, err := strconv.Atoi(parts[0])
+ if err != nil {
+ return
+ }
+ col, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return
+ }
+ cell, err = CoordinatesToCellName(col, row)
+ return
+ }
+ refs := strings.Split(refText, ":")
+ fromRef, toRef := refs[0], ""
+ if len(refs) == 2 {
+ toRef = refs[1]
+ }
+ if a1.Number == 0 {
+ from, err := R1C1ToA1(refs[0])
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ }
+ fromRef = from
+ if len(refs) == 2 {
+ to, err := R1C1ToA1(refs[1])
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ }
+ toRef = to
+ }
+ }
+ if len(refs) == 1 {
+ value, err := fn.f.GetCellValue(fn.sheet, fromRef)
+ if err != nil {
+ return newErrorFormulaArg(formulaErrorREF, formulaErrorREF)
+ }
+ return newStringFormulaArg(value)
+ }
+ arg, _ := fn.f.parseReference(fn.ctx, fn.sheet, fromRef+":"+toRef)
+ return arg
+}
+
+// LOOKUP function performs an approximate match lookup in a one-column or
+// one-row range, and returns the corresponding value from another one-column
+// or one-row range. The syntax of the function is:
+//
+// LOOKUP(lookup_value,lookup_vector,[result_vector])
+func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg {
+ arrayForm, lookupValue, lookupVector, errArg := checkLookupArgs(argsList)
+ if errArg.Type == ArgError {
+ return errArg
+ }
+ cols, matchIdx, ok := iterateLookupArgs(lookupValue, lookupVector)
+ if ok && matchIdx == -1 {
+ matchIdx = len(cols) - 1
+ }
+ var column []formulaArg
+ if argsList.Len() == 3 {
+ column = lookupCol(argsList.Back().Value.(formulaArg), 0)
+ } else if arrayForm && len(lookupVector.Matrix[0]) > 1 {
+ column = lookupCol(lookupVector, 1)
+ } else {
+ column = cols
+ }
+ if matchIdx < 0 || matchIdx >= len(column) {
+ return newErrorFormulaArg(formulaErrorNA, "LOOKUP no result found")
+ }
+ return column[matchIdx]
+}
+
+// lookupCol extract columns for LOOKUP.
+func lookupCol(arr formulaArg, idx int) []formulaArg {
+ col := arr.List
+ if arr.Type == ArgMatrix {
+ col = nil
+ for _, r := range arr.Matrix {
+ if len(r) > 0 {
+ col = append(col, r[idx])
+ continue
+ }
+ col = append(col, newEmptyFormulaArg())
+ }
+ }
+ return col
+}
+
+// ROW function returns the first row number within a supplied reference or
+// the number of the current row. The syntax of the function is:
+//
+// ROW([reference])
+func (fn *formulaFuncs) ROW(argsList *list.List) formulaArg {
+ if argsList.Len() > 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROW requires at most 1 argument")
+ }
+ if argsList.Len() == 1 {
+ if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 {
+ return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRanges.Front().Value.(cellRange).From.Row))
+ }
+ if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 {
+ return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef).Row))
+ }
+ return newErrorFormulaArg(formulaErrorVALUE, "invalid reference")
+ }
+ _, row, _ := CellNameToCoordinates(fn.cell)
+ return newNumberFormulaArg(float64(row))
+}
+
+// ROWS function takes an Excel range and returns the number of rows that are
+// contained within the range. The syntax of the function is:
+//
+// ROWS(array)
+func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument")
+ }
+ minVal, maxVal := calcColsRowsMinMax(false, argsList)
+ if maxVal == TotalRows {
+ return newNumberFormulaArg(TotalRows)
+ }
+ result := maxVal - minVal + 1
+ if maxVal == minVal {
+ if minVal == 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "invalid reference")
+ }
+ return newNumberFormulaArg(float64(1))
+ }
+ return newNumberFormulaArg(float64(result))
+}
+
+// Web Functions
+
+// ENCODEURL function returns a URL-encoded string, replacing certain
+// non-alphanumeric characters with the percentage symbol (%) and a
+// hexadecimal number. The syntax of the function is:
+//
+// ENCODEURL(url)
+func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg {
+ if argsList.Len() != 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ENCODEURL requires 1 argument")
+ }
+ token := argsList.Front().Value.(formulaArg).Value()
+ return newStringFormulaArg(strings.ReplaceAll(url.QueryEscape(token), "+", "%20"))
+}
+
+// Financial Functions
+
+// validateFrequency check the number of coupon payments per year if be equal to 1, 2 or 4.
+func validateFrequency(freq float64) bool {
+ return freq == 1 || freq == 2 || freq == 4
+}
+
+// ACCRINT function returns the accrued interest in a security that pays
+// periodic interest. The syntax of the function is:
+//
+// ACCRINT(issue,first_interest,settlement,rate,par,frequency,[basis],[calc_method])
+func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg {
+ if argsList.Len() < 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACCRINT requires at least 6 arguments")
+ }
+ if argsList.Len() > 8 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACCRINT allows at most 8 arguments")
+ }
+ args := fn.prepareDataValueArgs(3, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ issue, settlement := args.List[0], args.List[2]
+ rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ par := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber || par.Type != ArgNumber || frequency.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() >= 7 {
+ if basis = argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ if argsList.Len() == 8 {
+ if cm := argsList.Back().Value.(formulaArg).ToBool(); cm.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ }
+ frac1 := yearFrac(issue.Number, settlement.Number, int(basis.Number))
+ if frac1.Type != ArgNumber {
+ return frac1
+ }
+ return newNumberFormulaArg(par.Number * rate.Number * frac1.Number)
+}
+
+// ACCRINTM function returns the accrued interest in a security that pays
+// interest at maturity. The syntax of the function is:
+//
+// ACCRINTM(issue,settlement,rate,[par],[basis])
+func (fn *formulaFuncs) ACCRINTM(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 && argsList.Len() != 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ACCRINTM requires 4 or 5 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ issue, settlement := args.List[0], args.List[1]
+ if settlement.Number < issue.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ par := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber || par.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if par.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ frac := yearFrac(issue.Number, settlement.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ return newNumberFormulaArg(frac.Number * rate.Number * par.Number)
+}
+
+// prepareAmorArgs checking and prepare arguments for the formula functions
+// AMORDEGRC and AMORLINC.
+func (fn *formulaFuncs) prepareAmorArgs(name string, argsList *list.List) formulaArg {
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires cost to be number argument", name))
+ }
+ if cost.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires cost >= 0", name))
+ }
+ args := list.New().Init()
+ args.PushBack(argsList.Front().Next().Value.(formulaArg))
+ datePurchased := fn.DATEVALUE(args)
+ if datePurchased.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ args.Init()
+ args.PushBack(argsList.Front().Next().Next().Value.(formulaArg))
+ firstPeriod := fn.DATEVALUE(args)
+ if firstPeriod.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if firstPeriod.Number < datePurchased.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ salvage := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if salvage.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if salvage.Number < 0 || salvage.Number > cost.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ period := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if period.Type != ArgNumber || period.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ rate := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber || rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 7 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ return newListFormulaArg([]formulaArg{cost, datePurchased, firstPeriod, salvage, period, rate, basis})
+}
+
+// AMORDEGRC function is provided for users of the French accounting system.
+// The function calculates the prorated linear depreciation of an asset for a
+// specified accounting period. The syntax of the function is:
+//
+// AMORDEGRC(cost,date_purchased,first_period,salvage,period,rate,[basis])
+func (fn *formulaFuncs) AMORDEGRC(argsList *list.List) formulaArg {
+ if argsList.Len() != 6 && argsList.Len() != 7 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AMORDEGRC requires 6 or 7 arguments")
+ }
+ args := fn.prepareAmorArgs("AMORDEGRC", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ cost, datePurchased, firstPeriod, salvage, period, rate, basis := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6]
+ if rate.Number >= 0.5 {
+ return newErrorFormulaArg(formulaErrorNUM, "AMORDEGRC requires rate to be < 0.5")
+ }
+ assetsLife, amorCoeff := 1/rate.Number, 2.5
+ if assetsLife < 3 {
+ amorCoeff = 1
+ } else if assetsLife < 5 {
+ amorCoeff = 1.5
+ } else if assetsLife <= 6 {
+ amorCoeff = 2
+ }
+ rate.Number *= amorCoeff
+ frac := yearFrac(datePurchased.Number, firstPeriod.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ nRate := float64(int((frac.Number * cost.Number * rate.Number) + 0.5))
+ cost.Number -= nRate
+ rest := cost.Number - salvage.Number
+ for n := 0; n < int(period.Number); n++ {
+ nRate = float64(int((cost.Number * rate.Number) + 0.5))
+ rest -= nRate
+ if rest < 0 {
+ switch int(period.Number) - n {
+ case 0:
+ case 1:
+ return newNumberFormulaArg(float64(int((cost.Number * 0.5) + 0.5)))
+ default:
+ return newNumberFormulaArg(0)
+ }
+ }
+ cost.Number -= nRate
+ }
+ return newNumberFormulaArg(nRate)
+}
+
+// AMORLINC function is provided for users of the French accounting system.
+// The function calculates the prorated linear depreciation of an asset for a
+// specified accounting period. The syntax of the function is:
+//
+// AMORLINC(cost,date_purchased,first_period,salvage,period,rate,[basis])
+func (fn *formulaFuncs) AMORLINC(argsList *list.List) formulaArg {
+ if argsList.Len() != 6 && argsList.Len() != 7 {
+ return newErrorFormulaArg(formulaErrorVALUE, "AMORLINC requires 6 or 7 arguments")
+ }
+ args := fn.prepareAmorArgs("AMORLINC", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ cost, datePurchased, firstPeriod, salvage, period, rate, basis := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6]
+ frac := yearFrac(datePurchased.Number, firstPeriod.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ rate1 := frac.Number * cost.Number * rate.Number
+ if period.Number == 0 {
+ return newNumberFormulaArg(rate1)
+ }
+ rate2 := cost.Number * rate.Number
+ delta := cost.Number - salvage.Number
+ periods := int((delta - rate1) / rate2)
+ if int(period.Number) <= periods {
+ return newNumberFormulaArg(rate2)
+ } else if int(period.Number)-1 == periods {
+ return newNumberFormulaArg(delta - rate2*float64(periods) - math.Nextafter(rate1, rate1))
+ }
+ return newNumberFormulaArg(0)
+}
+
+// prepareCouponArgs checking and prepare arguments for the formula functions
+// COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPPCD, COUPNUM and COUPNCD.
+func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 3 && argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 or 4 arguments", name))
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ if settlement.Number >= maturity.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name))
+ }
+ frequency := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if frequency.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 4 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ return newListFormulaArg([]formulaArg{settlement, maturity, frequency, basis})
+}
+
+// is30BasisMethod determine if the financial day count basis rules is 30/360
+// methods.
+func is30BasisMethod(basis int) bool {
+ return basis == 0 || basis == 4
+}
+
+// getDaysInMonthRange return the day by given year, month range and day count
+// basis.
+func getDaysInMonthRange(fromMonth, toMonth int) int {
+ if fromMonth > toMonth {
+ return 0
+ }
+ return (toMonth - fromMonth + 1) * 30
+}
+
+// getDayOnBasis returns the day by given date and day count basis.
+func getDayOnBasis(y, m, d, basis int) int {
+ if !is30BasisMethod(basis) {
+ return d
+ }
+ day := d
+ dim := getDaysInMonth(y, m)
+ if day > 30 || d >= dim || day >= dim {
+ day = 30
+ }
+ return day
+}
+
+// coupdays returns the number of days that base on date range and the day
+// count basis to be used.
+func coupdays(from, to time.Time, basis int) float64 {
+ days := 0
+ fromY, fromM, fromD := from.Date()
+ toY, toM, toD := to.Date()
+ fromDay, toDay := getDayOnBasis(fromY, int(fromM), fromD, basis), getDayOnBasis(toY, int(toM), toD, basis)
+ if !is30BasisMethod(basis) {
+ return (daysBetween(excelMinTime1900.Unix(), makeDate(toY, toM, toDay)) + 1) - (daysBetween(excelMinTime1900.Unix(), makeDate(fromY, fromM, fromDay)) + 1)
+ }
+ if basis == 0 {
+ if (int(fromM) == 2 || fromDay < 30) && toD == 31 {
+ toDay = 31
+ }
+ } else {
+ if int(fromM) == 2 && fromDay == 30 {
+ fromDay = getDaysInMonth(fromY, 2)
+ }
+ if int(toM) == 2 && toDay == 30 {
+ toDay = getDaysInMonth(toY, 2)
+ }
+ }
+ if fromY < toY || (fromY == toY && int(fromM) < int(toM)) {
+ days = 30 - fromDay + 1
+ fromD = 1
+ fromDay = 1
+ date := time.Date(fromY, fromM, fromD, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0)
+ if date.Year() < toY {
+ days += getDaysInMonthRange(int(date.Month()), 12)
+ date = date.AddDate(0, 13-int(date.Month()), 0)
+ }
+ days += getDaysInMonthRange(int(date.Month()), int(toM)-1)
+ }
+ if days += toDay - fromDay; days > 0 {
+ return float64(days)
+ }
+ return 0
}
-// lcm returns the least common multiple of two supplied integers.
-func lcm(a, b float64) float64 {
- a = math.Trunc(a)
- b = math.Trunc(b)
- if a == 0 && b == 0 {
- return 0
+// COUPDAYBS function calculates the number of days from the beginning of a
+// coupon's period to the settlement date. The syntax of the function is:
+//
+// COUPDAYBS(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPDAYBS(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPDAYBS", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement := timeFromExcelTime(args.List[0].Number, false)
+ pcd := timeFromExcelTime(fn.COUPPCD(argsList).Number, false)
+ return newNumberFormulaArg(coupdays(pcd, settlement, int(args.List[3].Number)))
+}
+
+// COUPDAYS function calculates the number of days in a coupon period that
+// contains the settlement date. The syntax of the function is:
+//
+// COUPDAYS(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPDAYS(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPDAYS", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ freq := args.List[2].Number
+ basis := int(args.List[3].Number)
+ if basis == 1 {
+ pcd := timeFromExcelTime(fn.COUPPCD(argsList).Number, false)
+ next := pcd.AddDate(0, 12/int(freq), 0)
+ return newNumberFormulaArg(coupdays(pcd, next, basis))
+ }
+ return newNumberFormulaArg(float64(getYearDays(0, basis)) / freq)
+}
+
+// COUPDAYSNC function calculates the number of days from the settlement date
+// to the next coupon date. The syntax of the function is:
+//
+// COUPDAYSNC(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPDAYSNC(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPDAYSNC", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement := timeFromExcelTime(args.List[0].Number, false)
+ basis := int(args.List[3].Number)
+ ncd := timeFromExcelTime(fn.COUPNCD(argsList).Number, false)
+ return newNumberFormulaArg(coupdays(settlement, ncd, basis))
+}
+
+// coupons is an implementation of the formula functions COUPNCD and COUPPCD.
+func (fn *formulaFuncs) coupons(name string, arg formulaArg) formulaArg {
+ settlement := timeFromExcelTime(arg.List[0].Number, false)
+ maturity := timeFromExcelTime(arg.List[1].Number, false)
+ maturityDays := (maturity.Year()-settlement.Year())*12 + (int(maturity.Month()) - int(settlement.Month()))
+ coupon := 12 / int(arg.List[2].Number)
+ mod := maturityDays % coupon
+ year := settlement.Year()
+ month := int(settlement.Month())
+ if mod == 0 && settlement.Day() >= maturity.Day() {
+ month += coupon
+ } else {
+ month += mod
+ }
+ if name != "COUPNCD" {
+ month -= coupon
+ }
+ if month > 11 {
+ year++
+ month -= 12
+ } else if month < 0 {
+ year--
+ month += 12
+ }
+ day, lastDay := maturity.Day(), time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
+ days := getDaysInMonth(lastDay.Year(), int(lastDay.Month()))
+ if getDaysInMonth(maturity.Year(), int(maturity.Month())) == maturity.Day() {
+ day = days
+ } else if day > 27 && day > days {
+ day = days
+ }
+ return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(year, time.Month(month), day)) + 1)
+}
+
+// COUPNCD function calculates the number of coupons payable, between a
+// security's settlement date and maturity date, rounded up to the nearest
+// whole coupon. The syntax of the function is:
+//
+// COUPNCD(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPNCD(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPNCD", argsList)
+ if args.Type != ArgList {
+ return args
}
- return a * b / gcd(a, b)
+ return fn.coupons("COUPNCD", args)
}
-// LCM function returns the least common multiple of two or more supplied
-// integers. The syntax of the function is:
+// COUPNUM function calculates the number of coupons payable, between a
+// security's settlement date and maturity date, rounded up to the nearest
+// whole coupon. The syntax of the function is:
//
-// LCM(number1,[number2],...)
+// COUPNUM(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPNUM", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ frac := yearFrac(args.List[0].Number, args.List[1].Number, 0)
+ return newNumberFormulaArg(math.Ceil(frac.Number * args.List[2].Number))
+}
+
+// COUPPCD function returns the previous coupon date, before the settlement
+// date for a security. The syntax of the function is:
//
-func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("LCM requires at least 1 argument")
- return
+// COUPPCD(settlement,maturity,frequency,[basis])
+func (fn *formulaFuncs) COUPPCD(argsList *list.List) formulaArg {
+ args := fn.prepareCouponArgs("COUPPCD", argsList)
+ if args.Type != ArgList {
+ return args
}
- var (
- val float64
- nums = []float64{}
- )
- for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg).String
- if token == "" {
+ return fn.coupons("COUPPCD", args)
+}
+
+// CUMIPMT function calculates the cumulative interest paid on a loan or
+// investment, between two specified periods. The syntax of the function is:
+//
+// CUMIPMT(rate,nper,pv,start_period,end_period,type)
+func (fn *formulaFuncs) CUMIPMT(argsList *list.List) formulaArg {
+ return fn.cumip("CUMIPMT", argsList)
+}
+
+// CUMPRINC function calculates the cumulative payment on the principal of a
+// loan or investment, between two specified periods. The syntax of the
+// function is:
+//
+// CUMPRINC(rate,nper,pv,start_period,end_period,type)
+func (fn *formulaFuncs) CUMPRINC(argsList *list.List) formulaArg {
+ return fn.cumip("CUMPRINC", argsList)
+}
+
+// cumip is an implementation of the formula functions CUMIPMT and CUMPRINC.
+func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 6 arguments", name))
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ nper := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ start := argsList.Back().Prev().Prev().Value.(formulaArg).ToNumber()
+ if start.Type != ArgNumber {
+ return start
+ }
+ end := argsList.Back().Prev().Value.(formulaArg).ToNumber()
+ if end.Type != ArgNumber {
+ return end
+ }
+ typ := argsList.Back().Value.(formulaArg).ToNumber()
+ if typ.Type != ArgNumber {
+ return typ
+ }
+ if typ.Number != 0 && typ.Number != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if start.Number < 1 || start.Number > end.Number {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ num := 0.0
+ for per := start.Number; per <= end.Number; per++ {
+ args := list.New().Init()
+ args.PushBack(rate)
+ args.PushBack(newNumberFormulaArg(per))
+ args.PushBack(nper)
+ args.PushBack(pv)
+ args.PushBack(newNumberFormulaArg(0))
+ args.PushBack(typ)
+ if name == "CUMIPMT" {
+ num += fn.IPMT(args).Number
continue
}
- if val, err = strconv.ParseFloat(token, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ num += fn.PPMT(args).Number
+ }
+ return newNumberFormulaArg(num)
+}
+
+// calcDbArgsCompare implements common arguments' comparison for DB and DDB.
+func calcDbArgsCompare(cost, salvage, life, period formulaArg) bool {
+ return (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1)
+}
+
+// DB function calculates the depreciation of an asset, using the Fixed
+// Declining Balance Method, for each period of the asset's lifetime. The
+// syntax of the function is:
+//
+// DB(cost,salvage,life,period,[month])
+func (fn *formulaFuncs) DB(argsList *list.List) formulaArg {
+ if argsList.Len() < 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DB requires at least 4 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DB allows at most 5 arguments")
+ }
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber {
+ return cost
+ }
+ salvage := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if salvage.Type != ArgNumber {
+ return salvage
+ }
+ life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if life.Type != ArgNumber {
+ return life
+ }
+ period := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if period.Type != ArgNumber {
+ return period
+ }
+ month := newNumberFormulaArg(12)
+ if argsList.Len() == 5 {
+ if month = argsList.Back().Value.(formulaArg).ToNumber(); month.Type != ArgNumber {
+ return month
}
- nums = append(nums, val)
}
- if nums[0] < 0 {
- err = errors.New("LCM only accepts positive arguments")
- return
+ if cost.Number == 0 {
+ return newNumberFormulaArg(0)
}
- if len(nums) == 1 {
- result = fmt.Sprintf("%g", nums[0])
- return
+ if calcDbArgsCompare(cost, salvage, life, period) || (month.Number < 1) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
}
- cm := nums[0]
- for i := 1; i < len(nums); i++ {
- if nums[i] < 0 {
- err = errors.New("LCM only accepts positive arguments")
- return
+ dr := 1 - math.Pow(salvage.Number/cost.Number, 1/life.Number)
+ dr = math.Round(dr*1000) / 1000
+ pd, depreciation := 0.0, 0.0
+ for per := 1; per <= int(period.Number); per++ {
+ if per == 1 {
+ depreciation = cost.Number * dr * month.Number / 12
+ } else if per == int(life.Number+1) {
+ depreciation = (cost.Number - pd) * dr * (12 - month.Number) / 12
+ } else {
+ depreciation = (cost.Number - pd) * dr
}
- cm = lcm(cm, nums[i])
+ pd += depreciation
}
- result = fmt.Sprintf("%g", cm)
- return
+ return newNumberFormulaArg(depreciation)
}
-// LN function calculates the natural logarithm of a given number. The syntax
-// of the function is:
-//
-// LN(number)
+// DDB function calculates the depreciation of an asset, using the Double
+// Declining Balance Method, or another specified depreciation rate. The
+// syntax of the function is:
//
-func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("LN requires 1 numeric argument")
- return
+// DDB(cost,salvage,life,period,[factor])
+func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg {
+ if argsList.Len() < 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DDB requires at least 4 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DDB allows at most 5 arguments")
+ }
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber {
+ return cost
+ }
+ salvage := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if salvage.Type != ArgNumber {
+ return salvage
+ }
+ life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if life.Type != ArgNumber {
+ return life
+ }
+ period := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if period.Type != ArgNumber {
+ return period
+ }
+ factor := newNumberFormulaArg(2)
+ if argsList.Len() == 5 {
+ if factor = argsList.Back().Value.(formulaArg).ToNumber(); factor.Type != ArgNumber {
+ return factor
+ }
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if cost.Number == 0 {
+ return newNumberFormulaArg(0)
}
- result = fmt.Sprintf("%g", math.Log(number))
- return
+ if calcDbArgsCompare(cost, salvage, life, period) || (factor.Number <= 0.0) || (period.Number > life.Number) {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ pd, depreciation := 0.0, 0.0
+ for per := 1; per <= int(period.Number); per++ {
+ depreciation = math.Min((cost.Number-pd)*(factor.Number/life.Number), cost.Number-salvage.Number-pd)
+ pd += depreciation
+ }
+ return newNumberFormulaArg(depreciation)
}
-// LOG function calculates the logarithm of a given number, to a supplied
-// base. The syntax of the function is:
+// prepareDataValueArgs convert first N arguments to data value for the
+// formula functions.
+func (fn *formulaFuncs) prepareDataValueArgs(n int, argsList *list.List) formulaArg {
+ l := list.New()
+ var dataValues []formulaArg
+ getDateValue := func(arg formulaArg, l *list.List) formulaArg {
+ switch arg.Type {
+ case ArgNumber:
+ break
+ case ArgString:
+ num := arg.ToNumber()
+ if num.Type == ArgNumber {
+ arg = num
+ break
+ }
+ l.Init()
+ l.PushBack(arg)
+ arg = fn.DATEVALUE(l)
+ if arg.Type == ArgError {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ default:
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ return arg
+ }
+ for i, arg := 0, argsList.Front(); i < n; arg = arg.Next() {
+ dataValue := getDateValue(arg.Value.(formulaArg), l)
+ if dataValue.Type != ArgNumber {
+ return dataValue
+ }
+ dataValues = append(dataValues, dataValue)
+ i++
+ }
+ return newListFormulaArg(dataValues)
+}
+
+// discIntrate is an implementation of the formula functions DISC and INTRATE.
+func (fn *formulaFuncs) discIntrate(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 4 && argsList.Len() != 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 4 or 5 arguments", name))
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity, argName := args.List[0], args.List[1], "pr"
+ if maturity.Number <= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name))
+ }
+ prInvestment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if prInvestment.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if prInvestment.Number <= 0 {
+ if name == "INTRATE" {
+ argName = "investment"
+ }
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires %s > 0", name, argName))
+ }
+ redemption := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if redemption.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name))
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ if name == "INTRATE" {
+ return newNumberFormulaArg((redemption.Number - prInvestment.Number) / prInvestment.Number / frac.Number)
+ }
+ return newNumberFormulaArg((redemption.Number - prInvestment.Number) / redemption.Number / frac.Number)
+}
+
+// DISC function calculates the Discount Rate for a security. The syntax of
+// the function is:
//
-// LOG(number,[base])
+// DISC(settlement,maturity,pr,redemption,[basis])
+func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg {
+ return fn.discIntrate("DISC", argsList)
+}
+
+// DOLLAR function rounds a supplied number to a specified number of decimal
+// places and then converts this into a text string with a currency format. The
+// syntax of the function is:
//
-func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) {
+// DOLLAR(number,[decimals])
+func (fn *formulaFuncs) DOLLAR(argsList *list.List) formulaArg {
if argsList.Len() == 0 {
- err = errors.New("LOG requires at least 1 argument")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "DOLLAR requires at least 1 argument")
}
if argsList.Len() > 2 {
- err = errors.New("LOG allows at most 2 arguments")
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "DOLLAR requires 1 or 2 arguments")
+ }
+ numArg := argsList.Front().Value.(formulaArg)
+ n := numArg.ToNumber()
+ if n.Type != ArgNumber {
+ return n
+ }
+ decimals, dot, value := 2, ".", numArg.Value()
+ if argsList.Len() == 2 {
+ d := argsList.Back().Value.(formulaArg).ToNumber()
+ if d.Type != ArgNumber {
+ return d
+ }
+ if d.Number < 0 {
+ value = strconv.FormatFloat(fn.round(n.Number, d.Number, down), 'f', -1, 64)
+ }
+ if d.Number >= 128 {
+ return newErrorFormulaArg(formulaErrorVALUE, "decimal value should be less than 128")
+ }
+ if decimals = int(d.Number); decimals < 0 {
+ decimals, dot = 0, ""
+ }
}
- number, base := 0.0, 10.0
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ symbol := map[CultureName]string{
+ CultureNameUnknown: "$",
+ CultureNameEnUS: "$",
+ CultureNameJaJP: "¥",
+ CultureNameKoKR: "\u20a9",
+ CultureNameZhCN: "¥",
+ CultureNameZhTW: "NT$",
+ }[fn.f.options.CultureInfo]
+ numFmtCode := fmt.Sprintf("%s#,##0%s%s;(%s#,##0%s%s)",
+ symbol, dot, strings.Repeat("0", decimals), symbol, dot, strings.Repeat("0", decimals))
+ return newStringFormulaArg(format(value, numFmtCode, false, CellTypeNumber, nil))
+}
+
+// DOLLARDE function converts a dollar value in fractional notation, into a
+// dollar value expressed as a decimal. The syntax of the function is:
+//
+// DOLLARDE(fractional_dollar,fraction)
+func (fn *formulaFuncs) DOLLARDE(argsList *list.List) formulaArg {
+ return fn.dollar("DOLLARDE", argsList)
+}
+
+// DOLLARFR function converts a dollar value in decimal notation, into a
+// dollar value that is expressed in fractional notation. The syntax of the
+// function is:
+//
+// DOLLARFR(decimal_dollar,fraction)
+func (fn *formulaFuncs) DOLLARFR(argsList *list.List) formulaArg {
+ return fn.dollar("DOLLARFR", argsList)
+}
+
+// dollar is an implementation of the formula functions DOLLARDE and DOLLARFR.
+func (fn *formulaFuncs) dollar(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name))
}
- if argsList.Len() > 1 {
- if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
+ dollar := argsList.Front().Value.(formulaArg).ToNumber()
+ if dollar.Type != ArgNumber {
+ return dollar
}
- if number == 0 {
- err = errors.New(formulaErrorNUM)
- return
+ frac := argsList.Back().Value.(formulaArg).ToNumber()
+ if frac.Type != ArgNumber {
+ return frac
}
- if base == 0 {
- err = errors.New(formulaErrorNUM)
- return
+ if frac.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- if base == 1 {
- err = errors.New(formulaErrorDIV)
- return
+ if frac.Number == 0 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
}
- result = fmt.Sprintf("%g", math.Log(number)/math.Log(base))
- return
+ cents := math.Mod(dollar.Number, 1)
+ if name == "DOLLARDE" {
+ cents /= frac.Number
+ cents *= math.Pow(10, math.Ceil(math.Log10(frac.Number)))
+ } else {
+ cents *= frac.Number
+ cents *= math.Pow(10, -math.Ceil(math.Log10(frac.Number)))
+ }
+ return newNumberFormulaArg(math.Floor(dollar.Number) + cents)
}
-// LOG10 function calculates the base 10 logarithm of a given number. The
+// prepareDurationArgs checking and prepare arguments for the formula
+// functions DURATION and MDURATION.
+func (fn *formulaFuncs) prepareDurationArgs(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 5 && argsList.Len() != 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 5 or 6 arguments", name))
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ if settlement.Number >= maturity.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name))
+ }
+ coupon := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if coupon.Type != ArgNumber {
+ return coupon
+ }
+ if coupon.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires coupon >= 0", name))
+ }
+ yld := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if yld.Type != ArgNumber {
+ return yld
+ }
+ if yld.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires yld >= 0", name))
+ }
+ frequency := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if frequency.Type != ArgNumber {
+ return frequency
+ }
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 6 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ return newListFormulaArg([]formulaArg{settlement, maturity, coupon, yld, frequency, basis})
+}
+
+// duration is an implementation of the formula function DURATION.
+func (fn *formulaFuncs) duration(settlement, maturity, coupon, yld, frequency, basis formulaArg) formulaArg {
+ frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ argumments := list.New().Init()
+ argumments.PushBack(settlement)
+ argumments.PushBack(maturity)
+ argumments.PushBack(frequency)
+ argumments.PushBack(basis)
+ coups := fn.COUPNUM(argumments)
+ duration := 0.0
+ p := 0.0
+ coupon.Number *= 100 / frequency.Number
+ yld.Number /= frequency.Number
+ yld.Number++
+ diff := frac.Number*frequency.Number - coups.Number
+ for t := 1.0; t < coups.Number; t++ {
+ tDiff := t + diff
+ add := coupon.Number / math.Pow(yld.Number, tDiff)
+ p += add
+ duration += tDiff * add
+ }
+ add := (coupon.Number + 100) / math.Pow(yld.Number, coups.Number+diff)
+ p += add
+ duration += (coups.Number + diff) * add
+ duration /= p
+ duration /= frequency.Number
+ return newNumberFormulaArg(duration)
+}
+
+// DURATION function calculates the Duration (specifically, the Macaulay
+// Duration) of a security that pays periodic interest, assuming a par value
+// of $100. The syntax of the function is:
+//
+// DURATION(settlement,maturity,coupon,yld,frequency,[basis])
+func (fn *formulaFuncs) DURATION(argsList *list.List) formulaArg {
+ args := fn.prepareDurationArgs("DURATION", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ return fn.duration(args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5])
+}
+
+// EFFECT function returns the effective annual interest rate for a given
+// nominal interest rate and number of compounding periods per year. The
// syntax of the function is:
//
-// LOG10(number)
+// EFFECT(nominal_rate,npery)
+func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EFFECT requires 2 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ npery := argsList.Back().Value.(formulaArg).ToNumber()
+ if npery.Type != ArgNumber {
+ return npery
+ }
+ if rate.Number <= 0 || npery.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(math.Pow(1+rate.Number/npery.Number, npery.Number) - 1)
+}
+
+// EUROCONVERT function convert a number to euro or from euro to a
+// participating currency. You can also use it to convert a number from one
+// participating currency to another by using the euro as an intermediary
+// (triangulation). The syntax of the function is:
//
-func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("LOG10 requires 1 numeric argument")
- return
+// EUROCONVERT(number,sourcecurrency,targetcurrency[,fullprecision,triangulationprecision])
+func (fn *formulaFuncs) EUROCONVERT(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EUROCONVERT requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "EUROCONVERT allows at most 5 arguments")
+ }
+ number := argsList.Front().Value.(formulaArg).ToNumber()
+ if number.Type != ArgNumber {
+ return number
+ }
+ sourceCurrency := argsList.Front().Next().Value.(formulaArg).Value()
+ targetCurrency := argsList.Front().Next().Next().Value.(formulaArg).Value()
+ fullPrec, triangulationPrec := newBoolFormulaArg(false), newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if fullPrec = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool(); fullPrec.Type != ArgNumber {
+ return fullPrec
+ }
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if argsList.Len() == 5 {
+ if triangulationPrec = argsList.Back().Value.(formulaArg).ToNumber(); triangulationPrec.Type != ArgNumber {
+ return triangulationPrec
+ }
}
- result = fmt.Sprintf("%g", math.Log10(number))
- return
+ convertTable := map[string][]float64{
+ "EUR": {1.0, 2},
+ "ATS": {13.7603, 2},
+ "BEF": {40.3399, 0},
+ "DEM": {1.95583, 2},
+ "ESP": {166.386, 0},
+ "FIM": {5.94573, 2},
+ "FRF": {6.55957, 2},
+ "IEP": {0.787564, 2},
+ "ITL": {1936.27, 0},
+ "LUF": {40.3399, 0},
+ "NLG": {2.20371, 2},
+ "PTE": {200.482, 2},
+ "GRD": {340.750, 2},
+ "SIT": {239.640, 2},
+ "MTL": {0.429300, 2},
+ "CYP": {0.585274, 2},
+ "SKK": {30.1260, 2},
+ "EEK": {15.6466, 2},
+ "LVL": {0.702804, 2},
+ "LTL": {3.45280, 2},
+ }
+ source, ok := convertTable[sourceCurrency]
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ target, ok := convertTable[targetCurrency]
+ if !ok {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if sourceCurrency == targetCurrency {
+ return number
+ }
+ var res float64
+ if sourceCurrency == "EUR" {
+ res = number.Number * target[0]
+ } else {
+ intermediate := number.Number / source[0]
+ if triangulationPrec.Number != 0 {
+ ratio := math.Pow(10, triangulationPrec.Number)
+ intermediate = math.Round(intermediate*ratio) / ratio
+ }
+ res = intermediate * target[0]
+ }
+ if fullPrec.Number != 1 {
+ ratio := math.Pow(10, target[1])
+ res = math.Round(res*ratio) / ratio
+ }
+ return newNumberFormulaArg(res)
}
-func minor(sqMtx [][]float64, idx int) [][]float64 {
- ret := [][]float64{}
- for i := range sqMtx {
- if i == 0 {
- continue
+// FV function calculates the Future Value of an investment with periodic
+// constant payments and a constant interest rate. The syntax of the function
+// is:
+//
+// FV(rate,nper,[pmt],[pv],[type])
+func (fn *formulaFuncs) FV(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FV requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FV allows at most 5 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ nper := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pmt := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pmt.Type != ArgNumber {
+ return pmt
+ }
+ pv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if pv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); pv.Type != ArgNumber {
+ return pv
}
- row := []float64{}
- for j := range sqMtx {
- if j == idx {
- continue
- }
- row = append(row, sqMtx[i][j])
+ }
+ if argsList.Len() == 5 {
+ if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber {
+ return typ
}
- ret = append(ret, row)
}
- return ret
+ if typ.Number != 0 && typ.Number != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if rate.Number != 0 {
+ return newNumberFormulaArg(-pv.Number*math.Pow(1+rate.Number, nper.Number) - pmt.Number*(1+rate.Number*typ.Number)*(math.Pow(1+rate.Number, nper.Number)-1)/rate.Number)
+ }
+ return newNumberFormulaArg(-pv.Number - pmt.Number*nper.Number)
}
-// det determinant of the 2x2 matrix.
-func det(sqMtx [][]float64) float64 {
- if len(sqMtx) == 2 {
- m00 := sqMtx[0][0]
- m01 := sqMtx[0][1]
- m10 := sqMtx[1][0]
- m11 := sqMtx[1][1]
- return m00*m11 - m10*m01
+// FVSCHEDULE function calculates the Future Value of an investment with a
+// variable interest rate. The syntax of the function is:
+//
+// FVSCHEDULE(principal,schedule)
+func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "FVSCHEDULE requires 2 arguments")
}
- var res, sgn float64 = 0, 1
- for j := range sqMtx {
- res += sgn * sqMtx[0][j] * det(minor(sqMtx, j))
- sgn *= -1
+ pri := argsList.Front().Value.(formulaArg).ToNumber()
+ if pri.Type != ArgNumber {
+ return pri
}
- return res
+ principal := pri.Number
+ for _, arg := range argsList.Back().Value.(formulaArg).ToList() {
+ if arg.Value() == "" {
+ continue
+ }
+ rate := arg.ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ principal *= 1 + rate.Number
+ }
+ return newNumberFormulaArg(principal)
}
-// MDETERM calculates the determinant of a square matrix. The
-// syntax of the function is:
+// INTRATE function calculates the interest rate for a fully invested
+// security. The syntax of the function is:
//
-// MDETERM(array)
+// INTRATE(settlement,maturity,investment,redemption,[basis])
+func (fn *formulaFuncs) INTRATE(argsList *list.List) formulaArg {
+ return fn.discIntrate("INTRATE", argsList)
+}
+
+// IPMT function calculates the interest payment, during a specific period of a
+// loan or investment that is paid in constant periodic payments, with a
+// constant interest rate. The syntax of the function is:
//
-func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) {
- var num float64
- var numMtx = [][]float64{}
- var strMtx = argsList.Front().Value.(formulaArg).Matrix
- if argsList.Len() < 1 {
- return
+// IPMT(rate,per,nper,pv,[fv],[type])
+func (fn *formulaFuncs) IPMT(argsList *list.List) formulaArg {
+ return fn.ipmt("IPMT", argsList)
+}
+
+// calcIpmt is part of the implementation ipmt.
+func calcIpmt(name string, typ, per, pmt, pv, rate formulaArg) formulaArg {
+ capital, interest, principal := pv.Number, 0.0, 0.0
+ for i := 1; i <= int(per.Number); i++ {
+ if typ.Number != 0 && i == 1 {
+ interest = 0
+ } else {
+ interest = -capital * rate.Number
+ }
+ principal = pmt.Number - interest
+ capital += principal
}
- var rows = len(strMtx)
- for _, row := range argsList.Front().Value.(formulaArg).Matrix {
- if len(row) != rows {
- err = errors.New(formulaErrorVALUE)
- return
+ if name == "IPMT" {
+ return newNumberFormulaArg(interest)
+ }
+ return newNumberFormulaArg(principal)
+}
+
+// ipmt is an implementation of the formula functions IPMT and PPMT.
+func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 4 arguments", name))
+ }
+ if argsList.Len() > 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 6 arguments", name))
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ per := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if per.Type != ArgNumber {
+ return per
+ }
+ nper := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pv := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0)
+ if argsList.Len() >= 5 {
+ if fv = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber {
+ return fv
}
- numRow := []float64{}
- for _, ele := range row {
- if num, err = strconv.ParseFloat(ele.String, 64); err != nil {
- return
- }
- numRow = append(numRow, num)
+ }
+ if argsList.Len() == 6 {
+ if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber {
+ return typ
}
- numMtx = append(numMtx, numRow)
}
- result = fmt.Sprintf("%g", det(numMtx))
- return
+ if typ.Number != 0 && typ.Number != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if per.Number <= 0 || per.Number > nper.Number {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ args := list.New().Init()
+ args.PushBack(rate)
+ args.PushBack(nper)
+ args.PushBack(pv)
+ args.PushBack(fv)
+ args.PushBack(typ)
+ pmt := fn.PMT(args)
+ return calcIpmt(name, typ, per, pmt, pv, rate)
}
-// MOD function returns the remainder of a division between two supplied
-// numbers. The syntax of the function is:
+// IRR function returns the Internal Rate of Return for a supplied series of
+// periodic cash flows (i.e. an initial investment value and a series of net
+// income values). The syntax of the function is:
//
-// MOD(number,divisor)
+// IRR(values,[guess])
+func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg {
+ if argsList.Len() < 1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IRR requires at least 1 argument")
+ }
+ if argsList.Len() > 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "IRR allows at most 2 arguments")
+ }
+ values, guess := argsList.Front().Value.(formulaArg).ToList(), newNumberFormulaArg(0.1)
+ if argsList.Len() > 1 {
+ if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber {
+ return guess
+ }
+ }
+ x1, x2 := newNumberFormulaArg(0), guess
+ args := list.New().Init()
+ args.PushBack(x1)
+ for _, v := range values {
+ args.PushBack(v)
+ }
+ f1 := fn.NPV(args)
+ args.Front().Value = x2
+ f2 := fn.NPV(args)
+ for i := 0; i < maxFinancialIterations; i++ {
+ if f1.Number*f2.Number < 0 {
+ break
+ }
+ if math.Abs(f1.Number) < math.Abs(f2.Number) {
+ x1.Number += 1.6 * (x1.Number - x2.Number)
+ args.Front().Value = x1
+ f1 = fn.NPV(args)
+ continue
+ }
+ x2.Number += 1.6 * (x2.Number - x1.Number)
+ args.Front().Value = x2
+ f2 = fn.NPV(args)
+ }
+ if f1.Number*f2.Number > 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ args.Front().Value = x1
+ f := fn.NPV(args)
+ var rtb, dx, xMid, fMid float64
+ if f.Number < 0 {
+ rtb = x1.Number
+ dx = x2.Number - x1.Number
+ } else {
+ rtb = x2.Number
+ dx = x1.Number - x2.Number
+ }
+ for i := 0; i < maxFinancialIterations; i++ {
+ dx *= 0.5
+ xMid = rtb + dx
+ args.Front().Value = newNumberFormulaArg(xMid)
+ fMid = fn.NPV(args).Number
+ if fMid <= 0 {
+ rtb = xMid
+ }
+ if math.Abs(fMid) < financialPrecision || math.Abs(dx) < financialPrecision {
+ break
+ }
+ }
+ return newNumberFormulaArg(xMid)
+}
+
+// ISPMT function calculates the interest paid during a specific period of a
+// loan or investment. The syntax of the function is:
//
-func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("MOD requires 2 numeric arguments")
- return
+// ISPMT(rate,per,nper,pv)
+func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ISPMT requires 4 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ per := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if per.Type != ArgNumber {
+ return per
+ }
+ nper := argsList.Back().Prev().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pv := argsList.Back().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ pr, payment, num := pv.Number, pv.Number/nper.Number, 0.0
+ for i := 0; i <= int(per.Number); i++ {
+ num = rate.Number * pr * -1
+ pr -= payment
+ if i == int(nper.Number) {
+ num = 0
+ }
}
- var number, divisor float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newNumberFormulaArg(num)
+}
+
+// MDURATION function calculates the Modified Macaulay Duration of a security
+// that pays periodic interest, assuming a par value of $100. The syntax of
+// the function is:
+//
+// MDURATION(settlement,maturity,coupon,yld,frequency,[basis])
+func (fn *formulaFuncs) MDURATION(argsList *list.List) formulaArg {
+ args := fn.prepareDurationArgs("MDURATION", argsList)
+ if args.Type != ArgList {
+ return args
}
- if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ duration := fn.duration(args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5])
+ if duration.Type != ArgNumber {
+ return duration
}
- if divisor == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ return newNumberFormulaArg(duration.Number / (1 + args.List[3].Number/args.List[4].Number))
+}
+
+// MIRR function returns the Modified Internal Rate of Return for a supplied
+// series of periodic cash flows (i.e. a set of values, which includes an
+// initial investment value and a series of net income values). The syntax of
+// the function is:
+//
+// MIRR(values,finance_rate,reinvest_rate)
+func (fn *formulaFuncs) MIRR(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "MIRR requires 3 arguments")
+ }
+ values := argsList.Front().Value.(formulaArg).ToList()
+ financeRate := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if financeRate.Type != ArgNumber {
+ return financeRate
+ }
+ reinvestRate := argsList.Back().Value.(formulaArg).ToNumber()
+ if reinvestRate.Type != ArgNumber {
+ return reinvestRate
+ }
+ n, fr, rr, npvPos, npvNeg := len(values), 1+financeRate.Number, 1+reinvestRate.Number, 0.0, 0.0
+ for i, v := range values {
+ val := v.ToNumber()
+ if val.Number >= 0 {
+ npvPos += val.Number / math.Pow(rr, float64(i))
+ continue
+ }
+ npvNeg += val.Number / math.Pow(fr, float64(i))
}
- trunc, rem := math.Modf(number / divisor)
- if rem < 0 {
- trunc--
+ if npvNeg == 0 || npvPos == 0 || reinvestRate.Number <= -1 {
+ return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV)
}
- result = fmt.Sprintf("%g", number-divisor*trunc)
- return
+ return newNumberFormulaArg(math.Pow(-npvPos*math.Pow(rr, float64(n))/(npvNeg*rr), 1/(float64(n)-1)) - 1)
}
-// MROUND function rounds a supplied number up or down to the nearest multiple
-// of a given number. The syntax of the function is:
-//
-// MOD(number,multiple)
+// NOMINAL function returns the nominal interest rate for a given effective
+// interest rate and number of compounding periods per year. The syntax of
+// the function is:
//
-func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) {
+// NOMINAL(effect_rate,npery)
+func (fn *formulaFuncs) NOMINAL(argsList *list.List) formulaArg {
if argsList.Len() != 2 {
- err = errors.New("MROUND requires 2 numeric arguments")
- return
- }
- var number, multiple float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ return newErrorFormulaArg(formulaErrorVALUE, "NOMINAL requires 2 arguments")
}
- if multiple == 0 {
- err = errors.New(formulaErrorNUM)
- return
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- if multiple < 0 && number > 0 ||
- multiple > 0 && number < 0 {
- err = errors.New(formulaErrorNUM)
- return
+ npery := argsList.Back().Value.(formulaArg).ToNumber()
+ if npery.Type != ArgNumber {
+ return npery
}
- number, res := math.Modf(number / multiple)
- if math.Trunc(res+0.5) > 0 {
- number++
+ if rate.Number <= 0 || npery.Number < 1 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = fmt.Sprintf("%g", number*multiple)
- return
+ return newNumberFormulaArg(npery.Number * (math.Pow(rate.Number+1, 1/npery.Number) - 1))
}
-// MULTINOMIAL function calculates the ratio of the factorial of a sum of
-// supplied values to the product of factorials of those values. The syntax of
-// the function is:
-//
-// MULTINOMIAL(number1,[number2],...)
+// NPER function calculates the number of periods required to pay off a loan,
+// for a constant periodic payment and a constant interest rate. The syntax
+// of the function is:
//
-func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) {
- val, num, denom := 0.0, 0.0, 1.0
- for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg)
- if token.String == "" {
- continue
+// NPER(rate,pmt,pv,[fv],[type])
+func (fn *formulaFuncs) NPER(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NPER requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NPER allows at most 5 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ pmt := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if pmt.Type != ArgNumber {
+ return pmt
+ }
+ pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber {
+ return fv
}
- if val, err = strconv.ParseFloat(token.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ }
+ if argsList.Len() == 5 {
+ if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber {
+ return typ
}
- num += val
- denom *= fact(val)
}
- result = fmt.Sprintf("%g", fact(num)/denom)
- return
+ if typ.Number != 0 && typ.Number != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
+ }
+ if pmt.Number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if rate.Number != 0 {
+ p := math.Log((pmt.Number*(1+rate.Number*typ.Number)/rate.Number-fv.Number)/(pv.Number+pmt.Number*(1+rate.Number*typ.Number)/rate.Number)) / math.Log(1+rate.Number)
+ return newNumberFormulaArg(p)
+ }
+ return newNumberFormulaArg((-pv.Number - fv.Number) / pmt.Number)
}
-// MUNIT function returns the unit matrix for a specified dimension. The
+// NPV function calculates the Net Present Value of an investment, based on a
+// supplied discount rate, and a series of future payments and income. The
// syntax of the function is:
//
-// MUNIT(dimension)
-//
-func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("MUNIT requires 1 numeric argument")
- return
+// NPV(rate,value1,[value2],[value3],...)
+func (fn *formulaFuncs) NPV(argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "NPV requires at least 2 arguments")
}
- var dimension int
- if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- matrix := make([][]float64, 0, dimension)
- for i := 0; i < dimension; i++ {
- row := make([]float64, dimension)
- for j := 0; j < dimension; j++ {
- if i == j {
- row[j] = float64(1.0)
- } else {
- row[j] = float64(0.0)
- }
+ val, i := 0.0, 1
+ for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() {
+ num := arg.Value.(formulaArg).ToNumber()
+ if num.Type != ArgNumber {
+ continue
}
- matrix = append(matrix, row)
+ val += num.Number / math.Pow(1+rate.Number, float64(i))
+ i++
}
- return
+ return newNumberFormulaArg(val)
}
-// ODD function ounds a supplied number away from zero (i.e. rounds a positive
-// number up and a negative number down), to the next odd number. The syntax
-// of the function is:
-//
-// ODD(number)
-//
-func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ODD requires 1 numeric argument")
- return
- }
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+// aggrBetween is a part of implementation of the formula function ODDFPRICE.
+func aggrBetween(startPeriod, endPeriod float64, initialValue []float64, f func(acc []float64, index float64) []float64) []float64 {
+ var s []float64
+ if startPeriod <= endPeriod {
+ for i := startPeriod; i <= endPeriod; i++ {
+ s = append(s, i)
+ }
+ } else {
+ for i := startPeriod; i >= endPeriod; i-- {
+ s = append(s, i)
+ }
}
- if number == 0 {
- result = "1"
- return
+ return fold(f, initialValue, s)
+}
+
+// fold is a part of implementation of the formula function ODDFPRICE.
+func fold(f func(acc []float64, index float64) []float64, state []float64, source []float64) []float64 {
+ length, value := len(source), state
+ for index := 0; length > index; index++ {
+ value = f(value, source[index])
}
- sign := math.Signbit(number)
- m, frac := math.Modf((number - 1) / 2)
- val := m*2 + 1
- if frac != 0 {
- if !sign {
- val += 2
- } else {
- val -= 2
+ return value
+}
+
+// changeMonth is a part of implementation of the formula function ODDFPRICE.
+func changeMonth(date time.Time, numMonths float64, returnLastMonth bool) time.Time {
+ offsetDay := 0
+ if returnLastMonth && date.Day() == getDaysInMonth(date.Year(), int(date.Month())) {
+ offsetDay--
+ }
+ newDate := date.AddDate(0, int(numMonths), offsetDay)
+ if returnLastMonth {
+ lastDay := getDaysInMonth(newDate.Year(), int(newDate.Month()))
+ return timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), makeDate(newDate.Year(), newDate.Month(), lastDay))+1, false)
+ }
+ return newDate
+}
+
+// datesAggregate is a part of implementation of the formula function
+// ODDFPRICE.
+func datesAggregate(startDate, endDate time.Time, numMonths float64, f func(pcd, ncd time.Time) float64, acc float64, returnLastMonth bool) (time.Time, time.Time, float64) {
+ frontDate, trailingDate := startDate, endDate
+ s1 := frontDate.After(endDate) || frontDate.Equal(endDate)
+ s2 := endDate.After(frontDate) || endDate.Equal(frontDate)
+ stop := s2
+ if numMonths > 0 {
+ stop = s1
+ }
+ for !stop {
+ trailingDate = frontDate
+ frontDate = changeMonth(frontDate, numMonths, returnLastMonth)
+ fn := f(frontDate, trailingDate)
+ acc += fn
+ s1 = frontDate.After(endDate) || frontDate.Equal(endDate)
+ s2 = endDate.After(frontDate) || endDate.Equal(frontDate)
+ stop = s2
+ if numMonths > 0 {
+ stop = s1
}
}
- result = fmt.Sprintf("%g", val)
- return
+ return frontDate, trailingDate, acc
}
-// PI function returns the value of the mathematical constant π (pi), accurate
-// to 15 digits (14 decimal places). The syntax of the function is:
-//
-// PI()
-//
-func (fn *formulaFuncs) PI(argsList *list.List) (result string, err error) {
- if argsList.Len() != 0 {
- err = errors.New("PI accepts no arguments")
- return
+// coupNumber is a part of implementation of the formula function ODDFPRICE.
+func coupNumber(maturity, settlement, numMonths float64) float64 {
+ maturityTime, settlementTime := timeFromExcelTime(maturity, false), timeFromExcelTime(settlement, false)
+ my, mm, md := maturityTime.Year(), maturityTime.Month(), maturityTime.Day()
+ sy, sm, sd := settlementTime.Year(), settlementTime.Month(), settlementTime.Day()
+ couponsTemp, endOfMonthTemp := 0.0, getDaysInMonth(my, int(mm)) == md
+ endOfMonth := endOfMonthTemp
+ if !endOfMonthTemp && mm != 2 && md > 28 && md < getDaysInMonth(my, int(mm)) {
+ endOfMonth = getDaysInMonth(sy, int(sm)) == sd
+ }
+ startDate := changeMonth(settlementTime, 0, endOfMonth)
+ coupons := couponsTemp
+ if startDate.After(settlementTime) {
+ coupons++
+ }
+ date := changeMonth(startDate, numMonths, endOfMonth)
+ f := func(pcd, ncd time.Time) float64 {
+ return 1
+ }
+ _, _, result := datesAggregate(date, maturityTime, numMonths, f, coupons, endOfMonth)
+ return result
+}
+
+// prepareOddYldOrPrArg checking and prepare yield or price arguments for the
+// formula functions ODDFPRICE, ODDFYIELD, ODDLPRICE and ODDLYIELD.
+func prepareOddYldOrPrArg(name string, arg formulaArg) formulaArg {
+ yldOrPr := arg.ToNumber()
+ if yldOrPr.Type != ArgNumber {
+ return yldOrPr
}
- result = fmt.Sprintf("%g", math.Pi)
- return
+ if (name == "ODDFPRICE" || name == "ODDLPRICE") && yldOrPr.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires yld >= 0", name))
+ }
+ if (name == "ODDFYIELD" || name == "ODDLYIELD") && yldOrPr.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires pr > 0", name))
+ }
+ return yldOrPr
}
-// POWER function calculates a given number, raised to a supplied power.
-// The syntax of the function is:
-//
-// POWER(number,power)
-//
-func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("POWER requires 2 numeric arguments")
- return
+// prepareOddfArgs checking and prepare arguments for the formula
+// functions ODDFPRICE and ODDFYIELD.
+func (fn *formulaFuncs) prepareOddfArgs(name string, argsList *list.List) formulaArg {
+ dateValues := fn.prepareDataValueArgs(4, argsList)
+ if dateValues.Type != ArgList {
+ return dateValues
}
- var x, y float64
- if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ settlement, maturity, issue, firstCoupon := dateValues.List[0], dateValues.List[1], dateValues.List[2], dateValues.List[3]
+ if issue.Number >= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires settlement > issue", name))
}
- if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if settlement.Number >= firstCoupon.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires first_coupon > settlement", name))
}
- if x == 0 && y == 0 {
- err = errors.New(formulaErrorNUM)
- return
+ if firstCoupon.Number >= maturity.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > first_coupon", name))
}
- if x == 0 && y < 0 {
- err = errors.New(formulaErrorDIV)
- return
+ rate := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- result = fmt.Sprintf("%g", math.Pow(x, y))
- return
+ if rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name))
+ }
+ yldOrPr := prepareOddYldOrPrArg(name, argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg))
+ if yldOrPr.Type != ArgNumber {
+ return yldOrPr
+ }
+ redemption := argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if redemption.Type != ArgNumber {
+ return redemption
+ }
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name))
+ }
+ frequency := argsList.Front().Next().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if frequency.Type != ArgNumber {
+ return frequency
+ }
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 9 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ return newListFormulaArg([]formulaArg{settlement, maturity, issue, firstCoupon, rate, yldOrPr, redemption, frequency, basis})
}
-// PRODUCT function returns the product (multiplication) of a supplied set of
-// numerical values. The syntax of the function is:
+// ODDFPRICE function calculates the price per $100 face value of a security
+// with an odd (short or long) first period. The syntax of the function is:
//
-// PRODUCT(number1,[number2],...)
-//
-func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) {
- val, product := 0.0, 1.0
- for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg)
- switch token.Type {
- case ArgUnknown:
- continue
- case ArgString:
- if token.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(token.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- product = product * val
- case ArgMatrix:
- for _, row := range token.Matrix {
- for _, value := range row {
- if value.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(value.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- product = product * val
- }
- }
+// ODDFPRICE(settlement,maturity,issue,first_coupon,rate,yld,redemption,frequency,[basis])
+func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg {
+ if argsList.Len() != 8 && argsList.Len() != 9 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ODDFPRICE requires 8 or 9 arguments")
+ }
+ args := fn.prepareOddfArgs("ODDFPRICE", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8]
+ if basisArg.Number < 0 || basisArg.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
+ }
+ issueTime := timeFromExcelTime(issue.Number, false)
+ settlementTime := timeFromExcelTime(settlement.Number, false)
+ maturityTime := timeFromExcelTime(maturity.Number, false)
+ firstCouponTime := timeFromExcelTime(firstCoupon.Number, false)
+ basis := int(basisArg.Number)
+ monthDays := getDaysInMonth(maturityTime.Year(), int(maturityTime.Month()))
+ returnLastMonth := monthDays == maturityTime.Day()
+ numMonths := 12 / frequency.Number
+ numMonthsNeg := -numMonths
+ mat := changeMonth(maturityTime, numMonthsNeg, returnLastMonth)
+ pcd, _, _ := datesAggregate(mat, firstCouponTime, numMonthsNeg, func(d1, d2 time.Time) float64 {
+ return 0
+ }, 0, returnLastMonth)
+ if !pcd.Equal(firstCouponTime) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ fnArgs := list.New().Init()
+ fnArgs.PushBack(settlement)
+ fnArgs.PushBack(maturity)
+ fnArgs.PushBack(frequency)
+ fnArgs.PushBack(basisArg)
+ e := fn.COUPDAYS(fnArgs)
+ n := fn.COUPNUM(fnArgs)
+ m := frequency.Number
+ dfc := coupdays(issueTime, firstCouponTime, basis)
+ if dfc < e.Number {
+ dsc := coupdays(settlementTime, firstCouponTime, basis)
+ a := coupdays(issueTime, settlementTime, basis)
+ x := yld.Number/m + 1
+ y := dsc / e.Number
+ p1 := x
+ p3 := math.Pow(p1, n.Number-1+y)
+ term1 := redemption.Number / p3
+ term2 := 100 * rate.Number / m * dfc / e.Number / math.Pow(p1, y)
+ f := func(acc []float64, index float64) []float64 {
+ return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index-1+y)}
}
- }
- result = fmt.Sprintf("%g", product)
- return
+ term3 := aggrBetween(2, math.Floor(n.Number), []float64{0}, f)
+ p2 := rate.Number / m
+ term4 := a / e.Number * p2 * 100
+ return newNumberFormulaArg(term1 + term2 + term3[0] - term4)
+ }
+ fnArgs.Init()
+ fnArgs.PushBack(issue)
+ fnArgs.PushBack(firstCoupon)
+ fnArgs.PushBack(frequency)
+ nc := fn.COUPNUM(fnArgs)
+ lastCoupon := firstCoupon.Number
+ aggrFunc := func(acc []float64, index float64) []float64 {
+ lastCouponTime := timeFromExcelTime(lastCoupon, false)
+ earlyCoupon := daysBetween(excelMinTime1900.Unix(), makeDate(lastCouponTime.Year(), time.Month(float64(lastCouponTime.Month())+numMonthsNeg), lastCouponTime.Day())) + 1
+ earlyCouponTime := timeFromExcelTime(earlyCoupon, false)
+ nl := e.Number
+ if basis == 1 {
+ nl = coupdays(earlyCouponTime, lastCouponTime, basis)
+ }
+ dci := coupdays(issueTime, lastCouponTime, basis)
+ if index > 1 {
+ dci = nl
+ }
+ startDate := earlyCoupon
+ if issue.Number > earlyCoupon {
+ startDate = issue.Number
+ }
+ endDate := lastCoupon
+ if settlement.Number < lastCoupon {
+ endDate = settlement.Number
+ }
+ startDateTime := timeFromExcelTime(startDate, false)
+ endDateTime := timeFromExcelTime(endDate, false)
+ a := coupdays(startDateTime, endDateTime, basis)
+ lastCoupon = earlyCoupon
+ dcnl := acc[0]
+ anl := acc[1]
+ return []float64{dcnl + dci/nl, anl + a/nl}
+ }
+ ag := aggrBetween(math.Floor(nc.Number), 1, []float64{0, 0}, aggrFunc)
+ dcnl, anl := ag[0], ag[1]
+ dsc := 0.0
+ fnArgs.Init()
+ fnArgs.PushBack(settlement)
+ fnArgs.PushBack(firstCoupon)
+ fnArgs.PushBack(frequency)
+ if basis == 2 || basis == 3 {
+ d := timeFromExcelTime(fn.COUPNCD(fnArgs).Number, false)
+ dsc = coupdays(settlementTime, d, basis)
+ } else {
+ d := timeFromExcelTime(fn.COUPPCD(fnArgs).Number, false)
+ a := coupdays(d, settlementTime, basis)
+ dsc = e.Number - a
+ }
+ nq := coupNumber(firstCoupon.Number, settlement.Number, numMonths)
+ fnArgs.Init()
+ fnArgs.PushBack(firstCoupon)
+ fnArgs.PushBack(maturity)
+ fnArgs.PushBack(frequency)
+ fnArgs.PushBack(basisArg)
+ n = fn.COUPNUM(fnArgs)
+ x := yld.Number/m + 1
+ y := dsc / e.Number
+ p1 := x
+ p3 := math.Pow(p1, y+nq+n.Number)
+ term1 := redemption.Number / p3
+ term2 := 100 * rate.Number / m * dcnl / math.Pow(p1, nq+y)
+ f := func(acc []float64, index float64) []float64 {
+ return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index+nq+y)}
+ }
+ term3 := aggrBetween(1, math.Floor(n.Number), []float64{0}, f)
+ term4 := 100 * rate.Number / m * anl
+ return newNumberFormulaArg(term1 + term2 + term3[0] - term4)
}
-// QUOTIENT function returns the integer portion of a division between two
-// supplied numbers. The syntax of the function is:
-//
-// QUOTIENT(numerator,denominator)
+// getODDFPRICE is a part of implementation of the formula function ODDFPRICE.
+func getODDFPRICE(f func(yld float64) float64, x, cnt, prec float64) float64 {
+ const maxCnt = 20.0
+ d := func(f func(yld float64) float64, x float64) float64 {
+ return (f(x+prec) - f(x-prec)) / (2 * prec)
+ }
+ fx, Fx := f(x), d(f, x)
+ newX := x - (fx / Fx)
+ if math.Abs(newX-x) < prec {
+ return newX
+ } else if cnt > maxCnt {
+ return newX
+ }
+ return getODDFPRICE(f, newX, cnt+1, prec)
+}
+
+// ODDFYIELD function calculates the yield of a security with an odd (short or
+// long) first period. The syntax of the function is:
//
-func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("QUOTIENT requires 2 numeric arguments")
- return
+// ODDFYIELD(settlement,maturity,issue,first_coupon,rate,pr,redemption,frequency,[basis])
+func (fn *formulaFuncs) ODDFYIELD(argsList *list.List) formulaArg {
+ if argsList.Len() != 8 && argsList.Len() != 9 {
+ return newErrorFormulaArg(formulaErrorVALUE, "ODDFYIELD requires 8 or 9 arguments")
+ }
+ args := fn.prepareOddfArgs("ODDFYIELD", argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity, issue, firstCoupon, rate, pr, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8]
+ if basisArg.Number < 0 || basisArg.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
+ }
+ settlementTime := timeFromExcelTime(settlement.Number, false)
+ maturityTime := timeFromExcelTime(maturity.Number, false)
+ years := coupdays(settlementTime, maturityTime, int(basisArg.Number))
+ px := pr.Number - 100
+ num := rate.Number*years*100 - px
+ denum := px/4 + years*px/2 + years*100
+ guess := num / denum
+ f := func(yld float64) float64 {
+ fnArgs := list.New().Init()
+ fnArgs.PushBack(settlement)
+ fnArgs.PushBack(maturity)
+ fnArgs.PushBack(issue)
+ fnArgs.PushBack(firstCoupon)
+ fnArgs.PushBack(rate)
+ fnArgs.PushBack(newNumberFormulaArg(yld))
+ fnArgs.PushBack(redemption)
+ fnArgs.PushBack(frequency)
+ fnArgs.PushBack(basisArg)
+ return pr.Number - fn.ODDFPRICE(fnArgs).Number
+ }
+ if result := getODDFPRICE(f, guess, 0, 1e-7); !math.IsInf(result, 0) {
+ return newNumberFormulaArg(result)
+ }
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+}
+
+// prepareOddlArgs checking and prepare arguments for the formula
+// functions ODDLPRICE and ODDLYIELD.
+func (fn *formulaFuncs) prepareOddlArgs(name string, argsList *list.List) formulaArg {
+ dateValues := fn.prepareDataValueArgs(3, argsList)
+ if dateValues.Type != ArgList {
+ return dateValues
}
- var x, y float64
- if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ settlement, maturity, lastInterest := dateValues.List[0], dateValues.List[1], dateValues.List[2]
+ if lastInterest.Number >= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires settlement > last_interest", name))
}
- if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if settlement.Number >= maturity.Number {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name))
}
- if y == 0 {
- err = errors.New(formulaErrorDIV)
- return
+ rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- result = fmt.Sprintf("%g", math.Trunc(x/y))
- return
-}
-
-// RADIANS function converts radians into degrees. The syntax of the function is:
-//
-// RADIANS(angle)
-//
-func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("RADIANS requires 1 numeric argument")
- return
+ if rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name))
}
- var angle float64
- if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ yldOrPr := prepareOddYldOrPrArg(name, argsList.Front().Next().Next().Next().Next().Value.(formulaArg))
+ if yldOrPr.Type != ArgNumber {
+ return yldOrPr
}
- result = fmt.Sprintf("%g", math.Pi/180.0*angle)
- return
-}
-
-// RAND function generates a random real number between 0 and 1. The syntax of
-// the function is:
-//
-// RAND()
-//
-func (fn *formulaFuncs) RAND(argsList *list.List) (result string, err error) {
- if argsList.Len() != 0 {
- err = errors.New("RAND accepts no arguments")
- return
+ redemption := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if redemption.Type != ArgNumber {
+ return redemption
}
- result = fmt.Sprintf("%g", rand.New(rand.NewSource(time.Now().UnixNano())).Float64())
- return
-}
-
-// RANDBETWEEN function generates a random integer between two supplied
-// integers. The syntax of the function is:
-//
-// RANDBETWEEN(bottom,top)
-//
-func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("RANDBETWEEN requires 2 numeric arguments")
- return
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name))
}
- var bottom, top int64
- if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).String, 10, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ frequency := argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if frequency.Type != ArgNumber {
+ return frequency
}
- if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).String, 10, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- if top < bottom {
- err = errors.New(formulaErrorNUM)
- return
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 8 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
}
- result = fmt.Sprintf("%g", float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1)+bottom))
- return
+ return newListFormulaArg([]formulaArg{settlement, maturity, lastInterest, rate, yldOrPr, redemption, frequency, basis})
}
-// romanNumerals defined a numeral system that originated in ancient Rome and
-// remained the usual way of writing numbers throughout Europe well into the
-// Late Middle Ages.
-type romanNumerals struct {
- n float64
- s string
+// oddl is an implementation of the formula functions ODDLPRICE and ODDLYIELD.
+func (fn *formulaFuncs) oddl(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 7 && argsList.Len() != 8 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 7 or 8 arguments", name))
+ }
+ args := fn.prepareOddlArgs(name, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity, lastInterest, rate, prOrYld, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7]
+ if basisArg.Number < 0 || basisArg.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
+ }
+ settlementTime := timeFromExcelTime(settlement.Number, false)
+ maturityTime := timeFromExcelTime(maturity.Number, false)
+ basis := int(basisArg.Number)
+ numMonths := 12 / frequency.Number
+ fnArgs := list.New().Init()
+ fnArgs.PushBack(lastInterest)
+ fnArgs.PushBack(maturity)
+ fnArgs.PushBack(frequency)
+ fnArgs.PushBack(basisArg)
+ nc := fn.COUPNUM(fnArgs)
+ earlyCoupon := lastInterest.Number
+ aggrFunc := func(acc []float64, index float64) []float64 {
+ earlyCouponTime := timeFromExcelTime(earlyCoupon, false)
+ lateCouponTime := changeMonth(earlyCouponTime, numMonths, false)
+ lateCoupon, _ := timeToExcelTime(lateCouponTime, false)
+ nl := coupdays(earlyCouponTime, lateCouponTime, basis)
+ dci := coupdays(earlyCouponTime, maturityTime, basis)
+ if index < nc.Number {
+ dci = nl
+ }
+ var a float64
+ if lateCoupon < settlement.Number {
+ a = dci
+ } else if earlyCoupon < settlement.Number {
+ a = coupdays(earlyCouponTime, settlementTime, basis)
+ }
+ startDate := earlyCoupon
+ if settlement.Number > earlyCoupon {
+ startDate = settlement.Number
+ }
+ endDate := lateCoupon
+ if maturity.Number < lateCoupon {
+ endDate = maturity.Number
+ }
+ startDateTime := timeFromExcelTime(startDate, false)
+ endDateTime := timeFromExcelTime(endDate, false)
+ dsc := coupdays(startDateTime, endDateTime, basis)
+ earlyCoupon = lateCoupon
+ dcnl := acc[0]
+ anl := acc[1]
+ dscnl := acc[2]
+ return []float64{dcnl + dci/nl, anl + a/nl, dscnl + dsc/nl}
+ }
+ ag := aggrBetween(1, math.Floor(nc.Number), []float64{0, 0, 0}, aggrFunc)
+ dcnl, anl, dscnl := ag[0], ag[1], ag[2]
+ x := 100.0 * rate.Number / frequency.Number
+ term1 := dcnl*x + redemption.Number
+ if name == "ODDLPRICE" {
+ term2 := dscnl*prOrYld.Number/frequency.Number + 1
+ term3 := anl * x
+ return newNumberFormulaArg(term1/term2 - term3)
+ }
+ term2 := anl*x + prOrYld.Number
+ term3 := frequency.Number / dscnl
+ return newNumberFormulaArg((term1 - term2) / term2 * term3)
}
-var romanTable = [][]romanNumerals{{{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}},
- {{1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}},
- {{1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}},
- {{1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}},
- {{1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}}
+// ODDLPRICE function calculates the price per $100 face value of a security
+// with an odd (short or long) last period. The syntax of the function is:
+//
+// ODDLPRICE(settlement,maturity,last_interest,rate,yld,redemption,frequency,[basis])
+func (fn *formulaFuncs) ODDLPRICE(argsList *list.List) formulaArg {
+ return fn.oddl("ODDLPRICE", argsList)
+}
-// ROMAN function converts an arabic number to Roman. I.e. for a supplied
-// integer, the function returns a text string depicting the roman numeral
-// form of the number. The syntax of the function is:
+// ODDLYIELD function calculates the yield of a security with an odd (short or
+// long) last period. The syntax of the function is:
//
-// ROMAN(number,[form])
+// ODDLYIELD(settlement,maturity,last_interest,rate,pr,redemption,frequency,[basis])
+func (fn *formulaFuncs) ODDLYIELD(argsList *list.List) formulaArg {
+ return fn.oddl("ODDLYIELD", argsList)
+}
+
+// PDURATION function calculates the number of periods required for an
+// investment to reach a specified future value. The syntax of the function
+// is:
//
-func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("ROMAN requires at least 1 argument")
- return
+// PDURATION(rate,pv,fv)
+func (fn *formulaFuncs) PDURATION(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PDURATION requires 3 arguments")
}
- if argsList.Len() > 2 {
- err = errors.New("ROMAN allows at most 2 arguments")
- return
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- var number float64
- var form int
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ pv := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
}
- if argsList.Len() > 1 {
- if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ fv := argsList.Back().Value.(formulaArg).ToNumber()
+ if fv.Type != ArgNumber {
+ return fv
+ }
+ if rate.Number <= 0 || pv.Number <= 0 || fv.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg((math.Log(fv.Number) - math.Log(pv.Number)) / math.Log(1+rate.Number))
+}
+
+// PMT function calculates the constant periodic payment required to pay off
+// (or partially pay off) a loan or investment, with a constant interest
+// rate, over a specified period. The syntax of the function is:
+//
+// PMT(rate,nper,pv,[fv],[type])
+func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PMT requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PMT allows at most 5 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ nper := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber {
+ return fv
}
- if form < 0 {
- form = 0
- } else if form > 4 {
- form = 4
+ }
+ if argsList.Len() == 5 {
+ if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber {
+ return typ
}
}
- decimalTable := romanTable[0]
- switch form {
- case 1:
- decimalTable = romanTable[1]
- case 2:
- decimalTable = romanTable[2]
- case 3:
- decimalTable = romanTable[3]
- case 4:
- decimalTable = romanTable[4]
+ if typ.Number != 0 && typ.Number != 1 {
+ return newErrorFormulaArg(formulaErrorNA, formulaErrorNA)
}
- val := math.Trunc(number)
- buf := bytes.Buffer{}
- for _, r := range decimalTable {
- for val >= r.n {
- buf.WriteString(r.s)
- val -= r.n
- }
+ if rate.Number != 0 {
+ p := (-fv.Number - pv.Number*math.Pow(1+rate.Number, nper.Number)) / (1 + rate.Number*typ.Number) / ((math.Pow(1+rate.Number, nper.Number) - 1) / rate.Number)
+ return newNumberFormulaArg(p)
}
- result = buf.String()
- return
+ return newNumberFormulaArg((-pv.Number - fv.Number) / nper.Number)
}
-type roundMode byte
-
-const (
- closest roundMode = iota
- down
- up
-)
+// PPMT function calculates the payment on the principal, during a specific
+// period of a loan or investment that is paid in constant periodic payments,
+// with a constant interest rate. The syntax of the function is:
+//
+// PPMT(rate,per,nper,pv,[fv],[type])
+func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg {
+ return fn.ipmt("PPMT", argsList)
+}
-// round rounds a supplied number up or down.
-func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 {
- var significance float64
- if digits > 0 {
- significance = math.Pow(1/10.0, digits)
+// price is an implementation of the formula function PRICE.
+func (fn *formulaFuncs) price(settlement, maturity, rate, yld, redemption, frequency, basis formulaArg) formulaArg {
+ if basis.Number < 0 || basis.Number > 4 {
+ return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
+ }
+ argsList := list.New().Init()
+ argsList.PushBack(settlement)
+ argsList.PushBack(maturity)
+ argsList.PushBack(frequency)
+ argsList.PushBack(basis)
+ e := fn.COUPDAYS(argsList)
+ dsc := fn.COUPDAYSNC(argsList).Number / e.Number
+ n := fn.COUPNUM(argsList)
+ a := fn.COUPDAYBS(argsList)
+ ret := 0.0
+ if n.Number > 1 {
+ ret = redemption.Number / math.Pow(1+yld.Number/frequency.Number, n.Number-1+dsc)
+ ret -= 100 * rate.Number / frequency.Number * a.Number / e.Number
+ t1 := 100 * rate.Number / frequency.Number
+ t2 := 1 + yld.Number/frequency.Number
+ for k := 0.0; k < n.Number; k++ {
+ ret += t1 / math.Pow(t2, k+dsc)
+ }
} else {
- significance = math.Pow(10.0, -digits)
+ dsc = e.Number - a.Number
+ t1 := 100*(rate.Number/frequency.Number) + redemption.Number
+ t2 := (yld.Number/frequency.Number)*(dsc/e.Number) + 1
+ t3 := 100 * (rate.Number / frequency.Number) * (a.Number / e.Number)
+ ret = t1/t2 - t3
+ }
+ return newNumberFormulaArg(ret)
+}
+
+// checkPriceYieldArgs checking and prepare arguments for the formula functions
+// PRICE and YIELD.
+func checkPriceYieldArgs(name string, rate, prYld, redemption, frequency formulaArg) formulaArg {
+ if rate.Type != ArgNumber {
+ return rate
}
- val, res := math.Modf(number / significance)
- switch mode {
- case closest:
- const eps = 0.499999999
- if res >= eps {
- val++
- } else if res <= -eps {
- val--
+ if rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name))
+ }
+ if prYld.Type != ArgNumber {
+ return prYld
+ }
+ if redemption.Type != ArgNumber {
+ return redemption
+ }
+ if name == "PRICE" {
+ if prYld.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICE requires yld >= 0")
}
- case down:
- case up:
- if res > 0 {
- val++
- } else if res < 0 {
- val--
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICE requires redemption > 0")
}
}
- return val * significance
+ if name == "YIELD" {
+ if prYld.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELD requires pr > 0")
+ }
+ if redemption.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELD requires redemption >= 0")
+ }
+ }
+ if frequency.Type != ArgNumber {
+ return frequency
+ }
+ if !validateFrequency(frequency.Number) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newEmptyFormulaArg()
}
-// ROUND function rounds a supplied number up or down, to a specified number
-// of decimal places. The syntax of the function is:
+// priceYield is an implementation of the formula functions PRICE and YIELD.
+func (fn *formulaFuncs) priceYield(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 6 && argsList.Len() != 7 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 6 or 7 arguments", name))
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ prYld := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ redemption := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if arg := checkPriceYieldArgs(name, rate, prYld, redemption, frequency); arg.Type != ArgEmpty {
+ return arg
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 7 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ if name == "PRICE" {
+ return fn.price(settlement, maturity, rate, prYld, redemption, frequency, basis)
+ }
+ return fn.yield(settlement, maturity, rate, prYld, redemption, frequency, basis)
+}
+
+// PRICE function calculates the price, per $100 face value of a security that
+// pays periodic interest. The syntax of the function is:
//
-// ROUND(number,num_digits)
+// PRICE(settlement,maturity,rate,yld,redemption,frequency,[basis])
+func (fn *formulaFuncs) PRICE(argsList *list.List) formulaArg {
+ return fn.priceYield("PRICE", argsList)
+}
+
+// PRICEDISC function calculates the price, per $100 face value of a
+// discounted security. The syntax of the function is:
//
-func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("ROUND requires 2 numeric arguments")
- return
+// PRICEDISC(settlement,maturity,discount,redemption,[basis])
+func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 && argsList.Len() != 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PRICEDISC requires 4 or 5 arguments")
}
- var number, digits float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
}
- if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ settlement, maturity := args.List[0], args.List[1]
+ if maturity.Number <= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEDISC requires maturity > settlement")
}
- result = fmt.Sprintf("%g", fn.round(number, digits, closest))
- return
+ discount := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if discount.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if discount.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEDISC requires discount > 0")
+ }
+ redemption := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if redemption.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEDISC requires redemption > 0")
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
+ }
+ return newNumberFormulaArg(redemption.Number * (1 - discount.Number*frac.Number))
}
-// ROUNDDOWN function rounds a supplied number down towards zero, to a
-// specified number of decimal places. The syntax of the function is:
-//
-// ROUNDDOWN(number,num_digits)
+// PRICEMAT function calculates the price, per $100 face value of a security
+// that pays interest at maturity. The syntax of the function is:
//
-func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("ROUNDDOWN requires 2 numeric arguments")
- return
+// PRICEMAT(settlement,maturity,issue,rate,yld,[basis])
+func (fn *formulaFuncs) PRICEMAT(argsList *list.List) formulaArg {
+ if argsList.Len() != 5 && argsList.Len() != 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PRICEMAT requires 5 or 6 arguments")
}
- var number, digits float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ args := fn.prepareDataValueArgs(3, argsList)
+ if args.Type != ArgList {
+ return args
}
- if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ settlement, maturity, issue := args.List[0], args.List[1], args.List[2]
+ if settlement.Number >= maturity.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires maturity > settlement")
}
- result = fmt.Sprintf("%g", fn.round(number, digits, down))
- return
+ if issue.Number >= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires settlement > issue")
+ }
+ rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ if rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires rate >= 0")
+ }
+ yld := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if yld.Type != ArgNumber {
+ return yld
+ }
+ if yld.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires yld >= 0")
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 6 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ }
+ dsm := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if dsm.Type != ArgNumber {
+ return dsm
+ }
+ dis := yearFrac(issue.Number, settlement.Number, int(basis.Number))
+ dim := yearFrac(issue.Number, maturity.Number, int(basis.Number))
+ return newNumberFormulaArg(((1+dim.Number*rate.Number)/(1+dsm.Number*yld.Number) - dis.Number*rate.Number) * 100)
}
-// ROUNDUP function rounds a supplied number up, away from zero, to a
-// specified number of decimal places. The syntax of the function is:
-//
-// ROUNDUP(number,num_digits)
+// PV function calculates the Present Value of an investment, based on a
+// series of future payments. The syntax of the function is:
//
-func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) {
- if argsList.Len() != 2 {
- err = errors.New("ROUNDUP requires 2 numeric arguments")
- return
+// PV(rate,nper,pmt,[fv],[type])
+func (fn *formulaFuncs) PV(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PV requires at least 3 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "PV allows at most 5 arguments")
+ }
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ nper := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pmt := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pmt.Type != ArgNumber {
+ return pmt
+ }
+ fv := newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber {
+ return fv
+ }
}
- var number, digits float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ t := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if t = argsList.Back().Value.(formulaArg).ToNumber(); t.Type != ArgNumber {
+ return t
+ }
+ if t.Number != 0 {
+ t.Number = 1
+ }
}
- if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if rate.Number == 0 {
+ return newNumberFormulaArg(-pmt.Number*nper.Number - fv.Number)
}
- result = fmt.Sprintf("%g", fn.round(number, digits, up))
- return
+ return newNumberFormulaArg((((1-math.Pow(1+rate.Number, nper.Number))/rate.Number)*pmt.Number*(1+rate.Number*t.Number) - fv.Number) / math.Pow(1+rate.Number, nper.Number))
}
-// SEC function calculates the secant of a given angle. The syntax of the
-// function is:
-//
-// SEC(number)
+// rate is an implementation of the formula function RATE.
+func (fn *formulaFuncs) rate(nper, pmt, pv, fv, t, guess formulaArg) formulaArg {
+ maxIter, iter, isClose, epsMax, rate := 100, 0, false, 1e-6, guess.Number
+ for iter < maxIter && !isClose {
+ t1 := math.Pow(rate+1, nper.Number)
+ t2 := math.Pow(rate+1, nper.Number-1)
+ rt := rate*t.Number + 1
+ p0 := pmt.Number * (t1 - 1)
+ f1 := fv.Number + t1*pv.Number + p0*rt/rate
+ n1 := nper.Number * t2 * pv.Number
+ n2 := p0 * rt / math.Pow(rate, 2)
+ f2 := math.Nextafter(n1, n1) - math.Nextafter(n2, n2)
+ f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate
+ delta := f1 / (f2 + f3)
+ if math.Abs(delta) < epsMax {
+ isClose = true
+ }
+ iter++
+ rate -= delta
+ }
+ return newNumberFormulaArg(rate)
+}
+
+// RATE function calculates the interest rate required to pay off a specified
+// amount of a loan, or to reach a target amount on an investment, over a
+// given period. The syntax of the function is:
//
-func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SEC requires 1 numeric argument")
- return
+// RATE(nper,pmt,pv,[fv],[type],[guess])
+func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg {
+ if argsList.Len() < 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RATE requires at least 3 arguments")
+ }
+ if argsList.Len() > 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RATE allows at most 6 arguments")
+ }
+ nper := argsList.Front().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber {
+ return nper
+ }
+ pmt := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if pmt.Type != ArgNumber {
+ return pmt
+ }
+ pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pv.Type != ArgNumber {
+ return pv
+ }
+ fv := newNumberFormulaArg(0)
+ if argsList.Len() >= 4 {
+ if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber {
+ return fv
+ }
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ t := newNumberFormulaArg(0)
+ if argsList.Len() >= 5 {
+ if t = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); t.Type != ArgNumber {
+ return t
+ }
+ if t.Number != 0 {
+ t.Number = 1
+ }
}
- result = fmt.Sprintf("%g", math.Cos(number))
- return
+ guess := newNumberFormulaArg(0.1)
+ if argsList.Len() == 6 {
+ if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber {
+ return guess
+ }
+ }
+ return fn.rate(nper, pmt, pv, fv, t, guess)
}
-// SECH function calculates the hyperbolic secant (sech) of a supplied angle.
-// The syntax of the function is:
+// RECEIVED function calculates the amount received at maturity for a fully
+// invested security. The syntax of the function is:
//
-// SECH(number)
-//
-func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SECH requires 1 numeric argument")
- return
+// RECEIVED(settlement,maturity,investment,discount,[basis])
+func (fn *formulaFuncs) RECEIVED(argsList *list.List) formulaArg {
+ if argsList.Len() < 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED requires at least 4 arguments")
+ }
+ if argsList.Len() > 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED allows at most 5 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ investment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if investment.Type != ArgNumber {
+ return investment
+ }
+ discount := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if discount.Type != ArgNumber {
+ return discount
+ }
+ if discount.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "RECEIVED requires discount > 0")
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
}
- result = fmt.Sprintf("%g", 1/math.Cosh(number))
- return
+ return newNumberFormulaArg(investment.Number / (1 - discount.Number*frac.Number))
}
-// SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied
-// number. I.e. if the number is positive, the Sign function returns +1, if
-// the number is negative, the function returns -1 and if the number is 0
-// (zero), the function returns 0. The syntax of the function is:
-//
-// SIGN(number)
+// RRI function calculates the equivalent interest rate for an investment with
+// specified present value, future value and duration. The syntax of the
+// function is:
//
-func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SIGN requires 1 numeric argument")
- return
+// RRI(nper,pv,fv)
+func (fn *formulaFuncs) RRI(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "RRI requires 3 arguments")
}
- var val float64
- if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ nper := argsList.Front().Value.(formulaArg).ToNumber()
+ pv := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ fv := argsList.Back().Value.(formulaArg).ToNumber()
+ if nper.Type != ArgNumber || pv.Type != ArgNumber || fv.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- if val < 0 {
- result = "-1"
- return
+ if nper.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "RRI requires nper argument to be > 0")
}
- if val > 0 {
- result = "1"
- return
+ if pv.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "RRI requires pv argument to be > 0")
}
- result = "0"
- return
+ if fv.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "RRI requires fv argument to be >= 0")
+ }
+ return newNumberFormulaArg(math.Pow(fv.Number/pv.Number, 1/nper.Number) - 1)
}
-// SIN function calculates the sine of a given angle. The syntax of the
-// function is:
+// SLN function calculates the straight line depreciation of an asset for one
+// period. The syntax of the function is:
//
-// SIN(number)
+// SLN(cost,salvage,life)
+func (fn *formulaFuncs) SLN(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SLN requires 3 arguments")
+ }
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ salvage := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ life := argsList.Back().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber || salvage.Type != ArgNumber || life.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if life.Number == 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "SLN requires life argument to be > 0")
+ }
+ return newNumberFormulaArg((cost.Number - salvage.Number) / life.Number)
+}
+
+// SYD function calculates the sum-of-years' digits depreciation for a
+// specified period in the lifetime of an asset. The syntax of the function
+// is:
//
-func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SIN requires 1 numeric argument")
- return
+// SYD(cost,salvage,life,per)
+func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 {
+ return newErrorFormulaArg(formulaErrorVALUE, "SYD requires 4 arguments")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ salvage := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ life := argsList.Back().Prev().Value.(formulaArg).ToNumber()
+ per := argsList.Back().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber || salvage.Type != ArgNumber || life.Type != ArgNumber || per.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- result = fmt.Sprintf("%g", math.Sin(number))
- return
+ if life.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "SYD requires life argument to be > 0")
+ }
+ if per.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "SYD requires per argument to be > 0")
+ }
+ if per.Number > life.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(((cost.Number - salvage.Number) * (life.Number - per.Number + 1) * 2) / (life.Number * (life.Number + 1)))
}
-// SINH function calculates the hyperbolic sine (sinh) of a supplied number.
+// TBILLEQ function calculates the bond-equivalent yield for a Treasury Bill.
// The syntax of the function is:
//
-// SINH(number)
-//
-func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SINH requires 1 numeric argument")
- return
+// TBILLEQ(settlement,maturity,discount)
+func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TBILLEQ requires 3 arguments")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
}
- result = fmt.Sprintf("%g", math.Sinh(number))
- return
+ settlement, maturity := args.List[0], args.List[1]
+ dsm := maturity.Number - settlement.Number
+ if dsm > 365 || maturity.Number <= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ discount := argsList.Back().Value.(formulaArg).ToNumber()
+ if discount.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if discount.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg((365 * discount.Number) / (360 - discount.Number*dsm))
}
-// SQRT function calculates the positive square root of a supplied number. The
-// syntax of the function is:
+// TBILLPRICE function returns the price, per $100 face value, of a Treasury
+// Bill. The syntax of the function is:
//
-// SQRT(number)
-//
-func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SQRT requires 1 numeric argument")
- return
+// TBILLPRICE(settlement,maturity,discount)
+func (fn *formulaFuncs) TBILLPRICE(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TBILLPRICE requires 3 arguments")
}
- var res float64
- var value = argsList.Front().Value.(formulaArg).String
- if value == "" {
- result = "0"
- return
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
}
- if res, err = strconv.ParseFloat(value, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ settlement, maturity := args.List[0], args.List[1]
+ dsm := maturity.Number - settlement.Number
+ if dsm > 365 || maturity.Number <= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- if res < 0 {
- err = errors.New(formulaErrorNUM)
- return
+ discount := argsList.Back().Value.(formulaArg).ToNumber()
+ if discount.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
- result = fmt.Sprintf("%g", math.Sqrt(res))
- return
+ if discount.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(100 * (1 - discount.Number*dsm/360))
}
-// SQRTPI function returns the square root of a supplied number multiplied by
-// the mathematical constant, π. The syntax of the function is:
-//
-// SQRTPI(number)
+// TBILLYIELD function calculates the yield of a Treasury Bill. The syntax of
+// the function is:
//
-func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("SQRTPI requires 1 numeric argument")
- return
+// TBILLYIELD(settlement,maturity,pr)
+func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "TBILLYIELD requires 3 arguments")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
}
- result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi))
- return
+ settlement, maturity := args.List[0], args.List[1]
+ dsm := maturity.Number - settlement.Number
+ if dsm > 365 || maturity.Number <= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ pr := argsList.Back().Value.(formulaArg).ToNumber()
+ if pr.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if pr.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(((100 - pr.Number) / pr.Number) * (360 / dsm))
}
-// SUM function adds together a supplied set of numbers and returns the sum of
-// these values. The syntax of the function is:
-//
-// SUM(number1,[number2],...)
-//
-func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) {
- var val, sum float64
- for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg)
- switch token.Type {
- case ArgUnknown:
- continue
- case ArgString:
- if token.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(token.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- sum += val
- case ArgMatrix:
- for _, row := range token.Matrix {
- for _, value := range row {
- if value.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(value.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- sum += val
- }
+// prepareVdbArgs checking and prepare arguments for the formula function
+// VDB.
+func (fn *formulaFuncs) prepareVdbArgs(argsList *list.List) formulaArg {
+ cost := argsList.Front().Value.(formulaArg).ToNumber()
+ if cost.Type != ArgNumber {
+ return cost
+ }
+ if cost.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires cost >= 0")
+ }
+ salvage := argsList.Front().Next().Value.(formulaArg).ToNumber()
+ if salvage.Type != ArgNumber {
+ return salvage
+ }
+ if salvage.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires salvage >= 0")
+ }
+ life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if life.Type != ArgNumber {
+ return life
+ }
+ if life.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires life > 0")
+ }
+ startPeriod := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if startPeriod.Type != ArgNumber {
+ return startPeriod
+ }
+ if startPeriod.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires start_period > 0")
+ }
+ endPeriod := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if endPeriod.Type != ArgNumber {
+ return endPeriod
+ }
+ if startPeriod.Number > endPeriod.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires start_period <= end_period")
+ }
+ if endPeriod.Number > life.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "VDB requires end_period <= life")
+ }
+ factor := newNumberFormulaArg(2)
+ if argsList.Len() > 5 {
+ if factor = argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); factor.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if factor.Number < 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "VDB requires factor >= 0")
+ }
+ }
+ return newListFormulaArg([]formulaArg{cost, salvage, life, startPeriod, endPeriod, factor})
+}
+
+// vdb is a part of implementation of the formula function VDB.
+func (fn *formulaFuncs) vdb(cost, salvage, life, life1, period, factor formulaArg) formulaArg {
+ var ddb, vdb, sln, term float64
+ endInt, cs, nowSln := math.Ceil(period.Number), cost.Number-salvage.Number, false
+ ddbArgs := list.New()
+ for i := 1.0; i <= endInt; i++ {
+ if !nowSln {
+ ddbArgs.Init()
+ ddbArgs.PushBack(cost)
+ ddbArgs.PushBack(salvage)
+ ddbArgs.PushBack(life)
+ ddbArgs.PushBack(newNumberFormulaArg(i))
+ ddbArgs.PushBack(factor)
+ ddb = fn.DDB(ddbArgs).Number
+ sln = cs / (life1.Number - i + 1)
+ if sln > ddb {
+ term = sln
+ nowSln = true
+ } else {
+ term = ddb
+ cs -= ddb
}
+ } else {
+ term = sln
}
+ if i == endInt {
+ term *= period.Number + 1 - endInt
+ }
+ vdb += term
}
- result = fmt.Sprintf("%g", sum)
- return
+ return newNumberFormulaArg(vdb)
}
-// SUMIF function finds the values in a supplied array, that satisfy a given
-// criteria, and returns the sum of the corresponding values in a second
-// supplied array. The syntax of the function is:
-//
-// SUMIF(range,criteria,[sum_range])
+// VDB function calculates the depreciation of an asset, using the Double
+// Declining Balance Method, or another specified depreciation rate, for a
+// specified period (including partial periods). The syntax of the function
+// is:
//
-func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) {
- if argsList.Len() < 2 {
- err = errors.New("SUMIF requires at least 2 argument")
- return
- }
- var criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String)
- var rangeMtx = argsList.Front().Value.(formulaArg).Matrix
- var sumRange [][]formulaArg
- if argsList.Len() == 3 {
- sumRange = argsList.Back().Value.(formulaArg).Matrix
+// VDB(cost,salvage,life,start_period,end_period,[factor],[no_switch])
+func (fn *formulaFuncs) VDB(argsList *list.List) formulaArg {
+ if argsList.Len() < 5 || argsList.Len() > 7 {
+ return newErrorFormulaArg(formulaErrorVALUE, "VDB requires 5 or 7 arguments")
+ }
+ args := fn.prepareVdbArgs(argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ cost, salvage, life, startPeriod, endPeriod, factor := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5]
+ noSwitch := newBoolFormulaArg(false)
+ if argsList.Len() > 6 {
+ if noSwitch = argsList.Back().Value.(formulaArg).ToBool(); noSwitch.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
}
- var sum, val float64
- for rowIdx, row := range rangeMtx {
- for colIdx, col := range row {
- var ok bool
- fromVal := col.String
- if col.String == "" {
- continue
- }
- if ok, err = formulaCriteriaEval(fromVal, criteria); err != nil {
- return
- }
- if ok {
- if argsList.Len() == 3 {
- if len(sumRange) <= rowIdx || len(sumRange[rowIdx]) <= colIdx {
- continue
- }
- fromVal = sumRange[rowIdx][colIdx].String
- }
- if val, err = strconv.ParseFloat(fromVal, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- sum += val
+ startInt, endInt, vdb, ddbArgs := math.Floor(startPeriod.Number), math.Ceil(endPeriod.Number), newNumberFormulaArg(0), list.New()
+ if noSwitch.Number == 1 {
+ for i := startInt + 1; i <= endInt; i++ {
+ ddbArgs.Init()
+ ddbArgs.PushBack(cost)
+ ddbArgs.PushBack(salvage)
+ ddbArgs.PushBack(life)
+ ddbArgs.PushBack(newNumberFormulaArg(i))
+ ddbArgs.PushBack(factor)
+ term := fn.DDB(ddbArgs)
+ if i == startInt+1 {
+ term.Number *= math.Min(endPeriod.Number, startInt+1) - startPeriod.Number
+ } else if i == endInt {
+ term.Number *= endPeriod.Number + 1 - endInt
}
+ vdb.Number += term.Number
}
+ return vdb
}
- result = fmt.Sprintf("%g", sum)
- return
+ life1, part := life, 0.0
+ if startPeriod.Number != math.Floor(startPeriod.Number) && factor.Number > 1.0 && startPeriod.Number >= life.Number/2.0 {
+ part = startPeriod.Number - life.Number/2.0
+ startPeriod.Number = life.Number / 2.0
+ endPeriod.Number -= part
+ }
+ cost.Number -= fn.vdb(cost, salvage, life, life1, startPeriod, factor).Number
+ return fn.vdb(cost, salvage, life, newNumberFormulaArg(life.Number-startPeriod.Number), newNumberFormulaArg(endPeriod.Number-startPeriod.Number), factor)
}
-// SUMSQ function returns the sum of squares of a supplied set of values. The
-// syntax of the function is:
-//
-// SUMSQ(number1,[number2],...)
-//
-func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) {
- var val, sq float64
- for arg := argsList.Front(); arg != nil; arg = arg.Next() {
- token := arg.Value.(formulaArg)
- switch token.Type {
- case ArgString:
- if token.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(token.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- sq += val * val
- case ArgMatrix:
- for _, row := range token.Matrix {
- for _, value := range row {
- if value.String == "" {
- continue
- }
- if val, err = strconv.ParseFloat(value.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- sq += val * val
- }
+// prepareXArgs prepare arguments for the formula function XIRR and XNPV.
+func (fn *formulaFuncs) prepareXArgs(values, dates formulaArg) (valuesArg, datesArg []float64, err formulaArg) {
+ for _, arg := range values.ToList() {
+ if numArg := arg.ToNumber(); numArg.Type == ArgNumber {
+ valuesArg = append(valuesArg, numArg.Number)
+ continue
+ }
+ err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ return
+ }
+ if len(valuesArg) < 2 {
+ err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ return
+ }
+ date := 0.0
+ for _, arg := range dates.ToList() {
+ if arg.Type == ArgNumber {
+ datesArg = append(datesArg, arg.Number)
+ if arg.Number < date {
+ err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ return
}
+ date = arg.Number
+ continue
}
+ err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ return
+ }
+ if len(valuesArg) != len(datesArg) {
+ err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ return
}
- result = fmt.Sprintf("%g", sq)
+ err = newEmptyFormulaArg()
return
}
-// TAN function calculates the tangent of a given angle. The syntax of the
-// function is:
-//
-// TAN(number)
-//
-func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("TAN requires 1 numeric argument")
- return
+// xirr is an implementation of the formula function XIRR.
+func (fn *formulaFuncs) xirr(values, dates []float64, guess float64) formulaArg {
+ positive, negative := false, false
+ for i := 0; i < len(values); i++ {
+ if values[i] > 0 {
+ positive = true
+ }
+ if values[i] < 0 {
+ negative = true
+ }
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ if !positive || !negative {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ result, epsMax, count, maxIterate, err := guess, 1e-10, 0, 50, false
+ for {
+ resultValue := xirrPart1(values, dates, result)
+ newRate := result - resultValue/xirrPart2(values, dates, result)
+ epsRate := math.Abs(newRate - result)
+ result = newRate
+ count++
+ if epsRate <= epsMax || math.Abs(resultValue) <= epsMax {
+ break
+ }
+ if count > maxIterate {
+ err = true
+ break
+ }
}
- result = fmt.Sprintf("%g", math.Tan(number))
- return
+ if err || math.IsNaN(result) || math.IsInf(result, 0) {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ return newNumberFormulaArg(result)
}
-// TANH function calculates the hyperbolic tangent (tanh) of a supplied
-// number. The syntax of the function is:
-//
-// TANH(number)
+// xirrPart1 is a part of implementation of the formula function XIRR.
+func xirrPart1(values, dates []float64, rate float64) float64 {
+ r := rate + 1
+ result := values[0]
+ vlen := len(values)
+ firstDate := dates[0]
+ for i := 1; i < vlen; i++ {
+ result += values[i] / math.Pow(r, (dates[i]-firstDate)/365)
+ }
+ return result
+}
+
+// xirrPart2 is a part of implementation of the formula function XIRR.
+func xirrPart2(values, dates []float64, rate float64) float64 {
+ r := rate + 1
+ result := 0.0
+ vlen := len(values)
+ firstDate := dates[0]
+ for i := 1; i < vlen; i++ {
+ frac := (dates[i] - firstDate) / 365
+ result -= frac * values[i] / math.Pow(r, frac+1)
+ }
+ return result
+}
+
+// XIRR function returns the Internal Rate of Return for a supplied series of
+// cash flows (i.e. a set of values, which includes an initial investment
+// value and a series of net income values) occurring at a series of supplied
+// dates. The syntax of the function is:
//
-func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("TANH requires 1 numeric argument")
- return
+// XIRR(values,dates,[guess])
+func (fn *formulaFuncs) XIRR(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 && argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XIRR requires 2 or 3 arguments")
}
- var number float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ values, dates, err := fn.prepareXArgs(argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg))
+ if err.Type != ArgEmpty {
+ return err
}
- result = fmt.Sprintf("%g", math.Tanh(number))
- return
+ guess := newNumberFormulaArg(0)
+ if argsList.Len() == 3 {
+ if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
+ }
+ if guess.Number <= -1 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XIRR requires guess > -1")
+ }
+ }
+ return fn.xirr(values, dates, guess.Number)
}
-// TRUNC function truncates a supplied number to a specified number of decimal
-// places. The syntax of the function is:
+// XNPV function calculates the Net Present Value for a schedule of cash flows
+// that is not necessarily periodic. The syntax of the function is:
//
-// TRUNC(number,[number_digits])
-//
-func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("TRUNC requires at least 1 argument")
- return
+// XNPV(rate,values,dates)
+func (fn *formulaFuncs) XNPV(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XNPV requires 3 arguments")
}
- var number, digits, adjust, rtrim float64
- if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
+ rate := argsList.Front().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
}
- if argsList.Len() > 1 {
- if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- digits = math.Floor(digits)
+ if rate.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorVALUE, "XNPV requires rate > 0")
}
- adjust = math.Pow(10, digits)
- x := int((math.Abs(number) - math.Abs(float64(int(number)))) * adjust)
- if x != 0 {
- if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil {
- return
- }
+ values, dates, err := fn.prepareXArgs(argsList.Front().Next().Value.(formulaArg), argsList.Back().Value.(formulaArg))
+ if err.Type != ArgEmpty {
+ return err
}
- if (digits > 0) && (rtrim < adjust/10) {
- result = fmt.Sprintf("%g", number)
- return
+ date1, xnpv := dates[0], 0.0
+ for idx, value := range values {
+ xnpv += value / math.Pow(1+rate.Number, (dates[idx]-date1)/365)
}
- result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust)
- return
+ return newNumberFormulaArg(xnpv)
}
-// Statistical functions
-
-// COUNTA function returns the number of non-blanks within a supplied set of
-// cells or values. The syntax of the function is:
-//
-// COUNTA(value1,[value2],...)
-//
-func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) {
- var count int
- for token := argsList.Front(); token != nil; token = token.Next() {
- arg := token.Value.(formulaArg)
- switch arg.Type {
- case ArgString:
- if arg.String != "" {
- count++
- }
- case ArgMatrix:
- for _, row := range arg.Matrix {
- for _, value := range row {
- if value.String != "" {
- count++
- }
- }
+// yield is an implementation of the formula function YIELD.
+func (fn *formulaFuncs) yield(settlement, maturity, rate, pr, redemption, frequency, basis formulaArg) formulaArg {
+ priceN, yield1, yield2 := newNumberFormulaArg(0), newNumberFormulaArg(0), newNumberFormulaArg(1)
+ price1 := fn.price(settlement, maturity, rate, yield1, redemption, frequency, basis)
+ if price1.Type != ArgNumber {
+ return price1
+ }
+ price2 := fn.price(settlement, maturity, rate, yield2, redemption, frequency, basis)
+ yieldN := newNumberFormulaArg((yield2.Number - yield1.Number) * 0.5)
+ for iter := 0; iter < 100 && priceN.Number != pr.Number; iter++ {
+ priceN = fn.price(settlement, maturity, rate, yieldN, redemption, frequency, basis)
+ if pr.Number == price1.Number {
+ return yield1
+ } else if pr.Number == price2.Number {
+ return yield2
+ } else if pr.Number == priceN.Number {
+ return yieldN
+ } else if pr.Number < price2.Number {
+ yield2.Number *= 2.0
+ price2 = fn.price(settlement, maturity, rate, yield2, redemption, frequency, basis)
+ yieldN.Number = (yield2.Number - yield1.Number) * 0.5
+ } else {
+ if pr.Number < priceN.Number {
+ yield1 = yieldN
+ price1 = priceN
+ } else {
+ yield2 = yieldN
+ price2 = priceN
}
+ f1 := (yield2.Number - yield1.Number) * ((pr.Number - price2.Number) / (price1.Number - price2.Number))
+ yieldN.Number = yield2.Number - math.Nextafter(f1, f1)
}
}
- result = fmt.Sprintf("%d", count)
- return
+ return yieldN
}
-// MEDIAN function returns the statistical median (the middle value) of a list
-// of supplied numbers. The syntax of the function is:
+// YIELD function calculates the Yield of a security that pays periodic
+// interest. The syntax of the function is:
//
-// MEDIAN(number1,[number2],...)
+// YIELD(settlement,maturity,rate,pr,redemption,frequency,[basis])
+func (fn *formulaFuncs) YIELD(argsList *list.List) formulaArg {
+ return fn.priceYield("YIELD", argsList)
+}
+
+// YIELDDISC function calculates the annual yield of a discounted security.
+// The syntax of the function is:
//
-func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) {
- if argsList.Len() == 0 {
- err = errors.New("MEDIAN requires at least 1 argument")
- return
- }
- values := []float64{}
- var median, digits float64
- for token := argsList.Front(); token != nil; token = token.Next() {
- arg := token.Value.(formulaArg)
- switch arg.Type {
- case ArgString:
- if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- values = append(values, digits)
- case ArgMatrix:
- for _, row := range arg.Matrix {
- for _, value := range row {
- if value.String == "" {
- continue
- }
- if digits, err = strconv.ParseFloat(value.String, 64); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- values = append(values, digits)
- }
- }
+// YIELDDISC(settlement,maturity,pr,redemption,[basis])
+func (fn *formulaFuncs) YIELDDISC(argsList *list.List) formulaArg {
+ if argsList.Len() != 4 && argsList.Len() != 5 {
+ return newErrorFormulaArg(formulaErrorVALUE, "YIELDDISC requires 4 or 5 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ pr := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if pr.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if pr.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELDDISC requires pr > 0")
+ }
+ redemption := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if redemption.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ if redemption.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELDDISC requires redemption > 0")
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 5 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
}
- sort.Float64s(values)
- if len(values)%2 == 0 {
- median = (values[len(values)/2-1] + values[len(values)/2]) / 2
- } else {
- median = values[len(values)/2]
+ frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ if frac.Type != ArgNumber {
+ return frac
}
- result = fmt.Sprintf("%g", median)
- return
+ return newNumberFormulaArg((redemption.Number/pr.Number - 1) / frac.Number)
}
-// Information functions
-
-// ISBLANK function tests if a specified cell is blank (empty) and if so,
-// returns TRUE; Otherwise the function returns FALSE. The syntax of the
-// function is:
+// YIELDMAT function calculates the annual yield of a security that pays
+// interest at maturity. The syntax of the function is:
//
-// ISBLANK(value)
-//
-func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISBLANK requires 1 argument")
- return
+// YIELDMAT(settlement,maturity,issue,rate,pr,[basis])
+func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg {
+ if argsList.Len() != 5 && argsList.Len() != 6 {
+ return newErrorFormulaArg(formulaErrorVALUE, "YIELDMAT requires 5 or 6 arguments")
+ }
+ args := fn.prepareDataValueArgs(2, argsList)
+ if args.Type != ArgList {
+ return args
+ }
+ settlement, maturity := args.List[0], args.List[1]
+ arg := list.New().Init()
+ issue := argsList.Front().Next().Next().Value.(formulaArg).ToNumber()
+ if issue.Type != ArgNumber {
+ arg.PushBack(argsList.Front().Next().Next().Value.(formulaArg))
+ issue = fn.DATEVALUE(arg)
+ if issue.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
}
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- switch token.Type {
- case ArgUnknown:
- result = "TRUE"
- case ArgString:
- if token.String == "" {
- result = "TRUE"
+ if issue.Number >= settlement.Number {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires settlement > issue")
+ }
+ rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if rate.Type != ArgNumber {
+ return rate
+ }
+ if rate.Number < 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires rate >= 0")
+ }
+ pr := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
+ if pr.Type != ArgNumber {
+ return pr
+ }
+ if pr.Number <= 0 {
+ return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires pr > 0")
+ }
+ basis := newNumberFormulaArg(0)
+ if argsList.Len() == 6 {
+ if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
}
- return
+ dim := yearFrac(issue.Number, maturity.Number, int(basis.Number))
+ if dim.Type != ArgNumber {
+ return dim
+ }
+ dis := yearFrac(issue.Number, settlement.Number, int(basis.Number))
+ dsm := yearFrac(settlement.Number, maturity.Number, int(basis.Number))
+ f1 := dim.Number * rate.Number
+ result := 1 + math.Nextafter(f1, f1)
+ result /= pr.Number/100 + dis.Number*rate.Number
+ result--
+ result /= dsm.Number
+ return newNumberFormulaArg(result)
}
-// ISERR function tests if an initial supplied expression (or value) returns
-// any Excel Error, except the #N/A error. If so, the function returns the
-// logical value TRUE; If the supplied value is not an error or is the #N/A
-// error, the ISERR function returns FALSE. The syntax of the function is:
-//
-// ISERR(value)
-//
-func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISERR requires 1 argument")
- return
+// Database Functions
+
+// calcDatabase defines the structure for formula database.
+type calcDatabase struct {
+ col, row int
+ indexMap map[int]int
+ database [][]formulaArg
+ criteria [][]formulaArg
+}
+
+// newCalcDatabase function returns formula database by given data range of
+// cells containing the database, field and criteria range.
+func newCalcDatabase(database, field, criteria formulaArg) *calcDatabase {
+ db := calcDatabase{
+ indexMap: make(map[int]int),
+ database: database.Matrix,
+ criteria: criteria.Matrix,
+ }
+ exp := len(database.Matrix) < 2 || len(database.Matrix[0]) < 1 ||
+ len(criteria.Matrix) < 2 || len(criteria.Matrix[0]) < 1
+ if field.Type != ArgEmpty {
+ if db.col = db.columnIndex(database.Matrix, field); exp || db.col < 0 || len(db.database[0]) <= db.col {
+ return nil
+ }
+ return &db
}
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- if token.Type == ArgString {
- for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} {
- if errType == token.String {
- result = "TRUE"
+ if db.col = -1; exp {
+ return nil
+ }
+ return &db
+}
+
+// columnIndex return index by specifies column field within the database for
+// which user want to return the count of non-blank cells.
+func (db *calcDatabase) columnIndex(database [][]formulaArg, field formulaArg) int {
+ num := field.ToNumber()
+ if num.Type != ArgNumber && len(database) > 0 {
+ for i := 0; i < len(database[0]); i++ {
+ if title := database[0][i]; strings.EqualFold(title.Value(), field.Value()) {
+ return i
}
}
+ return -1
}
- return
+ return int(num.Number - 1)
}
-// ISERROR function tests if an initial supplied expression (or value) returns
-// an Excel Error, and if so, returns the logical value TRUE; Otherwise the
-// function returns FALSE. The syntax of the function is:
-//
-// ISERROR(value)
-//
-func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISERROR requires 1 argument")
- return
+// criteriaEval evaluate formula criteria expression.
+func (db *calcDatabase) criteriaEval() bool {
+ var (
+ columns, rows = len(db.criteria[0]), len(db.criteria)
+ criteria = db.criteria
+ k int
+ matched bool
+ )
+ if len(db.indexMap) == 0 {
+ fields := criteria[0]
+ for j := 0; j < columns; j++ {
+ if k = db.columnIndex(db.database, fields[j]); k < 0 {
+ return false
+ }
+ db.indexMap[j] = k
+ }
}
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- if token.Type == ArgString {
- for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} {
- if errType == token.String {
- result = "TRUE"
+ for i := 1; !matched && i < rows; i++ {
+ matched = true
+ for j := 0; matched && j < columns; j++ {
+ criteriaExp := db.criteria[i][j]
+ if criteriaExp.Value() == "" {
+ continue
}
+ criteria := formulaCriteriaParser(criteriaExp)
+ cell := db.database[db.row][db.indexMap[j]]
+ matched, _ = formulaCriteriaEval(cell, criteria)
}
}
- return
+ return matched
}
-// ISEVEN function tests if a supplied number (or numeric expression)
-// evaluates to an even number, and if so, returns TRUE; Otherwise, the
-// function returns FALSE. The syntax of the function is:
-//
-// ISEVEN(value)
-//
-func (fn *formulaFuncs) ISEVEN(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISEVEN requires 1 argument")
- return
+// value returns the current cell value.
+func (db *calcDatabase) value() formulaArg {
+ if db.col == -1 {
+ return db.database[db.row][len(db.database[db.row])-1]
}
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- var numeric int
- if token.Type == ArgString {
- if numeric, err = strconv.Atoi(token.String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if numeric == numeric/2*2 {
- result = "TRUE"
- return
+ return db.database[db.row][db.col]
+}
+
+// next will return true if find the matched cell in the database.
+func (db *calcDatabase) next() bool {
+ matched, rows := false, len(db.database)
+ for !matched && db.row < rows {
+ if db.row++; db.row < rows {
+ matched = db.criteriaEval()
}
}
- return
+ return matched
}
-// ISNA function tests if an initial supplied expression (or value) returns
-// the Excel #N/A Error, and if so, returns TRUE; Otherwise the function
-// returns FALSE. The syntax of the function is:
-//
-// ISNA(value)
-//
-func (fn *formulaFuncs) ISNA(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISNA requires 1 argument")
- return
+// database is an implementation of the formula functions DAVERAGE, DMAX and DMIN.
+func (fn *formulaFuncs) database(name string, argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name))
+ }
+ database := argsList.Front().Value.(formulaArg)
+ field := argsList.Front().Next().Value.(formulaArg)
+ criteria := argsList.Back().Value.(formulaArg)
+ db := newCalcDatabase(database, field, criteria)
+ if db == nil {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- if token.Type == ArgString && token.String == formulaErrorNA {
- result = "TRUE"
+ args := list.New()
+ for db.next() {
+ args.PushBack(db.value())
+ }
+ switch name {
+ case "DMAX":
+ return fn.MAX(args)
+ case "DMIN":
+ return fn.MIN(args)
+ case "DPRODUCT":
+ return fn.PRODUCT(args)
+ case "DSTDEV":
+ return fn.STDEV(args)
+ case "DSTDEVP":
+ return fn.STDEVP(args)
+ case "DSUM":
+ return fn.SUM(args)
+ case "DVAR":
+ return fn.VAR(args)
+ case "DVARP":
+ return fn.VARP(args)
+ default:
+ return fn.AVERAGE(args)
}
- return
}
-// ISNONTEXT function function tests if a supplied value is text. If not, the
-// function returns TRUE; If the supplied value is text, the function returns
-// FALSE. The syntax of the function is:
+// DAVERAGE function calculates the average (statistical mean) of values in a
+// field (column) in a database for selected records, that satisfy
+// user-specified criteria. The syntax of the function is:
//
-// ISNONTEXT(value)
-//
-func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISNONTEXT requires 1 argument")
- return
+// DAVERAGE(database,field,criteria)
+func (fn *formulaFuncs) DAVERAGE(argsList *list.List) formulaArg {
+ return fn.database("DAVERAGE", argsList)
+}
+
+// dcount is an implementation of the formula functions DCOUNT and DCOUNTA.
+func (fn *formulaFuncs) dcount(name string, argsList *list.List) formulaArg {
+ if argsList.Len() < 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
}
- token := argsList.Front().Value.(formulaArg)
- result = "TRUE"
- if token.Type == ArgString && token.String != "" {
- result = "FALSE"
+ if argsList.Len() > 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name))
}
- return
+ field := newEmptyFormulaArg()
+ criteria := argsList.Back().Value.(formulaArg)
+ if argsList.Len() > 2 {
+ field = argsList.Front().Next().Value.(formulaArg)
+ }
+ database := argsList.Front().Value.(formulaArg)
+ db := newCalcDatabase(database, field, criteria)
+ if db == nil {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ args := list.New()
+ for db.next() {
+ args.PushBack(db.value())
+ }
+ if name == "DCOUNT" {
+ return fn.COUNT(args)
+ }
+ return fn.COUNTA(args)
}
-// ISNUMBER function function tests if a supplied value is a number. If so,
-// the function returns TRUE; Otherwise it returns FALSE. The syntax of the
-// function is:
+// DCOUNT function returns the number of cells containing numeric values, in a
+// field (column) of a database for selected records only. The records to be
+// included in the count are those that satisfy a set of one or more
+// user-specified criteria. The syntax of the function is:
//
-// ISNUMBER(value)
+// DCOUNT(database,[field],criteria)
+func (fn *formulaFuncs) DCOUNT(argsList *list.List) formulaArg {
+ return fn.dcount("DCOUNT", argsList)
+}
+
+// DCOUNTA function returns the number of non-blank cells, in a field
+// (column) of a database for selected records only. The records to be
+// included in the count are those that satisfy a set of one or more
+// user-specified criteria. The syntax of the function is:
//
-func (fn *formulaFuncs) ISNUMBER(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISNUMBER requires 1 argument")
- return
- }
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- if token.Type == ArgString && token.String != "" {
- if _, err = strconv.Atoi(token.String); err == nil {
- result = "TRUE"
+// DCOUNTA(database,[field],criteria)
+func (fn *formulaFuncs) DCOUNTA(argsList *list.List) formulaArg {
+ return fn.dcount("DCOUNTA", argsList)
+}
+
+// DGET function returns a single value from a column of a database. The record
+// is selected via a set of one or more user-specified criteria. The syntax of
+// the function is:
+//
+// DGET(database,field,criteria)
+func (fn *formulaFuncs) DGET(argsList *list.List) formulaArg {
+ if argsList.Len() != 3 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DGET requires 3 arguments")
+ }
+ database := argsList.Front().Value.(formulaArg)
+ field := argsList.Front().Next().Value.(formulaArg)
+ criteria := argsList.Back().Value.(formulaArg)
+ db := newCalcDatabase(database, field, criteria)
+ if db == nil {
+ return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ }
+ value := newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ if db.next() {
+ if value = db.value(); db.next() {
+ return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
- err = nil
}
- return
+ return value
}
-// ISODD function tests if a supplied number (or numeric expression) evaluates
-// to an odd number, and if so, returns TRUE; Otherwise, the function returns
-// FALSE. The syntax of the function is:
+// DMAX function finds the maximum value in a field (column) in a database for
+// selected records only. The records to be included in the calculation are
+// defined by a set of one or more user-specified criteria. The syntax of the
+// function is:
//
-// ISODD(value)
+// DMAX(database,field,criteria)
+func (fn *formulaFuncs) DMAX(argsList *list.List) formulaArg {
+ return fn.database("DMAX", argsList)
+}
+
+// DMIN function finds the minimum value in a field (column) in a database for
+// selected records only. The records to be included in the calculation are
+// defined by a set of one or more user-specified criteria. The syntax of the
+// function is:
//
-func (fn *formulaFuncs) ISODD(argsList *list.List) (result string, err error) {
- if argsList.Len() != 1 {
- err = errors.New("ISODD requires 1 argument")
- return
- }
- token := argsList.Front().Value.(formulaArg)
- result = "FALSE"
- var numeric int
- if token.Type == ArgString {
- if numeric, err = strconv.Atoi(token.String); err != nil {
- err = errors.New(formulaErrorVALUE)
- return
- }
- if numeric != numeric/2*2 {
- result = "TRUE"
- return
- }
- }
- return
+// DMIN(database,field,criteria)
+func (fn *formulaFuncs) DMIN(argsList *list.List) formulaArg {
+ return fn.database("DMIN", argsList)
}
-// NA function returns the Excel #N/A error. This error message has the
-// meaning 'value not available' and is produced when an Excel Formula is
-// unable to find a value that it needs. The syntax of the function is:
+// DPRODUCT function calculates the product of a field (column) in a database
+// for selected records, that satisfy user-specified criteria. The syntax of
+// the function is:
+//
+// DPRODUCT(database,field,criteria)
+func (fn *formulaFuncs) DPRODUCT(argsList *list.List) formulaArg {
+ return fn.database("DPRODUCT", argsList)
+}
+
+// DSTDEV function calculates the sample standard deviation of a field
+// (column) in a database for selected records only. The records to be
+// included in the calculation are defined by a set of one or more
+// user-specified criteria. The syntax of the function is:
//
-// NA()
+// DSTDEV(database,field,criteria)
+func (fn *formulaFuncs) DSTDEV(argsList *list.List) formulaArg {
+ return fn.database("DSTDEV", argsList)
+}
+
+// DSTDEVP function calculates the standard deviation of a field (column) in a
+// database for selected records only. The records to be included in the
+// calculation are defined by a set of one or more user-specified criteria.
+// The syntax of the function is:
//
-func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) {
- if argsList.Len() != 0 {
- err = errors.New("NA accepts no arguments")
- return
+// DSTDEVP(database,field,criteria)
+func (fn *formulaFuncs) DSTDEVP(argsList *list.List) formulaArg {
+ return fn.database("DSTDEVP", argsList)
+}
+
+// DSUM function calculates the sum of a field (column) in a database for
+// selected records, that satisfy user-specified criteria. The syntax of the
+// function is:
+//
+// DSUM(database,field,criteria)
+func (fn *formulaFuncs) DSUM(argsList *list.List) formulaArg {
+ return fn.database("DSUM", argsList)
+}
+
+// DVAR function calculates the sample variance of a field (column) in a
+// database for selected records only. The records to be included in the
+// calculation are defined by a set of one or more user-specified criteria.
+// The syntax of the function is:
+//
+// DVAR(database,field,criteria)
+func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg {
+ return fn.database("DVAR", argsList)
+}
+
+// DVARP function calculates the variance (for an entire population), of the
+// values in a field (column) in a database for selected records only. The
+// records to be included in the calculation are defined by a set of one or
+// more user-specified criteria. The syntax of the function is:
+//
+// DVARP(database,field,criteria)
+func (fn *formulaFuncs) DVARP(argsList *list.List) formulaArg {
+ return fn.database("DVARP", argsList)
+}
+
+// DISPIMG function calculates the Kingsoft WPS Office embedded image ID. The
+// syntax of the function is:
+//
+// DISPIMG(picture_name,display_mode)
+func (fn *formulaFuncs) DISPIMG(argsList *list.List) formulaArg {
+ if argsList.Len() != 2 {
+ return newErrorFormulaArg(formulaErrorVALUE, "DISPIMG requires 2 numeric arguments")
}
- result = formulaErrorNA
- return
+ return argsList.Front().Value.(formulaArg)
}
diff --git a/calc_test.go b/calc_test.go
index 4298aa7a39..5f24cd3544 100644
--- a/calc_test.go
+++ b/calc_test.go
@@ -1,12 +1,27 @@
package excelize
import (
+ "container/list"
+ "math"
"path/filepath"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/xuri/efp"
)
+func prepareCalcData(cellData [][]interface{}) *File {
+ f := NewFile()
+ for r, row := range cellData {
+ for c, value := range row {
+ cell, _ := CoordinatesToCellName(c+1, r+1)
+ _ = f.SetCellValue("Sheet1", cell, value)
+ }
+ }
+ return f
+}
+
func TestCalcCellValue(t *testing.T) {
cellData := [][]interface{}{
{1, 4, nil, "Month", "Team", "Sales"},
@@ -19,137 +34,527 @@ func TestCalcCellValue(t *testing.T) {
{nil, nil, nil, "Feb", "South 1", 32080},
{nil, nil, nil, "Feb", "South 2", 45500},
}
- prepareData := func() *File {
- f := NewFile()
- for r, row := range cellData {
- for c, value := range row {
- cell, _ := CoordinatesToCellName(c+1, r+1)
- assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
- }
- }
- return f
- }
-
mathCalc := map[string]string{
+ "=2^3": "8",
+ "=1=1": "TRUE",
+ "=1=2": "FALSE",
+ "=1<2": "TRUE",
+ "=3<2": "FALSE",
+ "=1<\"-1\"": "TRUE",
+ "=\"-1\"<1": "FALSE",
+ "=\"-1\"<\"-2\"": "TRUE",
+ "=2<=3": "TRUE",
+ "=2<=1": "FALSE",
+ "=1<=\"-1\"": "TRUE",
+ "=\"-1\"<=1": "FALSE",
+ "=\"-1\"<=\"-2\"": "TRUE",
+ "=2>1": "TRUE",
+ "=2>3": "FALSE",
+ "=1>\"-1\"": "FALSE",
+ "=\"-1\">-1": "TRUE",
+ "=\"-1\">\"-2\"": "FALSE",
+ "=2>=1": "TRUE",
+ "=2>=3": "FALSE",
+ "=1>=\"-1\"": "FALSE",
+ "=\"-1\">=-1": "TRUE",
+ "=\"-1\">=\"-2\"": "FALSE",
+ "=-----1+1": "0",
+ "=------1+1": "2",
+ "=---1---1": "-2",
+ "=---1----1": "0",
+ "=1&2": "12",
+ "=15%": "0.15",
+ "=1+20%": "1.2",
+ "={1}+2": "3",
+ "=1+{2}": "3",
+ "={1}+{2}": "3",
+ "=A1+(B1-C1)": "5",
+ "=A1+(C1-B1)": "-3",
+ "=A1&B1&C1": "14",
+ "=B1+C1": "4",
+ "=C1+B1": "4",
+ "=C1+C1": "0",
+ "=\"A\"=\"A\"": "TRUE",
+ "=\"A\"<>\"A\"": "FALSE",
+ "=TRUE()&FALSE()": "TRUEFALSE",
+ "=TRUE()&FALSE()<>FALSE": "TRUE",
+ "=TRUE()&\"1\"": "TRUE1",
+ "=TRUE<>FALSE()": "TRUE",
+ "=TRUE<>1&\"x\"": "TRUE",
+ // Engineering Functions
+ // BESSELI
+ "=BESSELI(4.5,1)": "15.3892227537359",
+ "=BESSELI(32,1)": "5502845511211.25",
+ "=BESSELI({32},1)": "5502845511211.25",
+ "=BESSELI(32,{1})": "5502845511211.25",
+ "=BESSELI({32},{1})": "5502845511211.25",
+ // BESSELJ
+ "=BESSELJ(1.9,2)": "0.329925727692387",
+ "=BESSELJ({1.9},2)": "0.329925727692387",
+ "=BESSELJ(1.9,{2})": "0.329925727692387",
+ "=BESSELJ({1.9},{2})": "0.329925727692387",
+ // BESSELK
+ "=BESSELK(0.05,0)": "3.11423403428966",
+ "=BESSELK(0.05,1)": "19.9096743272486",
+ "=BESSELK(0.05,2)": "799.501207124235",
+ "=BESSELK(3,2)": "0.0615104585619118",
+ "=BESSELK({3},2)": "0.0615104585619118",
+ "=BESSELK(3,{2})": "0.0615104585619118",
+ "=BESSELK({3},{2})": "0.0615104585619118",
+ // BESSELY
+ "=BESSELY(0.05,0)": "-1.97931100684153",
+ "=BESSELY(0.05,1)": "-12.789855163794",
+ "=BESSELY(0.05,2)": "-509.61489554492",
+ "=BESSELY(9,2)": "-0.229082087487741",
+ "=BESSELY({9},2)": "-0.229082087487741",
+ "=BESSELY(9,{2})": "-0.229082087487741",
+ "=BESSELY({9},{2})": "-0.229082087487741",
+ // BIN2DEC
+ "=BIN2DEC(\"10\")": "2",
+ "=BIN2DEC(\"11\")": "3",
+ "=BIN2DEC(\"0000000010\")": "2",
+ "=BIN2DEC(\"1111111110\")": "-2",
+ "=BIN2DEC(\"110\")": "6",
+ "=BIN2DEC({\"110\"})": "6",
+ // BIN2HEX
+ "=BIN2HEX(\"10\")": "2",
+ "=BIN2HEX(\"0000000001\")": "1",
+ "=BIN2HEX(\"10\",10)": "0000000002",
+ "=BIN2HEX(\"1111111110\")": "FFFFFFFFFE",
+ "=BIN2HEX(\"11101\")": "1D",
+ "=BIN2HEX({\"11101\"})": "1D",
+ // BIN2OCT
+ "=BIN2OCT(\"101\")": "5",
+ "=BIN2OCT(\"0000000001\")": "1",
+ "=BIN2OCT(\"10\",10)": "0000000002",
+ "=BIN2OCT(\"1111111110\")": "7777777776",
+ "=BIN2OCT(\"1110\")": "16",
+ "=BIN2OCT({\"1110\"})": "16",
+ // BITAND
+ "=BITAND(13,14)": "12",
+ "=BITAND({13},14)": "12",
+ "=BITAND(13,{14})": "12",
+ "=BITAND({13},{14})": "12",
+ // BITLSHIFT
+ "=BITLSHIFT(5,2)": "20",
+ "=BITLSHIFT({3},5)": "96",
+ "=BITLSHIFT(3,5)": "96",
+ "=BITLSHIFT(3,{5})": "96",
+ "=BITLSHIFT({3},{5})": "96",
+ // BITOR
+ "=BITOR(9,12)": "13",
+ "=BITOR({9},12)": "13",
+ "=BITOR(9,{12})": "13",
+ "=BITOR({9},{12})": "13",
+ // BITRSHIFT
+ "=BITRSHIFT(20,2)": "5",
+ "=BITRSHIFT(52,4)": "3",
+ "=BITRSHIFT({52},4)": "3",
+ "=BITRSHIFT(52,{4})": "3",
+ "=BITRSHIFT({52},{4})": "3",
+ // BITXOR
+ "=BITXOR(5,6)": "3",
+ "=BITXOR(9,12)": "5",
+ "=BITXOR({9},12)": "5",
+ "=BITXOR(9,{12})": "5",
+ "=BITXOR({9},{12})": "5",
+ // COMPLEX
+ "=COMPLEX(5,2)": "5+2i",
+ "=COMPLEX(5,-9)": "5-9i",
+ "=COMPLEX(-1,2,\"j\")": "-1+2j",
+ "=COMPLEX(10,-5,\"i\")": "10-5i",
+ "=COMPLEX(0,5)": "5i",
+ "=COMPLEX(3,0)": "3",
+ "=COMPLEX(0,-2)": "-2i",
+ "=COMPLEX(0,0)": "0",
+ "=COMPLEX(0,-1,\"j\")": "-j",
+ // CONVERT
+ "=CONVERT(20.2,\"m\",\"yd\")": "22.0909886264217",
+ "=CONVERT(20.2,\"cm\",\"yd\")": "0.220909886264217",
+ "=CONVERT(0.2,\"gal\",\"tsp\")": "153.6",
+ "=CONVERT(5,\"gal\",\"l\")": "18.92705892",
+ "=CONVERT(0.02,\"Gm\",\"m\")": "20000000",
+ "=CONVERT(0,\"C\",\"F\")": "32",
+ "=CONVERT(1,\"ly^2\",\"ly^2\")": "1",
+ "=CONVERT(0.00194255938572296,\"sg\",\"ozm\")": "1",
+ "=CONVERT(5,\"kg\",\"kg\")": "5",
+ "=CONVERT(4.5359237E-01,\"kg\",\"lbm\")": "1",
+ "=CONVERT(0.2,\"kg\",\"hg\")": "2",
+ "=CONVERT(12.345000000000001,\"km\",\"m\")": "12345",
+ "=CONVERT(12345,\"m\",\"km\")": "12.345",
+ "=CONVERT(0.621371192237334,\"mi\",\"km\")": "1",
+ "=CONVERT(1.23450000000000E+05,\"ang\",\"um\")": "12.345",
+ "=CONVERT(1.23450000000000E+02,\"kang\",\"um\")": "12.345",
+ "=CONVERT(1000,\"dal\",\"hl\")": "100",
+ "=CONVERT(1,\"yd\",\"ft\")": "2.99999999999999",
+ "=CONVERT(20,\"C\",\"F\")": "68",
+ "=CONVERT(68,\"F\",\"C\")": "20",
+ "=CONVERT(293.15,\"K\",\"F\")": "68",
+ "=CONVERT(68,\"F\",\"K\")": "293.15",
+ "=CONVERT(-273.15,\"C\",\"K\")": "0",
+ "=CONVERT(-459.67,\"F\",\"K\")": "0",
+ "=CONVERT(295.65,\"K\",\"C\")": "22.5",
+ "=CONVERT(22.5,\"C\",\"K\")": "295.65",
+ "=CONVERT(1667.85,\"C\",\"K\")": "1941",
+ "=CONVERT(3034.13,\"F\",\"K\")": "1941",
+ "=CONVERT(3493.8,\"Rank\",\"K\")": "1941",
+ "=CONVERT(1334.28,\"Reau\",\"K\")": "1941",
+ "=CONVERT(1941,\"K\",\"Rank\")": "3493.8",
+ "=CONVERT(1941,\"K\",\"Reau\")": "1334.28",
+ "=CONVERT(123.45,\"K\",\"kel\")": "123.45",
+ "=CONVERT(123.45,\"C\",\"cel\")": "123.45",
+ "=CONVERT(123.45,\"F\",\"fah\")": "123.45",
+ "=CONVERT(16,\"bit\",\"byte\")": "2",
+ "=CONVERT(1,\"kbyte\",\"byte\")": "1000",
+ "=CONVERT(1,\"kibyte\",\"byte\")": "1024",
+ // DEC2BIN
+ "=DEC2BIN(2)": "10",
+ "=DEC2BIN(3)": "11",
+ "=DEC2BIN(2,10)": "0000000010",
+ "=DEC2BIN(-2)": "1111111110",
+ "=DEC2BIN(6)": "110",
+ // DEC2HEX
+ "=DEC2HEX(10)": "A",
+ "=DEC2HEX(31)": "1F",
+ "=DEC2HEX(16,10)": "0000000010",
+ "=DEC2HEX(-16)": "FFFFFFFFF0",
+ "=DEC2HEX(273)": "111",
+ // DEC2OCT
+ "=DEC2OCT(8)": "10",
+ "=DEC2OCT(18)": "22",
+ "=DEC2OCT(8,10)": "0000000010",
+ "=DEC2OCT(-8)": "7777777770",
+ "=DEC2OCT(237)": "355",
+ // DELTA
+ "=DELTA(5,4)": "0",
+ "=DELTA(1.00001,1)": "0",
+ "=DELTA(1.23,1.23)": "1",
+ "=DELTA(1)": "0",
+ "=DELTA(0)": "1",
+ // ERF
+ "=ERF(1.5)": "0.966105146475311",
+ "=ERF(0,1.5)": "0.966105146475311",
+ "=ERF(1,2)": "0.152621472069238",
+ // ERF.PRECISE
+ "=ERF.PRECISE(-1)": "-0.842700792949715",
+ "=ERF.PRECISE(1.5)": "0.966105146475311",
+ // ERFC
+ "=ERFC(0)": "1",
+ "=ERFC(0.5)": "0.479500122186953",
+ "=ERFC(-1)": "1.84270079294971",
+ // ERFC.PRECISE
+ "=ERFC.PRECISE(0)": "1",
+ "=ERFC.PRECISE(0.5)": "0.479500122186953",
+ "=ERFC.PRECISE(-1)": "1.84270079294971",
+ // GESTEP
+ "=GESTEP(1.2,0.001)": "1",
+ "=GESTEP(0.05,0.05)": "1",
+ "=GESTEP(-0.00001,0)": "0",
+ "=GESTEP(-0.00001)": "0",
+ // HEX2BIN
+ "=HEX2BIN(\"2\")": "10",
+ "=HEX2BIN(\"0000000001\")": "1",
+ "=HEX2BIN(\"2\",10)": "0000000010",
+ "=HEX2BIN(\"F0\")": "11110000",
+ "=HEX2BIN(\"1D\")": "11101",
+ // HEX2DEC
+ "=HEX2DEC(\"A\")": "10",
+ "=HEX2DEC(\"1F\")": "31",
+ "=HEX2DEC(\"0000000010\")": "16",
+ "=HEX2DEC(\"FFFFFFFFF0\")": "-16",
+ "=HEX2DEC(\"111\")": "273",
+ "=HEX2DEC(\"\")": "0",
+ // HEX2OCT
+ "=HEX2OCT(\"A\")": "12",
+ "=HEX2OCT(\"000000000F\")": "17",
+ "=HEX2OCT(\"8\",10)": "0000000010",
+ "=HEX2OCT(\"FFFFFFFFF8\")": "7777777770",
+ "=HEX2OCT(\"1F3\")": "763",
+ "=HEX2OCT({\"1F3\"})": "763",
+ // IMABS
+ "=IMABS(\"2j\")": "2",
+ "=IMABS(\"-1+2i\")": "2.23606797749979",
+ "=IMABS(COMPLEX(-1,2,\"j\"))": "2.23606797749979",
+ // IMAGINARY
+ "=IMAGINARY(\"5+2i\")": "2",
+ "=IMAGINARY(\"2-i\")": "-1",
+ "=IMAGINARY(6)": "0",
+ "=IMAGINARY(\"3i\")": "3",
+ "=IMAGINARY(\"4+i\")": "1",
+ // IMARGUMENT
+ "=IMARGUMENT(\"5+2i\")": "0.380506377112365",
+ "=IMARGUMENT(\"2-i\")": "-0.463647609000806",
+ "=IMARGUMENT(6)": "0",
+ // IMCONJUGATE
+ "=IMCONJUGATE(\"5+2i\")": "5-2i",
+ "=IMCONJUGATE(\"2-i\")": "2+i",
+ "=IMCONJUGATE(6)": "6",
+ "=IMCONJUGATE(\"3i\")": "-3i",
+ "=IMCONJUGATE(\"4+i\")": "4-i",
+ // IMCOS
+ "=IMCOS(0)": "1",
+ "=IMCOS(0.5)": "0.877582561890373",
+ "=IMCOS(\"3+0.5i\")": "-1.11634124452615-0.0735369737112366i",
+ // IMCOSH
+ "=IMCOSH(0.5)": "1.12762596520638",
+ "=IMCOSH(\"3+0.5i\")": "8.83520460650099+4.80282508274303i",
+ "=IMCOSH(\"2-i\")": "2.03272300701967-3.0518977991518i",
+ "=IMCOSH(COMPLEX(1,-1))": "0.833730025131149-0.988897705762865i",
+ // IMCOT
+ "=IMCOT(0.5)": "1.83048772171245",
+ "=IMCOT(\"3+0.5i\")": "-0.479345578747373-2.01609252150623i",
+ "=IMCOT(\"2-i\")": "-0.171383612909185+0.821329797493852i",
+ "=IMCOT(COMPLEX(1,-1))": "0.217621561854403+0.868014142895925i",
+ // IMCSC
+ "=IMCSC(\"j\")": "-0.850918128239322j",
+ // IMCSCH
+ "=IMCSCH(COMPLEX(1,-1))": "0.303931001628426+0.621518017170428i",
+ // IMDIV
+ "=IMDIV(\"5+2i\",\"1+i\")": "3.5-1.5i",
+ "=IMDIV(\"2+2i\",\"2+i\")": "1.2+0.4i",
+ "=IMDIV(COMPLEX(5,2),COMPLEX(0,1))": "2-5i",
+ // IMEXP
+ "=IMEXP(0)": "1",
+ "=IMEXP(0.5)": "1.64872127070013",
+ "=IMEXP(\"1-2i\")": "-1.13120438375681-2.47172667200482i",
+ "=IMEXP(COMPLEX(1,-1))": "1.46869393991589-2.28735528717884i",
+ // IMLN
+ "=IMLN(0.5)": "-0.693147180559945",
+ "=IMLN(\"3+0.5i\")": "1.11231177576217+0.165148677414627i",
+ "=IMLN(\"2-i\")": "0.80471895621705-0.463647609000806i",
+ "=IMLN(COMPLEX(1,-1))": "0.346573590279973-0.785398163397448i",
+ // IMLOG10
+ "=IMLOG10(0.5)": "-0.301029995663981",
+ "=IMLOG10(\"3+0.5i\")": "0.483070866369516+0.0717231592947926i",
+ "=IMLOG10(\"2-i\")": "0.349485002168009-0.201359598136687i",
+ "=IMLOG10(COMPLEX(1,-1))": "0.150514997831991-0.34109408846046i",
+ // IMREAL
+ "=IMREAL(\"5+2i\")": "5",
+ "=IMREAL(\"2+2i\")": "2",
+ "=IMREAL(6)": "6",
+ "=IMREAL(\"3i\")": "0",
+ "=IMREAL(COMPLEX(4,1))": "4",
+ // IMSEC
+ "=IMSEC(0.5)": "1.13949392732455",
+ "=IMSEC(\"3+0.5i\")": "-0.89191317974033+0.0587531781817398i",
+ "=IMSEC(\"2-i\")": "-0.41314934426694-0.687527438655479i",
+ "=IMSEC(COMPLEX(1,-1))": "0.498337030555187-0.591083841721045i",
+ // IMSECH
+ "=IMSECH(0.5)": "0.886818883970074",
+ "=IMSECH(\"3+0.5i\")": "0.0873665779621303-0.0474925494901607i",
+ "=IMSECH(\"2-i\")": "0.151176298265577+0.226973675393722i",
+ "=IMSECH(COMPLEX(1,-1))": "0.498337030555187+0.591083841721045i",
+ // IMSIN
+ "=IMSIN(0.5)": "0.479425538604203",
+ "=IMSIN(\"3+0.5i\")": "0.15913058529844-0.515880442452527i",
+ "=IMSIN(\"2-i\")": "1.40311925062204+0.489056259041294i",
+ "=IMSIN(COMPLEX(1,-1))": "1.29845758141598-0.634963914784736i",
+ // IMSINH
+ "=IMSINH(-0)": "0",
+ "=IMSINH(0.5)": "0.521095305493747",
+ "=IMSINH(\"3+0.5i\")": "8.79151234349371+4.82669427481082i",
+ "=IMSINH(\"2-i\")": "1.95960104142161-3.16577851321617i",
+ "=IMSINH(COMPLEX(1,-1))": "0.634963914784736-1.29845758141598i",
+ // IMSQRT
+ "=IMSQRT(\"i\")": "0.707106781186548+0.707106781186548i",
+ "=IMSQRT(\"2-i\")": "1.45534669022535-0.343560749722512i",
+ "=IMSQRT(\"5+2i\")": "2.27872385417085+0.438842116902254i",
+ "=IMSQRT(6)": "2.44948974278318",
+ "=IMSQRT(\"-2-4i\")": "1.11178594050284-1.79890743994787i",
+ // IMSUB
+ "=IMSUB(\"5+i\",\"1+4i\")": "4-3i",
+ "=IMSUB(\"9+2i\",6)": "3+2i",
+ "=IMSUB(COMPLEX(5,2),COMPLEX(0,1))": "5+i",
+ // IMSUM
+ "=IMSUM(\"1-i\",\"5+10i\",2)": "8+9i",
+ "=IMSUM(COMPLEX(5,2),COMPLEX(0,1))": "5+3i",
+ // IMTAN
+ "=IMTAN(-0)": "0",
+ "=IMTAN(0.5)": "0.54630248984379",
+ "=IMTAN(\"3+0.5i\")": "-0.111621050771583+0.469469993425885i",
+ "=IMTAN(\"2-i\")": "-0.243458201185725-1.16673625724092i",
+ "=IMTAN(COMPLEX(1,-1))": "0.271752585319512-1.08392332733869i",
+ // OCT2BIN
+ "=OCT2BIN(\"5\")": "101",
+ "=OCT2BIN(\"0000000001\")": "1",
+ "=OCT2BIN(\"2\",10)": "0000000010",
+ "=OCT2BIN(\"7777777770\")": "1111111000",
+ "=OCT2BIN(\"16\")": "1110",
+ // OCT2DEC
+ "=OCT2DEC(\"10\")": "8",
+ "=OCT2DEC(\"22\")": "18",
+ "=OCT2DEC(\"0000000010\")": "8",
+ "=OCT2DEC(\"7777777770\")": "-8",
+ "=OCT2DEC(\"355\")": "237",
+ // OCT2HEX
+ "=OCT2HEX(\"10\")": "8",
+ "=OCT2HEX(\"0000000007\")": "7",
+ "=OCT2HEX(\"10\",10)": "0000000008",
+ "=OCT2HEX(\"7777777770\")": "FFFFFFFFF8",
+ "=OCT2HEX(\"763\")": "1F3",
+ // Math and Trigonometric Functions
// ABS
- "=ABS(-1)": "1",
- "=ABS(-6.5)": "6.5",
- "=ABS(6.5)": "6.5",
- "=ABS(0)": "0",
- "=ABS(2-4.5)": "2.5",
+ "=ABS(-1)": "1",
+ "=ABS(-6.5)": "6.5",
+ "=ABS(6.5)": "6.5",
+ "=ABS(0)": "0",
+ "=ABS(2-4.5)": "2.5",
+ "=ABS(ABS(-1))": "1",
// ACOS
- "=ACOS(-1)": "3.141592653589793",
- "=ACOS(0)": "1.5707963267948966",
+ "=ACOS(-1)": "3.14159265358979",
+ "=ACOS(0)": "1.5707963267949",
+ "=ACOS(ABS(0))": "1.5707963267949",
// ACOSH
- "=ACOSH(1)": "0",
- "=ACOSH(2.5)": "1.566799236972411",
- "=ACOSH(5)": "2.2924316695611777",
- // ACOT
- "=_xlfn.ACOT(1)": "0.7853981633974483",
- "=_xlfn.ACOT(-2)": "2.677945044588987",
- "=_xlfn.ACOT(0)": "1.5707963267948966",
- // ACOTH
- "=_xlfn.ACOTH(-5)": "-0.2027325540540822",
- "=_xlfn.ACOTH(1.1)": "1.5222612188617113",
- "=_xlfn.ACOTH(2)": "0.5493061443340548",
+ "=ACOSH(1)": "0",
+ "=ACOSH(2.5)": "1.56679923697241",
+ "=ACOSH(5)": "2.29243166956118",
+ "=ACOSH(ACOSH(5))": "1.47138332153668",
+ // _xlfn.ACOT
+ "=_xlfn.ACOT(1)": "0.785398163397448",
+ "=_xlfn.ACOT(-2)": "2.67794504458899",
+ "=_xlfn.ACOT(0)": "1.5707963267949",
+ "=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009",
+ // _xlfn.ACOTH
+ "=_xlfn.ACOTH(-5)": "-0.202732554054082",
+ "=_xlfn.ACOTH(1.1)": "1.52226121886171",
+ "=_xlfn.ACOTH(2)": "0.549306144334055",
+ "=_xlfn.ACOTH(ABS(-2))": "0.549306144334055",
+ // _xlfn.AGGREGATE
+ "=_xlfn.AGGREGATE(1,0,A1:A6)": "1.5",
+ "=_xlfn.AGGREGATE(2,0,A1:A6)": "4",
+ "=_xlfn.AGGREGATE(3,0,A1:A6)": "4",
+ "=_xlfn.AGGREGATE(4,0,A1:A6)": "3",
+ "=_xlfn.AGGREGATE(5,0,A1:A6)": "0",
+ "=_xlfn.AGGREGATE(6,0,A1:A6)": "0",
+ "=_xlfn.AGGREGATE(7,0,A1:A6)": "1.29099444873581",
+ "=_xlfn.AGGREGATE(8,0,A1:A6)": "1.11803398874989",
+ "=_xlfn.AGGREGATE(9,0,A1:A6)": "6",
+ "=_xlfn.AGGREGATE(10,0,A1:A6)": "1.66666666666667",
+ "=_xlfn.AGGREGATE(11,0,A1:A6)": "1.25",
+ "=_xlfn.AGGREGATE(12,0,A1:A6)": "1.5",
+ "=_xlfn.AGGREGATE(14,0,A1:A6,1)": "3",
+ "=_xlfn.AGGREGATE(15,0,A1:A6,1)": "0",
+ "=_xlfn.AGGREGATE(16,0,A1:A6,1)": "3",
+ "=_xlfn.AGGREGATE(17,0,A1:A6,1)": "0.75",
+ "=_xlfn.AGGREGATE(19,0,A1:A6,1)": "0.25",
// ARABIC
- `=_xlfn.ARABIC("IV")`: "4",
- `=_xlfn.ARABIC("-IV")`: "-4",
- `=_xlfn.ARABIC("MCXX")`: "1120",
- `=_xlfn.ARABIC("")`: "0",
+ "=_xlfn.ARABIC(\"IV\")": "4",
+ "=_xlfn.ARABIC(\"-IV\")": "-4",
+ "=_xlfn.ARABIC(\"MCXX\")": "1120",
+ "=_xlfn.ARABIC(\"\")": "0",
+ "=_xlfn.ARABIC(\" ll lc \")": "-50",
// ASIN
- "=ASIN(-1)": "-1.5707963267948966",
- "=ASIN(0)": "0",
+ "=ASIN(-1)": "-1.5707963267949",
+ "=ASIN(0)": "0",
+ "=ASIN(ASIN(0))": "0",
// ASINH
- "=ASINH(0)": "0",
- "=ASINH(-0.5)": "-0.48121182505960347",
- "=ASINH(2)": "1.4436354751788103",
+ "=ASINH(0)": "0",
+ "=ASINH(-0.5)": "-0.481211825059603",
+ "=ASINH(2)": "1.44363547517881",
+ "=ASINH(ASINH(0))": "0",
// ATAN
- "=ATAN(-1)": "-0.7853981633974483",
- "=ATAN(0)": "0",
- "=ATAN(1)": "0.7853981633974483",
+ "=ATAN(-1)": "-0.785398163397448",
+ "=ATAN(0)": "0",
+ "=ATAN(1)": "0.785398163397448",
+ "=ATAN(ATAN(0))": "0",
// ATANH
- "=ATANH(-0.8)": "-1.0986122886681098",
- "=ATANH(0)": "0",
- "=ATANH(0.5)": "0.5493061443340548",
+ "=ATANH(-0.8)": "-1.09861228866811",
+ "=ATANH(0)": "0",
+ "=ATANH(0.5)": "0.549306144334055",
+ "=ATANH(ATANH(0))": "0",
// ATAN2
- "=ATAN2(1,1)": "0.7853981633974483",
- "=ATAN2(1,-1)": "-0.7853981633974483",
- "=ATAN2(4,0)": "0",
+ "=ATAN2(1,1)": "0.785398163397448",
+ "=ATAN2(1,-1)": "-0.785398163397448",
+ "=ATAN2(4,0)": "0",
+ "=ATAN2(4,ATAN2(4,0))": "0",
// BASE
- "=BASE(12,2)": "1100",
- "=BASE(12,2,8)": "00001100",
- "=BASE(100000,16)": "186A0",
+ "=BASE(12,2)": "1100",
+ "=BASE(12,2,8)": "00001100",
+ "=BASE(100000,16)": "186A0",
+ "=BASE(BASE(12,2),16)": "44C",
// CEILING
- "=CEILING(22.25,0.1)": "22.3",
- "=CEILING(22.25,0.5)": "22.5",
- "=CEILING(22.25,1)": "23",
- "=CEILING(22.25,10)": "30",
- "=CEILING(22.25,20)": "40",
- "=CEILING(-22.25,-0.1)": "-22.3",
- "=CEILING(-22.25,-1)": "-23",
- "=CEILING(-22.25,-5)": "-25",
- "=CEILING(22.25)": "23",
+ "=CEILING(22.25,0.1)": "22.3",
+ "=CEILING(22.25,0.5)": "22.5",
+ "=CEILING(22.25,1)": "23",
+ "=CEILING(22.25,10)": "30",
+ "=CEILING(22.25,20)": "40",
+ "=CEILING(-22.25,-0.1)": "-22.3",
+ "=CEILING(-22.25,-1)": "-23",
+ "=CEILING(-22.25,-5)": "-25",
+ "=CEILING(22.25)": "23",
+ "=CEILING(CEILING(22.25,0.1),0.1)": "22.3",
// _xlfn.CEILING.MATH
- "=_xlfn.CEILING.MATH(15.25,1)": "16",
- "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3",
- "=_xlfn.CEILING.MATH(15.25,5)": "20",
- "=_xlfn.CEILING.MATH(-15.25,1)": "-15",
- "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16
- "=_xlfn.CEILING.MATH(-15.25,10)": "-10",
- "=_xlfn.CEILING.MATH(-15.25)": "-15",
- "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10",
+ "=_xlfn.CEILING.MATH(15.25,1)": "16",
+ "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3",
+ "=_xlfn.CEILING.MATH(15.25,5)": "20",
+ "=_xlfn.CEILING.MATH(-15.25,1)": "-15",
+ "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16
+ "=_xlfn.CEILING.MATH(-15.25,10)": "-10",
+ "=_xlfn.CEILING.MATH(-15.25)": "-15",
+ "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10",
+ "=_xlfn.CEILING.MATH(_xlfn.CEILING.MATH(15.25,1),1)": "16",
// _xlfn.CEILING.PRECISE
- "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3",
- "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5",
- "=_xlfn.CEILING.PRECISE(22.25,1)": "23",
- "=_xlfn.CEILING.PRECISE(22.25)": "23",
- "=_xlfn.CEILING.PRECISE(22.25,10)": "30",
- "=_xlfn.CEILING.PRECISE(22.25,0)": "0",
- "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22",
- "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22",
- "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20",
+ "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3",
+ "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5",
+ "=_xlfn.CEILING.PRECISE(22.25,1)": "23",
+ "=_xlfn.CEILING.PRECISE(22.25)": "23",
+ "=_xlfn.CEILING.PRECISE(22.25,10)": "30",
+ "=_xlfn.CEILING.PRECISE(22.25,0)": "0",
+ "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22",
+ "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22",
+ "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20",
+ "=_xlfn.CEILING.PRECISE(_xlfn.CEILING.PRECISE(22.25,0.1),5)": "25",
// COMBIN
- "=COMBIN(6,1)": "6",
- "=COMBIN(6,2)": "15",
- "=COMBIN(6,3)": "20",
- "=COMBIN(6,4)": "15",
- "=COMBIN(6,5)": "6",
- "=COMBIN(6,6)": "1",
- "=COMBIN(0,0)": "1",
+ "=COMBIN(6,1)": "6",
+ "=COMBIN(6,2)": "15",
+ "=COMBIN(6,3)": "20",
+ "=COMBIN(6,4)": "15",
+ "=COMBIN(6,5)": "6",
+ "=COMBIN(6,6)": "1",
+ "=COMBIN(0,0)": "1",
+ "=COMBIN(6,COMBIN(0,0))": "6",
// _xlfn.COMBINA
- "=_xlfn.COMBINA(6,1)": "6",
- "=_xlfn.COMBINA(6,2)": "21",
- "=_xlfn.COMBINA(6,3)": "56",
- "=_xlfn.COMBINA(6,4)": "126",
- "=_xlfn.COMBINA(6,5)": "252",
- "=_xlfn.COMBINA(6,6)": "462",
- "=_xlfn.COMBINA(0,0)": "0",
+ "=_xlfn.COMBINA(6,1)": "6",
+ "=_xlfn.COMBINA(6,2)": "21",
+ "=_xlfn.COMBINA(6,3)": "56",
+ "=_xlfn.COMBINA(6,4)": "126",
+ "=_xlfn.COMBINA(6,5)": "252",
+ "=_xlfn.COMBINA(6,6)": "462",
+ "=_xlfn.COMBINA(0,0)": "0",
+ "=_xlfn.COMBINA(0,_xlfn.COMBINA(0,0))": "0",
// COS
"=COS(0.785398163)": "0.707106781467586",
"=COS(0)": "1",
+ "=-COS(0)": "-1",
+ "=COS(COS(0))": "0.54030230586814",
// COSH
- "=COSH(0)": "1",
- "=COSH(0.5)": "1.1276259652063807",
- "=COSH(-2)": "3.7621956910836314",
+ "=COSH(0)": "1",
+ "=COSH(0.5)": "1.12762596520638",
+ "=COSH(-2)": "3.76219569108363",
+ "=COSH(COSH(0))": "1.54308063481524",
// _xlfn.COT
- "=_xlfn.COT(0.785398163397448)": "0.9999999999999992",
+ "=_xlfn.COT(0.785398163397448)": "1",
+ "=_xlfn.COT(_xlfn.COT(0.45))": "-0.545473116787229",
// _xlfn.COTH
- "=_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499",
+ "=_xlfn.COTH(-3.14159265358979)": "-1.00374187319732",
+ "=_xlfn.COTH(_xlfn.COTH(1))": "1.15601401811395",
// _xlfn.CSC
- "=_xlfn.CSC(-6)": "3.5788995472544056",
+ "=_xlfn.CSC(-6)": "3.57889954725441",
"=_xlfn.CSC(1.5707963267949)": "1",
+ "=_xlfn.CSC(_xlfn.CSC(1))": "1.07785184031088",
// _xlfn.CSCH
- "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724",
+ "=_xlfn.CSCH(-3.14159265358979)": "-0.0865895375300472",
+ "=_xlfn.CSCH(_xlfn.CSCH(1))": "1.04451010395518",
// _xlfn.DECIMAL
- `=_xlfn.DECIMAL("1100",2)`: "12",
- `=_xlfn.DECIMAL("186A0",16)`: "100000",
- `=_xlfn.DECIMAL("31L0",32)`: "100000",
- `=_xlfn.DECIMAL("70122",8)`: "28754",
- `=_xlfn.DECIMAL("0x70122",8)`: "28754",
+ "=_xlfn.DECIMAL(\"1100\",2)": "12",
+ "=_xlfn.DECIMAL(\"186A0\",16)": "100000",
+ "=_xlfn.DECIMAL(\"31L0\",32)": "100000",
+ "=_xlfn.DECIMAL(\"70122\",8)": "28754",
+ "=_xlfn.DECIMAL(\"0x70122\",8)": "28754",
// DEGREES
- "=DEGREES(1)": "57.29577951308232",
- "=DEGREES(2.5)": "143.2394487827058",
+ "=DEGREES(1)": "57.2957795130823",
+ "=DEGREES(2.5)": "143.239448782706",
+ "=DEGREES(DEGREES(1))": "3282.80635001174",
// EVEN
"=EVEN(23)": "24",
"=EVEN(2.22)": "4",
@@ -157,112 +562,153 @@ func TestCalcCellValue(t *testing.T) {
"=EVEN(-0.3)": "-2",
"=EVEN(-11)": "-12",
"=EVEN(-4)": "-4",
+ "=EVEN((0))": "0",
// EXP
- "=EXP(100)": "2.6881171418161356E+43",
- "=EXP(0.1)": "1.1051709180756477",
- "=EXP(0)": "1",
- "=EXP(-5)": "0.006737946999085467",
+ "=EXP(100)": "2.68811714181614E+43",
+ "=EXP(0.1)": "1.10517091807565",
+ "=EXP(0)": "1",
+ "=EXP(-5)": "0.00673794699908547",
+ "=EXP(EXP(0))": "2.71828182845905",
// FACT
- "=FACT(3)": "6",
- "=FACT(6)": "720",
- "=FACT(10)": "3.6288E+06",
+ "=FACT(3)": "6",
+ "=FACT(6)": "720",
+ "=FACT(10)": "3628800",
+ "=FACT(FACT(3))": "720",
// FACTDOUBLE
- "=FACTDOUBLE(5)": "15",
- "=FACTDOUBLE(8)": "384",
- "=FACTDOUBLE(13)": "135135",
+ "=FACTDOUBLE(5)": "15",
+ "=FACTDOUBLE(8)": "384",
+ "=FACTDOUBLE(13)": "135135",
+ "=FACTDOUBLE(FACTDOUBLE(1))": "1",
// FLOOR
- "=FLOOR(26.75,0.1)": "26.700000000000003",
- "=FLOOR(26.75,0.5)": "26.5",
- "=FLOOR(26.75,1)": "26",
- "=FLOOR(26.75,10)": "20",
- "=FLOOR(26.75,20)": "20",
- "=FLOOR(-26.75,-0.1)": "-26.700000000000003",
- "=FLOOR(-26.75,-1)": "-26",
- "=FLOOR(-26.75,-5)": "-25",
+ "=FLOOR(26.75,0.1)": "26.7",
+ "=FLOOR(26.75,0.5)": "26.5",
+ "=FLOOR(26.75,1)": "26",
+ "=FLOOR(26.75,10)": "20",
+ "=FLOOR(26.75,20)": "20",
+ "=FLOOR(-26.75,-0.1)": "-26.7",
+ "=FLOOR(-26.75,-1)": "-26",
+ "=FLOOR(-26.75,-5)": "-25",
+ "=FLOOR(-2.05,2)": "-4",
+ "=FLOOR(FLOOR(26.75,1),1)": "26",
// _xlfn.FLOOR.MATH
- "=_xlfn.FLOOR.MATH(58.55)": "58",
- "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5",
- "=_xlfn.FLOOR.MATH(58.55,5)": "55",
- "=_xlfn.FLOOR.MATH(58.55,1,1)": "58",
- "=_xlfn.FLOOR.MATH(-58.55,1)": "-59",
- "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58",
- "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58
- "=_xlfn.FLOOR.MATH(-58.55,10)": "-60",
+ "=_xlfn.FLOOR.MATH(58.55)": "58",
+ "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5",
+ "=_xlfn.FLOOR.MATH(58.55,5)": "55",
+ "=_xlfn.FLOOR.MATH(58.55,1,1)": "58",
+ "=_xlfn.FLOOR.MATH(-58.55,1)": "-59",
+ "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58",
+ "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58
+ "=_xlfn.FLOOR.MATH(-58.55,10)": "-60",
+ "=_xlfn.FLOOR.MATH(_xlfn.FLOOR.MATH(1),10)": "0",
// _xlfn.FLOOR.PRECISE
- "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.700000000000003",
- "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5",
- "=_xlfn.FLOOR.PRECISE(26.75,1)": "26",
- "=_xlfn.FLOOR.PRECISE(26.75)": "26",
- "=_xlfn.FLOOR.PRECISE(26.75,10)": "20",
- "=_xlfn.FLOOR.PRECISE(26.75,0)": "0",
- "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27",
- "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27",
- "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30",
+ "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.7",
+ "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5",
+ "=_xlfn.FLOOR.PRECISE(26.75,1)": "26",
+ "=_xlfn.FLOOR.PRECISE(26.75)": "26",
+ "=_xlfn.FLOOR.PRECISE(26.75,10)": "20",
+ "=_xlfn.FLOOR.PRECISE(26.75,0)": "0",
+ "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27",
+ "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27",
+ "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30",
+ "=_xlfn.FLOOR.PRECISE(_xlfn.FLOOR.PRECISE(26.75),-5)": "25",
// GCD
"=GCD(0)": "0",
- `=GCD("",1)`: "1",
"=GCD(1,0)": "1",
+ "=GCD(\"0\",1)": "1",
"=GCD(1,5)": "1",
"=GCD(15,10,25)": "5",
"=GCD(0,8,12)": "4",
"=GCD(7,2)": "1",
+ "=GCD(1,GCD(1))": "1",
// INT
"=INT(100.9)": "100",
"=INT(5.22)": "5",
"=INT(5.99)": "5",
"=INT(-6.1)": "-7",
"=INT(-100.9)": "-101",
+ "=INT(INT(0))": "0",
// ISO.CEILING
- "=ISO.CEILING(22.25)": "23",
- "=ISO.CEILING(22.25,1)": "23",
- "=ISO.CEILING(22.25,0.1)": "22.3",
- "=ISO.CEILING(22.25,10)": "30",
- "=ISO.CEILING(-22.25,1)": "-22",
- "=ISO.CEILING(-22.25,0.1)": "-22.200000000000003",
- "=ISO.CEILING(-22.25,5)": "-20",
- "=ISO.CEILING(-22.25,0)": "0",
+ "=ISO.CEILING(22.25)": "23",
+ "=ISO.CEILING(22.25,1)": "23",
+ "=ISO.CEILING(22.25,0.1)": "22.3",
+ "=ISO.CEILING(22.25,10)": "30",
+ "=ISO.CEILING(-22.25,1)": "-22",
+ "=ISO.CEILING(-22.25,0.1)": "-22.2",
+ "=ISO.CEILING(-22.25,5)": "-20",
+ "=ISO.CEILING(-22.25,0)": "0",
+ "=ISO.CEILING(1,ISO.CEILING(1,0))": "0",
// LCM
- "=LCM(1,5)": "5",
- "=LCM(15,10,25)": "150",
- "=LCM(1,8,12)": "24",
- "=LCM(7,2)": "14",
- "=LCM(7)": "7",
- `=LCM("",1)`: "1",
- `=LCM(0,0)`: "0",
+ "=LCM(1,5)": "5",
+ "=LCM(15,10,25)": "150",
+ "=LCM(1,8,12)": "24",
+ "=LCM(7,2)": "14",
+ "=LCM(7)": "7",
+ "=LCM(\"\",1)": "1",
+ "=LCM(0,0)": "0",
+ "=LCM(0,LCM(0,0))": "0",
// LN
- "=LN(1)": "0",
- "=LN(100)": "4.605170185988092",
- "=LN(0.5)": "-0.6931471805599453",
+ "=LN(1)": "0",
+ "=LN(100)": "4.60517018598809",
+ "=LN(0.5)": "-0.693147180559945",
+ "=LN(LN(100))": "1.5271796258079",
// LOG
- "=LOG(64,2)": "6",
- "=LOG(100)": "2",
- "=LOG(4,0.5)": "-2",
- "=LOG(500)": "2.6989700043360183",
+ "=LOG(64,2)": "6",
+ "=LOG(100)": "2",
+ "=LOG(4,0.5)": "-2",
+ "=LOG(500)": "2.69897000433602",
+ "=LOG(LOG(100))": "0.301029995663981",
// LOG10
- "=LOG10(100)": "2",
- "=LOG10(1000)": "3",
- "=LOG10(0.001)": "-3",
- "=LOG10(25)": "1.3979400086720375",
+ "=LOG10(100)": "2",
+ "=LOG10(1000)": "3",
+ "=LOG10(0.001)": "-3",
+ "=LOG10(25)": "1.39794000867204",
+ "=LOG10(LOG10(100))": "0.301029995663981",
+ // IMLOG2
+ "=IMLOG2(\"5+2i\")": "2.42899049756379+0.548954663286635i",
+ "=IMLOG2(\"2-i\")": "1.16096404744368-0.668902106225488i",
+ "=IMLOG2(6)": "2.58496250072116",
+ "=IMLOG2(\"3i\")": "1.58496250072116+2.2661800709136i",
+ "=IMLOG2(\"4+i\")": "2.04373142062517+0.353429502416735i",
+ // IMPOWER
+ "=IMPOWER(\"2-i\",2)": "3-4i",
+ "=IMPOWER(\"2-i\",3)": "2-11i",
+ "=IMPOWER(9,0.5)": "3",
+ "=IMPOWER(\"2+4i\",-2)": "-0.03-0.04i",
+ // IMPRODUCT
+ "=IMPRODUCT(3,6)": "18",
+ "=IMPRODUCT(\"\",3,SUM(6))": "18",
+ "=IMPRODUCT(\"1-i\",\"5+10i\",2)": "30+10i",
+ "=IMPRODUCT(COMPLEX(5,2),COMPLEX(0,1))": "-2+5i",
+ "=IMPRODUCT(A1:C1)": "4",
+ // MINVERSE
+ "=MINVERSE(A1:B2)": "-0",
+ // MMULT
+ "=MMULT(0,0)": "0",
+ "=MMULT(2,4)": "8",
+ "=MMULT(A4:A4,A4:A4)": "0",
// MOD
- "=MOD(6,4)": "2",
- "=MOD(6,3)": "0",
- "=MOD(6,2.5)": "1",
- "=MOD(6,1.333)": "0.6680000000000001",
- "=MOD(-10.23,1)": "0.7699999999999996",
+ "=MOD(6,4)": "2",
+ "=MOD(6,3)": "0",
+ "=MOD(6,2.5)": "1",
+ "=MOD(6,1.333)": "0.668",
+ "=MOD(-10.23,1)": "0.77",
+ "=MOD(MOD(1,1),1)": "0",
// MROUND
- "=MROUND(333.7,0.5)": "333.5",
- "=MROUND(333.8,1)": "334",
- "=MROUND(333.3,2)": "334",
- "=MROUND(555.3,400)": "400",
- "=MROUND(555,1000)": "1000",
- "=MROUND(-555.7,-1)": "-556",
- "=MROUND(-555.4,-1)": "-555",
- "=MROUND(-1555,-1000)": "-2000",
+ "=MROUND(333.7,0.5)": "333.5",
+ "=MROUND(333.8,1)": "334",
+ "=MROUND(333.3,2)": "334",
+ "=MROUND(555.3,400)": "400",
+ "=MROUND(555,1000)": "1000",
+ "=MROUND(-555.7,-1)": "-556",
+ "=MROUND(-555.4,-1)": "-555",
+ "=MROUND(-1555,-1000)": "-2000",
+ "=MROUND(MROUND(1,1),1)": "1",
// MULTINOMIAL
- "=MULTINOMIAL(3,1,2,5)": "27720",
- `=MULTINOMIAL("",3,1,2,5)`: "27720",
+ "=MULTINOMIAL(3,1,2,5)": "27720",
+ "=MULTINOMIAL(\"\",3,1,2,5)": "27720",
+ "=MULTINOMIAL(MULTINOMIAL(1))": "1",
// _xlfn.MUNIT
- "=_xlfn.MUNIT(4)": "", // not support currently
+ "=_xlfn.MUNIT(4)": "1",
// ODD
"=ODD(22)": "23",
"=ODD(1.22)": "3",
@@ -271,83 +717,170 @@ func TestCalcCellValue(t *testing.T) {
"=ODD(-1.3)": "-3",
"=ODD(-10)": "-11",
"=ODD(-3)": "-3",
+ "=ODD(ODD(1))": "1",
// PI
- "=PI()": "3.141592653589793",
+ "=PI()": "3.14159265358979",
// POWER
- "=POWER(4,2)": "16",
+ "=POWER(4,2)": "16",
+ "=POWER(4,POWER(1,1))": "4",
// PRODUCT
- "=PRODUCT(3,6)": "18",
- `=PRODUCT("",3,6)`: "18",
+ "=PRODUCT(3,6)": "18",
+ "=PRODUCT(\"3\",\"6\")": "18",
+ "=PRODUCT(PRODUCT(1),3,6)": "18",
+ "=PRODUCT(C1:C2)": "1",
// QUOTIENT
- "=QUOTIENT(5,2)": "2",
- "=QUOTIENT(4.5,3.1)": "1",
- "=QUOTIENT(-10,3)": "-3",
+ "=QUOTIENT(5,2)": "2",
+ "=QUOTIENT(4.5,3.1)": "1",
+ "=QUOTIENT(-10,3)": "-3",
+ "=QUOTIENT(QUOTIENT(1,2),3)": "0",
// RADIANS
- "=RADIANS(50)": "0.8726646259971648",
- "=RADIANS(-180)": "-3.141592653589793",
- "=RADIANS(180)": "3.141592653589793",
- "=RADIANS(360)": "6.283185307179586",
+ "=RADIANS(50)": "0.872664625997165",
+ "=RADIANS(-180)": "-3.14159265358979",
+ "=RADIANS(180)": "3.14159265358979",
+ "=RADIANS(360)": "6.28318530717959",
+ "=RADIANS(RADIANS(360))": "0.109662271123215",
// ROMAN
- "=ROMAN(499,0)": "CDXCIX",
- "=ROMAN(1999,0)": "MCMXCIX",
- "=ROMAN(1999,1)": "MLMVLIV",
- "=ROMAN(1999,2)": "MXMIX",
- "=ROMAN(1999,3)": "MVMIV",
- "=ROMAN(1999,4)": "MIM",
- "=ROMAN(1999,-1)": "MCMXCIX",
- "=ROMAN(1999,5)": "MIM",
+ "=ROMAN(499,0)": "CDXCIX",
+ "=ROMAN(1999,0)": "MCMXCIX",
+ "=ROMAN(1999,1)": "MLMVLIV",
+ "=ROMAN(1999,2)": "MXMIX",
+ "=ROMAN(1999,3)": "MVMIV",
+ "=ROMAN(1999,4)": "MIM",
+ "=ROMAN(1999,-1)": "MCMXCIX",
+ "=ROMAN(1999,5)": "MIM",
+ "=ROMAN(1999,ODD(1))": "MLMVLIV",
// ROUND
- "=ROUND(100.319,1)": "100.30000000000001",
- "=ROUND(5.28,1)": "5.300000000000001",
- "=ROUND(5.9999,3)": "6.000000000000002",
- "=ROUND(99.5,0)": "100",
- "=ROUND(-6.3,0)": "-6",
- "=ROUND(-100.5,0)": "-101",
- "=ROUND(-22.45,1)": "-22.5",
- "=ROUND(999,-1)": "1000",
- "=ROUND(991,-1)": "990",
+ "=ROUND(100.319,1)": "100.3",
+ "=ROUND(5.28,1)": "5.3",
+ "=ROUND(5.9999,3)": "6",
+ "=ROUND(99.5,0)": "100",
+ "=ROUND(-6.3,0)": "-6",
+ "=ROUND(-100.5,0)": "-101",
+ "=ROUND(-22.45,1)": "-22.5",
+ "=ROUND(999,-1)": "1000",
+ "=ROUND(991,-1)": "990",
+ "=ROUND(ROUND(100,1),-1)": "100",
// ROUNDDOWN
- "=ROUNDDOWN(99.999,1)": "99.9",
- "=ROUNDDOWN(99.999,2)": "99.99000000000002",
- "=ROUNDDOWN(99.999,0)": "99",
- "=ROUNDDOWN(99.999,-1)": "90",
- "=ROUNDDOWN(-99.999,2)": "-99.99000000000002",
- "=ROUNDDOWN(-99.999,-1)": "-90",
+ "=ROUNDDOWN(99.999,1)": "99.9",
+ "=ROUNDDOWN(99.999,2)": "99.99",
+ "=ROUNDDOWN(99.999,0)": "99",
+ "=ROUNDDOWN(99.999,-1)": "90",
+ "=ROUNDDOWN(-99.999,2)": "-99.99",
+ "=ROUNDDOWN(-99.999,-1)": "-90",
+ "=ROUNDDOWN(ROUNDDOWN(100,1),-1)": "100",
// ROUNDUP
- "=ROUNDUP(11.111,1)": "11.200000000000001",
- "=ROUNDUP(11.111,2)": "11.120000000000003",
- "=ROUNDUP(11.111,0)": "12",
- "=ROUNDUP(11.111,-1)": "20",
- "=ROUNDUP(-11.111,2)": "-11.120000000000003",
- "=ROUNDUP(-11.111,-1)": "-20",
+ "=ROUNDUP(11.111,1)": "11.2",
+ "=ROUNDUP(11.111,2)": "11.12",
+ "=ROUNDUP(11.111,0)": "12",
+ "=ROUNDUP(11.111,-1)": "20",
+ "=ROUNDUP(-11.111,2)": "-11.12",
+ "=ROUNDUP(-11.111,-1)": "-20",
+ "=ROUNDUP(ROUNDUP(100,1),-1)": "100",
+ // SEARCH
+ "=SEARCH(\"s\",F1)": "1",
+ "=SEARCH(\"s\",F1,2)": "5",
+ "=SEARCH(\"e\",F1)": "4",
+ "=SEARCH(\"e*\",F1)": "4",
+ "=SEARCH(\"?e\",F1)": "3",
+ "=SEARCH(\"??e\",F1)": "2",
+ "=SEARCH(6,F2)": "2",
+ "=SEARCH(\"?\",\"你好world\")": "1",
+ "=SEARCH(\"?l\",\"你好world\")": "5",
+ "=SEARCH(\"?+\",\"你好 1+2\")": "4",
+ "=SEARCH(\" ?+\",\"你好 1+2\")": "3",
+ // SEARCHB
+ "=SEARCHB(\"s\",F1)": "1",
+ "=SEARCHB(\"s\",F1,2)": "5",
+ "=SEARCHB(\"e\",F1)": "4",
+ "=SEARCHB(\"e*\",F1)": "4",
+ "=SEARCHB(\"?e\",F1)": "3",
+ "=SEARCHB(\"??e\",F1)": "2",
+ "=SEARCHB(6,F2)": "2",
+ "=SEARCHB(\"?\",\"你好world\")": "5",
+ "=SEARCHB(\"?l\",\"你好world\")": "7",
+ "=SEARCHB(\"?+\",\"你好 1+2\")": "6",
+ "=SEARCHB(\" ?+\",\"你好 1+2\")": "5",
// SEC
"=_xlfn.SEC(-3.14159265358979)": "-1",
"=_xlfn.SEC(0)": "1",
+ "=_xlfn.SEC(_xlfn.SEC(0))": "0.54030230586814",
// SECH
"=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547",
"=_xlfn.SECH(0)": "1",
+ "=_xlfn.SECH(_xlfn.SECH(0))": "0.648054273663885",
+ // SERIESSUM
+ "=SERIESSUM(1,2,3,A1:A4)": "6",
+ "=SERIESSUM(1,2,3,A1:B5)": "15",
// SIGN
"=SIGN(9.5)": "1",
"=SIGN(-9.5)": "-1",
"=SIGN(0)": "0",
"=SIGN(0.00000001)": "1",
"=SIGN(6-7)": "-1",
+ "=SIGN(SIGN(-1))": "-1",
// SIN
- "=SIN(0.785398163)": "0.7071067809055092",
+ "=SIN(0.785398163)": "0.707106780905509",
+ "=SIN(SIN(1))": "0.745624141665558",
// SINH
- "=SINH(0)": "0",
- "=SINH(0.5)": "0.5210953054937474",
- "=SINH(-2)": "-3.626860407847019",
+ "=SINH(0)": "0",
+ "=SINH(0.5)": "0.521095305493747",
+ "=SINH(-2)": "-3.62686040784702",
+ "=SINH(SINH(0))": "0",
// SQRT
- "=SQRT(4)": "2",
+ "=SQRT(4)": "2",
+ "=SQRT(SQRT(16))": "2",
// SQRTPI
- "=SQRTPI(5)": "3.963327297606011",
- "=SQRTPI(0.2)": "0.7926654595212022",
- "=SQRTPI(100)": "17.72453850905516",
- "=SQRTPI(0)": "0",
+ "=SQRTPI(5)": "3.96332729760601",
+ "=SQRTPI(0.2)": "0.792665459521202",
+ "=SQRTPI(100)": "17.7245385090552",
+ "=SQRTPI(0)": "0",
+ "=SQRTPI(SQRTPI(0))": "0",
+ // STDEV
+ "=STDEV(F2:F9)": "10724.9782875238",
+ "=STDEV(MUNIT(2))": "0.577350269189626",
+ "=STDEV(0,INT(0))": "0",
+ "=STDEV(INT(1),INT(1))": "0",
+ // STDEV.S
+ "=STDEV.S(F2:F9)": "10724.9782875238",
+ // STDEVA
+ "=STDEVA(F2:F9)": "10724.9782875238",
+ "=STDEVA(MUNIT(2))": "0.577350269189626",
+ "=STDEVA(0,INT(0))": "0",
+ // POISSON.DIST
+ "=POISSON.DIST(20,25,FALSE)": "0.0519174686084913",
+ "=POISSON.DIST(35,40,TRUE)": "0.242414197690103",
+ // POISSON
+ "=POISSON(20,25,FALSE)": "0.0519174686084913",
+ "=POISSON(35,40,TRUE)": "0.242414197690103",
+ // SUBTOTAL
+ "=SUBTOTAL(1,A1:A6)": "1.5",
+ "=SUBTOTAL(2,A1:A6)": "4",
+ "=SUBTOTAL(3,A1:A6)": "4",
+ "=SUBTOTAL(4,A1:A6)": "3",
+ "=SUBTOTAL(5,A1:A6)": "0",
+ "=SUBTOTAL(6,A1:A6)": "0",
+ "=SUBTOTAL(7,A1:A6)": "1.29099444873581",
+ "=SUBTOTAL(8,A1:A6)": "1.11803398874989",
+ "=SUBTOTAL(9,A1:A6)": "6",
+ "=SUBTOTAL(10,A1:A6)": "1.66666666666667",
+ "=SUBTOTAL(11,A1:A6)": "1.25",
+ "=SUBTOTAL(101,A1:A6)": "1.5",
+ "=SUBTOTAL(102,A1:A6)": "4",
+ "=SUBTOTAL(103,A1:A6)": "4",
+ "=SUBTOTAL(104,A1:A6)": "3",
+ "=SUBTOTAL(105,A1:A6)": "0",
+ "=SUBTOTAL(106,A1:A6)": "0",
+ "=SUBTOTAL(107,A1:A6)": "1.29099444873581",
+ "=SUBTOTAL(108,A1:A6)": "1.11803398874989",
+ "=SUBTOTAL(109,A1:A6)": "6",
+ "=SUBTOTAL(109,A1:A6,A1:A6)": "12",
+ "=SUBTOTAL(110,A1:A6)": "1.66666666666667",
+ "=SUBTOTAL(111,A1:A6)": "1.25",
+ "=SUBTOTAL(111,A1:A6,A1:A6)": "1.25",
// SUM
"=SUM(1,2)": "3",
- `=SUM("",1,2)`: "3",
+ "=SUM(\"1\",\"2\")": "3",
+ "=SUM(\"\",1,2)": "3",
"=SUM(1,2+3)": "6",
"=SUM(SUM(1,2),2)": "5",
"=(-2-SUM(-4+7))*5": "-25",
@@ -359,371 +892,3763 @@ func TestCalcCellValue(t *testing.T) {
"=SUM(SUM(1+2/1)*2-3/2,2)": "6.5",
"=((3+5*2)+3)/5+(-6)/4*2+3": "3.2",
"=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2",
- "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664",
+ "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.6666666666667",
+ "=SUM(1+ROW())": "2",
+ "=SUM((SUM(2))+1)": "3",
+ "=IF(2<0, 1, (4))": "4",
+ "=IF(2>0, (1), 4)": "1",
+ "=IF(2>0, (A1)*2.5, 4)": "2.5",
+ "=SUM({1,2,3,4,\"\"})": "10",
// SUMIF
- `=SUMIF(F1:F5, ">100")`: "146554",
- `=SUMIF(D3:D7,"Jan",F2:F5)`: "112114",
- `=SUMIF(D2:D9,"Feb",F2:F9)`: "157559",
- `=SUMIF(E2:E9,"North 1",F2:F9)`: "66582",
- `=SUMIF(E2:E9,"North*",F2:F9)`: "138772",
+ "=SUMIF(F1:F5, \"\")": "0",
+ "=SUMIF(A1:A5, \"3\")": "3",
+ "=SUMIF(F1:F5, \"=36693\")": "36693",
+ "=SUMIF(F1:F5, \"<100\")": "0",
+ "=SUMIF(F1:F5, \"<=36693\")": "93233",
+ "=SUMIF(F1:F5, \">100\")": "146554",
+ "=SUMIF(F1:F5, \">=100\")": "146554",
+ "=SUMIF(F1:F5, \">=text\")": "0",
+ "=SUMIF(F1:F5, \"*Jan\",F2:F5)": "0",
+ "=SUMIF(D3:D7,\"Jan\",F2:F5)": "112114",
+ "=SUMIF(D2:D9,\"Feb\",F2:F9)": "157559",
+ "=SUMIF(E2:E9,\"North 1\",F2:F9)": "66582",
+ "=SUMIF(E2:E9,\"North*\",F2:F9)": "138772",
+ "=SUMIF(D1:D3,\"Month\",D1:D3)": "0",
+ // SUMPRODUCT
+ "=SUMPRODUCT(A1,B1)": "4",
+ "=SUMPRODUCT(A1:A2,B1:B2)": "14",
+ "=SUMPRODUCT(A1:A3,B1:B3)": "14",
+ "=SUMPRODUCT(A1:B3)": "15",
+ "=SUMPRODUCT(A1:A3,B1:B3,B2:B4)": "20",
// SUMSQ
- "=SUMSQ(A1:A4)": "14",
- "=SUMSQ(A1,B1,A2,B2,6)": "82",
- `=SUMSQ("",A1,B1,A2,B2,6)`: "82",
+ "=SUMSQ(A1:A4)": "14",
+ "=SUMSQ(A1,B1,A2,B2,6)": "82",
+ "=SUMSQ(\"\",A1,B1,A2,B2,6)": "82",
+ "=SUMSQ(1,SUMSQ(1))": "2",
+ "=SUMSQ(\"1\",SUMSQ(1))": "2",
+ "=SUMSQ(MUNIT(3))": "3",
+ // SUMX2MY2
+ "=SUMX2MY2(A1:A4,B1:B4)": "-36",
+ // SUMX2PY2
+ "=SUMX2PY2(A1:A4,B1:B4)": "46",
+ // SUMXMY2
+ "=SUMXMY2(A1:A4,B1:B4)": "18",
// TAN
- "=TAN(1.047197551)": "1.732050806782486",
+ "=TAN(1.047197551)": "1.73205080678249",
"=TAN(0)": "0",
+ "=TAN(TAN(0))": "0",
// TANH
- "=TANH(0)": "0",
- "=TANH(0.5)": "0.46211715726000974",
- "=TANH(-2)": "-0.9640275800758169",
+ "=TANH(0)": "0",
+ "=TANH(0.5)": "0.46211715726001",
+ "=TANH(-2)": "-0.964027580075817",
+ "=TANH(TANH(0))": "0",
// TRUNC
- "=TRUNC(99.999,1)": "99.9",
- "=TRUNC(99.999,2)": "99.99",
- "=TRUNC(99.999)": "99",
- "=TRUNC(99.999,-1)": "90",
- "=TRUNC(-99.999,2)": "-99.99",
- "=TRUNC(-99.999,-1)": "-90",
- // Statistical functions
+ "=TRUNC(99.999,1)": "99.9",
+ "=TRUNC(99.999,2)": "99.99",
+ "=TRUNC(99.999)": "99",
+ "=TRUNC(99.999,-1)": "90",
+ "=TRUNC(-99.999,2)": "-99.99",
+ "=TRUNC(-99.999,-1)": "-90",
+ "=TRUNC(TRUNC(1),-1)": "0",
+ // Statistical Functions
+ // AVEDEV
+ "=AVEDEV(1,2)": "0.5",
+ "=AVERAGE(A1:A4,B1:B4)": "2.5",
+ // AVERAGE
+ "=AVERAGE(INT(1))": "1",
+ "=AVERAGE(A1)": "1",
+ "=AVERAGE(A1:A2)": "1.5",
+ "=AVERAGE(D2:F9)": "38014.125",
+ // AVERAGEA
+ "=AVERAGEA(INT(1))": "1",
+ "=AVERAGEA(A1)": "1",
+ "=AVERAGEA(\"1\")": "1",
+ "=AVERAGEA(A1:A2)": "1.5",
+ "=AVERAGEA(D2:F9)": "12671.375",
+ // BETA.DIST
+ "=BETA.DIST(0.4,4,5,TRUE,0,1)": "0.4059136",
+ "=BETA.DIST(0.6,4,5,FALSE,0,1)": "1.548288",
+ // BETADIST
+ "=BETADIST(0.4,4,5)": "0.4059136",
+ "=BETADIST(0.4,4,5,0,1)": "0.4059136",
+ "=BETADIST(0.4,4,5,0.4,1)": "0",
+ "=BETADIST(1,2,2,1,3)": "0",
+ "=BETADIST(0.4,4,5,0.2,0.4)": "1",
+ "=BETADIST(0.4,4,1)": "0.0256",
+ "=BETADIST(0.4,1,5)": "0.92224",
+ "=BETADIST(3,4,6,2,4)": "0.74609375",
+ "=BETADIST(0.4,2,100)": "1",
+ "=BETADIST(0.75,3,4)": "0.96240234375",
+ "=BETADIST(0.2,0.7,4)": "0.71794309318323",
+ "=BETADIST(0.01,3,4)": "1.955359E-05",
+ "=BETADIST(0.75,130,140)": "1",
+ // BETAINV
+ "=BETAINV(0.2,4,5,0,1)": "0.303225844664082",
+ // BETA.INV
+ "=BETA.INV(0.2,4,5,0,1)": "0.303225844664082",
+ // BINOMDIST
+ "=BINOMDIST(10,100,0.5,FALSE)": "1.36554263874631E-17",
+ "=BINOMDIST(50,100,0.5,FALSE)": "0.0795892373871787",
+ "=BINOMDIST(65,100,0.5,FALSE)": "0.000863855665741652",
+ "=BINOMDIST(10,100,0.5,TRUE)": "1.53164508771899E-17",
+ "=BINOMDIST(50,100,0.5,TRUE)": "0.539794618693589",
+ "=BINOMDIST(65,100,0.5,TRUE)": "0.999105034804256",
+ // BINOM.DIST
+ "=BINOM.DIST(10,100,0.5,FALSE)": "1.36554263874631E-17",
+ "=BINOM.DIST(50,100,0.5,FALSE)": "0.0795892373871787",
+ "=BINOM.DIST(65,100,0.5,FALSE)": "0.000863855665741652",
+ "=BINOM.DIST(10,100,0.5,TRUE)": "1.53164508771899E-17",
+ "=BINOM.DIST(50,100,0.5,TRUE)": "0.539794618693589",
+ "=BINOM.DIST(65,100,0.5,TRUE)": "0.999105034804256",
+ // BINOM.DIST.RANGE
+ "=BINOM.DIST.RANGE(100,0.5,0,40)": "0.0284439668204904",
+ "=BINOM.DIST.RANGE(100,0.5,45,55)": "0.728746975926165",
+ "=BINOM.DIST.RANGE(100,0.5,50,100)": "0.539794618693589",
+ "=BINOM.DIST.RANGE(100,0.5,50)": "0.0795892373871787",
+ // BINOM.INV
+ "=BINOM.INV(0,0.5,0.75)": "0",
+ "=BINOM.INV(0.1,0.1,0.75)": "0",
+ "=BINOM.INV(0.6,0.4,0.75)": "0",
+ "=BINOM.INV(2,0.4,0.75)": "1",
+ "=BINOM.INV(100,0.5,20%)": "46",
+ "=BINOM.INV(100,0.5,50%)": "50",
+ "=BINOM.INV(100,0.5,90%)": "56",
+ // CHIDIST
+ "=CHIDIST(0.5,3)": "0.918891411654676",
+ "=CHIDIST(8,3)": "0.0460117056892314",
+ "=CHIDIST(40,4)": "4.32842260712097E-08",
+ "=CHIDIST(42,4)": "1.66816329414062E-08",
+ // CHIINV
+ "=CHIINV(0.5,1)": "0.454936423119572",
+ "=CHIINV(0.75,1)": "0.101531044267622",
+ "=CHIINV(0.1,2)": "4.60517018598809",
+ "=CHIINV(0.8,2)": "0.446287102628419",
+ // CHISQ.DIST
+ "=CHISQ.DIST(0,2,TRUE)": "0",
+ "=CHISQ.DIST(4,1,TRUE)": "0.954499736103642",
+ "=CHISQ.DIST(1180,1180,FALSE)": "0.00821093706387967",
+ "=CHISQ.DIST(2,1,FALSE)": "0.103776874355149",
+ "=CHISQ.DIST(3,2,FALSE)": "0.111565080074215",
+ "=CHISQ.DIST(2,3,FALSE)": "0.207553748710297",
+ "=CHISQ.DIST(1425,1,FALSE)": "3.88315098887099E-312",
+ "=CHISQ.DIST(3,2,TRUE)": "0.77686983985157",
+ // CHISQ.DIST.RT
+ "=CHISQ.DIST.RT(0.5,3)": "0.918891411654676",
+ "=CHISQ.DIST.RT(8,3)": "0.0460117056892314",
+ "=CHISQ.DIST.RT(40,4)": "4.32842260712097E-08",
+ "=CHISQ.DIST.RT(42,4)": "1.66816329414062E-08",
+ // CHISQ.INV
+ "=CHISQ.INV(0,2)": "0",
+ "=CHISQ.INV(0.75,1)": "1.32330369693147",
+ "=CHISQ.INV(0.1,2)": "0.210721031315653",
+ "=CHISQ.INV(0.8,2)": "3.2188758248682",
+ "=CHISQ.INV(0.25,3)": "1.21253290304567",
+ // CHISQ.INV.RT
+ "=CHISQ.INV.RT(0.75,1)": "0.101531044267622",
+ "=CHISQ.INV.RT(0.1,2)": "4.60517018598809",
+ "=CHISQ.INV.RT(0.8,2)": "0.446287102628419",
+ // CONFIDENCE
+ "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414",
+ // CONFIDENCE.NORM
+ "=CONFIDENCE.NORM(0.05,0.07,100)": "0.0137197479028414",
+ // CONFIDENCE.T
+ "=CONFIDENCE.T(0.05,0.07,100)": "0.0138895186611049",
+ // CORREL
+ "=CORREL(A1:A5,B1:B5)": "1",
+ // COUNT
+ "=COUNT()": "0",
+ "=COUNT(E1:F2,\"text\",1,INT(2),\"0\")": "4",
// COUNTA
- `=COUNTA()`: "0",
- `=COUNTA(A1:A5,B2:B5,"text",1,2)`: "8",
+ "=COUNTA()": "0",
+ "=COUNTA(A1:A5,B2:B5,\"text\",1,INT(2))": "8",
+ "=COUNTA(COUNTA(1),MUNIT(1))": "2",
+ "=COUNTA(D1:D2)": "2",
+ // COUNTBLANK
+ "=COUNTBLANK(MUNIT(1))": "0",
+ "=COUNTBLANK(1)": "0",
+ "=COUNTBLANK(B1:C1)": "1",
+ "=COUNTBLANK(C1)": "0",
+ // COUNTIF
+ "=COUNTIF(D1:D9,\"Jan\")": "4",
+ "=COUNTIF(D1:D9,\"<>Jan\")": "5",
+ "=COUNTIF(A1:F9,\">=50000\")": "2",
+ "=COUNTIF(A1:F9,TRUE)": "0",
+ // COUNTIFS
+ "=COUNTIFS(A1:A9,2,D1:D9,\"Jan\")": "1",
+ "=COUNTIFS(F1:F9,\">20000\",D1:D9,\"Jan\")": "4",
+ "=COUNTIFS(F1:F9,\">60000\",D1:D9,\"Jan\")": "0",
+ // CRITBINOM
+ "=CRITBINOM(0,0.5,0.75)": "0",
+ "=CRITBINOM(0.1,0.1,0.75)": "0",
+ "=CRITBINOM(0.6,0.4,0.75)": "0",
+ "=CRITBINOM(2,0.4,0.75)": "1",
+ "=CRITBINOM(100,0.5,20%)": "46",
+ "=CRITBINOM(100,0.5,50%)": "50",
+ "=CRITBINOM(100,0.5,90%)": "56",
+ // DEVSQ
+ "=DEVSQ(1,3,5,2,9,7)": "47.5",
+ "=DEVSQ(A1:D2)": "10",
+ // FISHER
+ "=FISHER(-0.9)": "-1.47221948958322",
+ "=FISHER(-0.25)": "-0.255412811882995",
+ "=FISHER(0.8)": "1.09861228866811",
+ "=FISHER(\"0.8\")": "1.09861228866811",
+ "=FISHER(INT(0))": "0",
+ // FISHERINV
+ "=FISHERINV(-0.2)": "-0.197375320224904",
+ "=FISHERINV(INT(0))": "0",
+ "=FISHERINV(\"0\")": "0",
+ "=FISHERINV(2.8)": "0.992631520201128",
+ // FORECAST
+ "=FORECAST(7,A1:A7,B1:B7)": "4",
+ // FORECAST.LINEAR
+ "=FORECAST.LINEAR(7,A1:A7,B1:B7)": "4",
+ // FREQUENCY
+ "=SUM(FREQUENCY(A2,B2))": "1",
+ "=SUM(FREQUENCY(A1:A5,B1:B2))": "4",
+ // GAMMA
+ "=GAMMA(0.1)": "9.51350769866873",
+ "=GAMMA(INT(1))": "1",
+ "=GAMMA(1.5)": "0.886226925452758",
+ "=GAMMA(5.5)": "52.3427777845535",
+ "=GAMMA(\"5.5\")": "52.3427777845535",
+ // GAMMA.DIST
+ "=GAMMA.DIST(6,3,2,FALSE)": "0.112020903827694",
+ "=GAMMA.DIST(6,3,2,TRUE)": "0.576809918873156",
+ // GAMMADIST
+ "=GAMMADIST(6,3,2,FALSE)": "0.112020903827694",
+ "=GAMMADIST(6,3,2,TRUE)": "0.576809918873156",
+ // GAMMA.INV
+ "=GAMMA.INV(0.5,3,2)": "5.34812062744712",
+ "=GAMMA.INV(0.5,0.5,1)": "0.227468211559786",
+ // GAMMAINV
+ "=GAMMAINV(0.5,3,2)": "5.34812062744712",
+ "=GAMMAINV(0.5,0.5,1)": "0.227468211559786",
+ // GAMMALN
+ "=GAMMALN(4.5)": "2.45373657084244",
+ "=GAMMALN(INT(1))": "0",
+ // GAMMALN.PRECISE
+ "=GAMMALN.PRECISE(0.4)": "0.796677817701784",
+ "=GAMMALN.PRECISE(4.5)": "2.45373657084244",
+ // GAUSS
+ "=GAUSS(-5)": "-0.499999713348428",
+ "=GAUSS(0)": "0",
+ "=GAUSS(\"0\")": "0",
+ "=GAUSS(0.1)": "0.039827837277029",
+ "=GAUSS(2.5)": "0.493790334674224",
+ // GEOMEAN
+ "=GEOMEAN(2.5,3,0.5,1,3)": "1.6226711115996",
+ // HARMEAN
+ "=HARMEAN(2.5,3,0.5,1,3)": "1.22950819672131",
+ "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.22950819672131",
+ // HYPGEOM.DIST
+ "=HYPGEOM.DIST(0,3,3,9,TRUE)": "0.238095238095238",
+ "=HYPGEOM.DIST(1,3,3,9,TRUE)": "0.773809523809524",
+ "=HYPGEOM.DIST(2,3,3,9,TRUE)": "0.988095238095238",
+ "=HYPGEOM.DIST(3,3,3,9,TRUE)": "1",
+ "=HYPGEOM.DIST(1,4,4,12,FALSE)": "0.452525252525253",
+ "=HYPGEOM.DIST(2,4,4,12,FALSE)": "0.339393939393939",
+ "=HYPGEOM.DIST(3,4,4,12,FALSE)": "0.0646464646464646",
+ "=HYPGEOM.DIST(4,4,4,12,FALSE)": "0.00202020202020202",
+ // HYPGEOMDIST
+ "=HYPGEOMDIST(1,4,4,12)": "0.452525252525253",
+ "=HYPGEOMDIST(2,4,4,12)": "0.339393939393939",
+ "=HYPGEOMDIST(3,4,4,12)": "0.0646464646464646",
+ "=HYPGEOMDIST(4,4,4,12)": "0.00202020202020202",
+ // INTERCEPT
+ "=INTERCEPT(A1:A4,B1:B4)": "-3",
+ // KURT
+ "=KURT(F1:F9)": "-1.03350350255137",
+ "=KURT(F1,F2:F9)": "-1.03350350255137",
+ "=KURT(INT(1),MUNIT(2))": "-3.33333333333334",
+ // EXPON.DIST
+ "=EXPON.DIST(0.5,1,TRUE)": "0.393469340287367",
+ "=EXPON.DIST(0.5,1,FALSE)": "0.606530659712633",
+ "=EXPON.DIST(2,1,TRUE)": "0.864664716763387",
+ // EXPONDIST
+ "=EXPONDIST(0.5,1,TRUE)": "0.393469340287367",
+ "=EXPONDIST(0.5,1,FALSE)": "0.606530659712633",
+ "=EXPONDIST(2,1,TRUE)": "0.864664716763387",
+ // FDIST
+ "=FDIST(5,1,2)": "0.154845745271483",
+ // F.DIST
+ "=F.DIST(1,2,5,TRUE)": "0.568798849628308",
+ "=F.DIST(1,2,5,FALSE)": "0.308000821694066",
+ // F.DIST.RT
+ "=F.DIST.RT(5,1,2)": "0.154845745271483",
+ // F.INV
+ "=F.INV(0.9,2,5)": "3.77971607877395",
+ // FINV
+ "=FINV(0.2,1,2)": "3.55555555555555",
+ "=FINV(0.6,1,2)": "0.380952380952381",
+ "=FINV(0.6,2,2)": "0.666666666666667",
+ "=FINV(0.6,4,4)": "0.763454070045235",
+ "=FINV(0.5,4,8)": "0.914645355977072",
+ "=FINV(0.1,79,86)": "1.32646097270444",
+ "=FINV(1,40,5)": "0",
+ // F.INV.RT
+ "=F.INV.RT(0.2,1,2)": "3.55555555555555",
+ "=F.INV.RT(0.6,1,2)": "0.380952380952381",
+ "=F.INV.RT(0.6,2,2)": "0.666666666666667",
+ "=F.INV.RT(0.6,4,4)": "0.763454070045235",
+ "=F.INV.RT(0.5,4,8)": "0.914645355977072",
+ "=F.INV.RT(0.1,79,86)": "1.32646097270444",
+ "=F.INV.RT(1,40,5)": "0",
+ // LOGINV
+ "=LOGINV(0.3,2,0.2)": "6.6533460753367",
+ // LOGINV
+ "=LOGNORM.INV(0.3,2,0.2)": "6.6533460753367",
+ // LOGNORM.DIST
+ "=LOGNORM.DIST(0.5,10,5,FALSE)": "0.0162104821842127",
+ "=LOGNORM.DIST(12,10,5,TRUE)": "0.0664171147992078",
+ // LOGNORMDIST
+ "=LOGNORMDIST(12,10,5)": "0.0664171147992078",
+ // NEGBINOM.DIST
+ "=NEGBINOM.DIST(6,12,0.5,FALSE)": "0.047210693359375",
+ "=NEGBINOM.DIST(12,12,0.5,FALSE)": "0.0805901288986206",
+ "=NEGBINOM.DIST(15,12,0.5,FALSE)": "0.057564377784729",
+ "=NEGBINOM.DIST(12,12,0.5,TRUE)": "0.580590128898621",
+ "=NEGBINOM.DIST(15,12,0.5,TRUE)": "0.778965830802917",
+ // NEGBINOMDIST
+ "=NEGBINOMDIST(6,12,0.5)": "0.047210693359375",
+ "=NEGBINOMDIST(12,12,0.5)": "0.0805901288986206",
+ "=NEGBINOMDIST(15,12,0.5)": "0.057564377784729",
+ // NORM.DIST
+ "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923",
+ "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215",
+ // NORMDIST
+ "=NORMDIST(0.8,1,0.3,TRUE)": "0.252492537546923",
+ "=NORMDIST(50,40,20,FALSE)": "0.017603266338215",
+ // NORM.INV
+ "=NORM.INV(0.6,5,2)": "5.50669420572",
+ // NORMINV
+ "=NORMINV(0.6,5,2)": "5.50669420572",
+ "=NORMINV(0.99,40,1.5)": "43.489521811582",
+ "=NORMINV(0.02,40,1.5)": "36.9193766364954",
+ // NORM.S.DIST
+ "=NORM.S.DIST(0.8,TRUE)": "0.788144601416603",
+ // NORMSDIST
+ "=NORMSDIST(1.333333)": "0.908788725604095",
+ "=NORMSDIST(0)": "0.5",
+ // NORM.S.INV
+ "=NORM.S.INV(0.25)": "-0.674489750223423",
+ // NORMSINV
+ "=NORMSINV(0.25)": "-0.674489750223423",
+ // LARGE
+ "=LARGE(A1:A5,1)": "3",
+ "=LARGE(A1:B5,2)": "4",
+ "=LARGE(A1,1)": "1",
+ "=LARGE(A1:F2,1)": "36693",
+ // MAX
+ "=MAX(1)": "1",
+ "=MAX(TRUE())": "1",
+ "=MAX(0.5,TRUE())": "1",
+ "=MAX(FALSE())": "0",
+ "=MAX(MUNIT(2))": "1",
+ "=MAX(INT(1))": "1",
+ "=MAX(\"0\",\"2\")": "2",
+ // MAXA
+ "=MAXA(1)": "1",
+ "=MAXA(TRUE())": "1",
+ "=MAXA(0.5,TRUE())": "1",
+ "=MAXA(FALSE())": "0",
+ "=MAXA(MUNIT(2))": "1",
+ "=MAXA(INT(1))": "1",
+ "=MAXA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "36693",
+ // MAXIFS
+ "=MAXIFS(F2:F4,A2:A4,\">0\")": "36693",
// MEDIAN
- "=MEDIAN(A1:A5,12)": "2",
- "=MEDIAN(A1:A5)": "1.5",
- // Information functions
+ "=MEDIAN(A1:A5,12)": "2",
+ "=MEDIAN(A1:A5)": "1.5",
+ "=MEDIAN(A1:A5,MEDIAN(A1:A5,12))": "2",
+ "=MEDIAN(\"0\",\"2\")": "1",
+ // MIN
+ "=MIN(1)": "1",
+ "=MIN(TRUE())": "1",
+ "=MIN(0.5,FALSE())": "0",
+ "=MIN(FALSE())": "0",
+ "=MIN(MUNIT(2))": "0",
+ "=MIN(INT(1))": "1",
+ "=MIN(2,\"1\")": "1",
+ // MINA
+ "=MINA(1)": "1",
+ "=MINA(TRUE())": "1",
+ "=MINA(0.5,FALSE())": "0",
+ "=MINA(FALSE())": "0",
+ "=MINA(MUNIT(2))": "0",
+ "=MINA(INT(1))": "1",
+ "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0",
+ // MINIFS
+ "=MINIFS(F2:F4,A2:A4,\">0\")": "22100",
+ // PEARSON
+ "=PEARSON(A1:A4,B1:B4)": "1",
+ // PERCENTILE.EXC
+ "=PERCENTILE.EXC(A1:A4,0.2)": "0",
+ "=PERCENTILE.EXC(A1:A4,0.6)": "2",
+ // PERCENTILE.INC
+ "=PERCENTILE.INC(A1:A4,0.2)": "0.6",
+ // PERCENTILE
+ "=PERCENTILE(A1:A4,0.2)": "0.6",
+ "=PERCENTILE(0,0)": "0",
+ // PERCENTRANK.EXC
+ "=PERCENTRANK.EXC(A1:B4,0)": "0.142",
+ "=PERCENTRANK.EXC(A1:B4,2)": "0.428",
+ "=PERCENTRANK.EXC(A1:B4,2.5)": "0.5",
+ "=PERCENTRANK.EXC(A1:B4,2.6,1)": "0.5",
+ "=PERCENTRANK.EXC(A1:B4,5)": "0.857",
+ // PERCENTRANK.INC
+ "=PERCENTRANK.INC(A1:B4,0)": "0",
+ "=PERCENTRANK.INC(A1:B4,2)": "0.4",
+ "=PERCENTRANK.INC(A1:B4,2.5)": "0.5",
+ "=PERCENTRANK.INC(A1:B4,2.6,1)": "0.5",
+ "=PERCENTRANK.INC(A1:B4,5)": "1",
+ // PERCENTRANK
+ "=PERCENTRANK(A1:B4,0)": "0",
+ "=PERCENTRANK(A1:B4,2)": "0.4",
+ "=PERCENTRANK(A1:B4,2.5)": "0.5",
+ "=PERCENTRANK(A1:B4,2.6,1)": "0.5",
+ "=PERCENTRANK(A1:B4,5)": "1",
+ // PERMUT
+ "=PERMUT(6,6)": "720",
+ "=PERMUT(7,6)": "5040",
+ "=PERMUT(10,6)": "151200",
+ // PERMUTATIONA
+ "=PERMUTATIONA(6,6)": "46656",
+ "=PERMUTATIONA(7,6)": "117649",
+ // PHI
+ "=PHI(-1.5)": "0.129517595665892",
+ "=PHI(0)": "0.398942280401433",
+ "=PHI(0.1)": "0.396952547477012",
+ "=PHI(1)": "0.241970724519143",
+ // QUARTILE
+ "=QUARTILE(A1:A4,2)": "1.5",
+ // QUARTILE.EXC
+ "=QUARTILE.EXC(A1:A4,1)": "0.25",
+ "=QUARTILE.EXC(A1:A4,2)": "1.5",
+ "=QUARTILE.EXC(A1:A4,3)": "2.75",
+ // QUARTILE.INC
+ "=QUARTILE.INC(A1:A4,0)": "0",
+ // RANK
+ "=RANK(1,A1:B5)": "5",
+ "=RANK(1,A1:B5,0)": "5",
+ "=RANK(1,A1:B5,1)": "2",
+ // RANK.EQ
+ "=RANK.EQ(1,A1:B5)": "5",
+ "=RANK.EQ(1,A1:B5,0)": "5",
+ "=RANK.EQ(1,A1:B5,1)": "2",
+ // RSQ
+ "=RSQ(A1:A4,B1:B4)": "1",
+ // SKEW
+ "=SKEW(1,2,3,4,3)": "-0.404796008910937",
+ "=SKEW(A1:B2)": "0",
+ "=SKEW(A1:D3)": "0",
+ // SKEW.P
+ "=SKEW.P(1,2,3,4,3)": "-0.27154541788364",
+ "=SKEW.P(A1:B2)": "0",
+ "=SKEW.P(A1:D3)": "0",
+ // SLOPE
+ "=SLOPE(A1:A4,B1:B4)": "1",
+ // SMALL
+ "=SMALL(A1:A5,1)": "0",
+ "=SMALL(A1:B5,2)": "1",
+ "=SMALL(A1,1)": "1",
+ "=SMALL(A1:F2,1)": "1",
+ // STANDARDIZE
+ "=STANDARDIZE(5.5,5,2)": "0.25",
+ "=STANDARDIZE(12,15,1.5)": "-2",
+ "=STANDARDIZE(-2,0,5)": "-0.4",
+ // STDEVP
+ "=STDEVP(A1:B2,6,-1)": "2.40947204913349",
+ // STDEV.P
+ "=STDEV.P(A1:B2,6,-1)": "2.40947204913349",
+ // STDEVPA
+ "=STDEVPA(1,3,5,2)": "1.4790199457749",
+ "=STDEVPA(1,3,5,2,1,0)": "1.63299316185545",
+ "=STDEVPA(1,3,5,2,TRUE,\"text\")": "1.63299316185545",
+ // T.DIST
+ "=T.DIST(1,10,TRUE)": "0.82955343384897",
+ "=T.DIST(-1,10,TRUE)": "0.17044656615103",
+ "=T.DIST(-1,10,FALSE)": "0.230361989229139",
+ // T.DIST.2T
+ "=T.DIST.2T(1,10)": "0.34089313230206",
+ // T.DIST.RT
+ "=T.DIST.RT(1,10)": "0.17044656615103",
+ "=T.DIST.RT(-1,10)": "0.82955343384897",
+ // TDIST
+ "=TDIST(1,10,1)": "0.17044656615103",
+ "=TDIST(1,10,2)": "0.34089313230206",
+ // T.INV
+ "=T.INV(0.25,10)": "-0.699812061312432",
+ "=T.INV(0.75,10)": "0.699812061312432",
+ // T.INV.2T
+ "=T.INV.2T(1,10)": "0",
+ "=T.INV.2T(0.5,10)": "0.699812061312432",
+ // TINV
+ "=TINV(1,10)": "0",
+ "=TINV(0.5,10)": "0.699812061312432",
+ // TRIMMEAN
+ "=TRIMMEAN(A1:B4,10%)": "2.5",
+ "=TRIMMEAN(A1:B4,70%)": "2.5",
+ // VAR
+ "=VAR(1,3,5,0,C1)": "4.91666666666667",
+ "=VAR(1,3,5,0,C1,TRUE)": "4",
+ // VARA
+ "=VARA(1,3,5,0,C1)": "4.91666666666667",
+ "=VARA(1,3,5,0,C1,TRUE)": "4",
+ // VARP
+ "=VARP(A1:A5)": "1.25",
+ "=VARP(1,3,5,0,C1,TRUE)": "3.2",
+ // VAR.P
+ "=VAR.P(A1:A5)": "1.25",
+ // VAR.S
+ "=VAR.S(1,3,5,0,C1)": "4.91666666666667",
+ "=VAR.S(1,3,5,0,C1,TRUE)": "4",
+ // VARPA
+ "=VARPA(1,3,5,0,C1)": "3.6875",
+ "=VARPA(1,3,5,0,C1,TRUE)": "3.2",
+ // WEIBULL
+ "=WEIBULL(1,3,1,FALSE)": "1.10363832351433",
+ "=WEIBULL(2,5,1.5,TRUE)": "0.985212776817482",
+ // WEIBULL.DIST
+ "=WEIBULL.DIST(1,3,1,FALSE)": "1.10363832351433",
+ "=WEIBULL.DIST(2,5,1.5,TRUE)": "0.985212776817482",
+ // Information Functions
+ // ERROR.TYPE
+ "=ERROR.TYPE(1/0)": "2",
+ "=ERROR.TYPE(COT(0))": "2",
+ "=ERROR.TYPE(XOR(\"text\"))": "3",
+ "=ERROR.TYPE(HEX2BIN(2,1))": "6",
+ "=ERROR.TYPE(NA())": "7",
// ISBLANK
"=ISBLANK(A1)": "FALSE",
"=ISBLANK(A5)": "TRUE",
// ISERR
- "=ISERR(A1)": "FALSE",
- "=ISERR(NA())": "FALSE",
+ "=ISERR(A1)": "FALSE",
+ "=ISERR(NA())": "FALSE",
+ "=ISERR(POWER(0,-1)))": "TRUE",
// ISERROR
- "=ISERROR(A1)": "FALSE",
- "=ISERROR(NA())": "TRUE",
+ "=ISERROR(A1)": "FALSE",
+ "=ISERROR(NA())": "TRUE",
+ "=ISERROR(\"#VALUE!\")": "FALSE",
// ISEVEN
"=ISEVEN(A1)": "FALSE",
"=ISEVEN(A2)": "TRUE",
+ "=ISEVEN(G1)": "TRUE",
+ // ISFORMULA
+ "=ISFORMULA(A1)": "FALSE",
+ "=ISFORMULA(\"A\")": "FALSE",
+ // ISLOGICAL
+ "=ISLOGICAL(TRUE)": "TRUE",
+ "=ISLOGICAL(FALSE)": "TRUE",
+ "=ISLOGICAL(A1=A2)": "TRUE",
+ "=ISLOGICAL(\"true\")": "TRUE",
+ "=ISLOGICAL(\"false\")": "TRUE",
+ "=ISLOGICAL(A1)": "FALSE",
+ "=ISLOGICAL(20/5)": "FALSE",
// ISNA
"=ISNA(A1)": "FALSE",
"=ISNA(NA())": "TRUE",
// ISNONTEXT
- "=ISNONTEXT(A1)": "FALSE",
- "=ISNONTEXT(A5)": "TRUE",
- `=ISNONTEXT("Excelize")`: "FALSE",
- "=ISNONTEXT(NA())": "FALSE",
+ "=ISNONTEXT(A1)": "TRUE",
+ "=ISNONTEXT(A5)": "TRUE",
+ "=ISNONTEXT(\"Excelize\")": "FALSE",
+ "=ISNONTEXT(NA())": "TRUE",
// ISNUMBER
- "=ISNUMBER(A1)": "TRUE",
- "=ISNUMBER(D1)": "FALSE",
+ "=ISNUMBER(A1)": "TRUE",
+ "=ISNUMBER(D1)": "FALSE",
+ "=ISNUMBER(A1:B1)": "TRUE",
// ISODD
"=ISODD(A1)": "TRUE",
"=ISODD(A2)": "FALSE",
- // NA
- "=NA()": "#N/A",
+ // ISREF
+ "=ISREF(B1)": "TRUE",
+ "=ISREF(B1:B2)": "TRUE",
+ "=ISREF(\"text\")": "FALSE",
+ "=ISREF(B1*B2)": "FALSE",
+ // ISTEXT
+ "=ISTEXT(D1)": "TRUE",
+ "=ISTEXT(A1)": "FALSE",
+ // N
+ "=N(10)": "10",
+ "=N(\"10\")": "10",
+ "=N(\"x\")": "0",
+ "=N(TRUE)": "1",
+ "=N(FALSE)": "0",
+ // SHEET
+ "=SHEET()": "1",
+ "=SHEET(\"Sheet1\")": "1",
+ // SHEETS
+ "=SHEETS()": "1",
+ "=SHEETS(A1)": "1",
+ // TYPE
+ "=TYPE(2)": "1",
+ "=TYPE(10/2)": "1",
+ "=TYPE(C2)": "1",
+ "=TYPE(\"text\")": "2",
+ "=TYPE(TRUE)": "4",
+ "=TYPE(NA())": "16",
+ "=TYPE(MUNIT(2))": "64",
+ // T
+ "=T(\"text\")": "text",
+ "=T(N(10))": "",
+ // Logical Functions
+ // AND
+ "=AND(0)": "FALSE",
+ "=AND(1)": "TRUE",
+ "=AND(1,0)": "FALSE",
+ "=AND(0,1)": "FALSE",
+ "=AND(1=1)": "TRUE",
+ "=AND(1<2)": "TRUE",
+ "=AND(1>2,2<3,2>0,3>1)": "FALSE",
+ "=AND(1=1),1=1": "TRUE",
+ "=AND(\"TRUE\",\"FALSE\")": "FALSE",
+ // FALSE
+ "=FALSE()": "FALSE",
+ // IFERROR
+ "=IFERROR(1/2,0)": "0.5",
+ "=IFERROR(ISERROR(),0)": "0",
+ "=IFERROR(1/0,0)": "0",
+ "=IFERROR(G1,2)": "0",
+ "=IFERROR(B2/MROUND(A2,1),0)": "2.5",
+ // IFNA
+ "=IFNA(1,\"not found\")": "1",
+ "=IFNA(NA(),\"not found\")": "not found",
+ "=IFNA(HLOOKUP(D2,D:D,1,2),\"not found\")": "not found",
+ // IFS
+ "=IFS(4>1,5/4,4<-1,-5/4,TRUE,0)": "1.25",
+ "=IFS(-2>1,5/-2,-2<-1,-5/-2,TRUE,0)": "2.5",
+ "=IFS(0>1,5/0,0<-1,-5/0,TRUE,0)": "0",
+ // NOT
+ "=NOT(FALSE())": "TRUE",
+ "=NOT(\"false\")": "TRUE",
+ "=NOT(\"true\")": "FALSE",
+ "=NOT(ISBLANK(B1))": "TRUE",
+ // OR
+ "=OR(1)": "TRUE",
+ "=OR(0)": "FALSE",
+ "=OR(1=2,2=2)": "TRUE",
+ "=OR(1=2,2=3)": "FALSE",
+ "=OR(1=1,2=3)": "TRUE",
+ "=OR(\"TRUE\",\"FALSE\")": "TRUE",
+ "=OR(A1:B1)": "TRUE",
+ // SWITCH
+ "=SWITCH(1,1,\"A\",2,\"B\",3,\"C\",\"N\")": "A",
+ "=SWITCH(3,1,\"A\",2,\"B\",3,\"C\",\"N\")": "C",
+ "=SWITCH(4,1,\"A\",2,\"B\",3,\"C\",\"N\")": "N",
+ // TRUE
+ "=TRUE()": "TRUE",
+ // XOR
+ "=XOR(1>0,2>0)": "FALSE",
+ "=XOR(1>0,0>1)": "TRUE",
+ "=XOR(1>0,0>1,INT(0),INT(1),A1:A4,2)": "FALSE",
+ // Date and Time Functions
+ // DATE
+ "=DATE(2020,10,21)": "44125",
+ "=DATE(2020,10,21)+1": "44126",
+ "=DATE(1900,1,1)": "1",
+ // DATEDIF
+ "=DATEDIF(43101,43101,\"D\")": "0",
+ "=DATEDIF(43101,43891,\"d\")": "790",
+ "=DATEDIF(43101,43891,\"Y\")": "2",
+ "=DATEDIF(42156,44242,\"y\")": "5",
+ "=DATEDIF(43101,43891,\"M\")": "26",
+ "=DATEDIF(42171,44242,\"m\")": "67",
+ "=DATEDIF(42156,44454,\"MD\")": "14",
+ "=DATEDIF(42171,44242,\"md\")": "30",
+ "=DATEDIF(43101,43891,\"YM\")": "2",
+ "=DATEDIF(42171,44242,\"ym\")": "7",
+ "=DATEDIF(43101,43891,\"YD\")": "59",
+ "=DATEDIF(36526,73110,\"YD\")": "60",
+ "=DATEDIF(42171,44242,\"yd\")": "244",
+ // DATEVALUE
+ "=DATEVALUE(\"01/01/16\")": "42370",
+ "=DATEVALUE(\"01/01/2016\")": "42370",
+ "=DATEVALUE(\"01/01/29\")": "47119",
+ "=DATEVALUE(\"01/01/30\")": "10959",
+ // DAY
+ "=DAY(0)": "0",
+ "=DAY(INT(7))": "7",
+ "=DAY(\"35\")": "4",
+ "=DAY(42171)": "16",
+ "=DAY(\"2-28-1900\")": "28",
+ "=DAY(\"31-May-2015\")": "31",
+ "=DAY(\"01/03/2019 12:14:16\")": "3",
+ "=DAY(\"January 25, 2020 01 AM\")": "25",
+ "=DAY(\"January 25, 2020 01:03 AM\")": "25",
+ "=DAY(\"January 25, 2020 12:00:00 AM\")": "25",
+ "=DAY(\"1900-1-1\")": "1",
+ "=DAY(\"12-1-1900\")": "1",
+ "=DAY(\"3-January-1900\")": "3",
+ "=DAY(\"3-February-2000\")": "3",
+ "=DAY(\"3-February-2008\")": "3",
+ "=DAY(\"01/25/20\")": "25",
+ "=DAY(\"01/25/31\")": "25",
+ // DAYS
+ "=DAYS(2,1)": "1",
+ "=DAYS(INT(2),INT(1))": "1",
+ "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32",
+ // DAYS360
+ "=DAYS360(\"10/10/2020\", \"10/10/2020\")": "0",
+ "=DAYS360(\"01/30/1999\", \"02/28/1999\")": "28",
+ "=DAYS360(\"01/31/1999\", \"02/28/1999\")": "28",
+ "=DAYS360(\"12/12/1999\", \"08/31/1999\")": "-101",
+ "=DAYS360(\"12/12/1999\", \"11/30/1999\")": "-12",
+ "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE)": "-12",
+ "=DAYS360(\"01/31/1999\", \"03/31/1999\",TRUE)": "60",
+ "=DAYS360(\"01/31/1999\", \"03/31/2000\",FALSE)": "420",
+ // EDATE
+ "=EDATE(\"01/01/2021\",-1)": "44166",
+ "=EDATE(\"01/31/2020\",1)": "43890",
+ "=EDATE(\"01/29/2020\",12)": "44225",
+ "=EDATE(\"6/12/2021\",-14)": "43933",
+ // EOMONTH
+ "=EOMONTH(\"01/01/2021\",-1)": "44196",
+ "=EOMONTH(\"01/29/2020\",12)": "44227",
+ "=EOMONTH(\"01/12/2021\",-18)": "43677",
+ // HOUR
+ "=HOUR(1)": "0",
+ "=HOUR(43543.5032060185)": "12",
+ "=HOUR(\"43543.5032060185\")": "12",
+ "=HOUR(\"13:00:55\")": "13",
+ "=HOUR(\"1:00 PM\")": "13",
+ "=HOUR(\"12/09/2015 08:55\")": "8",
+ // ISOWEEKNUM
+ "=ISOWEEKNUM(42370)": "53",
+ "=ISOWEEKNUM(\"42370\")": "53",
+ "=ISOWEEKNUM(\"01/01/2005\")": "53",
+ "=ISOWEEKNUM(\"02/02/2005\")": "5",
+ // MINUTE
+ "=MINUTE(1)": "0",
+ "=MINUTE(0.04)": "57",
+ "=MINUTE(\"0.04\")": "57",
+ "=MINUTE(\"13:35:55\")": "35",
+ "=MINUTE(\"12/09/2015 08:55\")": "55",
+ // MONTH
+ "=MONTH(42171)": "6",
+ "=MONTH(\"31-May-2015\")": "5",
+ // YEAR
+ "=YEAR(15)": "1900",
+ "=YEAR(\"15\")": "1900",
+ "=YEAR(2048)": "1905",
+ "=YEAR(42171)": "2015",
+ "=YEAR(\"29-May-2015\")": "2015",
+ "=YEAR(\"05/03/1984\")": "1984",
+ // YEARFRAC
+ "=YEARFRAC(42005,42005)": "0",
+ "=YEARFRAC(42005,42094)": "0.25",
+ "=YEARFRAC(42005,42094,0)": "0.25",
+ "=YEARFRAC(42005,42094,1)": "0.243835616438356",
+ "=YEARFRAC(42005,42094,2)": "0.247222222222222",
+ "=YEARFRAC(42005,42094,3)": "0.243835616438356",
+ "=YEARFRAC(42005,42094,4)": "0.247222222222222",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\")": "0.25",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",0)": "0.25",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",1)": "0.243835616438356",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",2)": "0.247222222222222",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",3)": "0.243835616438356",
+ "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",4)": "0.247222222222222",
+ "=YEARFRAC(\"01/01/2015\",42094)": "0.25",
+ "=YEARFRAC(42005,\"03/31/2015\",0)": "0.25",
+ "=YEARFRAC(\"01/31/2015\",\"03/31/2015\")": "0.166666666666667",
+ "=YEARFRAC(\"01/30/2015\",\"03/31/2015\")": "0.166666666666667",
+ "=YEARFRAC(\"02/29/2000\", \"02/29/2008\")": "8",
+ "=YEARFRAC(\"02/29/2000\", \"02/29/2008\",1)": "7.99817518248175",
+ "=YEARFRAC(\"02/29/2000\", \"01/29/2001\",1)": "0.915300546448087",
+ "=YEARFRAC(\"02/29/2000\", \"03/29/2000\",1)": "0.0792349726775956",
+ "=YEARFRAC(\"01/31/2000\", \"03/29/2000\",4)": "0.163888888888889",
+ // SECOND
+ "=SECOND(\"13:35:55\")": "55",
+ "=SECOND(\"13:10:60\")": "0",
+ "=SECOND(\"13:10:61\")": "1",
+ "=SECOND(\"08:17:00\")": "0",
+ "=SECOND(\"12/09/2015 08:55\")": "0",
+ "=SECOND(\"12/09/2011 08:17:23\")": "23",
+ "=SECOND(\"43543.5032060185\")": "37",
+ "=SECOND(43543.5032060185)": "37",
+ // TIME
+ "=TIME(5,44,32)": "0.239259259259259",
+ "=TIME(\"5\",\"44\",\"32\")": "0.239259259259259",
+ "=TIME(0,0,73)": "0.000844907407407407",
+ // TIMEVALUE
+ "=TIMEVALUE(\"2:23\")": "0.0993055555555555",
+ "=TIMEVALUE(\"2:23 am\")": "0.0993055555555555",
+ "=TIMEVALUE(\"2:23 PM\")": "0.599305555555556",
+ "=TIMEVALUE(\"14:23:00\")": "0.599305555555556",
+ "=TIMEVALUE(\"00:02:23\")": "0.00165509259259259",
+ "=TIMEVALUE(\"01/01/2011 02:23\")": "0.0993055555555555",
+ // WEEKDAY
+ "=WEEKDAY(0)": "7",
+ "=WEEKDAY(47119)": "2",
+ "=WEEKDAY(\"12/25/2012\")": "3",
+ "=WEEKDAY(\"12/25/2012\",1)": "3",
+ "=WEEKDAY(\"12/25/2012\",2)": "2",
+ "=WEEKDAY(\"12/25/2012\",3)": "1",
+ "=WEEKDAY(\"12/25/2012\",11)": "2",
+ "=WEEKDAY(\"12/25/2012\",12)": "1",
+ "=WEEKDAY(\"12/25/2012\",13)": "7",
+ "=WEEKDAY(\"12/25/2012\",14)": "6",
+ "=WEEKDAY(\"12/25/2012\",15)": "5",
+ "=WEEKDAY(\"12/25/2012\",16)": "4",
+ "=WEEKDAY(\"12/25/2012\",17)": "3",
+ // WEEKNUM
+ "=WEEKNUM(\"01/01/2011\")": "1",
+ "=WEEKNUM(\"01/03/2011\")": "2",
+ "=WEEKNUM(\"01/13/2008\")": "3",
+ "=WEEKNUM(\"01/21/2008\")": "4",
+ "=WEEKNUM(\"01/30/2008\")": "5",
+ "=WEEKNUM(\"02/04/2008\")": "6",
+ "=WEEKNUM(\"01/02/2017\",2)": "2",
+ "=WEEKNUM(\"01/02/2017\",12)": "1",
+ "=WEEKNUM(\"12/31/2017\",21)": "52",
+ "=WEEKNUM(\"01/01/2017\",21)": "52",
+ "=WEEKNUM(\"01/01/2021\",21)": "53",
+ // Text Functions
+ // ARRAYTOTEXT
+ "=ARRAYTOTEXT(A1:D2)": "1, 4, , Month, 2, 5, , Jan",
+ "=ARRAYTOTEXT(A1:D2,0)": "1, 4, , Month, 2, 5, , Jan",
+ "=ARRAYTOTEXT(A1:D2,1)": "{1,4,,\"Month\";2,5,,\"Jan\"}",
+ // CHAR
+ "=CHAR(65)": "A",
+ "=CHAR(97)": "a",
+ "=CHAR(63)": "?",
+ "=CHAR(51)": "3",
+ // CLEAN
+ "=CLEAN(\"\u0009clean text\")": "clean text",
+ "=CLEAN(0)": "0",
+ // CODE
+ "=CODE(\"Alpha\")": "65",
+ "=CODE(\"alpha\")": "97",
+ "=CODE(\"?\")": "63",
+ "=CODE(\"3\")": "51",
+ "=CODE(\"\")": "0",
+ // CONCAT
+ "=CONCAT(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02",
+ "=CONCAT(MUNIT(2))": "1001",
+ "=CONCAT(A1:B2)": "1425",
+ // CONCATENATE
+ "=CONCATENATE(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02",
+ "=CONCATENATE(MUNIT(2))": "1001",
+ "=CONCATENATE(A1:B2)": "1425",
+ // DBCS
+ "=DBCS(\"\")": "",
+ "=DBCS(123.456)": "123.456",
+ "=DBCS(\"123.456\")": "123.456",
+ // EXACT
+ "=EXACT(1,\"1\")": "TRUE",
+ "=EXACT(1,1)": "TRUE",
+ "=EXACT(\"A\",\"a\")": "FALSE",
+ // FIXED
+ "=FIXED(5123.591)": "5,123.591",
+ "=FIXED(5123.591,1)": "5,123.6",
+ "=FIXED(5123.591,0)": "5,124",
+ "=FIXED(5123.591,-1)": "5,120",
+ "=FIXED(5123.591,-2)": "5,100",
+ "=FIXED(5123.591,-3,TRUE)": "5000",
+ "=FIXED(5123.591,-5)": "0",
+ "=FIXED(-77262.23973,-5)": "-100,000",
+ // FIND
+ "=FIND(\"T\",\"Original Text\")": "10",
+ "=FIND(\"t\",\"Original Text\")": "13",
+ "=FIND(\"i\",\"Original Text\")": "3",
+ "=FIND(\"i\",\"Original Text\",4)": "5",
+ "=FIND(\"\",\"Original Text\")": "1",
+ "=FIND(\"\",\"Original Text\",2)": "2",
+ "=FIND(\"s\",\"Sales\",2)": "5",
+ "=FIND(D1:E2,\"Month\")": "1",
+ // FINDB
+ "=FINDB(\"T\",\"Original Text\")": "10",
+ "=FINDB(\"t\",\"Original Text\")": "13",
+ "=FINDB(\"i\",\"Original Text\")": "3",
+ "=FINDB(\"i\",\"Original Text\",4)": "5",
+ "=FINDB(\"\",\"Original Text\")": "1",
+ "=FINDB(\"\",\"Original Text\",2)": "2",
+ "=FINDB(\"s\",\"Sales\",2)": "5",
+ // LEFT
+ "=LEFT(\"Original Text\")": "O",
+ "=LEFT(\"Original Text\",4)": "Orig",
+ "=LEFT(\"Original Text\",0)": "",
+ "=LEFT(\"Original Text\",13)": "Original Text",
+ "=LEFT(\"Original Text\",20)": "Original Text",
+ "=LEFT(\"オリジナルテキスト\")": "オ",
+ "=LEFT(\"オリジナルテキスト\",2)": "オリ",
+ "=LEFT(\"オリジナルテキスト\",5)": "オリジナル",
+ "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ",
+ "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト",
+ // LEFTB
+ "=LEFTB(\"Original Text\")": "O",
+ "=LEFTB(\"Original Text\",4)": "Orig",
+ "=LEFTB(\"Original Text\",0)": "",
+ "=LEFTB(\"Original Text\",13)": "Original Text",
+ "=LEFTB(\"Original Text\",20)": "Original Text",
+ // LEN
+ "=LEN(\"\")": "0",
+ "=LEN(D1)": "5",
+ "=LEN(\"テキスト\")": "4",
+ "=LEN(\"オリジナルテキスト\")": "9",
+ "=LEN(7+LEN(A1&B1&C1))": "1",
+ "=LEN(8+LEN(A1+(C1-B1)))": "2",
+ // LENB
+ "=LENB(\"\")": "0",
+ "=LENB(D1)": "5",
+ "=LENB(\"テキスト\")": "8",
+ "=LENB(\"オリジナルテキスト\")": "18",
+ // LOWER
+ "=LOWER(\"test\")": "test",
+ "=LOWER(\"TEST\")": "test",
+ "=LOWER(\"Test\")": "test",
+ "=LOWER(\"TEST 123\")": "test 123",
+ // MID
+ "=MID(\"Original Text\",7,1)": "a",
+ "=MID(\"Original Text\",4,7)": "ginal T",
+ "=MID(\"255 years\",3,1)": "5",
+ "=MID(\"text\",3,6)": "xt",
+ "=MID(\"text\",6,0)": "",
+ "=MID(\"你好World\",5,1)": "r",
+ "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8",
+ "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD",
+ // MIDB
+ "=MIDB(\"Original Text\",7,1)": "a",
+ "=MIDB(\"Original Text\",4,7)": "ginal T",
+ "=MIDB(\"255 years\",3,1)": "5",
+ "=MIDB(\"text\",3,6)": "xt",
+ "=MIDB(\"text\",6,0)": "",
+ "=MIDB(\"你好World\",5,1)": "W",
+ "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA",
+ "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3",
+ // PROPER
+ "=PROPER(\"this is a test sentence\")": "This Is A Test Sentence",
+ "=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence",
+ "=PROPER(\"123tEST teXT\")": "123Test Text",
+ "=PROPER(\"Mr. SMITH's address\")": "Mr. Smith'S Address",
+ // REPLACE
+ "=REPLACE(\"test string\",7,3,\"X\")": "test sXng",
+ "=REPLACE(\"second test string\",8,4,\"XXX\")": "second XXX string",
+ "=REPLACE(\"text\",5,0,\" and char\")": "text and char",
+ "=REPLACE(\"text\",1,20,\"char and \")": "char and ",
+ // REPLACEB
+ "=REPLACEB(\"test string\",7,3,\"X\")": "test sXng",
+ "=REPLACEB(\"second test string\",8,4,\"XXX\")": "second XXX string",
+ "=REPLACEB(\"text\",5,0,\" and char\")": "text and char",
+ "=REPLACEB(\"text\",1,20,\"char and \")": "char and ",
+ // REPT
+ "=REPT(\"*\",0)": "",
+ "=REPT(\"*\",1)": "*",
+ "=REPT(\"**\",2)": "****",
+ // RIGHT
+ "=RIGHT(\"Original Text\")": "t",
+ "=RIGHT(\"Original Text\",4)": "Text",
+ "=RIGHT(\"Original Text\",0)": "",
+ "=RIGHT(\"Original Text\",13)": "Original Text",
+ "=RIGHT(\"Original Text\",20)": "Original Text",
+ "=RIGHT(\"オリジナルテキスト\")": "ト",
+ "=RIGHT(\"オリジナルテキスト\",2)": "スト",
+ "=RIGHT(\"オリジナルテキスト\",4)": "テキスト",
+ "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト",
+ "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト",
+ // RIGHTB
+ "=RIGHTB(\"Original Text\")": "t",
+ "=RIGHTB(\"Original Text\",4)": "Text",
+ "=RIGHTB(\"Original Text\",0)": "",
+ "=RIGHTB(\"Original Text\",13)": "Original Text",
+ "=RIGHTB(\"Original Text\",20)": "Original Text",
+ // SUBSTITUTE
+ "=SUBSTITUTE(\"abab\",\"a\",\"X\")": "XbXb",
+ "=SUBSTITUTE(\"abab\",\"a\",\"X\",2)": "abXb",
+ "=SUBSTITUTE(\"abab\",\"x\",\"X\",2)": "abab",
+ "=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old",
+ "=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")": "John is 6 years old",
+ // TEXT
+ "=TEXT(\"07/07/2015\",\"mm/dd/yyyy\")": "07/07/2015",
+ "=TEXT(42192,\"mm/dd/yyyy\")": "07/07/2015",
+ "=TEXT(42192,\"mmm dd yyyy\")": "Jul 07 2015",
+ "=TEXT(0.75,\"hh:mm\")": "18:00",
+ "=TEXT(36.363636,\"0.00\")": "36.36",
+ "=TEXT(567.9,\"$#,##0.00\")": "$567.90",
+ "=TEXT(-5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "- $5.00",
+ "=TEXT(5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "+ $5.00",
+ // TEXTAFTER
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\")": "'s, red hood",
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "'s, red hood",
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x",
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "",
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",-1)": "",
+ "=TEXTAFTER(\"Jones,Bob\",\",\")": "Bob",
+ "=TEXTAFTER(\"12 ft x 20 ft\",\" x \")": "20 ft",
+ "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",1)": "112-Red-Y",
+ "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",2)": "Red-Y",
+ "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-1)": "Y",
+ "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-2)": "Red-Y",
+ "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-3)": "112-Red-Y",
+ "=TEXTAFTER(\"ABX-123-Red-XYZ\",\"-\",-4,0,1)": "ABX-123-Red-XYZ",
+ "=TEXTAFTER(\"ABX-123-Red-XYZ\",\"A\")": "BX-123-Red-XYZ",
+ // TEXTBEFORE
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\")": "Red riding ",
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "Red riding ",
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x",
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "Red riding hood's, red hood",
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",-1)": "Red riding hood's, red ",
+ "=TEXTBEFORE(\"Jones,Bob\",\",\")": "Jones",
+ "=TEXTBEFORE(\"12 ft x 20 ft\",\" x \")": "12 ft",
+ "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",1)": "ABX",
+ "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",2)": "ABX-112",
+ "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-1)": "ABX-112-Red",
+ "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-2)": "ABX-112",
+ "=TEXTBEFORE(\"ABX-123-Red-XYZ\",\"-\",4,0,1)": "ABX-123-Red-XYZ",
+ "=TEXTBEFORE(\"ABX-112-Red-Y\",\"A\")": "",
+ // TEXTJOIN
+ "=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4",
+ "=TEXTJOIN(A4,TRUE,A1:B2)": "1040205",
+ "=TEXTJOIN(\",\",FALSE,A1:C2)": "1,4,,2,5,",
+ "=TEXTJOIN(\",\",TRUE,A1:C2)": "1,4,2,5",
+ "=TEXTJOIN(\",\",TRUE,MUNIT(2))": "1,0,0,1",
+ // TRIM
+ "=TRIM(\" trim text \")": "trim text",
+ "=TRIM(0)": "0",
+ // UNICHAR
+ "=UNICHAR(65)": "A",
+ "=UNICHAR(97)": "a",
+ "=UNICHAR(63)": "?",
+ "=UNICHAR(51)": "3",
+ // UNICODE
+ "=UNICODE(\"Alpha\")": "65",
+ "=UNICODE(\"alpha\")": "97",
+ "=UNICODE(\"?\")": "63",
+ "=UNICODE(\"3\")": "51",
+ // UPPER
+ "=UPPER(\"test\")": "TEST",
+ "=UPPER(\"TEST\")": "TEST",
+ "=UPPER(\"Test\")": "TEST",
+ "=UPPER(\"TEST 123\")": "TEST 123",
+ // VALUE
+ "=VALUE(\"50\")": "50",
+ "=VALUE(\"1.0E-07\")": "0.0000001",
+ "=VALUE(\"5,000\")": "5000",
+ "=VALUE(\"20%\")": "0.2",
+ "=VALUE(\"12:00:00\")": "0.5",
+ "=VALUE(\"01/02/2006 15:04:05\")": "38719.6278356481",
+ // VALUETOTEXT
+ "=VALUETOTEXT(A1)": "1",
+ "=VALUETOTEXT(A1,0)": "1",
+ "=VALUETOTEXT(A1,1)": "1",
+ "=VALUETOTEXT(D1)": "Month",
+ "=VALUETOTEXT(D1,0)": "Month",
+ "=VALUETOTEXT(D1,1)": "\"Month\"",
+ // Conditional Functions
+ // IF
+ "=IF(1=1)": "TRUE",
+ "=IF(1<>1)": "FALSE",
+ "=IF(5<0, \"negative\", \"positive\")": "positive",
+ "=IF(-2<0, \"negative\", \"positive\")": "negative",
+ "=IF(1=1, \"equal\", \"notequal\")": "equal",
+ "=IF(1<>1, \"equal\", \"notequal\")": "notequal",
+ "=IF(\"A\"=\"A\", \"equal\", \"notequal\")": "equal",
+ "=IF(\"A\"<>\"A\", \"equal\", \"notequal\")": "notequal",
+ "=IF(FALSE,0,ROUND(4/2,0))": "2",
+ "=IF(TRUE,ROUND(4/2,0),0)": "2",
+ "=IF(A4>0.4,\"TRUE\",\"FALSE\")": "FALSE",
+ // Excel Lookup and Reference Functions
+ // ADDRESS
+ "=ADDRESS(1,1,1,TRUE)": "$A$1",
+ "=ADDRESS(1,2,1,TRUE)": "$B$1",
+ "=ADDRESS(1,1,1,FALSE)": "R1C1",
+ "=ADDRESS(1,2,1,FALSE)": "R1C2",
+ "=ADDRESS(1,1,2,TRUE)": "A$1",
+ "=ADDRESS(1,2,2,TRUE)": "B$1",
+ "=ADDRESS(1,1,2,FALSE)": "R1C[1]",
+ "=ADDRESS(1,2,2,FALSE)": "R1C[2]",
+ "=ADDRESS(1,1,3,TRUE)": "$A1",
+ "=ADDRESS(1,2,3,TRUE)": "$B1",
+ "=ADDRESS(1,1,3,FALSE)": "R[1]C1",
+ "=ADDRESS(1,2,3,FALSE)": "R[1]C2",
+ "=ADDRESS(1,1,4,TRUE)": "A1",
+ "=ADDRESS(1,2,4,TRUE)": "B1",
+ "=ADDRESS(1,1,4,FALSE)": "R[1]C[1]",
+ "=ADDRESS(1,2,4,FALSE)": "R[1]C[2]",
+ "=ADDRESS(1,1,4,TRUE,\"\")": "!A1",
+ "=ADDRESS(1,2,4,TRUE,\"\")": "!B1",
+ "=ADDRESS(1,1,4,TRUE,\"Sheet1\")": "Sheet1!A1",
+ // CHOOSE
+ "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown",
+ "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red",
+ "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9",
+ // COLUMN
+ "=COLUMN()": "3",
+ "=COLUMN(Sheet1!A1)": "1",
+ "=COLUMN(Sheet1!A1:B1:C1)": "1",
+ "=COLUMN(Sheet1!F1:G1)": "6",
+ "=COLUMN(H1)": "8",
+ // COLUMNS
+ "=COLUMNS(B1)": "1",
+ "=COLUMNS(1:1)": "16384",
+ "=COLUMNS(Sheet1!1:1)": "16384",
+ "=COLUMNS(B1:E5)": "4",
+ "=COLUMNS(Sheet1!E5:H7:B1)": "7",
+ "=COLUMNS(E5:H7:B1:C1:Z1:C1:B1)": "25",
+ "=COLUMNS(E5:B1)": "4",
+ "=COLUMNS(EM38:HZ81)": "92",
+ // HLOOKUP
+ "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan",
+ "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440",
+ "=HLOOKUP(INT(F3),F3:F8,3,FALSE)": "34440",
+ "=HLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1",
+ // HYPERLINK
+ "=HYPERLINK(\"https://github.com/xuri/excelize\")": "https://github.com/xuri/excelize",
+ "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\")": "Excelize",
+ // VLOOKUP
+ "=VLOOKUP(D2,D:D,1,FALSE)": "Jan",
+ "=VLOOKUP(D2,D1:D10,1)": "Jan",
+ "=VLOOKUP(D2,D1:D11,1)": "Feb",
+ "=VLOOKUP(D2,D1:D10,1,FALSE)": "Jan",
+ "=VLOOKUP(INT(36693),F2:F2,1,FALSE)": "36693",
+ "=VLOOKUP(INT(F2),F3:F9,1)": "32080",
+ "=VLOOKUP(INT(F2),F3:F9,1,TRUE)": "32080",
+ "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "0",
+ "=VLOOKUP(A1,A3:B5,1)": "0",
+ "=VLOOKUP(A1:A2,A1:A1,1)": "1",
+ "=VLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1",
+ // INDEX
+ "=INDEX(0,0,0)": "0",
+ "=INDEX(A1,0,0)": "1",
+ "=INDEX(A1:A1,0,0)": "1",
+ "=SUM(INDEX(A1:B1,1))": "5",
+ "=SUM(INDEX(A1:B1,1,0))": "5",
+ "=SUM(INDEX(A1:B2,2,0))": "7",
+ "=SUM(INDEX(A1:B4,0,2))": "9",
+ "=SUM(INDEX(E1:F5,5,2))": "34440",
+ // INDIRECT
+ "=INDIRECT(\"E1\")": "Team",
+ "=INDIRECT(\"E\"&1)": "Team",
+ "=INDIRECT(\"E\"&ROW())": "Team",
+ "=INDIRECT(\"E\"&ROW(),TRUE)": "Team",
+ "=INDIRECT(\"R1C5\",FALSE)": "Team",
+ "=INDIRECT(\"R\"&1&\"C\"&5,FALSE)": "Team",
+ "=SUM(INDIRECT(\"A1:B2\"))": "12",
+ "=SUM(INDIRECT(\"A1:B2\",TRUE))": "12",
+ "=SUM(INDIRECT(\"R1C1:R2C2\",FALSE))": "12",
+ // LOOKUP
+ "=LOOKUP(F8,F8:F9,F8:F9)": "32080",
+ "=LOOKUP(F8,F8:F9,D8:D9)": "Feb",
+ "=LOOKUP(E3,E2:E5,F2:F5)": "22100",
+ "=LOOKUP(E3,E2:F5)": "22100",
+ "=LOOKUP(F3+1,F3:F4,F3:F4)": "22100",
+ "=LOOKUP(F4+1,F3:F4,F3:F4)": "53321",
+ "=LOOKUP(1,MUNIT(1))": "1",
+ "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1",
+ // ROW
+ "=ROW()": "1",
+ "=ROW(Sheet1!A1)": "1",
+ "=ROW(Sheet1!A1:B2:C3)": "1",
+ "=ROW(Sheet1!F5:G6)": "5",
+ "=ROW(A8)": "8",
+ // ROWS
+ "=ROWS(B1)": "1",
+ "=ROWS(B:B)": "1048576",
+ "=ROWS(Sheet1!B:B)": "1048576",
+ "=ROWS(B1:E5)": "5",
+ "=ROWS(Sheet1!E5:H7:B1)": "7",
+ "=ROWS(E5:H8:B2:C3:Z26:C3:B2)": "25",
+ "=ROWS(E5:B1)": "5",
+ "=ROWS(EM38:HZ81)": "44",
+ // Web Functions
+ // ENCODEURL
+ "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As",
+ // Financial Functions
+ // ACCRINT
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,0,TRUE)": "1600",
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,0,FALSE)": "1600",
+ // ACCRINTM
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000)": "800",
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,3)": "800",
+ // AMORDEGRC
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%)": "42",
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,4)": "42",
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,40%,4)": "42",
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,25%,4)": "41",
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",109,1,25%,4)": "54",
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",110,2,25%,4)": "0",
+ // AMORLINC
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,4)": "30",
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,0%,4)": "0",
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,20,15%,4)": "0",
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,6,15%,4)": "0.6875",
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,0,15%,4)": "16.8125",
+ // COUPDAYBS
+ "=COUPDAYBS(\"02/24/2000\",\"11/24/2000\",4,4)": "0",
+ "=COUPDAYBS(\"03/27/2000\",\"11/29/2000\",4,4)": "28",
+ "=COUPDAYBS(\"02/29/2000\",\"04/01/2000\",4,4)": "58",
+ "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4)": "66",
+ "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4,1)": "68",
+ "=COUPDAYBS(\"10/31/2011\",\"02/26/2012\",4,0)": "65",
+ // COUPDAYS
+ "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4)": "90",
+ "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,1)": "92",
+ // COUPDAYSNC
+ "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4)": "24",
+ "=COUPDAYSNC(\"04/01/2012\",\"03/31/2020\",2)": "179",
+ // COUPNCD
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4)": "40568",
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40568",
+ "=COUPNCD(\"10/25/2011\",\"01/01/2012\",4)": "40909",
+ "=COUPNCD(\"04/01/2012\",\"03/31/2020\",2)": "41182",
+ "=COUPNCD(\"01/01/2000\",\"08/30/2001\",2)": "36585",
+ // COUPNUM
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4)": "8",
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0)": "8",
+ "=COUPNUM(\"09/30/2017\",\"03/31/2021\",4,0)": "14",
+ // COUPPCD
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4)": "40476",
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40476",
+ "=COUPPCD(\"10/25/2011\",\"01/01/2012\",4)": "40817",
+ // CUMIPMT
+ "=CUMIPMT(0.05/12,60,50000,1,12,0)": "-2294.97753732664",
+ "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.10006657389",
+ // CUMPRINC
+ "=CUMPRINC(0.05/12,60,50000,1,12,0)": "-9027.76264907988",
+ "=CUMPRINC(0.05/12,60,50000,13,24,0)": "-9489.64011983263",
+ // DB
+ "=DB(0,1000,5,1)": "0",
+ "=DB(10000,1000,5,1)": "3690",
+ "=DB(10000,1000,5,2)": "2328.39",
+ "=DB(10000,1000,5,1,6)": "1845",
+ "=DB(10000,1000,5,6,6)": "238.527124587882",
+ // DDB
+ "=DDB(0,1000,5,1)": "0",
+ "=DDB(10000,1000,5,1)": "4000",
+ "=DDB(10000,1000,5,2)": "2400",
+ "=DDB(10000,1000,5,3)": "1440",
+ "=DDB(10000,1000,5,4)": "864",
+ "=DDB(10000,1000,5,5)": "296",
+ // DISC
+ "=DISC(\"04/01/2016\",\"03/31/2021\",95,100)": "0.01",
+ // DOLLAR
+ "=DOLLAR(1234.56)": "$1,234.56",
+ "=DOLLAR(1234.56,0)": "$1,235",
+ "=DOLLAR(1234.56,1)": "$1,234.6",
+ "=DOLLAR(1234.56,2)": "$1,234.56",
+ "=DOLLAR(1234.56,3)": "$1,234.560",
+ "=DOLLAR(1234.56,-2)": "$1,200",
+ "=DOLLAR(1234.56,-3)": "$1,000",
+ "=DOLLAR(-1234.56,3)": "($1,234.560)",
+ "=DOLLAR(-1234.56,-3)": "($1,000)",
+ // DOLLARDE
+ "=DOLLARDE(1.01,16)": "1.0625",
+ // DOLLARFR
+ "=DOLLARFR(1.0625,16)": "1.01",
+ // DURATION
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.67442279848313",
+ // EFFECT
+ "=EFFECT(0.1,4)": "0.103812890625",
+ "=EFFECT(0.025,2)": "0.02515625",
+ // EUROCONVERT
+ "=EUROCONVERT(1.47,\"EUR\",\"EUR\")": "1.47",
+ "=EUROCONVERT(1.47,\"EUR\",\"DEM\")": "2.88",
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\")": "0.44",
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",FALSE)": "0.44",
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",FALSE,3)": "0.44",
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3)": "0.43810592",
+ // FV
+ "=FV(0.05/12,60,-1000)": "68006.0828408434",
+ "=FV(0.1/4,16,-2000,0,1)": "39729.4608941662",
+ "=FV(0,16,-2000)": "32000",
+ // FVSCHEDULE
+ "=FVSCHEDULE(10000,A1:A5)": "240000",
+ "=FVSCHEDULE(10000,0.5)": "15000",
+ // INTRATE
+ "=INTRATE(\"04/01/2005\",\"03/31/2010\",1000,2125)": "0.225",
+ // IPMT
+ "=IPMT(0.05/12,2,60,50000)": "-205.26988187972",
+ "=IPMT(0.035/4,2,8,0,5000,1)": "5.25745523782908",
+ // ISPMT
+ "=ISPMT(0.05/12,1,60,50000)": "-204.861111111111",
+ "=ISPMT(0.05/12,2,60,50000)": "-201.388888888889",
+ "=ISPMT(0.05/12,2,1,50000)": "208.333333333333",
+ // MDURATION
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.54355176321876",
+ // NOMINAL
+ "=NOMINAL(0.025,12)": "0.0247180352381129",
+ // NPER
+ "=NPER(0.04,-6000,50000)": "10.3380350715077",
+ "=NPER(0,-6000,50000)": "8.33333333333333",
+ "=NPER(0.06/4,-2000,60000,30000,1)": "52.7947737092748",
+ // NPV
+ "=NPV(0.02,-5000,\"\",800)": "-4133.02575932334",
+ // ODDFPRICE
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "107.691830256629",
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,1)": "106.766915010929",
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,3)": "106.7819138147",
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,4)": "106.771913772467",
+ "=ODDFPRICE(\"11/11/2008\",\"03/01/2021\",\"10/15/2008\",\"03/01/2009\",7.85%,6.25%,100,2,1)": "113.597717474079",
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"09/30/2017\",5.5%,3.5%,100,4,0)": "106.72930611878",
+ "=ODDFPRICE(\"11/11/2008\",\"03/29/2021\",\"08/15/2008\",\"03/29/2009\",0.0785,0.0625,100,2,1)": "113.61826640814",
+ // ODDFYIELD
+ "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,1)": "0.0495998049937776",
+ "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,2)": "0.0496289417392839",
+ "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,4,1)": "0.0464750282973541",
+ // ODDLPRICE
+ "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,2)": "5.0517841252892",
+ "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,4,1)": "10.3667274303228",
+ // ODDLYIELD
+ "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,2)": "0.0451922356291692",
+ "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,4,1)": "0.0882287538349037",
+ // PDURATION
+ "=PDURATION(0.04,10000,15000)": "10.3380350715076",
+ // PMT
+ "=PMT(0,8,0,5000,1)": "-625",
+ "=PMT(0.035/4,8,0,5000,1)": "-600.852027180466",
+ // PRICE
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2)": "110.655105178443",
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,4)": "110.655105178443",
+ "=PRICE(\"04/01/2012\",\"03/31/2020\",12%,10%,100,2)": "110.834483593216",
+ "=PRICE(\"01/01/2010\",\"06/30/2010\",0.5,1,1,1,4)": "8.92419088847661",
+ // PPMT
+ "=PPMT(0.05/12,2,60,50000)": "-738.291800320824",
+ "=PPMT(0.035/4,2,8,0,5000,1)": "-606.109482418295",
+ // PRICEDISC
+ "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100)": "90",
+ "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100,3)": "90",
+ "=PRICEDISC(\"42826\",\"03/31/2021\",2.5%,100,3)": "90",
+ // PRICEMAT
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": "107.170454545455",
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,0)": "107.170454545455",
+ // PV
+ "=PV(0,60,1000)": "-60000",
+ "=PV(5%/12,60,1000)": "-52990.7063239275",
+ "=PV(10%/4,16,2000,0,1)": "-26762.7554528811",
+ // RATE
+ "=RATE(60,-1000,50000)": "0.0061834131621292",
+ "=RATE(24,-800,0,20000,1)": "0.00325084350160374",
+ "=RATE(48,-200,8000,3,1,0.5)": "0.0080412665831637",
+ // RECEIVED
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%)": "1290.32258064516",
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,0)": "1290.32258064516",
+ // RRI
+ "=RRI(10,10000,15000)": "0.0413797439924106",
+ // SLN
+ "=SLN(10000,1000,5)": "1800",
+ // SYD
+ "=SYD(10000,1000,5,1)": "3000",
+ "=SYD(10000,1000,5,2)": "2400",
+ // TBILLEQ
+ "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",2.5%)": "0.0256680731364276",
+ // TBILLPRICE
+ "=TBILLPRICE(\"02/01/2017\",\"06/30/2017\",2.75%)": "98.8618055555556",
+ // TBILLYIELD
+ "=TBILLYIELD(\"02/01/2017\",\"06/30/2017\",99)": "0.024405125076266",
+ // VDB
+ "=VDB(10000,1000,5,0,1)": "4000",
+ "=VDB(10000,1000,5,1,3)": "3840",
+ "=VDB(10000,1000,5,3,5)": "1160",
+ "=VDB(10000,1000,5,3,5,0.2,FALSE)": "3600",
+ "=VDB(10000,1000,5,3,5,0.2,TRUE)": "693.633024",
+ "=VDB(24000,3000,10,0,0.875,2)": "4200",
+ "=VDB(24000,3000,10,0.1,1)": "4233.6",
+ "=VDB(24000,3000,10,0.1,1,1)": "2138.4",
+ "=VDB(24000,3000,100,50,100,1)": "10377.2944184652",
+ "=VDB(24000,3000,100,50,100,2)": "5740.0723220908",
+ // YIELD
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4)": "0.0975631546829798",
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,4)": "0.0976269355643988",
+ "=YIELD(\"01/01/2010\",\"06/30/2010\",0.5,1,1,1,4)": "1.91285866099894",
+ "=YIELD(\"01/01/2010\",\"06/30/2010\",0,1,1,1,4)": "0",
+ "=YIELD(\"01/01/2010\",\"01/02/2020\",100,68.15518653988686,1,1,1)": "64",
+ // YIELDDISC
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031",
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031",
+ // YIELDMAT
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101)": "0.0419422478838651",
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": "0.0419422478838651",
+ // DISPIMG
+ "=_xlfn.DISPIMG(\"ID_********************************\",1)": "ID_********************************",
}
for formula, expected := range mathCalc {
- f := prepareData()
+ f := prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
- assert.NoError(t, err)
+ assert.NoError(t, err, formula)
assert.Equal(t, expected, result, formula)
}
- mathCalcError := map[string]string{
+ mathCalcError := map[string][]string{
+ "=1/0": {"", "#DIV/0!"},
+ "1^\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "\"text\"^1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "1+\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "\"text\"+1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "1-\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "\"text\"-1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "1*\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "\"text\"*1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "1/\"text\"": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ "\"text\"/1": {"", "strconv.ParseFloat: parsing \"text\": invalid syntax"},
+ // Engineering Functions
+ // BESSELI
+ "=BESSELI()": {"#VALUE!", "BESSELI requires 2 numeric arguments"},
+ "=BESSELI(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELI(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // BESSELJ
+ "=BESSELJ()": {"#VALUE!", "BESSELJ requires 2 numeric arguments"},
+ "=BESSELJ(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELJ(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // BESSELK
+ "=BESSELK()": {"#VALUE!", "BESSELK requires 2 numeric arguments"},
+ "=BESSELK(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELK(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELK(-1,0)": {"#NUM!", "#NUM!"},
+ "=BESSELK(1,-1)": {"#NUM!", "#NUM!"},
+ // BESSELY
+ "=BESSELY()": {"#VALUE!", "BESSELY requires 2 numeric arguments"},
+ "=BESSELY(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELY(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BESSELY(-1,0)": {"#NUM!", "#NUM!"},
+ "=BESSELY(1,-1)": {"#NUM!", "#NUM!"},
+ // BIN2DEC
+ "=BIN2DEC()": {"#VALUE!", "BIN2DEC requires 1 numeric argument"},
+ "=BIN2DEC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // BIN2HEX
+ "=BIN2HEX()": {"#VALUE!", "BIN2HEX requires at least 1 argument"},
+ "=BIN2HEX(1,1,1)": {"#VALUE!", "BIN2HEX allows at most 2 arguments"},
+ "=BIN2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BIN2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BIN2HEX(12345678901,10)": {"#NUM!", "#NUM!"},
+ "=BIN2HEX(1,-1)": {"#NUM!", "#NUM!"},
+ "=BIN2HEX(31,1)": {"#NUM!", "#NUM!"},
+ // BIN2OCT
+ "=BIN2OCT()": {"#VALUE!", "BIN2OCT requires at least 1 argument"},
+ "=BIN2OCT(1,1,1)": {"#VALUE!", "BIN2OCT allows at most 2 arguments"},
+ "=BIN2OCT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BIN2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BIN2OCT(-12345678901 ,10)": {"#NUM!", "#NUM!"},
+ "=BIN2OCT(1,-1)": {"#NUM!", "#NUM!"},
+ "=BIN2OCT(8,1)": {"#NUM!", "#NUM!"},
+ // BITAND
+ "=BITAND()": {"#VALUE!", "BITAND requires 2 numeric arguments"},
+ "=BITAND(-1,2)": {"#NUM!", "#NUM!"},
+ "=BITAND(2^48,2)": {"#NUM!", "#NUM!"},
+ "=BITAND(1,-1)": {"#NUM!", "#NUM!"},
+ "=BITAND(\"\",-1)": {"#NUM!", "#NUM!"},
+ "=BITAND(1,\"\")": {"#NUM!", "#NUM!"},
+ "=BITAND(1,2^48)": {"#NUM!", "#NUM!"},
+ // BITLSHIFT
+ "=BITLSHIFT()": {"#VALUE!", "BITLSHIFT requires 2 numeric arguments"},
+ "=BITLSHIFT(-1,2)": {"#NUM!", "#NUM!"},
+ "=BITLSHIFT(2^48,2)": {"#NUM!", "#NUM!"},
+ "=BITLSHIFT(1,-1)": {"#NUM!", "#NUM!"},
+ "=BITLSHIFT(\"\",-1)": {"#NUM!", "#NUM!"},
+ "=BITLSHIFT(1,\"\")": {"#NUM!", "#NUM!"},
+ "=BITLSHIFT(1,2^48)": {"#NUM!", "#NUM!"},
+ // BITOR
+ "=BITOR()": {"#VALUE!", "BITOR requires 2 numeric arguments"},
+ "=BITOR(-1,2)": {"#NUM!", "#NUM!"},
+ "=BITOR(2^48,2)": {"#NUM!", "#NUM!"},
+ "=BITOR(1,-1)": {"#NUM!", "#NUM!"},
+ "=BITOR(\"\",-1)": {"#NUM!", "#NUM!"},
+ "=BITOR(1,\"\")": {"#NUM!", "#NUM!"},
+ "=BITOR(1,2^48)": {"#NUM!", "#NUM!"},
+ // BITRSHIFT
+ "=BITRSHIFT()": {"#VALUE!", "BITRSHIFT requires 2 numeric arguments"},
+ "=BITRSHIFT(-1,2)": {"#NUM!", "#NUM!"},
+ "=BITRSHIFT(2^48,2)": {"#NUM!", "#NUM!"},
+ "=BITRSHIFT(1,-1)": {"#NUM!", "#NUM!"},
+ "=BITRSHIFT(\"\",-1)": {"#NUM!", "#NUM!"},
+ "=BITRSHIFT(1,\"\")": {"#NUM!", "#NUM!"},
+ "=BITRSHIFT(1,2^48)": {"#NUM!", "#NUM!"},
+ // BITXOR
+ "=BITXOR()": {"#VALUE!", "BITXOR requires 2 numeric arguments"},
+ "=BITXOR(-1,2)": {"#NUM!", "#NUM!"},
+ "=BITXOR(2^48,2)": {"#NUM!", "#NUM!"},
+ "=BITXOR(1,-1)": {"#NUM!", "#NUM!"},
+ "=BITXOR(\"\",-1)": {"#NUM!", "#NUM!"},
+ "=BITXOR(1,\"\")": {"#NUM!", "#NUM!"},
+ "=BITXOR(1,2^48)": {"#NUM!", "#NUM!"},
+ // COMPLEX
+ "=COMPLEX()": {"#VALUE!", "COMPLEX requires at least 2 arguments"},
+ "=COMPLEX(10,-5,\"\")": {"#VALUE!", "#VALUE!"},
+ "=COMPLEX(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=COMPLEX(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=COMPLEX(10,-5,\"i\",0)": {"#VALUE!", "COMPLEX allows at most 3 arguments"},
+ // CONVERT
+ "=CONVERT()": {"#VALUE!", "CONVERT requires 3 arguments"},
+ "=CONVERT(\"\",\"m\",\"yd\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONVERT(20.2,\"m\",\"C\")": {"#N/A", "#N/A"},
+ "=CONVERT(20.2,\"\",\"C\")": {"#N/A", "#N/A"},
+ "=CONVERT(100,\"dapt\",\"pt\")": {"#N/A", "#N/A"},
+ "=CONVERT(1,\"ft\",\"day\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"kpt\",\"lt\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"lt\",\"kpt\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"kiqt\",\"pt\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"pt\",\"kiqt\")": {"#N/A", "#N/A"},
+ "=CONVERT(12345.6,\"baton\",\"cwt\")": {"#N/A", "#N/A"},
+ "=CONVERT(12345.6,\"cwt\",\"baton\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"xxxx\",\"m\")": {"#N/A", "#N/A"},
+ "=CONVERT(234.56,\"m\",\"xxxx\")": {"#N/A", "#N/A"},
+ // DEC2BIN
+ "=DEC2BIN()": {"#VALUE!", "DEC2BIN requires at least 1 argument"},
+ "=DEC2BIN(1,1,1)": {"#VALUE!", "DEC2BIN allows at most 2 arguments"},
+ "=DEC2BIN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2BIN(-513,10)": {"#NUM!", "#NUM!"},
+ "=DEC2BIN(1,-1)": {"#NUM!", "#NUM!"},
+ "=DEC2BIN(2,1)": {"#NUM!", "#NUM!"},
+ // DEC2HEX
+ "=DEC2HEX()": {"#VALUE!", "DEC2HEX requires at least 1 argument"},
+ "=DEC2HEX(1,1,1)": {"#VALUE!", "DEC2HEX allows at most 2 arguments"},
+ "=DEC2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2HEX(-549755813888,10)": {"#NUM!", "#NUM!"},
+ "=DEC2HEX(1,-1)": {"#NUM!", "#NUM!"},
+ "=DEC2HEX(31,1)": {"#NUM!", "#NUM!"},
+ // DEC2OCT
+ "=DEC2OCT()": {"#VALUE!", "DEC2OCT requires at least 1 argument"},
+ "=DEC2OCT(1,1,1)": {"#VALUE!", "DEC2OCT allows at most 2 arguments"},
+ "=DEC2OCT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DEC2OCT(-536870912 ,10)": {"#NUM!", "#NUM!"},
+ "=DEC2OCT(1,-1)": {"#NUM!", "#NUM!"},
+ "=DEC2OCT(8,1)": {"#NUM!", "#NUM!"},
+ // DELTA
+ "=DELTA()": {"#VALUE!", "DELTA requires at least 1 argument"},
+ "=DELTA(0,0,0)": {"#VALUE!", "DELTA allows at most 2 arguments"},
+ "=DELTA(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DELTA(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ERF
+ "=ERF()": {"#VALUE!", "ERF requires at least 1 argument"},
+ "=ERF(0,0,0)": {"#VALUE!", "ERF allows at most 2 arguments"},
+ "=ERF(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ERF(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ERF.PRECISE
+ "=ERF.PRECISE()": {"#VALUE!", "ERF.PRECISE requires 1 argument"},
+ "=ERF.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ERFC
+ "=ERFC()": {"#VALUE!", "ERFC requires 1 argument"},
+ "=ERFC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ERFC.PRECISE
+ "=ERFC.PRECISE()": {"#VALUE!", "ERFC.PRECISE requires 1 argument"},
+ "=ERFC.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // GESTEP
+ "=GESTEP()": {"#VALUE!", "GESTEP requires at least 1 argument"},
+ "=GESTEP(0,0,0)": {"#VALUE!", "GESTEP allows at most 2 arguments"},
+ "=GESTEP(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GESTEP(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // HEX2BIN
+ "=HEX2BIN()": {"#VALUE!", "HEX2BIN requires at least 1 argument"},
+ "=HEX2BIN(1,1,1)": {"#VALUE!", "HEX2BIN allows at most 2 arguments"},
+ "=HEX2BIN(\"X\",1)": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"},
+ "=HEX2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HEX2BIN(-513,10)": {"#NUM!", "strconv.ParseInt: parsing \"-\": invalid syntax"},
+ "=HEX2BIN(1,-1)": {"#NUM!", "#NUM!"},
+ "=HEX2BIN(2,1)": {"#NUM!", "#NUM!"},
+ // HEX2DEC
+ "=HEX2DEC()": {"#VALUE!", "HEX2DEC requires 1 numeric argument"},
+ "=HEX2DEC(\"X\")": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"},
+ // HEX2OCT
+ "=HEX2OCT()": {"#VALUE!", "HEX2OCT requires at least 1 argument"},
+ "=HEX2OCT(1,1,1)": {"#VALUE!", "HEX2OCT allows at most 2 arguments"},
+ "=HEX2OCT(\"X\",1)": {"#NUM!", "strconv.ParseInt: parsing \"X\": invalid syntax"},
+ "=HEX2OCT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HEX2OCT(-513,10)": {"#NUM!", "strconv.ParseInt: parsing \"-\": invalid syntax"},
+ "=HEX2OCT(1,-1)": {"#NUM!", "#NUM!"},
+ // IMABS
+ "=IMABS()": {"#VALUE!", "IMABS requires 1 argument"},
+ "=IMABS(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMAGINARY
+ "=IMAGINARY()": {"#VALUE!", "IMAGINARY requires 1 argument"},
+ "=IMAGINARY(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMARGUMENT
+ "=IMARGUMENT()": {"#VALUE!", "IMARGUMENT requires 1 argument"},
+ "=IMARGUMENT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMCONJUGATE
+ "=IMCONJUGATE()": {"#VALUE!", "IMCONJUGATE requires 1 argument"},
+ "=IMCONJUGATE(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMCOS
+ "=IMCOS()": {"#VALUE!", "IMCOS requires 1 argument"},
+ "=IMCOS(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMCOSH
+ "=IMCOSH()": {"#VALUE!", "IMCOSH requires 1 argument"},
+ "=IMCOSH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMCOT
+ "=IMCOT()": {"#VALUE!", "IMCOT requires 1 argument"},
+ "=IMCOT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMCSC
+ "=IMCSC()": {"#VALUE!", "IMCSC requires 1 argument"},
+ "=IMCSC(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMCSC(0)": {"#NUM!", "#NUM!"},
+ // IMCSCH
+ "=IMCSCH()": {"#VALUE!", "IMCSCH requires 1 argument"},
+ "=IMCSCH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMCSCH(0)": {"#NUM!", "#NUM!"},
+ // IMDIV
+ "=IMDIV()": {"#VALUE!", "IMDIV requires 2 arguments"},
+ "=IMDIV(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMDIV(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMDIV(1,0)": {"#NUM!", "#NUM!"},
+ // IMEXP
+ "=IMEXP()": {"#VALUE!", "IMEXP requires 1 argument"},
+ "=IMEXP(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMLN
+ "=IMLN()": {"#VALUE!", "IMLN requires 1 argument"},
+ "=IMLN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMLN(0)": {"#NUM!", "#NUM!"},
+ // IMLOG10
+ "=IMLOG10()": {"#VALUE!", "IMLOG10 requires 1 argument"},
+ "=IMLOG10(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMLOG10(0)": {"#NUM!", "#NUM!"},
+ // IMLOG2
+ "=IMLOG2()": {"#VALUE!", "IMLOG2 requires 1 argument"},
+ "=IMLOG2(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMLOG2(0)": {"#NUM!", "#NUM!"},
+ // IMPOWER
+ "=IMPOWER()": {"#VALUE!", "IMPOWER requires 2 arguments"},
+ "=IMPOWER(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMPOWER(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMPOWER(0,0)": {"#NUM!", "#NUM!"},
+ "=IMPOWER(0,-1)": {"#NUM!", "#NUM!"},
+ // IMPRODUCT
+ "=IMPRODUCT(\"x\")": {"#NUM!", "strconv.ParseComplex: parsing \"x\": invalid syntax"},
+ "=IMPRODUCT(A1:D1)": {"#NUM!", "strconv.ParseComplex: parsing \"Month\": invalid syntax"},
+ // IMREAL
+ "=IMREAL()": {"#VALUE!", "IMREAL requires 1 argument"},
+ "=IMREAL(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSEC
+ "=IMSEC()": {"#VALUE!", "IMSEC requires 1 argument"},
+ "=IMSEC(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSECH
+ "=IMSECH()": {"#VALUE!", "IMSECH requires 1 argument"},
+ "=IMSECH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSIN
+ "=IMSIN()": {"#VALUE!", "IMSIN requires 1 argument"},
+ "=IMSIN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSINH
+ "=IMSINH()": {"#VALUE!", "IMSINH requires 1 argument"},
+ "=IMSINH(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSQRT
+ "=IMSQRT()": {"#VALUE!", "IMSQRT requires 1 argument"},
+ "=IMSQRT(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSUB
+ "=IMSUB()": {"#VALUE!", "IMSUB requires 2 arguments"},
+ "=IMSUB(0,\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ "=IMSUB(\"\",0)": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMSUM
+ "=IMSUM()": {"#VALUE!", "IMSUM requires at least 1 argument"},
+ "=IMSUM(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // IMTAN
+ "=IMTAN()": {"#VALUE!", "IMTAN requires 1 argument"},
+ "=IMTAN(\"\")": {"#NUM!", "strconv.ParseComplex: parsing \"\": invalid syntax"},
+ // OCT2BIN
+ "=OCT2BIN()": {"#VALUE!", "OCT2BIN requires at least 1 argument"},
+ "=OCT2BIN(1,1,1)": {"#VALUE!", "OCT2BIN allows at most 2 arguments"},
+ "=OCT2BIN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=OCT2BIN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=OCT2BIN(-536870912 ,10)": {"#NUM!", "#NUM!"},
+ "=OCT2BIN(1,-1)": {"#NUM!", "#NUM!"},
+ // OCT2DEC
+ "=OCT2DEC()": {"#VALUE!", "OCT2DEC requires 1 numeric argument"},
+ "=OCT2DEC(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // OCT2HEX
+ "=OCT2HEX()": {"#VALUE!", "OCT2HEX requires at least 1 argument"},
+ "=OCT2HEX(1,1,1)": {"#VALUE!", "OCT2HEX allows at most 2 arguments"},
+ "=OCT2HEX(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=OCT2HEX(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=OCT2HEX(-536870912 ,10)": {"#NUM!", "#NUM!"},
+ "=OCT2HEX(1,-1)": {"#NUM!", "#NUM!"},
+ // Math and Trigonometric Functions
// ABS
- "=ABS()": "ABS requires 1 numeric argument",
- `=ABS("X")`: "#VALUE!",
- "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`,
+ "=ABS()": {"#VALUE!", "ABS requires 1 numeric argument"},
+ "=ABS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ABS(~)": {"#NAME?", "invalid reference"},
// ACOS
- "=ACOS()": "ACOS requires 1 numeric argument",
- `=ACOS("X")`: "#VALUE!",
+ "=ACOS()": {"#VALUE!", "ACOS requires 1 numeric argument"},
+ "=ACOS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ACOS(ACOS(0))": {"#NUM!", "#NUM!"},
// ACOSH
- "=ACOSH()": "ACOSH requires 1 numeric argument",
- `=ACOSH("X")`: "#VALUE!",
+ "=ACOSH()": {"#VALUE!", "ACOSH requires 1 numeric argument"},
+ "=ACOSH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.ACOT
- "=_xlfn.ACOT()": "ACOT requires 1 numeric argument",
- `=_xlfn.ACOT("X")`: "#VALUE!",
+ "=_xlfn.ACOT()": {"#VALUE!", "ACOT requires 1 numeric argument"},
+ "=_xlfn.ACOT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.ACOTH
- "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument",
- `=_xlfn.ACOTH("X")`: "#VALUE!",
+ "=_xlfn.ACOTH()": {"#VALUE!", "ACOTH requires 1 numeric argument"},
+ "=_xlfn.ACOTH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.ACOTH(_xlfn.ACOTH(2))": {"#NUM!", "#NUM!"},
+ // _xlfn.AGGREGATE
+ "=_xlfn.AGGREGATE()": {"#VALUE!", "AGGREGATE requires at least 3 arguments"},
+ "=_xlfn.AGGREGATE(\"\",0,A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=_xlfn.AGGREGATE(1,\"\",A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=_xlfn.AGGREGATE(0,A4:A5)": {"#VALUE!", "AGGREGATE has invalid function_num"},
+ "=_xlfn.AGGREGATE(1,8,A4:A5)": {"#VALUE!", "AGGREGATE has invalid options"},
+ "=_xlfn.AGGREGATE(1,0,A5:A6)": {"#DIV/0!", "#DIV/0!"},
+ "=_xlfn.AGGREGATE(13,0,A1:A6)": {"#N/A", "#N/A"},
+ "=_xlfn.AGGREGATE(18,0,A1:A6,1)": {"#NUM!", "#NUM!"},
// _xlfn.ARABIC
- "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument",
+ "=_xlfn.ARABIC()": {"#VALUE!", "ARABIC requires 1 numeric argument"},
+ "=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": {"#VALUE!", "#VALUE!"},
// ASIN
- "=ASIN()": "ASIN requires 1 numeric argument",
- `=ASIN("X")`: "#VALUE!",
+ "=ASIN()": {"#VALUE!", "ASIN requires 1 numeric argument"},
+ "=ASIN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ASINH
- "=ASINH()": "ASINH requires 1 numeric argument",
- `=ASINH("X")`: "#VALUE!",
+ "=ASINH()": {"#VALUE!", "ASINH requires 1 numeric argument"},
+ "=ASINH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ATAN
- "=ATAN()": "ATAN requires 1 numeric argument",
- `=ATAN("X")`: "#VALUE!",
+ "=ATAN()": {"#VALUE!", "ATAN requires 1 numeric argument"},
+ "=ATAN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ATANH
- "=ATANH()": "ATANH requires 1 numeric argument",
- `=ATANH("X")`: "#VALUE!",
+ "=ATANH()": {"#VALUE!", "ATANH requires 1 numeric argument"},
+ "=ATANH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ATAN2
- "=ATAN2()": "ATAN2 requires 2 numeric arguments",
- `=ATAN2("X",0)`: "#VALUE!",
- `=ATAN2(0,"X")`: "#VALUE!",
+ "=ATAN2()": {"#VALUE!", "ATAN2 requires 2 numeric arguments"},
+ "=ATAN2(\"X\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ATAN2(0,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// BASE
- "=BASE()": "BASE requires at least 2 arguments",
- "=BASE(1,2,3,4)": "BASE allows at most 3 arguments",
- "=BASE(1,1)": "radix must be an integer >= 2 and <= 36",
- `=BASE("X",2)`: "#VALUE!",
- `=BASE(1,"X")`: "#VALUE!",
- `=BASE(1,2,"X")`: "#VALUE!",
+ "=BASE()": {"#VALUE!", "BASE requires at least 2 arguments"},
+ "=BASE(1,2,3,4)": {"#VALUE!", "BASE allows at most 3 arguments"},
+ "=BASE(1,1)": {"#VALUE!", "radix must be an integer >= 2 and <= 36"},
+ "=BASE(\"X\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=BASE(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=BASE(1,2,\"X\")": {"#VALUE!", "strconv.Atoi: parsing \"X\": invalid syntax"},
// CEILING
- "=CEILING()": "CEILING requires at least 1 argument",
- "=CEILING(1,2,3)": "CEILING allows at most 2 arguments",
- "=CEILING(1,-1)": "negative sig to CEILING invalid",
- `=CEILING("X",0)`: "#VALUE!",
- `=CEILING(0,"X")`: "#VALUE!",
+ "=CEILING()": {"#VALUE!", "CEILING requires at least 1 argument"},
+ "=CEILING(1,2,3)": {"#VALUE!", "CEILING allows at most 2 arguments"},
+ "=CEILING(1,-1)": {"#VALUE!", "negative sig to CEILING invalid"},
+ "=CEILING(\"X\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=CEILING(0,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.CEILING.MATH
- "=_xlfn.CEILING.MATH()": "CEILING.MATH requires at least 1 argument",
- "=_xlfn.CEILING.MATH(1,2,3,4)": "CEILING.MATH allows at most 3 arguments",
- `=_xlfn.CEILING.MATH("X")`: "#VALUE!",
- `=_xlfn.CEILING.MATH(1,"X")`: "#VALUE!",
- `=_xlfn.CEILING.MATH(1,2,"X")`: "#VALUE!",
+ "=_xlfn.CEILING.MATH()": {"#VALUE!", "CEILING.MATH requires at least 1 argument"},
+ "=_xlfn.CEILING.MATH(1,2,3,4)": {"#VALUE!", "CEILING.MATH allows at most 3 arguments"},
+ "=_xlfn.CEILING.MATH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.CEILING.MATH(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.CEILING.MATH(1,2,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.CEILING.PRECISE
- "=_xlfn.CEILING.PRECISE()": "CEILING.PRECISE requires at least 1 argument",
- "=_xlfn.CEILING.PRECISE(1,2,3)": "CEILING.PRECISE allows at most 2 arguments",
- `=_xlfn.CEILING.PRECISE("X",2)`: "#VALUE!",
- `=_xlfn.CEILING.PRECISE(1,"X")`: "#VALUE!",
+ "=_xlfn.CEILING.PRECISE()": {"#VALUE!", "CEILING.PRECISE requires at least 1 argument"},
+ "=_xlfn.CEILING.PRECISE(1,2,3)": {"#VALUE!", "CEILING.PRECISE allows at most 2 arguments"},
+ "=_xlfn.CEILING.PRECISE(\"X\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.CEILING.PRECISE(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// COMBIN
- "=COMBIN()": "COMBIN requires 2 argument",
- "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen",
- `=COMBIN("X",1)`: "#VALUE!",
- `=COMBIN(-1,"X")`: "#VALUE!",
+ "=COMBIN()": {"#VALUE!", "COMBIN requires 2 argument"},
+ "=COMBIN(-1,1)": {"#VALUE!", "COMBIN requires number >= number_chosen"},
+ "=COMBIN(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=COMBIN(-1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.COMBINA
- "=_xlfn.COMBINA()": "COMBINA requires 2 argument",
- "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen",
- "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen",
- `=_xlfn.COMBINA("X",1)`: "#VALUE!",
- `=_xlfn.COMBINA(-1,"X")`: "#VALUE!",
+ "=_xlfn.COMBINA()": {"#VALUE!", "COMBINA requires 2 argument"},
+ "=_xlfn.COMBINA(-1,1)": {"#VALUE!", "COMBINA requires number > number_chosen"},
+ "=_xlfn.COMBINA(-1,-1)": {"#VALUE!", "COMBIN requires number >= number_chosen"},
+ "=_xlfn.COMBINA(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.COMBINA(-1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// COS
- "=COS()": "COS requires 1 numeric argument",
- `=COS("X")`: "#VALUE!",
+ "=COS()": {"#VALUE!", "COS requires 1 numeric argument"},
+ "=COS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// COSH
- "=COSH()": "COSH requires 1 numeric argument",
- `=COSH("X")`: "#VALUE!",
+ "=COSH()": {"#VALUE!", "COSH requires 1 numeric argument"},
+ "=COSH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.COT
- "=COT()": "COT requires 1 numeric argument",
- `=COT("X")`: "#VALUE!",
- "=COT(0)": "#DIV/0!",
+ "=COT()": {"#VALUE!", "COT requires 1 numeric argument"},
+ "=COT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=COT(0)": {"#DIV/0!", "#DIV/0!"},
// _xlfn.COTH
- "=COTH()": "COTH requires 1 numeric argument",
- `=COTH("X")`: "#VALUE!",
- "=COTH(0)": "#DIV/0!",
+ "=COTH()": {"#VALUE!", "COTH requires 1 numeric argument"},
+ "=COTH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=COTH(0)": {"#DIV/0!", "#DIV/0!"},
// _xlfn.CSC
- "=_xlfn.CSC()": "CSC requires 1 numeric argument",
- `=_xlfn.CSC("X")`: "#VALUE!",
- "=_xlfn.CSC(0)": "#DIV/0!",
+ "=_xlfn.CSC()": {"#VALUE!", "CSC requires 1 numeric argument"},
+ "=_xlfn.CSC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.CSC(0)": {"#DIV/0!", "#DIV/0!"},
// _xlfn.CSCH
- "=_xlfn.CSCH()": "CSCH requires 1 numeric argument",
- `=_xlfn.CSCH("X")`: "#VALUE!",
- "=_xlfn.CSCH(0)": "#DIV/0!",
+ "=_xlfn.CSCH()": {"#VALUE!", "CSCH requires 1 numeric argument"},
+ "=_xlfn.CSCH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.CSCH(0)": {"#DIV/0!", "#DIV/0!"},
// _xlfn.DECIMAL
- "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments",
- `=_xlfn.DECIMAL("X", 2)`: "#VALUE!",
- `=_xlfn.DECIMAL(2000, "X")`: "#VALUE!",
+ "=_xlfn.DECIMAL()": {"#VALUE!", "DECIMAL requires 2 numeric arguments"},
+ "=_xlfn.DECIMAL(\"X\",2)": {"#VALUE!", "strconv.ParseInt: parsing \"X\": invalid syntax"},
+ "=_xlfn.DECIMAL(2000,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// DEGREES
- "=DEGREES()": "DEGREES requires 1 numeric argument",
- `=DEGREES("X")`: "#VALUE!",
- "=DEGREES(0)": "#DIV/0!",
+ "=DEGREES()": {"#VALUE!", "DEGREES requires 1 numeric argument"},
+ "=DEGREES(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=DEGREES(0)": {"#DIV/0!", "#DIV/0!"},
// EVEN
- "=EVEN()": "EVEN requires 1 numeric argument",
- `=EVEN("X")`: "#VALUE!",
+ "=EVEN()": {"#VALUE!", "EVEN requires 1 numeric argument"},
+ "=EVEN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// EXP
- "=EXP()": "EXP requires 1 numeric argument",
- `=EXP("X")`: "#VALUE!",
+ "=EXP()": {"#VALUE!", "EXP requires 1 numeric argument"},
+ "=EXP(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// FACT
- "=FACT()": "FACT requires 1 numeric argument",
- `=FACT("X")`: "#VALUE!",
- "=FACT(-1)": "#NUM!",
+ "=FACT()": {"#VALUE!", "FACT requires 1 numeric argument"},
+ "=FACT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=FACT(-1)": {"#NUM!", "#NUM!"},
// FACTDOUBLE
- "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument",
- `=FACTDOUBLE("X")`: "#VALUE!",
- "=FACTDOUBLE(-1)": "#NUM!",
+ "=FACTDOUBLE()": {"#VALUE!", "FACTDOUBLE requires 1 numeric argument"},
+ "=FACTDOUBLE(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=FACTDOUBLE(-1)": {"#NUM!", "#NUM!"},
// FLOOR
- "=FLOOR()": "FLOOR requires 2 numeric arguments",
- `=FLOOR("X",-1)`: "#VALUE!",
- `=FLOOR(1,"X")`: "#VALUE!",
- "=FLOOR(1,-1)": "#NUM!",
+ "=FLOOR()": {"#VALUE!", "FLOOR requires 2 numeric arguments"},
+ "=FLOOR(\"X\",-1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=FLOOR(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=FLOOR(1,-1)": {"#NUM!", "invalid arguments to FLOOR"},
// _xlfn.FLOOR.MATH
- "=_xlfn.FLOOR.MATH()": "FLOOR.MATH requires at least 1 argument",
- "=_xlfn.FLOOR.MATH(1,2,3,4)": "FLOOR.MATH allows at most 3 arguments",
- `=_xlfn.FLOOR.MATH("X",2,3)`: "#VALUE!",
- `=_xlfn.FLOOR.MATH(1,"X",3)`: "#VALUE!",
- `=_xlfn.FLOOR.MATH(1,2,"X")`: "#VALUE!",
+ "=_xlfn.FLOOR.MATH()": {"#VALUE!", "FLOOR.MATH requires at least 1 argument"},
+ "=_xlfn.FLOOR.MATH(1,2,3,4)": {"#VALUE!", "FLOOR.MATH allows at most 3 arguments"},
+ "=_xlfn.FLOOR.MATH(\"X\",2,3)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.FLOOR.MATH(1,\"X\",3)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.FLOOR.MATH(1,2,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.FLOOR.PRECISE
- "=_xlfn.FLOOR.PRECISE()": "FLOOR.PRECISE requires at least 1 argument",
- "=_xlfn.FLOOR.PRECISE(1,2,3)": "FLOOR.PRECISE allows at most 2 arguments",
- `=_xlfn.FLOOR.PRECISE("X",2)`: "#VALUE!",
- `=_xlfn.FLOOR.PRECISE(1,"X")`: "#VALUE!",
+ "=_xlfn.FLOOR.PRECISE()": {"#VALUE!", "FLOOR.PRECISE requires at least 1 argument"},
+ "=_xlfn.FLOOR.PRECISE(1,2,3)": {"#VALUE!", "FLOOR.PRECISE allows at most 2 arguments"},
+ "=_xlfn.FLOOR.PRECISE(\"X\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.FLOOR.PRECISE(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// GCD
- "=GCD()": "GCD requires at least 1 argument",
- "=GCD(-1)": "GCD only accepts positive arguments",
- "=GCD(1,-1)": "GCD only accepts positive arguments",
- `=GCD("X")`: "#VALUE!",
+ "=GCD()": {"#VALUE!", "GCD requires at least 1 argument"},
+ "=GCD(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GCD(-1)": {"#VALUE!", "GCD only accepts positive arguments"},
+ "=GCD(1,-1)": {"#VALUE!", "GCD only accepts positive arguments"},
+ "=GCD(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// INT
- "=INT()": "INT requires 1 numeric argument",
- `=INT("X")`: "#VALUE!",
+ "=INT()": {"#VALUE!", "INT requires 1 numeric argument"},
+ "=INT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ISO.CEILING
- "=ISO.CEILING()": "ISO.CEILING requires at least 1 argument",
- "=ISO.CEILING(1,2,3)": "ISO.CEILING allows at most 2 arguments",
- `=ISO.CEILING("X",2)`: "#VALUE!",
- `=ISO.CEILING(1,"X")`: "#VALUE!",
+ "=ISO.CEILING()": {"#VALUE!", "ISO.CEILING requires at least 1 argument"},
+ "=ISO.CEILING(1,2,3)": {"#VALUE!", "ISO.CEILING allows at most 2 arguments"},
+ "=ISO.CEILING(\"X\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ISO.CEILING(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// LCM
- "=LCM()": "LCM requires at least 1 argument",
- "=LCM(-1)": "LCM only accepts positive arguments",
- "=LCM(1,-1)": "LCM only accepts positive arguments",
- `=LCM("X")`: "#VALUE!",
+ "=LCM()": {"#VALUE!", "LCM requires at least 1 argument"},
+ "=LCM(-1)": {"#VALUE!", "LCM only accepts positive arguments"},
+ "=LCM(1,-1)": {"#VALUE!", "LCM only accepts positive arguments"},
+ "=LCM(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// LN
- "=LN()": "LN requires 1 numeric argument",
- `=LN("X")`: "#VALUE!",
+ "=LN()": {"#VALUE!", "LN requires 1 numeric argument"},
+ "=LN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// LOG
- "=LOG()": "LOG requires at least 1 argument",
- "=LOG(1,2,3)": "LOG allows at most 2 arguments",
- `=LOG("X",1)`: "#VALUE!",
- `=LOG(1,"X")`: "#VALUE!",
- "=LOG(0,0)": "#NUM!",
- "=LOG(1,0)": "#NUM!",
- "=LOG(1,1)": "#DIV/0!",
+ "=LOG()": {"#VALUE!", "LOG requires at least 1 argument"},
+ "=LOG(1,2,3)": {"#VALUE!", "LOG allows at most 2 arguments"},
+ "=LOG(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=LOG(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=LOG(0,0)": {"#NUM!", "#DIV/0!"},
+ "=LOG(1,0)": {"#NUM!", "#DIV/0!"},
+ "=LOG(1,1)": {"#DIV/0!", "#DIV/0!"},
// LOG10
- "=LOG10()": "LOG10 requires 1 numeric argument",
- `=LOG10("X")`: "#VALUE!",
+ "=LOG10()": {"#VALUE!", "LOG10 requires 1 numeric argument"},
+ "=LOG10(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ // MDETERM
+ "=MDETERM()": {"#VALUE!", "MDETERM requires 1 argument"},
+ // MINVERSE
+ "=MINVERSE()": {"#VALUE!", "MINVERSE requires 1 argument"},
+ "=MINVERSE(B3:C4)": {"#VALUE!", "#VALUE!"},
+ "=MINVERSE(A1:C2)": {"#VALUE!", "#VALUE!"},
+ "=MINVERSE(A4:A4)": {"#NUM!", "#NUM!"},
+ // MMULT
+ "=MMULT()": {"#VALUE!", "MMULT requires 2 argument"},
+ "=MMULT(A1:B2,B3:C4)": {"#VALUE!", "#VALUE!"},
+ "=MMULT(B3:C4,A1:B2)": {"#VALUE!", "#VALUE!"},
+ "=MMULT(A1:A2,B1:B2)": {"#VALUE!", "#VALUE!"},
// MOD
- "=MOD()": "MOD requires 2 numeric arguments",
- "=MOD(6,0)": "#DIV/0!",
- `=MOD("X",0)`: "#VALUE!",
- `=MOD(6,"X")`: "#VALUE!",
+ "=MOD()": {"#VALUE!", "MOD requires 2 numeric arguments"},
+ "=MOD(6,0)": {"#DIV/0!", "MOD divide by zero"},
+ "=MOD(\"X\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=MOD(6,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// MROUND
- "=MROUND()": "MROUND requires 2 numeric arguments",
- "=MROUND(1,0)": "#NUM!",
- "=MROUND(1,-1)": "#NUM!",
- `=MROUND("X",0)`: "#VALUE!",
- `=MROUND(1,"X")`: "#VALUE!",
+ "=MROUND()": {"#VALUE!", "MROUND requires 2 numeric arguments"},
+ "=MROUND(1,0)": {"#NUM!", "#NUM!"},
+ "=MROUND(1,-1)": {"#NUM!", "#NUM!"},
+ "=MROUND(\"X\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=MROUND(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// MULTINOMIAL
- `=MULTINOMIAL("X")`: "#VALUE!",
+ "=MULTINOMIAL(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.MUNIT
- "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently
- `=_xlfn.MUNIT("X")`: "#VALUE!", // not support currently
+ "=_xlfn.MUNIT()": {"#VALUE!", "MUNIT requires 1 numeric argument"},
+ "=_xlfn.MUNIT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=_xlfn.MUNIT(-1)": {"#VALUE!", ""},
// ODD
- "=ODD()": "ODD requires 1 numeric argument",
- `=ODD("X")`: "#VALUE!",
+ "=ODD()": {"#VALUE!", "ODD requires 1 numeric argument"},
+ "=ODD(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// PI
- "=PI(1)": "PI accepts no arguments",
+ "=PI(1)": {"#VALUE!", "PI accepts no arguments"},
// POWER
- `=POWER("X",1)`: "#VALUE!",
- `=POWER(1,"X")`: "#VALUE!",
- "=POWER(0,0)": "#NUM!",
- "=POWER(0,-1)": "#DIV/0!",
- "=POWER(1)": "POWER requires 2 numeric arguments",
+ "=POWER(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=POWER(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=POWER(0,0)": {"#NUM!", "#NUM!"},
+ "=POWER(0,-1)": {"#DIV/0!", "#DIV/0!"},
+ "=POWER(1)": {"#VALUE!", "POWER requires 2 numeric arguments"},
// PRODUCT
- `=PRODUCT("X")`: "#VALUE!",
+ "=PRODUCT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=PRODUCT(\"\",3,6)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// QUOTIENT
- `=QUOTIENT("X",1)`: "#VALUE!",
- `=QUOTIENT(1,"X")`: "#VALUE!",
- "=QUOTIENT(1,0)": "#DIV/0!",
- "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments",
+ "=QUOTIENT(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=QUOTIENT(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=QUOTIENT(1,0)": {"#DIV/0!", "#DIV/0!"},
+ "=QUOTIENT(1)": {"#VALUE!", "QUOTIENT requires 2 numeric arguments"},
// RADIANS
- `=RADIANS("X")`: "#VALUE!",
- "=RADIANS()": "RADIANS requires 1 numeric argument",
+ "=RADIANS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=RADIANS()": {"#VALUE!", "RADIANS requires 1 numeric argument"},
// RAND
- "=RAND(1)": "RAND accepts no arguments",
+ "=RAND(1)": {"#VALUE!", "RAND accepts no arguments"},
// RANDBETWEEN
- `=RANDBETWEEN("X",1)`: "#VALUE!",
- `=RANDBETWEEN(1,"X")`: "#VALUE!",
- "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments",
- "=RANDBETWEEN(2,1)": "#NUM!",
+ "=RANDBETWEEN(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=RANDBETWEEN(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=RANDBETWEEN()": {"#VALUE!", "RANDBETWEEN requires 2 numeric arguments"},
+ "=RANDBETWEEN(2,1)": {"#NUM!", "#NUM!"},
// ROMAN
- "=ROMAN()": "ROMAN requires at least 1 argument",
- "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments",
- `=ROMAN("X")`: "#VALUE!",
- `=ROMAN("X",1)`: "#VALUE!",
+ "=ROMAN()": {"#VALUE!", "ROMAN requires at least 1 argument"},
+ "=ROMAN(1,2,3)": {"#VALUE!", "ROMAN allows at most 2 arguments"},
+ "=ROMAN(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ROMAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ROMAN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// ROUND
- "=ROUND()": "ROUND requires 2 numeric arguments",
- `=ROUND("X",1)`: "#VALUE!",
- `=ROUND(1,"X")`: "#VALUE!",
+ "=ROUND()": {"#VALUE!", "ROUND requires 2 numeric arguments"},
+ "=ROUND(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ROUND(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ROUNDDOWN
- "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments",
- `=ROUNDDOWN("X",1)`: "#VALUE!",
- `=ROUNDDOWN(1,"X")`: "#VALUE!",
+ "=ROUNDDOWN()": {"#VALUE!", "ROUNDDOWN requires 2 numeric arguments"},
+ "=ROUNDDOWN(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ROUNDDOWN(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// ROUNDUP
- "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments",
- `=ROUNDUP("X",1)`: "#VALUE!",
- `=ROUNDUP(1,"X")`: "#VALUE!",
+ "=ROUNDUP()": {"#VALUE!", "ROUNDUP requires 2 numeric arguments"},
+ "=ROUNDUP(\"X\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=ROUNDUP(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ // SEARCH
+ "=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"},
+ "=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"},
+ "=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"},
+ "=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // SEARCHB
+ "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"},
+ "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"},
+ "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"},
+ "=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"},
+ "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// SEC
- "=_xlfn.SEC()": "SEC requires 1 numeric argument",
- `=_xlfn.SEC("X")`: "#VALUE!",
+ "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"},
+ "=_xlfn.SEC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// _xlfn.SECH
- "=_xlfn.SECH()": "SECH requires 1 numeric argument",
- `=_xlfn.SECH("X")`: "#VALUE!",
+ "=_xlfn.SECH()": {"#VALUE!", "SECH requires 1 numeric argument"},
+ "=_xlfn.SECH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ // SERIESSUM
+ "=SERIESSUM()": {"#VALUE!", "SERIESSUM requires 4 arguments"},
+ "=SERIESSUM(\"\",2,3,A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SERIESSUM(1,\"\",3,A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SERIESSUM(1,2,\"\",A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SERIESSUM(1,2,3,A1:D1)": {"#VALUE!", "strconv.ParseFloat: parsing \"Month\": invalid syntax"},
// SIGN
- "=SIGN()": "SIGN requires 1 numeric argument",
- `=SIGN("X")`: "#VALUE!",
+ "=SIGN()": {"#VALUE!", "SIGN requires 1 numeric argument"},
+ "=SIGN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// SIN
- "=SIN()": "SIN requires 1 numeric argument",
- `=SIN("X")`: "#VALUE!",
+ "=SIN()": {"#VALUE!", "SIN requires 1 numeric argument"},
+ "=SIN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// SINH
- "=SINH()": "SINH requires 1 numeric argument",
- `=SINH("X")`: "#VALUE!",
+ "=SINH()": {"#VALUE!", "SINH requires 1 numeric argument"},
+ "=SINH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// SQRT
- "=SQRT()": "SQRT requires 1 numeric argument",
- `=SQRT("X")`: "#VALUE!",
- "=SQRT(-1)": "#NUM!",
+ "=SQRT()": {"#VALUE!", "SQRT requires 1 numeric argument"},
+ "=SQRT(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SQRT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=SQRT(-1)": {"#NUM!", "#NUM!"},
// SQRTPI
- "=SQRTPI()": "SQRTPI requires 1 numeric argument",
- `=SQRTPI("X")`: "#VALUE!",
+ "=SQRTPI()": {"#VALUE!", "SQRTPI requires 1 numeric argument"},
+ "=SQRTPI(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ // STDEV
+ "=STDEV()": {"#VALUE!", "STDEV requires at least 1 argument"},
+ "=STDEV(E2:E9)": {"#DIV/0!", "#DIV/0!"},
+ // STDEV.S
+ "=STDEV.S()": {"#VALUE!", "STDEV.S requires at least 1 argument"},
+ // STDEVA
+ "=STDEVA()": {"#VALUE!", "STDEVA requires at least 1 argument"},
+ "=STDEVA(E2:E9)": {"#DIV/0!", "#DIV/0!"},
+ // POISSON.DIST
+ "=POISSON.DIST()": {"#VALUE!", "POISSON.DIST requires 3 arguments"},
+ // POISSON
+ "=POISSON()": {"#VALUE!", "POISSON requires 3 arguments"},
+ "=POISSON(\"\",0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=POISSON(0,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=POISSON(0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=POISSON(0,-1,TRUE)": {"#N/A", "#N/A"},
+ // PROB
+ "=PROB()": {"#VALUE!", "PROB requires at least 3 arguments"},
+ "=PROB(A1:A2,B1:B2,1,1,1)": {"#VALUE!", "PROB requires at most 4 arguments"},
+ "=PROB(A1:A2,B1:B2,\"\")": {"#VALUE!", "#VALUE!"},
+ "=PROB(A1:A2,B1:B2,1,\"\")": {"#VALUE!", "#VALUE!"},
+ "=PROB(A1,B1,1)": {"#NUM!", "#NUM!"},
+ "=PROB(A1:A2,B1:B3,1)": {"#N/A", "#N/A"},
+ "=PROB(A1:A2,B1:C2,1)": {"#N/A", "#N/A"},
+ "=PROB(A1:A2,B1:B2,1)": {"#NUM!", "#NUM!"},
+ // SUBTOTAL
+ "=SUBTOTAL()": {"#VALUE!", "SUBTOTAL requires at least 2 arguments"},
+ "=SUBTOTAL(\"\",A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SUBTOTAL(0,A4:A5)": {"#VALUE!", "SUBTOTAL has invalid function_num"},
+ "=SUBTOTAL(1,A5:A6)": {"#DIV/0!", "#DIV/0!"},
// SUM
- "=SUM((": "formula not valid",
- "=SUM(-)": "formula not valid",
- "=SUM(1+)": "formula not valid",
- "=SUM(1-)": "formula not valid",
- "=SUM(1*)": "formula not valid",
- "=SUM(1/)": "formula not valid",
- `=SUM("X")`: "#VALUE!",
+ "=SUM((": {"", ErrInvalidFormula.Error()},
+ "=SUM(-)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()},
+ "=SUM(1+)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()},
+ "=SUM(1-)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()},
+ "=SUM(1*)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()},
+ "=SUM(1/)": {ErrInvalidFormula.Error(), ErrInvalidFormula.Error()},
+ "=SUM(1*SUM(1/0))": {"#DIV/0!", "#DIV/0!"},
+ "=SUM(1*SUM(1/0)*1)": {"", "#DIV/0!"},
// SUMIF
- "=SUMIF()": "SUMIF requires at least 2 argument",
+ "=SUMIF()": {"#VALUE!", "SUMIF requires at least 2 arguments"},
// SUMSQ
- `=SUMSQ("X")`: "#VALUE!",
+ "=SUMSQ(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=SUMSQ(C1:D2)": {"#VALUE!", "strconv.ParseFloat: parsing \"Month\": invalid syntax"},
+ // SUMPRODUCT
+ "=SUMPRODUCT()": {"#VALUE!", "SUMPRODUCT requires at least 1 argument"},
+ "=SUMPRODUCT(A1,B1:B2)": {"#VALUE!", "#VALUE!"},
+ "=SUMPRODUCT(A1,D1)": {"#VALUE!", "#VALUE!"},
+ "=SUMPRODUCT(A1:A3,D1:D3)": {"#VALUE!", "#VALUE!"},
+ "=SUMPRODUCT(A1:A2,B1:B3)": {"#VALUE!", "#VALUE!"},
+ "=SUMPRODUCT(\"\")": {"#VALUE!", "#VALUE!"},
+ "=SUMPRODUCT(A1,NA())": {"#N/A", "#N/A"},
+ // SUMX2MY2
+ "=SUMX2MY2()": {"#VALUE!", "SUMX2MY2 requires 2 arguments"},
+ "=SUMX2MY2(A1,B1:B2)": {"#N/A", "#N/A"},
+ // SUMX2PY2
+ "=SUMX2PY2()": {"#VALUE!", "SUMX2PY2 requires 2 arguments"},
+ "=SUMX2PY2(A1,B1:B2)": {"#N/A", "#N/A"},
+ // SUMXMY2
+ "=SUMXMY2()": {"#VALUE!", "SUMXMY2 requires 2 arguments"},
+ "=SUMXMY2(A1,B1:B2)": {"#N/A", "#N/A"},
// TAN
- "=TAN()": "TAN requires 1 numeric argument",
- `=TAN("X")`: "#VALUE!",
+ "=TAN()": {"#VALUE!", "TAN requires 1 numeric argument"},
+ "=TAN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// TANH
- "=TANH()": "TANH requires 1 numeric argument",
- `=TANH("X")`: "#VALUE!",
+ "=TANH()": {"#VALUE!", "TANH requires 1 numeric argument"},
+ "=TANH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// TRUNC
- "=TRUNC()": "TRUNC requires at least 1 argument",
- `=TRUNC("X")`: "#VALUE!",
- `=TRUNC(1,"X")`: "#VALUE!",
- // Statistical functions
+ "=TRUNC()": {"#VALUE!", "TRUNC requires at least 1 argument"},
+ "=TRUNC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ "=TRUNC(1,\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
+ // Statistical Functions
+ // AVEDEV
+ "=AVEDEV()": {"#VALUE!", "AVEDEV requires at least 1 argument"},
+ "=AVEDEV(\"\")": {"#VALUE!", "#VALUE!"},
+ "=AVEDEV(1,\"\")": {"#VALUE!", "#VALUE!"},
+ // AVERAGE
+ "=AVERAGE(H1)": {"#DIV/0!", "#DIV/0!"},
+ // AVERAGEA
+ "=AVERAGEA(H1)": {"#DIV/0!", "#DIV/0!"},
+ // AVERAGEIF
+ "=AVERAGEIF()": {"#VALUE!", "AVERAGEIF requires at least 2 arguments"},
+ "=AVERAGEIF(H1,\"\")": {"#DIV/0!", "#DIV/0!"},
+ "=AVERAGEIF(D1:D3,\"Month\",D1:D3)": {"#DIV/0!", "#DIV/0!"},
+ "=AVERAGEIF(C1:C3,\"Month\",D1:D3)": {"#DIV/0!", "#DIV/0!"},
+ // BETA.DIST
+ "=BETA.DIST()": {"#VALUE!", "BETA.DIST requires at least 4 arguments"},
+ "=BETA.DIST(0.4,4,5,TRUE,0,1,0)": {"#VALUE!", "BETA.DIST requires at most 6 arguments"},
+ "=BETA.DIST(\"\",4,5,TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,\"\",5,TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,4,\"\",TRUE,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,4,5,\"\",0,1)": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,4,5,TRUE,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,4,5,TRUE,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.DIST(0.4,0,5,TRUE,0,1)": {"#NUM!", "#NUM!"},
+ "=BETA.DIST(0.4,4,0,TRUE,0,0)": {"#NUM!", "#NUM!"},
+ "=BETA.DIST(0.4,4,5,TRUE,0.5,1)": {"#NUM!", "#NUM!"},
+ "=BETA.DIST(0.4,4,5,TRUE,0,0.3)": {"#NUM!", "#NUM!"},
+ "=BETA.DIST(0.4,4,5,TRUE,0.4,0.4)": {"#NUM!", "#NUM!"},
+ // BETADIST
+ "=BETADIST()": {"#VALUE!", "BETADIST requires at least 3 arguments"},
+ "=BETADIST(0.4,4,5,0,1,0)": {"#VALUE!", "BETADIST requires at most 5 arguments"},
+ "=BETADIST(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETADIST(0.4,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETADIST(0.4,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETADIST(0.4,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETADIST(0.4,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETADIST(2,4,5,3,1)": {"#NUM!", "#NUM!"},
+ "=BETADIST(2,4,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETADIST(0.4,0,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETADIST(0.4,4,0,0,1)": {"#NUM!", "#NUM!"},
+ "=BETADIST(0.4,4,5,0.4,0.4)": {"#NUM!", "#NUM!"},
+ // BETAINV
+ "=BETAINV()": {"#VALUE!", "BETAINV requires at least 3 arguments"},
+ "=BETAINV(0.2,4,5,0,1,0)": {"#VALUE!", "BETAINV requires at most 5 arguments"},
+ "=BETAINV(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETAINV(0.2,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETAINV(0.2,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETAINV(0.2,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETAINV(0.2,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETAINV(0,4,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETAINV(1,4,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETAINV(0.2,0,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETAINV(0.2,4,0,0,1)": {"#NUM!", "#NUM!"},
+ "=BETAINV(0.2,4,5,2,2)": {"#NUM!", "#NUM!"},
+ // BETA.INV
+ "=BETA.INV()": {"#VALUE!", "BETA.INV requires at least 3 arguments"},
+ "=BETA.INV(0.2,4,5,0,1,0)": {"#VALUE!", "BETA.INV requires at most 5 arguments"},
+ "=BETA.INV(\"\",4,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.INV(0.2,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.INV(0.2,4,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.INV(0.2,4,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.INV(0.2,4,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BETA.INV(0,4,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETA.INV(1,4,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETA.INV(0.2,0,5,0,1)": {"#NUM!", "#NUM!"},
+ "=BETA.INV(0.2,4,0,0,1)": {"#NUM!", "#NUM!"},
+ "=BETA.INV(0.2,4,5,2,2)": {"#NUM!", "#NUM!"},
+ // BINOMDIST
+ "=BINOMDIST()": {"#VALUE!", "BINOMDIST requires 4 arguments"},
+ "=BINOMDIST(\"\",100,0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOMDIST(10,\"\",0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOMDIST(10,100,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOMDIST(10,100,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=BINOMDIST(-1,100,0.5,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOMDIST(110,100,0.5,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOMDIST(10,100,-1,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOMDIST(10,100,2,FALSE)": {"#NUM!", "#NUM!"},
+ // BINOM.DIST
+ "=BINOM.DIST()": {"#VALUE!", "BINOM.DIST requires 4 arguments"},
+ "=BINOM.DIST(\"\",100,0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST(10,\"\",0.5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST(10,100,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST(10,100,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=BINOM.DIST(-1,100,0.5,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST(110,100,0.5,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST(10,100,-1,FALSE)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST(10,100,2,FALSE)": {"#NUM!", "#NUM!"},
+ // BINOM.DIST.RANGE
+ "=BINOM.DIST.RANGE()": {"#VALUE!", "BINOM.DIST.RANGE requires at least 3 arguments"},
+ "=BINOM.DIST.RANGE(100,0.5,0,40,0)": {"#VALUE!", "BINOM.DIST.RANGE requires at most 4 arguments"},
+ "=BINOM.DIST.RANGE(\"\",0.5,0,40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST.RANGE(100,\"\",0,40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST.RANGE(100,0.5,\"\",40)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST.RANGE(100,0.5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.DIST.RANGE(100,-1,0,40)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST.RANGE(100,2,0,40)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST.RANGE(100,0.5,-1,40)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST.RANGE(100,0.5,110,40)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST.RANGE(100,0.5,0,-1)": {"#NUM!", "#NUM!"},
+ "=BINOM.DIST.RANGE(100,0.5,0,110)": {"#NUM!", "#NUM!"},
+ // BINOM.INV
+ "=BINOM.INV()": {"#VALUE!", "BINOM.INV requires 3 numeric arguments"},
+ "=BINOM.INV(\"\",0.5,20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.INV(100,\"\",20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.INV(100,0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=BINOM.INV(-1,0.5,20%)": {"#NUM!", "#NUM!"},
+ "=BINOM.INV(100,-1,20%)": {"#NUM!", "#NUM!"},
+ "=BINOM.INV(100,2,20%)": {"#NUM!", "#NUM!"},
+ "=BINOM.INV(100,0.5,-1)": {"#NUM!", "#NUM!"},
+ "=BINOM.INV(100,0.5,2)": {"#NUM!", "#NUM!"},
+ "=BINOM.INV(1,1,20%)": {"#NUM!", "#NUM!"},
+ // CHIDIST
+ "=CHIDIST()": {"#VALUE!", "CHIDIST requires 2 numeric arguments"},
+ "=CHIDIST(\"\",3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHIDIST(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // CHIINV
+ "=CHIINV()": {"#VALUE!", "CHIINV requires 2 numeric arguments"},
+ "=CHIINV(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHIINV(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHIINV(0,1)": {"#NUM!", "#NUM!"},
+ "=CHIINV(2,1)": {"#NUM!", "#NUM!"},
+ "=CHIINV(0.5,0.5)": {"#NUM!", "#NUM!"},
+ // CHISQ.DIST
+ "=CHISQ.DIST()": {"#VALUE!", "CHISQ.DIST requires 3 arguments"},
+ "=CHISQ.DIST(\"\",2,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.DIST(3,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.DIST(3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=CHISQ.DIST(-1,2,TRUE)": {"#NUM!", "#NUM!"},
+ "=CHISQ.DIST(3,0,TRUE)": {"#NUM!", "#NUM!"},
+ // CHISQ.DIST.RT
+ "=CHISQ.DIST.RT()": {"#VALUE!", "CHISQ.DIST.RT requires 2 numeric arguments"},
+ "=CHISQ.DIST.RT(\"\",3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.DIST.RT(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // CHISQ.INV
+ "=CHISQ.INV()": {"#VALUE!", "CHISQ.INV requires 2 numeric arguments"},
+ "=CHISQ.INV(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.INV(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.INV(-1,1)": {"#NUM!", "#NUM!"},
+ "=CHISQ.INV(1,1)": {"#NUM!", "#NUM!"},
+ "=CHISQ.INV(0.5,0.5)": {"#NUM!", "#NUM!"},
+ "=CHISQ.INV(0.5,10000000001)": {"#NUM!", "#NUM!"},
+ // CHISQ.INV.RT
+ "=CHISQ.INV.RT()": {"#VALUE!", "CHISQ.INV.RT requires 2 numeric arguments"},
+ "=CHISQ.INV.RT(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.INV.RT(0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CHISQ.INV.RT(0,1)": {"#NUM!", "#NUM!"},
+ "=CHISQ.INV.RT(2,1)": {"#NUM!", "#NUM!"},
+ "=CHISQ.INV.RT(0.5,0.5)": {"#NUM!", "#NUM!"},
+ // CONFIDENCE
+ "=CONFIDENCE()": {"#VALUE!", "CONFIDENCE requires 3 numeric arguments"},
+ "=CONFIDENCE(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE(0,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE(1,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE(0.05,0,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE(0.05,0.07,0.5)": {"#NUM!", "#NUM!"},
+ // CONFIDENCE.NORM
+ "=CONFIDENCE.NORM()": {"#VALUE!", "CONFIDENCE.NORM requires 3 numeric arguments"},
+ "=CONFIDENCE.NORM(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.NORM(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.NORM(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.NORM(0,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.NORM(1,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.NORM(0.05,0,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.NORM(0.05,0.07,0.5)": {"#NUM!", "#NUM!"},
+ // CORREL
+ "=CORREL()": {"#VALUE!", "CORREL requires 2 arguments"},
+ "=CORREL(A1:A3,B1:B5)": {"#N/A", "#N/A"},
+ "=CORREL(A1:A1,B1:B1)": {"#DIV/0!", "#DIV/0!"},
+ // CONFIDENCE.T
+ "=CONFIDENCE.T()": {"#VALUE!", "CONFIDENCE.T requires 3 arguments"},
+ "=CONFIDENCE.T(\"\",0.07,100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.T(0.05,\"\",100)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.T(0.05,0.07,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CONFIDENCE.T(0,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.T(1,0.07,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.T(0.05,0,100)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.T(0.05,0.07,0)": {"#NUM!", "#NUM!"},
+ "=CONFIDENCE.T(0.05,0.07,1)": {"#DIV/0!", "#DIV/0!"},
+ // COUNTBLANK
+ "=COUNTBLANK()": {"#VALUE!", "COUNTBLANK requires 1 argument"},
+ "=COUNTBLANK(1,2)": {"#VALUE!", "COUNTBLANK requires 1 argument"},
+ // COUNTIF
+ "=COUNTIF()": {"#VALUE!", "COUNTIF requires 2 arguments"},
+ // COUNTIFS
+ "=COUNTIFS()": {"#VALUE!", "COUNTIFS requires at least 2 arguments"},
+ "=COUNTIFS(A1:A9,2,D1:D9)": {"#N/A", "#N/A"},
+ // CRITBINOM
+ "=CRITBINOM()": {"#VALUE!", "CRITBINOM requires 3 numeric arguments"},
+ "=CRITBINOM(\"\",0.5,20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CRITBINOM(100,\"\",20%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CRITBINOM(100,0.5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CRITBINOM(-1,0.5,20%)": {"#NUM!", "#NUM!"},
+ "=CRITBINOM(100,-1,20%)": {"#NUM!", "#NUM!"},
+ "=CRITBINOM(100,2,20%)": {"#NUM!", "#NUM!"},
+ "=CRITBINOM(100,0.5,-1)": {"#NUM!", "#NUM!"},
+ "=CRITBINOM(100,0.5,2)": {"#NUM!", "#NUM!"},
+ "=CRITBINOM(1,1,20%)": {"#NUM!", "#NUM!"},
+ // DEVSQ
+ "=DEVSQ()": {"#VALUE!", "DEVSQ requires at least 1 numeric argument"},
+ "=DEVSQ(D1:D2)": {"#N/A", "#N/A"},
+ // FISHER
+ "=FISHER()": {"#VALUE!", "FISHER requires 1 numeric argument"},
+ "=FISHER(2)": {"#N/A", "#N/A"},
+ "=FISHER(\"2\")": {"#N/A", "#N/A"},
+ "=FISHER(INT(-2)))": {"#N/A", "#N/A"},
+ "=FISHER(F1)": {"#VALUE!", "FISHER requires 1 numeric argument"},
+ // FISHERINV
+ "=FISHERINV()": {"#VALUE!", "FISHERINV requires 1 numeric argument"},
+ "=FISHERINV(F1)": {"#VALUE!", "FISHERINV requires 1 numeric argument"},
+ // FORECAST
+ "=FORECAST()": {"#VALUE!", "FORECAST requires 3 arguments"},
+ "=FORECAST(\"\",A1:A7,B1:B7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FORECAST(1,A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=FORECAST(1,A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // FORECAST.LINEAR
+ "=FORECAST.LINEAR()": {"#VALUE!", "FORECAST.LINEAR requires 3 arguments"},
+ "=FORECAST.LINEAR(\"\",A1:A7,B1:B7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FORECAST.LINEAR(1,A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=FORECAST.LINEAR(1,A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // FREQUENCY
+ "=FREQUENCY()": {"#VALUE!", "FREQUENCY requires 2 arguments"},
+ "=FREQUENCY(NA(),A1:A3)": {"#N/A", "#N/A"},
+ "=FREQUENCY(A1:A3,NA())": {"#N/A", "#N/A"},
+ // GAMMA
+ "=GAMMA()": {"#VALUE!", "GAMMA requires 1 numeric argument"},
+ "=GAMMA(F1)": {"#VALUE!", "GAMMA requires 1 numeric argument"},
+ "=GAMMA(0)": {"#N/A", "#N/A"},
+ "=GAMMA(\"0\")": {"#N/A", "#N/A"},
+ "=GAMMA(INT(0))": {"#N/A", "#N/A"},
+ // GAMMA.DIST
+ "=GAMMA.DIST()": {"#VALUE!", "GAMMA.DIST requires 4 arguments"},
+ "=GAMMA.DIST(\"\",3,2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.DIST(6,\"\",2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.DIST(6,3,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.DIST(6,3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=GAMMA.DIST(-1,3,2,FALSE)": {"#NUM!", "#NUM!"},
+ "=GAMMA.DIST(6,0,2,FALSE)": {"#NUM!", "#NUM!"},
+ "=GAMMA.DIST(6,3,0,FALSE)": {"#NUM!", "#NUM!"},
+ // GAMMADIST
+ "=GAMMADIST()": {"#VALUE!", "GAMMADIST requires 4 arguments"},
+ "=GAMMADIST(\"\",3,2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMADIST(6,\"\",2,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMADIST(6,3,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMADIST(6,3,2,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=GAMMADIST(-1,3,2,FALSE)": {"#NUM!", "#NUM!"},
+ "=GAMMADIST(6,0,2,FALSE)": {"#NUM!", "#NUM!"},
+ "=GAMMADIST(6,3,0,FALSE)": {"#NUM!", "#NUM!"},
+ // GAMMA.INV
+ "=GAMMA.INV()": {"#VALUE!", "GAMMA.INV requires 3 arguments"},
+ "=GAMMA.INV(\"\",3,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.INV(0.5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.INV(0.5,3,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMA.INV(-1,3,2)": {"#NUM!", "#NUM!"},
+ "=GAMMA.INV(2,3,2)": {"#NUM!", "#NUM!"},
+ "=GAMMA.INV(0.5,0,2)": {"#NUM!", "#NUM!"},
+ "=GAMMA.INV(0.5,3,0)": {"#NUM!", "#NUM!"},
+ // GAMMAINV
+ "=GAMMAINV()": {"#VALUE!", "GAMMAINV requires 3 arguments"},
+ "=GAMMAINV(\"\",3,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMAINV(0.5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMAINV(0.5,3,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMAINV(-1,3,2)": {"#NUM!", "#NUM!"},
+ "=GAMMAINV(2,3,2)": {"#NUM!", "#NUM!"},
+ "=GAMMAINV(0.5,0,2)": {"#NUM!", "#NUM!"},
+ "=GAMMAINV(0.5,3,0)": {"#NUM!", "#NUM!"},
+ // GAMMALN
+ "=GAMMALN()": {"#VALUE!", "GAMMALN requires 1 numeric argument"},
+ "=GAMMALN(F1)": {"#VALUE!", "GAMMALN requires 1 numeric argument"},
+ "=GAMMALN(0)": {"#N/A", "#N/A"},
+ "=GAMMALN(INT(0))": {"#N/A", "#N/A"},
+ // GAMMALN.PRECISE
+ "=GAMMALN.PRECISE()": {"#VALUE!", "GAMMALN.PRECISE requires 1 numeric argument"},
+ "=GAMMALN.PRECISE(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=GAMMALN.PRECISE(0)": {"#NUM!", "#NUM!"},
+ // GAUSS
+ "=GAUSS()": {"#VALUE!", "GAUSS requires 1 numeric argument"},
+ "=GAUSS(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // GEOMEAN
+ "=GEOMEAN()": {"#VALUE!", "GEOMEAN requires at least 1 numeric argument"},
+ "=GEOMEAN(0)": {"#NUM!", "#NUM!"},
+ "=GEOMEAN(D1:D2)": {"#NUM!", "#NUM!"},
+ "=GEOMEAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // HARMEAN
+ "=HARMEAN()": {"#VALUE!", "HARMEAN requires at least 1 argument"},
+ "=HARMEAN(-1)": {"#N/A", "#N/A"},
+ "=HARMEAN(0)": {"#N/A", "#N/A"},
+ // HYPGEOM.DIST
+ "=HYPGEOM.DIST()": {"#VALUE!", "HYPGEOM.DIST requires 5 arguments"},
+ "=HYPGEOM.DIST(\"\",4,4,12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOM.DIST(1,\"\",4,12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOM.DIST(1,4,\"\",12,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOM.DIST(1,4,4,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOM.DIST(1,4,4,12,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=HYPGEOM.DIST(-1,4,4,12,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(2,1,4,12,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(2,4,1,12,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(2,2,2,1,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(1,0,4,12,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(1,4,4,2,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(1,4,0,12,FALSE)": {"#NUM!", "#NUM!"},
+ "=HYPGEOM.DIST(1,4,4,0,FALSE)": {"#NUM!", "#NUM!"},
+ // HYPGEOMDIST
+ "=HYPGEOMDIST()": {"#VALUE!", "HYPGEOMDIST requires 4 numeric arguments"},
+ "=HYPGEOMDIST(\"\",4,4,12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOMDIST(1,\"\",4,12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOMDIST(1,4,\"\",12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOMDIST(1,4,4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=HYPGEOMDIST(-1,4,4,12)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(2,1,4,12)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(2,4,1,12)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(2,2,2,1)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(1,0,4,12)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(1,4,4,2)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(1,4,0,12)": {"#NUM!", "#NUM!"},
+ "=HYPGEOMDIST(1,4,4,0)": {"#NUM!", "#NUM!"},
+ // INTERCEPT
+ "=INTERCEPT()": {"#VALUE!", "INTERCEPT requires 2 arguments"},
+ "=INTERCEPT(A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=INTERCEPT(A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // KURT
+ "=KURT()": {"#VALUE!", "KURT requires at least 1 argument"},
+ "=KURT(F1,INT(1))": {"#DIV/0!", "#DIV/0!"},
+ // EXPON.DIST
+ "=EXPON.DIST()": {"#VALUE!", "EXPON.DIST requires 3 arguments"},
+ "=EXPON.DIST(\"\",1,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EXPON.DIST(0,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EXPON.DIST(0,1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=EXPON.DIST(-1,1,TRUE)": {"#NUM!", "#NUM!"},
+ "=EXPON.DIST(1,0,TRUE)": {"#NUM!", "#NUM!"},
+ // EXPONDIST
+ "=EXPONDIST()": {"#VALUE!", "EXPONDIST requires 3 arguments"},
+ "=EXPONDIST(\"\",1,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EXPONDIST(0,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EXPONDIST(0,1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=EXPONDIST(-1,1,TRUE)": {"#NUM!", "#NUM!"},
+ "=EXPONDIST(1,0,TRUE)": {"#NUM!", "#NUM!"},
+ // FDIST
+ "=FDIST()": {"#VALUE!", "FDIST requires 3 arguments"},
+ "=FDIST(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FDIST(5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FDIST(5,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FDIST(-1,1,2)": {"#NUM!", "#NUM!"},
+ "=FDIST(5,0,2)": {"#NUM!", "#NUM!"},
+ "=FDIST(5,10000000000,2)": {"#NUM!", "#NUM!"},
+ "=FDIST(5,1,0)": {"#NUM!", "#NUM!"},
+ "=FDIST(5,1,10000000000)": {"#NUM!", "#NUM!"},
+ // F.DIST
+ "=F.DIST()": {"#VALUE!", "F.DIST requires 4 arguments"},
+ "=F.DIST(\"\",2,5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST(1,\"\",5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST(1,2,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST(1,2,5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=F.DIST(-1,1,2,TRUE)": {"#NUM!", "#NUM!"},
+ "=F.DIST(5,0,2,TRUE)": {"#NUM!", "#NUM!"},
+ "=F.DIST(5,10000000000,2,TRUE)": {"#NUM!", "#NUM!"},
+ "=F.DIST(5,1,0,TRUE)": {"#NUM!", "#NUM!"},
+ "=F.DIST(5,1,10000000000,TRUE)": {"#NUM!", "#NUM!"},
+ // F.DIST.RT
+ "=F.DIST.RT()": {"#VALUE!", "F.DIST.RT requires 3 arguments"},
+ "=F.DIST.RT(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST.RT(5,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST.RT(5,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.DIST.RT(-1,1,2)": {"#NUM!", "#NUM!"},
+ "=F.DIST.RT(5,0,2)": {"#NUM!", "#NUM!"},
+ "=F.DIST.RT(5,10000000000,2)": {"#NUM!", "#NUM!"},
+ "=F.DIST.RT(5,1,0)": {"#NUM!", "#NUM!"},
+ "=F.DIST.RT(5,1,10000000000)": {"#NUM!", "#NUM!"},
+ // F.INV
+ "=F.INV()": {"#VALUE!", "F.INV requires 3 arguments"},
+ "=F.INV(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV(0,1,2)": {"#NUM!", "#NUM!"},
+ "=F.INV(0.2,0.5,2)": {"#NUM!", "#NUM!"},
+ "=F.INV(0.2,1,0.5)": {"#NUM!", "#NUM!"},
+ // FINV
+ "=FINV()": {"#VALUE!", "FINV requires 3 arguments"},
+ "=FINV(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FINV(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FINV(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FINV(0,1,2)": {"#NUM!", "#NUM!"},
+ "=FINV(0.2,0.5,2)": {"#NUM!", "#NUM!"},
+ "=FINV(0.2,1,0.5)": {"#NUM!", "#NUM!"},
+ // F.INV.RT
+ "=F.INV.RT()": {"#VALUE!", "F.INV.RT requires 3 arguments"},
+ "=F.INV.RT(\"\",1,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV.RT(0.2,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV.RT(0.2,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=F.INV.RT(0,1,2)": {"#NUM!", "#NUM!"},
+ "=F.INV.RT(0.2,0.5,2)": {"#NUM!", "#NUM!"},
+ "=F.INV.RT(0.2,1,0.5)": {"#NUM!", "#NUM!"},
+ // LOGINV
+ "=LOGINV()": {"#VALUE!", "LOGINV requires 3 arguments"},
+ "=LOGINV(\"\",2,0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGINV(0.3,\"\",0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGINV(0.3,2,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGINV(0,2,0.2)": {"#NUM!", "#NUM!"},
+ "=LOGINV(1,2,0.2)": {"#NUM!", "#NUM!"},
+ "=LOGINV(0.3,2,0)": {"#NUM!", "#NUM!"},
+ // LOGNORM.INV
+ "=LOGNORM.INV()": {"#VALUE!", "LOGNORM.INV requires 3 arguments"},
+ "=LOGNORM.INV(\"\",2,0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.INV(0.3,\"\",0.2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.INV(0.3,2,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.INV(0,2,0.2)": {"#NUM!", "#NUM!"},
+ "=LOGNORM.INV(1,2,0.2)": {"#NUM!", "#NUM!"},
+ "=LOGNORM.INV(0.3,2,0)": {"#NUM!", "#NUM!"},
+ // LOGNORM.DIST
+ "=LOGNORM.DIST()": {"#VALUE!", "LOGNORM.DIST requires 4 arguments"},
+ "=LOGNORM.DIST(\"\",10,5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.DIST(0.5,\"\",5,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.DIST(0.5,10,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORM.DIST(0.5,10,5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=LOGNORM.DIST(0,10,5,FALSE)": {"#NUM!", "#NUM!"},
+ "=LOGNORM.DIST(0.5,10,0,FALSE)": {"#NUM!", "#NUM!"},
+ // LOGNORMDIST
+ "=LOGNORMDIST()": {"#VALUE!", "LOGNORMDIST requires 3 arguments"},
+ "=LOGNORMDIST(\"\",10,5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORMDIST(12,\"\",5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORMDIST(12,10,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LOGNORMDIST(0,2,5)": {"#NUM!", "#NUM!"},
+ "=LOGNORMDIST(12,10,0)": {"#NUM!", "#NUM!"},
+ // NEGBINOM.DIST
+ "=NEGBINOM.DIST()": {"#VALUE!", "NEGBINOM.DIST requires 4 arguments"},
+ "=NEGBINOM.DIST(\"\",12,0.5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOM.DIST(6,\"\",0.5,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOM.DIST(6,12,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOM.DIST(6,12,0.5,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=NEGBINOM.DIST(-1,12,0.5,TRUE)": {"#NUM!", "#NUM!"},
+ "=NEGBINOM.DIST(6,0,0.5,TRUE)": {"#NUM!", "#NUM!"},
+ "=NEGBINOM.DIST(6,12,-1,TRUE)": {"#NUM!", "#NUM!"},
+ "=NEGBINOM.DIST(6,12,2,TRUE)": {"#NUM!", "#NUM!"},
+ // NEGBINOMDIST
+ "=NEGBINOMDIST()": {"#VALUE!", "NEGBINOMDIST requires 3 arguments"},
+ "=NEGBINOMDIST(\"\",12,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOMDIST(6,\"\",0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOMDIST(6,12,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NEGBINOMDIST(-1,12,0.5)": {"#NUM!", "#NUM!"},
+ "=NEGBINOMDIST(6,0,0.5)": {"#NUM!", "#NUM!"},
+ "=NEGBINOMDIST(6,12,-1)": {"#NUM!", "#NUM!"},
+ "=NEGBINOMDIST(6,12,2)": {"#NUM!", "#NUM!"},
+ // NORM.DIST
+ "=NORM.DIST()": {"#VALUE!", "NORM.DIST requires 4 arguments"},
+ // NORMDIST
+ "=NORMDIST()": {"#VALUE!", "NORMDIST requires 4 arguments"},
+ "=NORMDIST(\"\",0,0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMDIST(0,\"\",0,FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMDIST(0,0,\"\",FALSE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMDIST(0,0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=NORMDIST(0,0,-1,TRUE)": {"#N/A", "#N/A"},
+ // NORM.INV
+ "=NORM.INV()": {"#VALUE!", "NORM.INV requires 3 arguments"},
+ // NORMINV
+ "=NORMINV()": {"#VALUE!", "NORMINV requires 3 arguments"},
+ "=NORMINV(\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMINV(0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMINV(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NORMINV(0,0,-1)": {"#N/A", "#N/A"},
+ "=NORMINV(-1,0,0)": {"#N/A", "#N/A"},
+ "=NORMINV(0,0,0)": {"#NUM!", "#NUM!"},
+ // NORM.S.DIST
+ "=NORM.S.DIST()": {"#VALUE!", "NORM.S.DIST requires 2 numeric arguments"},
+ // NORMSDIST
+ "=NORMSDIST()": {"#VALUE!", "NORMSDIST requires 1 numeric argument"},
+ // NORM.S.INV
+ "=NORM.S.INV()": {"#VALUE!", "NORM.S.INV requires 1 numeric argument"},
+ // NORMSINV
+ "=NORMSINV()": {"#VALUE!", "NORMSINV requires 1 numeric argument"},
+ // LARGE
+ "=LARGE()": {"#VALUE!", "LARGE requires 2 arguments"},
+ "=LARGE(A1:A5,0)": {"#NUM!", "k should be > 0"},
+ "=LARGE(A1:A5,6)": {"#NUM!", "k should be <= length of array"},
+ "=LARGE(A1:A5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // MAX
+ "=MAX()": {"#VALUE!", "MAX requires at least 1 argument"},
+ "=MAX(NA())": {"#N/A", "#N/A"},
+ // MAXA
+ "=MAXA()": {"#VALUE!", "MAXA requires at least 1 argument"},
+ "=MAXA(NA())": {"#N/A", "#N/A"},
+ // MAXIFS
+ "=MAXIFS()": {"#VALUE!", "MAXIFS requires at least 3 arguments"},
+ "=MAXIFS(F2:F4,A2:A4,\">0\",D2:D9)": {"#N/A", "#N/A"},
// MEDIAN
- "=MEDIAN()": "MEDIAN requires at least 1 argument",
- // Information functions
+ "=MEDIAN()": {"#VALUE!", "MEDIAN requires at least 1 argument"},
+ "=MEDIAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MEDIAN(D1:D2)": {"#NUM!", "#NUM!"},
+ // MIN
+ "=MIN()": {"#VALUE!", "MIN requires at least 1 argument"},
+ "=MIN(NA())": {"#N/A", "#N/A"},
+ // MINA
+ "=MINA()": {"#VALUE!", "MINA requires at least 1 argument"},
+ "=MINA(NA())": {"#N/A", "#N/A"},
+ // MINIFS
+ "=MINIFS()": {"#VALUE!", "MINIFS requires at least 3 arguments"},
+ "=MINIFS(F2:F4,A2:A4,\"<0\",D2:D9)": {"#N/A", "#N/A"},
+ // PEARSON
+ "=PEARSON()": {"#VALUE!", "PEARSON requires 2 arguments"},
+ "=PEARSON(A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=PEARSON(A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // PERCENTILE.EXC
+ "=PERCENTILE.EXC()": {"#VALUE!", "PERCENTILE.EXC requires 2 arguments"},
+ "=PERCENTILE.EXC(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTILE.EXC(A1:A4,-1)": {"#NUM!", "#NUM!"},
+ "=PERCENTILE.EXC(A1:A4,0)": {"#NUM!", "#NUM!"},
+ "=PERCENTILE.EXC(A1:A4,1)": {"#NUM!", "#NUM!"},
+ "=PERCENTILE.EXC(NA(),0.5)": {"#NUM!", "#NUM!"},
+ // PERCENTILE.INC
+ "=PERCENTILE.INC()": {"#VALUE!", "PERCENTILE.INC requires 2 arguments"},
+ // PERCENTILE
+ "=PERCENTILE()": {"#VALUE!", "PERCENTILE requires 2 arguments"},
+ "=PERCENTILE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTILE(0,-1)": {"#N/A", "#N/A"},
+ "=PERCENTILE(NA(),1)": {"#N/A", "#N/A"},
+ // PERCENTRANK.EXC
+ "=PERCENTRANK.EXC()": {"#VALUE!", "PERCENTRANK.EXC requires 2 or 3 arguments"},
+ "=PERCENTRANK.EXC(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK.EXC(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK.EXC(A1:B4,0,0)": {"#NUM!", "PERCENTRANK.EXC arguments significance should be > 1"},
+ "=PERCENTRANK.EXC(A1:B4,6)": {"#N/A", "#N/A"},
+ "=PERCENTRANK.EXC(NA(),1)": {"#N/A", "#N/A"},
+ // PERCENTRANK.INC
+ "=PERCENTRANK.INC()": {"#VALUE!", "PERCENTRANK.INC requires 2 or 3 arguments"},
+ "=PERCENTRANK.INC(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK.INC(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK.INC(A1:B4,0,0)": {"#NUM!", "PERCENTRANK.INC arguments significance should be > 1"},
+ "=PERCENTRANK.INC(A1:B4,6)": {"#N/A", "#N/A"},
+ "=PERCENTRANK.INC(NA(),1)": {"#N/A", "#N/A"},
+ // PERCENTRANK
+ "=PERCENTRANK()": {"#VALUE!", "PERCENTRANK requires 2 or 3 arguments"},
+ "=PERCENTRANK(A1:B4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK(A1:B4,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERCENTRANK(A1:B4,0,0)": {"#NUM!", "PERCENTRANK arguments significance should be > 1"},
+ "=PERCENTRANK(A1:B4,6)": {"#N/A", "#N/A"},
+ "=PERCENTRANK(NA(),1)": {"#N/A", "#N/A"},
+ // PERMUT
+ "=PERMUT()": {"#VALUE!", "PERMUT requires 2 numeric arguments"},
+ "=PERMUT(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERMUT(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERMUT(6,8)": {"#N/A", "#N/A"},
+ // PERMUTATIONA
+ "=PERMUTATIONA()": {"#VALUE!", "PERMUTATIONA requires 2 numeric arguments"},
+ "=PERMUTATIONA(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERMUTATIONA(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PERMUTATIONA(-1,0)": {"#N/A", "#N/A"},
+ "=PERMUTATIONA(0,-1)": {"#N/A", "#N/A"},
+ // PHI
+ "=PHI()": {"#VALUE!", "PHI requires 1 argument"},
+ "=PHI(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // QUARTILE
+ "=QUARTILE()": {"#VALUE!", "QUARTILE requires 2 arguments"},
+ "=QUARTILE(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=QUARTILE(A1:A4,-1)": {"#NUM!", "#NUM!"},
+ "=QUARTILE(A1:A4,5)": {"#NUM!", "#NUM!"},
+ // QUARTILE.EXC
+ "=QUARTILE.EXC()": {"#VALUE!", "QUARTILE.EXC requires 2 arguments"},
+ "=QUARTILE.EXC(A1:A4,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=QUARTILE.EXC(A1:A4,0)": {"#NUM!", "#NUM!"},
+ "=QUARTILE.EXC(A1:A4,4)": {"#NUM!", "#NUM!"},
+ // QUARTILE.INC
+ "=QUARTILE.INC()": {"#VALUE!", "QUARTILE.INC requires 2 arguments"},
+ // RANK
+ "=RANK()": {"#VALUE!", "RANK requires at least 2 arguments"},
+ "=RANK(1,A1:B5,0,0)": {"#VALUE!", "RANK requires at most 3 arguments"},
+ "=RANK(-1,A1:B5)": {"#N/A", "#N/A"},
+ "=RANK(\"\",A1:B5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RANK(1,A1:B5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // RANK.EQ
+ "=RANK.EQ()": {"#VALUE!", "RANK.EQ requires at least 2 arguments"},
+ "=RANK.EQ(1,A1:B5,0,0)": {"#VALUE!", "RANK.EQ requires at most 3 arguments"},
+ "=RANK.EQ(-1,A1:B5)": {"#N/A", "#N/A"},
+ "=RANK.EQ(\"\",A1:B5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RANK.EQ(1,A1:B5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // RSQ
+ "=RSQ()": {"#VALUE!", "RSQ requires 2 arguments"},
+ "=RSQ(A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=RSQ(A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // SKEW
+ "=SKEW()": {"#VALUE!", "SKEW requires at least 1 argument"},
+ "=SKEW(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SKEW(0)": {"#DIV/0!", "#DIV/0!"},
+ // SKEW.P
+ "=SKEW.P()": {"#VALUE!", "SKEW.P requires at least 1 argument"},
+ "=SKEW.P(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SKEW.P(0)": {"#DIV/0!", "#DIV/0!"},
+ // SLOPE
+ "=SLOPE()": {"#VALUE!", "SLOPE requires 2 arguments"},
+ "=SLOPE(A1:A2,B1:B1)": {"#N/A", "#N/A"},
+ "=SLOPE(A4,A4)": {"#DIV/0!", "#DIV/0!"},
+ // SMALL
+ "=SMALL()": {"#VALUE!", "SMALL requires 2 arguments"},
+ "=SMALL(A1:A5,0)": {"#NUM!", "k should be > 0"},
+ "=SMALL(A1:A5,6)": {"#NUM!", "k should be <= length of array"},
+ "=SMALL(A1:A5,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // STANDARDIZE
+ "=STANDARDIZE()": {"#VALUE!", "STANDARDIZE requires 3 arguments"},
+ "=STANDARDIZE(\"\",0,5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=STANDARDIZE(0,\"\",5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=STANDARDIZE(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=STANDARDIZE(0,0,0)": {"#N/A", "#N/A"},
+ // STDEVP
+ "=STDEVP()": {"#VALUE!", "STDEVP requires at least 1 argument"},
+ "=STDEVP(\"\")": {"#DIV/0!", "#DIV/0!"},
+ // STDEV.P
+ "=STDEV.P()": {"#VALUE!", "STDEV.P requires at least 1 argument"},
+ "=STDEV.P(\"\")": {"#DIV/0!", "#DIV/0!"},
+ // STDEVPA
+ "=STDEVPA()": {"#VALUE!", "STDEVPA requires at least 1 argument"},
+ "=STDEVPA(\"\")": {"#DIV/0!", "#DIV/0!"},
+ // T.DIST
+ "=T.DIST()": {"#VALUE!", "T.DIST requires 3 arguments"},
+ "=T.DIST(\"\",10,TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST(1,\"\",TRUE)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST(1,10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=T.DIST(1,0,TRUE)": {"#NUM!", "#NUM!"},
+ "=T.DIST(1,-1,FALSE)": {"#NUM!", "#NUM!"},
+ "=T.DIST(1,0,FALSE)": {"#DIV/0!", "#DIV/0!"},
+ // T.DIST.2T
+ "=T.DIST.2T()": {"#VALUE!", "T.DIST.2T requires 2 arguments"},
+ "=T.DIST.2T(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST.2T(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST.2T(-1,10)": {"#NUM!", "#NUM!"},
+ "=T.DIST.2T(1,0)": {"#NUM!", "#NUM!"},
+ // T.DIST.RT
+ "=T.DIST.RT()": {"#VALUE!", "T.DIST.RT requires 2 arguments"},
+ "=T.DIST.RT(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST.RT(1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.DIST.RT(1,0)": {"#NUM!", "#NUM!"},
+ // TDIST
+ "=TDIST()": {"#VALUE!", "TDIST requires 3 arguments"},
+ "=TDIST(\"\",10,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TDIST(1,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TDIST(1,10,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TDIST(-1,10,1)": {"#NUM!", "#NUM!"},
+ "=TDIST(1,0,1)": {"#NUM!", "#NUM!"},
+ "=TDIST(1,10,0)": {"#NUM!", "#NUM!"},
+ // T.INV
+ "=T.INV()": {"#VALUE!", "T.INV requires 2 arguments"},
+ "=T.INV(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.INV(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.INV(0,10)": {"#NUM!", "#NUM!"},
+ "=T.INV(1,10)": {"#NUM!", "#NUM!"},
+ "=T.INV(0.25,0.5)": {"#NUM!", "#NUM!"},
+ // T.INV.2T
+ "=T.INV.2T()": {"#VALUE!", "T.INV.2T requires 2 arguments"},
+ "=T.INV.2T(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.INV.2T(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=T.INV.2T(0,10)": {"#NUM!", "#NUM!"},
+ "=T.INV.2T(0.25,0.5)": {"#NUM!", "#NUM!"},
+ // TINV
+ "=TINV()": {"#VALUE!", "TINV requires 2 arguments"},
+ "=TINV(\"\",10)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TINV(0.25,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TINV(0,10)": {"#NUM!", "#NUM!"},
+ "=TINV(0.25,0.5)": {"#NUM!", "#NUM!"},
+ // TRIMMEAN
+ "=TRIMMEAN()": {"#VALUE!", "TRIMMEAN requires 2 arguments"},
+ "=TRIMMEAN(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TRIMMEAN(A1,1)": {"#NUM!", "#NUM!"},
+ "=TRIMMEAN(A1,-1)": {"#NUM!", "#NUM!"},
+ // VAR
+ "=VAR()": {"#VALUE!", "VAR requires at least 1 argument"},
+ // VARA
+ "=VARA()": {"#VALUE!", "VARA requires at least 1 argument"},
+ // VARP
+ "=VARP()": {"#VALUE!", "VARP requires at least 1 argument"},
+ "=VARP(\"\")": {"#DIV/0!", "#DIV/0!"},
+ // VAR.P
+ "=VAR.P()": {"#VALUE!", "VAR.P requires at least 1 argument"},
+ "=VAR.P(\"\")": {"#DIV/0!", "#DIV/0!"},
+ // VAR.S
+ "=VAR.S()": {"#VALUE!", "VAR.S requires at least 1 argument"},
+ // VARPA
+ "=VARPA()": {"#VALUE!", "VARPA requires at least 1 argument"},
+ // WEIBULL
+ "=WEIBULL()": {"#VALUE!", "WEIBULL requires 4 arguments"},
+ "=WEIBULL(\"\",1,1,FALSE)": {"#VALUE!", "#VALUE!"},
+ "=WEIBULL(1,0,1,FALSE)": {"#N/A", "#N/A"},
+ "=WEIBULL(1,1,-1,FALSE)": {"#N/A", "#N/A"},
+ // WEIBULL.DIST
+ "=WEIBULL.DIST()": {"#VALUE!", "WEIBULL.DIST requires 4 arguments"},
+ "=WEIBULL.DIST(\"\",1,1,FALSE)": {"#VALUE!", "#VALUE!"},
+ "=WEIBULL.DIST(1,0,1,FALSE)": {"#N/A", "#N/A"},
+ "=WEIBULL.DIST(1,1,-1,FALSE)": {"#N/A", "#N/A"},
+ // Z.TEST
+ "=Z.TEST(A1)": {"#VALUE!", "Z.TEST requires at least 2 arguments"},
+ "=Z.TEST(A1,0,0,0)": {"#VALUE!", "Z.TEST accepts at most 3 arguments"},
+ "=Z.TEST(H1,0)": {"#N/A", "#N/A"},
+ "=Z.TEST(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=Z.TEST(A1,1)": {"#DIV/0!", "#DIV/0!"},
+ "=Z.TEST(A1,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ZTEST
+ "=ZTEST(A1)": {"#VALUE!", "ZTEST requires at least 2 arguments"},
+ "=ZTEST(A1,0,0,0)": {"#VALUE!", "ZTEST accepts at most 3 arguments"},
+ "=ZTEST(H1,0)": {"#N/A", "#N/A"},
+ "=ZTEST(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ZTEST(A1,1)": {"#DIV/0!", "#DIV/0!"},
+ "=ZTEST(A1,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // Information Functions
+ // ERROR.TYPE
+ "=ERROR.TYPE()": {"#VALUE!", "ERROR.TYPE requires 1 argument"},
+ "=ERROR.TYPE(1)": {"#N/A", "#N/A"},
// ISBLANK
- "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument",
+ "=ISBLANK(A1,A2)": {"#VALUE!", "ISBLANK requires 1 argument"},
// ISERR
- "=ISERR()": "ISERR requires 1 argument",
+ "=ISERR()": {"#VALUE!", "ISERR requires 1 argument"},
// ISERROR
- "=ISERROR()": "ISERROR requires 1 argument",
+ "=ISERROR()": {"#VALUE!", "ISERROR requires 1 argument"},
// ISEVEN
- "=ISEVEN()": "ISEVEN requires 1 argument",
+ "=ISEVEN()": {"#VALUE!", "ISEVEN requires 1 argument"},
+ "=ISEVEN(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=ISEVEN(A1:A2)": {"#VALUE!", "#VALUE!"},
+ // ISFORMULA
+ "=ISFORMULA()": {"#VALUE!", "ISFORMULA requires 1 argument"},
+ // ISLOGICAL
+ "=ISLOGICAL()": {"#VALUE!", "ISLOGICAL requires 1 argument"},
// ISNA
- "=ISNA()": "ISNA requires 1 argument",
+ "=ISNA()": {"#VALUE!", "ISNA requires 1 argument"},
// ISNONTEXT
- "=ISNONTEXT()": "ISNONTEXT requires 1 argument",
+ "=ISNONTEXT()": {"#VALUE!", "ISNONTEXT requires 1 argument"},
// ISNUMBER
- "=ISNUMBER()": "ISNUMBER requires 1 argument",
+ "=ISNUMBER()": {"#VALUE!", "ISNUMBER requires 1 argument"},
// ISODD
- "=ISODD()": "ISODD requires 1 argument",
+ "=ISODD()": {"#VALUE!", "ISODD requires 1 argument"},
+ "=ISODD(\"text\")": {"#VALUE!", "#VALUE!"},
+ // ISREF
+ "=ISREF()": {"#VALUE!", "ISREF requires 1 argument"},
+ // ISTEXT
+ "=ISTEXT()": {"#VALUE!", "ISTEXT requires 1 argument"},
+ // N
+ "=N()": {"#VALUE!", "N requires 1 argument"},
+ "=N(NA())": {"#N/A", "#N/A"},
// NA
- "=NA(1)": "NA accepts no arguments",
+ "=NA()": {"#N/A", "#N/A"},
+ "=NA(1)": {"#VALUE!", "NA accepts no arguments"},
+ // SHEET
+ "=SHEET(\"\",\"\")": {"#VALUE!", "SHEET accepts at most 1 argument"},
+ "=SHEET(\"Sheet2\")": {"#N/A", "#N/A"},
+ // SHEETS
+ "=SHEETS(\"\",\"\")": {"#VALUE!", "SHEETS accepts at most 1 argument"},
+ "=SHEETS(\"Sheet1\")": {"#N/A", "#N/A"},
+ // TYPE
+ "=TYPE()": {"#VALUE!", "TYPE requires 1 argument"},
+ // T
+ "=T()": {"#VALUE!", "T requires 1 argument"},
+ "=T(NA())": {"#N/A", "#N/A"},
+ // Logical Functions
+ // AND
+ "=AND(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=AND(A1:B1)": {"#VALUE!", "#VALUE!"},
+ "=AND(\"1\",\"TRUE\",\"FALSE\")": {"#VALUE!", "#VALUE!"},
+ "=AND()": {"#VALUE!", "AND requires at least 1 argument"},
+ "=AND(1" + strings.Repeat(",1", 30) + ")": {"#VALUE!", "AND accepts at most 30 arguments"},
+ // FALSE
+ "=FALSE(A1)": {"#VALUE!", "FALSE takes no arguments"},
+ // IFERROR
+ "=IFERROR()": {"#VALUE!", "IFERROR requires 2 arguments"},
+ // IFNA
+ "=IFNA()": {"#VALUE!", "IFNA requires 2 arguments"},
+ // IFS
+ "=IFS()": {"#VALUE!", "IFS requires at least 2 arguments"},
+ "=IFS(FALSE,FALSE)": {"#N/A", "#N/A"},
+ // NOT
+ "=NOT()": {"#VALUE!", "NOT requires 1 argument"},
+ "=NOT(NOT())": {"#VALUE!", "NOT requires 1 argument"},
+ "=NOT(\"\")": {"#VALUE!", "NOT expects 1 boolean or numeric argument"},
+ // OR
+ "=OR(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=OR(\"1\",\"TRUE\",\"FALSE\")": {"#VALUE!", "#VALUE!"},
+ "=OR()": {"#VALUE!", "OR requires at least 1 argument"},
+ "=OR(1" + strings.Repeat(",1", 30) + ")": {"#VALUE!", "OR accepts at most 30 arguments"},
+ // SWITCH
+ "=SWITCH()": {"#VALUE!", "SWITCH requires at least 3 arguments"},
+ "=SWITCH(0,1,2)": {"#N/A", "#N/A"},
+ // TRUE
+ "=TRUE(A1)": {"#VALUE!", "TRUE takes no arguments"},
+ // XOR
+ "=XOR()": {"#VALUE!", "XOR requires at least 1 argument"},
+ "=XOR(\"1\")": {"#VALUE!", "#VALUE!"},
+ "=XOR(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=XOR(XOR(\"text\"))": {"#VALUE!", "#VALUE!"},
+ // Date and Time Functions
+ // DATE
+ "=DATE()": {"#VALUE!", "DATE requires 3 number arguments"},
+ "=DATE(\"text\",10,21)": {"#VALUE!", "DATE requires 3 number arguments"},
+ "=DATE(2020,\"text\",21)": {"#VALUE!", "DATE requires 3 number arguments"},
+ "=DATE(2020,10,\"text\")": {"#VALUE!", "DATE requires 3 number arguments"},
+ // DATEDIF
+ "=DATEDIF()": {"#VALUE!", "DATEDIF requires 3 number arguments"},
+ "=DATEDIF(\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DATEDIF(43891,43101,\"Y\")": {"#NUM!", "start_date > end_date"},
+ "=DATEDIF(43101,43891,\"x\")": {"#VALUE!", "DATEDIF has invalid unit"},
+ // DATEVALUE
+ "=DATEVALUE()": {"#VALUE!", "DATEVALUE requires 1 argument"},
+ "=DATEVALUE(\"01/01\")": {"#VALUE!", "#VALUE!"}, // valid in Excel, which uses years by the system date
+ "=DATEVALUE(\"1900-0-0\")": {"#VALUE!", "#VALUE!"},
+ // DAY
+ "=DAY()": {"#VALUE!", "DAY requires exactly 1 argument"},
+ "=DAY(-1)": {"#NUM!", "DAY only accepts positive argument"},
+ "=DAY(0,0)": {"#VALUE!", "DAY requires exactly 1 argument"},
+ "=DAY(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 9223372036854775808 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 9223372036854775808:00 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 00:9223372036854775808 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 9223372036854775808:00.0 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 0:1" + strings.Repeat("0", 309) + ".0 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 9223372036854775808:00:00 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 0:9223372036854775808:0 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 0:0:1" + strings.Repeat("0", 309) + " AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 0:61:0 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 0:00:60 AM\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 24:00:00\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 2020 00:00:10001\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"9223372036854775808/25/2020\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01/9223372036854775808/2020\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01/25/9223372036854775808\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01/25/10000\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01/25/100\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 9223372036854775808, 2020\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 9223372036854775808\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 10000\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"9223372036854775808-25-2020\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01-9223372036854775808-2020\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"01-25-9223372036854775808\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"1900-0-0\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"14-25-1900\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"3-January-9223372036854775808\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"9223372036854775808-January-1900\")": {"#VALUE!", "#VALUE!"},
+ "=DAY(\"0-January-1900\")": {"#VALUE!", "#VALUE!"},
+ // DAYS
+ "=DAYS()": {"#VALUE!", "DAYS requires 2 arguments"},
+ "=DAYS(\"\",0)": {"#VALUE!", "#VALUE!"},
+ "=DAYS(0,\"\")": {"#VALUE!", "#VALUE!"},
+ "=DAYS(NA(),0)": {"#VALUE!", "#VALUE!"},
+ "=DAYS(0,NA())": {"#VALUE!", "#VALUE!"},
+ // DAYS360
+ "=DAYS360(\"12/12/1999\")": {"#VALUE!", "DAYS360 requires at least 2 arguments"},
+ "=DAYS360(\"12/12/1999\", \"11/30/1999\",TRUE,\"\")": {"#VALUE!", "DAYS360 requires at most 3 arguments"},
+ "=DAYS360(\"12/12/1999\", \"11/30/1999\",\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=DAYS360(\"12/12/1999\", \"\")": {"#VALUE!", "#VALUE!"},
+ "=DAYS360(\"\", \"11/30/1999\")": {"#VALUE!", "#VALUE!"},
+ // EDATE
+ "=EDATE()": {"#VALUE!", "EDATE requires 2 arguments"},
+ "=EDATE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EDATE(-1,0)": {"#NUM!", "#NUM!"},
+ "=EDATE(\"\",0)": {"#VALUE!", "#VALUE!"},
+ "=EDATE(\"January 25, 100\",0)": {"#VALUE!", "#VALUE!"},
+ // EOMONTH
+ "=EOMONTH()": {"#VALUE!", "EOMONTH requires 2 arguments"},
+ "=EOMONTH(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EOMONTH(-1,0)": {"#NUM!", "#NUM!"},
+ "=EOMONTH(\"\",0)": {"#VALUE!", "#VALUE!"},
+ "=EOMONTH(\"January 25, 100\",0)": {"#VALUE!", "#VALUE!"},
+ // HOUR
+ "=HOUR()": {"#VALUE!", "HOUR requires exactly 1 argument"},
+ "=HOUR(-1)": {"#NUM!", "HOUR only accepts positive argument"},
+ "=HOUR(\"\")": {"#VALUE!", "#VALUE!"},
+ "=HOUR(\"25:10:55\")": {"#VALUE!", "#VALUE!"},
+ // ISOWEEKNUM
+ "=ISOWEEKNUM()": {"#VALUE!", "ISOWEEKNUM requires 1 argument"},
+ "=ISOWEEKNUM(\"\")": {"#VALUE!", "#VALUE!"},
+ "=ISOWEEKNUM(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ "=ISOWEEKNUM(-1)": {"#NUM!", "#NUM!"},
+ // MINUTE
+ "=MINUTE()": {"#VALUE!", "MINUTE requires exactly 1 argument"},
+ "=MINUTE(-1)": {"#NUM!", "MINUTE only accepts positive argument"},
+ "=MINUTE(\"\")": {"#VALUE!", "#VALUE!"},
+ "=MINUTE(\"13:60:55\")": {"#VALUE!", "#VALUE!"},
+ // MONTH
+ "=MONTH()": {"#VALUE!", "MONTH requires exactly 1 argument"},
+ "=MONTH(0,0)": {"#VALUE!", "MONTH requires exactly 1 argument"},
+ "=MONTH(-1)": {"#NUM!", "MONTH only accepts positive argument"},
+ "=MONTH(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=MONTH(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ // YEAR
+ "=YEAR()": {"#VALUE!", "YEAR requires exactly 1 argument"},
+ "=YEAR(0,0)": {"#VALUE!", "YEAR requires exactly 1 argument"},
+ "=YEAR(-1)": {"#NUM!", "YEAR only accepts positive argument"},
+ "=YEAR(\"text\")": {"#VALUE!", "#VALUE!"},
+ "=YEAR(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ // YEARFRAC
+ "=YEARFRAC()": {"#VALUE!", "YEARFRAC requires 3 or 4 arguments"},
+ "=YEARFRAC(42005,42094,5)": {"#NUM!", "invalid basis"},
+ "=YEARFRAC(\"\",42094,5)": {"#VALUE!", "#VALUE!"},
+ "=YEARFRAC(42005,\"\",5)": {"#VALUE!", "#VALUE!"},
+ "=YEARFRAC(42005,42094,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // NOW
+ "=NOW(A1)": {"#VALUE!", "NOW accepts no arguments"},
+ // SECOND
+ "=SECOND()": {"#VALUE!", "SECOND requires exactly 1 argument"},
+ "=SECOND(-1)": {"#NUM!", "SECOND only accepts positive argument"},
+ "=SECOND(\"\")": {"#VALUE!", "#VALUE!"},
+ "=SECOND(\"25:55\")": {"#VALUE!", "#VALUE!"},
+ // TIME
+ "=TIME()": {"#VALUE!", "TIME requires 3 number arguments"},
+ "=TIME(\"\",0,0)": {"#VALUE!", "TIME requires 3 number arguments"},
+ "=TIME(0,0,-1)": {"#NUM!", "#NUM!"},
+ // TIMEVALUE
+ "=TIMEVALUE()": {"#VALUE!", "TIMEVALUE requires exactly 1 argument"},
+ "=TIMEVALUE(1)": {"#VALUE!", "#VALUE!"},
+ "=TIMEVALUE(-1)": {"#VALUE!", "#VALUE!"},
+ "=TIMEVALUE(\"25:55\")": {"#VALUE!", "#VALUE!"},
+ // TODAY
+ "=TODAY(A1)": {"#VALUE!", "TODAY accepts no arguments"},
+ // WEEKDAY
+ "=WEEKDAY()": {"#VALUE!", "WEEKDAY requires at least 1 argument"},
+ "=WEEKDAY(0,1,0)": {"#VALUE!", "WEEKDAY allows at most 2 arguments"},
+ "=WEEKDAY(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=WEEKDAY(\"\",1)": {"#VALUE!", "#VALUE!"},
+ "=WEEKDAY(0,0)": {"#VALUE!", "#VALUE!"},
+ "=WEEKDAY(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ "=WEEKDAY(-1,1)": {"#NUM!", "#NUM!"},
+ // WEEKNUM
+ "=WEEKNUM()": {"#VALUE!", "WEEKNUM requires at least 1 argument"},
+ "=WEEKNUM(0,1,0)": {"#VALUE!", "WEEKNUM allows at most 2 arguments"},
+ "=WEEKNUM(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=WEEKNUM(\"\",1)": {"#VALUE!", "#VALUE!"},
+ "=WEEKNUM(\"January 25, 100\")": {"#VALUE!", "#VALUE!"},
+ "=WEEKNUM(0,0)": {"#NUM!", "#NUM!"},
+ "=WEEKNUM(-1,1)": {"#NUM!", "#NUM!"},
+ // Text Functions
+ // ARRAYTOTEXT
+ "=ARRAYTOTEXT()": {"#VALUE!", "ARRAYTOTEXT requires at least 1 argument"},
+ "=ARRAYTOTEXT(A1,0,0)": {"#VALUE!", "ARRAYTOTEXT allows at most 2 arguments"},
+ "=ARRAYTOTEXT(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ARRAYTOTEXT(A1,2)": {"#VALUE!", "#VALUE!"},
+ // CHAR
+ "=CHAR()": {"#VALUE!", "CHAR requires 1 argument"},
+ "=CHAR(-1)": {"#VALUE!", "#VALUE!"},
+ "=CHAR(256)": {"#VALUE!", "#VALUE!"},
+ "=CHAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // CLEAN
+ "=CLEAN()": {"#VALUE!", "CLEAN requires 1 argument"},
+ "=CLEAN(1,2)": {"#VALUE!", "CLEAN requires 1 argument"},
+ // CODE
+ "=CODE()": {"#VALUE!", "CODE requires 1 argument"},
+ "=CODE(1,2)": {"#VALUE!", "CODE requires 1 argument"},
+ // CONCAT
+ "=CONCAT(NA())": {"#N/A", "#N/A"},
+ "=CONCAT(1,1/0)": {"#DIV/0!", "#DIV/0!"},
+ // CONCATENATE
+ "=CONCATENATE(NA())": {"#N/A", "#N/A"},
+ "=CONCATENATE(1,1/0)": {"#DIV/0!", "#DIV/0!"},
+ // DBCS
+ "=DBCS(NA())": {"#N/A", "#N/A"},
+ "=DBCS()": {"#VALUE!", "DBCS requires 1 argument"},
+ // EXACT
+ "=EXACT()": {"#VALUE!", "EXACT requires 2 arguments"},
+ "=EXACT(1,2,3)": {"#VALUE!", "EXACT requires 2 arguments"},
+ // FIXED
+ "=FIXED()": {"#VALUE!", "FIXED requires at least 1 argument"},
+ "=FIXED(0,1,2,3)": {"#VALUE!", "FIXED allows at most 3 arguments"},
+ "=FIXED(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FIXED(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FIXED(0,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ // FIND
+ "=FIND()": {"#VALUE!", "FIND requires at least 2 arguments"},
+ "=FIND(1,2,3,4)": {"#VALUE!", "FIND allows at most 3 arguments"},
+ "=FIND(\"x\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=FIND(\"x\",\"x\",-1)": {"#VALUE!", "#VALUE!"},
+ "=FIND(\"x\",\"x\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // FINDB
+ "=FINDB()": {"#VALUE!", "FINDB requires at least 2 arguments"},
+ "=FINDB(1,2,3,4)": {"#VALUE!", "FINDB allows at most 3 arguments"},
+ "=FINDB(\"x\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=FINDB(\"x\",\"x\",-1)": {"#VALUE!", "#VALUE!"},
+ "=FINDB(\"x\",\"x\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // LEFT
+ "=LEFT()": {"#VALUE!", "LEFT requires at least 1 argument"},
+ "=LEFT(\"\",2,3)": {"#VALUE!", "LEFT allows at most 2 arguments"},
+ "=LEFT(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LEFT(\"\",-1)": {"#VALUE!", "#VALUE!"},
+ // LEFTB
+ "=LEFTB()": {"#VALUE!", "LEFTB requires at least 1 argument"},
+ "=LEFTB(\"\",2,3)": {"#VALUE!", "LEFTB allows at most 2 arguments"},
+ "=LEFTB(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=LEFTB(\"\",-1)": {"#VALUE!", "#VALUE!"},
+ // LEN
+ "=LEN()": {"#VALUE!", "LEN requires 1 string argument"},
+ // LENB
+ "=LENB()": {"#VALUE!", "LENB requires 1 string argument"},
+ // LOWER
+ "=LOWER()": {"#VALUE!", "LOWER requires 1 argument"},
+ "=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"},
+ // MID
+ "=MID()": {"#VALUE!", "MID requires 3 arguments"},
+ "=MID(\"\",0,1)": {"#VALUE!", "#VALUE!"},
+ "=MID(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
+ "=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // MIDB
+ "=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"},
+ "=MIDB(\"\",0,1)": {"#VALUE!", "#VALUE!"},
+ "=MIDB(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
+ "=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // PROPER
+ "=PROPER()": {"#VALUE!", "PROPER requires 1 argument"},
+ "=PROPER(1,2)": {"#VALUE!", "PROPER requires 1 argument"},
+ // REPLACE
+ "=REPLACE()": {"#VALUE!", "REPLACE requires 4 arguments"},
+ "=REPLACE(\"text\",0,4,\"string\")": {"#VALUE!", "#VALUE!"},
+ "=REPLACE(\"text\",\"\",0,\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=REPLACE(\"text\",1,\"\",\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // REPLACEB
+ "=REPLACEB()": {"#VALUE!", "REPLACEB requires 4 arguments"},
+ "=REPLACEB(\"text\",0,4,\"string\")": {"#VALUE!", "#VALUE!"},
+ "=REPLACEB(\"text\",\"\",0,\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=REPLACEB(\"text\",1,\"\",\"string\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // REPT
+ "=REPT()": {"#VALUE!", "REPT requires 2 arguments"},
+ "=REPT(INT(0),2)": {"#VALUE!", "REPT requires first argument to be a string"},
+ "=REPT(\"*\",\"*\")": {"#VALUE!", "REPT requires second argument to be a number"},
+ "=REPT(\"*\",-1)": {"#VALUE!", "REPT requires second argument to be >= 0"},
+ // RIGHT
+ "=RIGHT()": {"#VALUE!", "RIGHT requires at least 1 argument"},
+ "=RIGHT(\"\",2,3)": {"#VALUE!", "RIGHT allows at most 2 arguments"},
+ "=RIGHT(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RIGHT(\"\",-1)": {"#VALUE!", "#VALUE!"},
+ // RIGHTB
+ "=RIGHTB()": {"#VALUE!", "RIGHTB requires at least 1 argument"},
+ "=RIGHTB(\"\",2,3)": {"#VALUE!", "RIGHTB allows at most 2 arguments"},
+ "=RIGHTB(\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RIGHTB(\"\",-1)": {"#VALUE!", "#VALUE!"},
+ // SUBSTITUTE
+ "=SUBSTITUTE()": {"#VALUE!", "SUBSTITUTE requires 3 or 4 arguments"},
+ "=SUBSTITUTE(\"\",\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=SUBSTITUTE(\"\",\"\",\"\",0)": {"#VALUE!", "instance_num should be > 0"},
+ // TEXT
+ "=TEXT()": {"#VALUE!", "TEXT requires 2 arguments"},
+ "=TEXT(NA(),\"\")": {"#N/A", "#N/A"},
+ "=TEXT(0,NA())": {"#N/A", "#N/A"},
+ // TEXTAFTER
+ "=TEXTAFTER()": {"#VALUE!", "TEXTAFTER requires at least 2 arguments"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTAFTER accepts at most 6 arguments"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=TEXTAFTER(\"\",\"hood\")": {"#N/A", "#N/A"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"},
+ "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"},
+ // TEXTBEFORE
+ "=TEXTBEFORE()": {"#VALUE!", "TEXTBEFORE requires at least 2 arguments"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTBEFORE accepts at most 6 arguments"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=TEXTBEFORE(\"\",\"hood\")": {"#N/A", "#N/A"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"},
+ "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"},
+ // TEXTJOIN
+ "=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"},
+ "=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"},
+ "=TEXTJOIN(\"\",TRUE,NA())": {"#N/A", "#N/A"},
+ "=TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": {"#VALUE!", "TEXTJOIN accepts at most 252 arguments"},
+ "=TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": {"#VALUE!", "TEXTJOIN function exceeds 32767 characters"},
+ // TRIM
+ "=TRIM()": {"#VALUE!", "TRIM requires 1 argument"},
+ "=TRIM(1,2)": {"#VALUE!", "TRIM requires 1 argument"},
+ // UNICHAR
+ "=UNICHAR()": {"#VALUE!", "UNICHAR requires 1 argument"},
+ "=UNICHAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=UNICHAR(55296)": {"#VALUE!", "#VALUE!"},
+ "=UNICHAR(0)": {"#VALUE!", "#VALUE!"},
+ // UNICODE
+ "=UNICODE()": {"#VALUE!", "UNICODE requires 1 argument"},
+ "=UNICODE(\"\")": {"#VALUE!", "#VALUE!"},
+ // VALUE
+ "=VALUE()": {"#VALUE!", "VALUE requires 1 argument"},
+ "=VALUE(\"\")": {"#VALUE!", "#VALUE!"},
+ // VALUETOTEXT
+ "=VALUETOTEXT()": {"#VALUE!", "VALUETOTEXT requires at least 1 argument"},
+ "=VALUETOTEXT(A1,0,0)": {"#VALUE!", "VALUETOTEXT allows at most 2 arguments"},
+ "=VALUETOTEXT(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VALUETOTEXT(A1,2)": {"#VALUE!", "#VALUE!"},
+ // UPPER
+ "=UPPER()": {"#VALUE!", "UPPER requires 1 argument"},
+ "=UPPER(1,2)": {"#VALUE!", "UPPER requires 1 argument"},
+ // Conditional Functions
+ // IF
+ "=IF()": {"#VALUE!", "IF requires at least 1 argument"},
+ "=IF(0,1,2,3)": {"#VALUE!", "IF accepts at most 3 arguments"},
+ "=IF(D1,1,2)": {"#VALUE!", "strconv.ParseBool: parsing \"Month\": invalid syntax"},
+ // Excel Lookup and Reference Functions
+ // ADDRESS
+ "=ADDRESS()": {"#VALUE!", "ADDRESS requires at least 2 arguments"},
+ "=ADDRESS(1,1,1,TRUE,\"Sheet1\",0)": {"#VALUE!", "ADDRESS requires at most 5 arguments"},
+ "=ADDRESS(\"\",1,1,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1,\"\",1,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1,1,\"\",TRUE)": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1,1,1,\"\")": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1,1,0,TRUE)": {"#NUM!", "#NUM!"},
+ "=ADDRESS(1,16385,2,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1,16385,3,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=ADDRESS(1048577,1,1,TRUE)": {"#VALUE!", "#VALUE!"},
+ // CHOOSE
+ "=CHOOSE()": {"#VALUE!", "CHOOSE requires 2 arguments"},
+ "=CHOOSE(\"index_num\",0)": {"#VALUE!", "CHOOSE requires first argument of type number"},
+ "=CHOOSE(2,0)": {"#VALUE!", "index_num should be <= to the number of values"},
+ "=CHOOSE(1,NA())": {"#N/A", "#N/A"},
+ // COLUMN
+ "=COLUMN(1,2)": {"#VALUE!", "COLUMN requires at most 1 argument"},
+ "=COLUMN(\"\")": {"#VALUE!", "invalid reference"},
+ "=COLUMN(Sheet1)": {"#NAME?", "invalid reference"},
+ "=COLUMN(Sheet1!A1!B1)": {"#NAME?", "invalid reference"},
+ "=COLUMN(Sheet1!A1:Sheet2!A2)": {"#NAME?", "invalid reference"},
+ "=COLUMN(Sheet1!A1:1A)": {"#NAME?", "invalid reference"},
+ // COLUMNS
+ "=COLUMNS()": {"#VALUE!", "COLUMNS requires 1 argument"},
+ "=COLUMNS(1)": {"#VALUE!", "invalid reference"},
+ "=COLUMNS(\"\")": {"#VALUE!", "invalid reference"},
+ "=COLUMNS(Sheet1)": {"#NAME?", "invalid reference"},
+ "=COLUMNS(Sheet1!A1!B1)": {"#NAME?", "invalid reference"},
+ "=COLUMNS(Sheet1!Sheet1)": {"#NAME?", "invalid reference"},
+ // FORMULATEXT
+ "=FORMULATEXT()": {"#VALUE!", "FORMULATEXT requires 1 argument"},
+ "=FORMULATEXT(1)": {"#VALUE!", "#VALUE!"},
+ // HLOOKUP
+ "=HLOOKUP()": {"#VALUE!", "HLOOKUP requires at least 3 arguments"},
+ "=HLOOKUP(D2,D1,1,FALSE)": {"#VALUE!", "HLOOKUP requires second argument of table array"},
+ "=HLOOKUP(D2,D:D,FALSE,FALSE)": {"#VALUE!", "HLOOKUP requires numeric row argument"},
+ "=HLOOKUP(D2,D:D,1,FALSE,FALSE)": {"#VALUE!", "HLOOKUP requires at most 4 arguments"},
+ "=HLOOKUP(D2,D:D,1,2)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(D2,D10:D10,1,FALSE)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(D2,D2:D3,4,FALSE)": {"#N/A", "HLOOKUP has invalid row index"},
+ "=HLOOKUP(D2,C:C,1,FALSE)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(ISNUMBER(1),F3:F9,1)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(INT(1),E2:E9,1)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(MUNIT(2),MUNIT(3),1)": {"#N/A", "HLOOKUP no result found"},
+ "=HLOOKUP(A1:B2,B2:B3,1)": {"#N/A", "HLOOKUP no result found"},
+ // MATCH
+ "=MATCH()": {"#VALUE!", "MATCH requires 1 or 2 arguments"},
+ "=MATCH(0,A1:A1,0,0)": {"#VALUE!", "MATCH requires 1 or 2 arguments"},
+ "=MATCH(0,A1:A1,\"x\")": {"#VALUE!", "MATCH requires numeric match_type argument"},
+ "=MATCH(0,A1)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"},
+ "=MATCH(0,A1:B2)": {"#N/A", "MATCH arguments lookup_array should be one-dimensional array"},
+ "=MATCH(0,A1:B1)": {"#N/A", "#N/A"},
+ // TRANSPOSE
+ "=TRANSPOSE()": {"#VALUE!", "TRANSPOSE requires 1 argument"},
+ // HYPERLINK
+ "=HYPERLINK()": {"#VALUE!", "HYPERLINK requires at least 1 argument"},
+ "=HYPERLINK(\"https://github.com/xuri/excelize\",\"Excelize\",\"\")": {"#VALUE!", "HYPERLINK allows at most 2 arguments"},
+ // VLOOKUP
+ "=VLOOKUP()": {"#VALUE!", "VLOOKUP requires at least 3 arguments"},
+ "=VLOOKUP(D2,D1,1,FALSE)": {"#VALUE!", "VLOOKUP requires second argument of table array"},
+ "=VLOOKUP(D2,D:D,FALSE,FALSE)": {"#VALUE!", "VLOOKUP requires numeric col argument"},
+ "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": {"#VALUE!", "VLOOKUP requires at most 4 arguments"},
+ "=VLOOKUP(D2,D10:D10,1,FALSE)": {"#N/A", "VLOOKUP no result found"},
+ "=VLOOKUP(D2,D:D,2,FALSE)": {"#N/A", "VLOOKUP has invalid column index"},
+ "=VLOOKUP(D2,C:C,1,FALSE)": {"#N/A", "VLOOKUP no result found"},
+ "=VLOOKUP(ISNUMBER(1),F3:F9,1)": {"#N/A", "VLOOKUP no result found"},
+ "=VLOOKUP(INT(1),E2:E9,1)": {"#N/A", "VLOOKUP no result found"},
+ "=VLOOKUP(MUNIT(2),MUNIT(3),1)": {"#N/A", "VLOOKUP no result found"},
+ "=VLOOKUP(1,G1:H2,1,FALSE)": {"#N/A", "VLOOKUP no result found"},
+ // INDEX
+ "=INDEX()": {"#VALUE!", "INDEX requires 2 or 3 arguments"},
+ "=INDEX(A1,2)": {"#REF!", "INDEX row_num out of range"},
+ "=INDEX(A1,0,2)": {"#REF!", "INDEX col_num out of range"},
+ "=INDEX(A1:A1,2)": {"#REF!", "INDEX row_num out of range"},
+ "=INDEX(A1:A1,0,2)": {"#REF!", "INDEX col_num out of range"},
+ "=INDEX(A1:B2,2,3)": {"#REF!", "INDEX col_num out of range"},
+ "=INDEX(A1:A2,0,0)": {"#VALUE!", "#VALUE!"},
+ "=INDEX(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=INDEX(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // INDIRECT
+ "=INDIRECT()": {"#VALUE!", "INDIRECT requires 1 or 2 arguments"},
+ "=INDIRECT(\"E\"&1,TRUE,1)": {"#VALUE!", "INDIRECT requires 1 or 2 arguments"},
+ "=INDIRECT(\"R1048577C1\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=INDIRECT(\"E1048577\")": {"#REF!", "#REF!"},
+ "=INDIRECT(\"R1048577C1\",FALSE)": {"#REF!", "#REF!"},
+ "=INDIRECT(\"R1C16385\",FALSE)": {"#REF!", "#REF!"},
+ "=INDIRECT(\"\",FALSE)": {"#REF!", "#REF!"},
+ "=INDIRECT(\"R C1\",FALSE)": {"#REF!", "#REF!"},
+ "=INDIRECT(\"R1C \",FALSE)": {"#REF!", "#REF!"},
+ "=INDIRECT(\"R1C1:R2C \",FALSE)": {"#REF!", "#REF!"},
+ // LOOKUP
+ "=LOOKUP()": {"#VALUE!", "LOOKUP requires at least 2 arguments"},
+ "=LOOKUP(D2,D1,D2)": {"#VALUE!", "LOOKUP requires second argument of table array"},
+ "=LOOKUP(D2,D1,D2,FALSE)": {"#VALUE!", "LOOKUP requires at most 3 arguments"},
+ "=LOOKUP(1,MUNIT(0))": {"#VALUE!", "LOOKUP requires not empty range as second argument"},
+ "=LOOKUP(D1,MUNIT(1),MUNIT(1))": {"#N/A", "LOOKUP no result found"},
+ // ROW
+ "=ROW(1,2)": {"#VALUE!", "ROW requires at most 1 argument"},
+ "=ROW(\"\")": {"#VALUE!", "invalid reference"},
+ "=ROW(Sheet1)": {"#NAME?", "invalid reference"},
+ "=ROW(Sheet1!A1!B1)": {"#NAME?", "invalid reference"},
+ // ROWS
+ "=ROWS()": {"#VALUE!", "ROWS requires 1 argument"},
+ "=ROWS(1)": {"#VALUE!", "invalid reference"},
+ "=ROWS(\"\")": {"#VALUE!", "invalid reference"},
+ "=ROWS(Sheet1)": {"#NAME?", "invalid reference"},
+ "=ROWS(Sheet1!A1!B1)": {"#NAME?", "invalid reference"},
+ "=ROWS(Sheet1!Sheet1)": {"#NAME?", "invalid reference"},
+ // Web Functions
+ // ENCODEURL
+ "=ENCODEURL()": {"#VALUE!", "ENCODEURL requires 1 argument"},
+ // Financial Functions
+ // ACCRINT
+ "=ACCRINT()": {"#VALUE!", "ACCRINT requires at least 6 arguments"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE,0)": {"#VALUE!", "ACCRINT allows at most 8 arguments"},
+ "=ACCRINT(\"\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"},
+ "=ACCRINT(\"01/01/2012\",\"\",\"12/31/2013\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"\",8%,10000,4,1,FALSE)": {"#VALUE!", "#VALUE!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",\"\",10000,4,1,FALSE)": {"#NUM!", "#NUM!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,\"\",4,1,FALSE)": {"#NUM!", "#NUM!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,3)": {"#NUM!", "#NUM!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,\"\",1,FALSE)": {"#NUM!", "#NUM!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,\"\",FALSE)": {"#NUM!", "#NUM!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,1,\"\")": {"#VALUE!", "#VALUE!"},
+ "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,5,FALSE)": {"#NUM!", "invalid basis"},
+ // ACCRINTM
+ "=ACCRINTM()": {"#VALUE!", "ACCRINTM requires 4 or 5 arguments"},
+ "=ACCRINTM(\"\",\"01/01/2012\",8%,10000)": {"#VALUE!", "#VALUE!"},
+ "=ACCRINTM(\"01/01/2012\",\"\",8%,10000)": {"#VALUE!", "#VALUE!"},
+ "=ACCRINTM(\"12/31/2012\",\"01/01/2012\",8%,10000)": {"#NUM!", "#NUM!"},
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",\"\",10000)": {"#NUM!", "#NUM!"},
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,\"\",10000)": {"#NUM!", "#NUM!"},
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,-1,10000)": {"#NUM!", "#NUM!"},
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,\"\")": {"#NUM!", "#NUM!"},
+ "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,5)": {"#NUM!", "invalid basis"},
+ // AMORDEGRC
+ "=AMORDEGRC()": {"#VALUE!", "AMORDEGRC requires 6 or 7 arguments"},
+ "=AMORDEGRC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORDEGRC requires cost to be number argument"},
+ "=AMORDEGRC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORDEGRC requires cost >= 0"},
+ "=AMORDEGRC(150,\"\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "#VALUE!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"\",20,1,20%)": {"#VALUE!", "#VALUE!"},
+ "=AMORDEGRC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": {"#NUM!", "#NUM!"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,50%)": {"#NUM!", "AMORDEGRC requires rate to be < 0.5"},
+ "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": {"#NUM!", "invalid basis"},
+ // AMORLINC
+ "=AMORLINC()": {"#VALUE!", "AMORLINC requires 6 or 7 arguments"},
+ "=AMORLINC(\"\",\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORLINC requires cost to be number argument"},
+ "=AMORLINC(-1,\"01/01/2015\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "AMORLINC requires cost >= 0"},
+ "=AMORLINC(150,\"\",\"09/30/2015\",20,1,20%)": {"#VALUE!", "#VALUE!"},
+ "=AMORLINC(150,\"01/01/2015\",\"\",20,1,20%)": {"#VALUE!", "#VALUE!"},
+ "=AMORLINC(150,\"09/30/2015\",\"01/01/2015\",20,1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",\"\",1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",-1,1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,\"\",20%)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,-1,20%)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,\"\")": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,-1)": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,\"\")": {"#NUM!", "#NUM!"},
+ "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,5)": {"#NUM!", "invalid basis"},
+ // COUPDAYBS
+ "=COUPDAYBS()": {"#VALUE!", "COUPDAYBS requires 3 or 4 arguments"},
+ "=COUPDAYBS(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYBS(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPDAYBS(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYBS requires maturity > settlement"},
+ // COUPDAYS
+ "=COUPDAYS()": {"#VALUE!", "COUPDAYS requires 3 or 4 arguments"},
+ "=COUPDAYS(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYS(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPDAYS(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYS requires maturity > settlement"},
+ // COUPDAYSNC
+ "=COUPDAYSNC()": {"#VALUE!", "COUPDAYSNC requires 3 or 4 arguments"},
+ "=COUPDAYSNC(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYSNC(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPDAYSNC(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPDAYSNC requires maturity > settlement"},
+ // COUPNCD
+ "=COUPNCD()": {"#VALUE!", "COUPNCD requires 3 or 4 arguments"},
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPNCD requires 3 or 4 arguments"},
+ "=COUPNCD(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPNCD(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPNCD(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"},
+ "=COUPNCD(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPNCD requires maturity > settlement"},
+ // COUPNUM
+ "=COUPNUM()": {"#VALUE!", "COUPNUM requires 3 or 4 arguments"},
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPNUM requires 3 or 4 arguments"},
+ "=COUPNUM(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPNUM(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPNUM(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"},
+ "=COUPNUM(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPNUM requires maturity > settlement"},
+ // COUPPCD
+ "=COUPPCD()": {"#VALUE!", "COUPPCD requires 3 or 4 arguments"},
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": {"#VALUE!", "COUPPCD requires 3 or 4 arguments"},
+ "=COUPPCD(\"\",\"10/25/2012\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPPCD(\"01/01/2011\",\"\",4)": {"#VALUE!", "#VALUE!"},
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,\"\")": {"#NUM!", "#NUM!"},
+ "=COUPPCD(\"01/01/2011\",\"10/25/2012\",3)": {"#NUM!", "#NUM!"},
+ "=COUPPCD(\"10/25/2012\",\"01/01/2011\",4)": {"#NUM!", "COUPPCD requires maturity > settlement"},
+ // CUMIPMT
+ "=CUMIPMT()": {"#VALUE!", "CUMIPMT requires 6 arguments"},
+ "=CUMIPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=CUMIPMT(0,0,0,-1,0,0)": {"#N/A", "#N/A"},
+ "=CUMIPMT(0,0,0,1,0,0)": {"#N/A", "#N/A"},
+ "=CUMIPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMIPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMIPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMIPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMIPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMIPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // CUMPRINC
+ "=CUMPRINC()": {"#VALUE!", "CUMPRINC requires 6 arguments"},
+ "=CUMPRINC(0,0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=CUMPRINC(0,0,0,-1,0,0)": {"#N/A", "#N/A"},
+ "=CUMPRINC(0,0,0,1,0,0)": {"#N/A", "#N/A"},
+ "=CUMPRINC(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMPRINC(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMPRINC(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMPRINC(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMPRINC(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=CUMPRINC(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // DB
+ "=DB()": {"#VALUE!", "DB requires at least 4 arguments"},
+ "=DB(0,0,0,0,0,0)": {"#VALUE!", "DB allows at most 5 arguments"},
+ "=DB(-1,0,0,0)": {"#N/A", "#N/A"},
+ "=DB(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DB(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DB(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DB(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DB(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // DDB
+ "=DDB()": {"#VALUE!", "DDB requires at least 4 arguments"},
+ "=DDB(0,0,0,0,0,0)": {"#VALUE!", "DDB allows at most 5 arguments"},
+ "=DDB(-1,0,0,0)": {"#N/A", "#N/A"},
+ "=DDB(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DDB(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DDB(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DDB(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DDB(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // DISC
+ "=DISC()": {"#VALUE!", "DISC requires 4 or 5 arguments"},
+ "=DISC(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=DISC(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"},
+ "=DISC(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "DISC requires maturity > settlement"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "DISC requires pr > 0"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "DISC requires redemption > 0"},
+ "=DISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"},
+ // DOLLAR
+ "DOLLAR()": {"#VALUE!", "DOLLAR requires at least 1 argument"},
+ "DOLLAR(0,0,0)": {"#VALUE!", "DOLLAR requires 1 or 2 arguments"},
+ "DOLLAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "DOLLAR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "DOLLAR(1,200)": {"#VALUE!", "decimal value should be less than 128"},
+ // DOLLARDE
+ "=DOLLARDE()": {"#VALUE!", "DOLLARDE requires 2 arguments"},
+ "=DOLLARDE(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DOLLARDE(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DOLLARDE(0,-1)": {"#NUM!", "#NUM!"},
+ "=DOLLARDE(0,0)": {"#DIV/0!", "#DIV/0!"},
+ // DOLLARFR
+ "=DOLLARFR()": {"#VALUE!", "DOLLARFR requires 2 arguments"},
+ "=DOLLARFR(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DOLLARFR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DOLLARFR(0,-1)": {"#NUM!", "#NUM!"},
+ "=DOLLARFR(0,0)": {"#DIV/0!", "#DIV/0!"},
+ // DURATION
+ "=DURATION()": {"#VALUE!", "DURATION requires 5 or 6 arguments"},
+ "=DURATION(\"\",\"03/31/2025\",10%,8%,4)": {"#VALUE!", "#VALUE!"},
+ "=DURATION(\"04/01/2015\",\"\",10%,8%,4)": {"#VALUE!", "#VALUE!"},
+ "=DURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": {"#NUM!", "DURATION requires maturity > settlement"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": {"#NUM!", "DURATION requires coupon >= 0"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": {"#NUM!", "DURATION requires yld >= 0"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": {"#NUM!", "#NUM!"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": {"#NUM!", "#NUM!"},
+ "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": {"#NUM!", "invalid basis"},
+ // EFFECT
+ "=EFFECT()": {"#VALUE!", "EFFECT requires 2 arguments"},
+ "=EFFECT(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EFFECT(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EFFECT(0,0)": {"#NUM!", "#NUM!"},
+ "=EFFECT(1,0)": {"#NUM!", "#NUM!"},
+ // EUROCONVERT
+ "=EUROCONVERT()": {"#VALUE!", "EUROCONVERT requires at least 3 arguments"},
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,3,1)": {"#VALUE!", "EUROCONVERT allows at most 5 arguments"},
+ "=EUROCONVERT(\"\",\"FRF\",\"DEM\",TRUE,3)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",\"\",3)": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=EUROCONVERT(1.47,\"FRF\",\"DEM\",TRUE,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=EUROCONVERT(1.47,\"\",\"DEM\")": {"#VALUE!", "#VALUE!"},
+ "=EUROCONVERT(1.47,\"FRF\",\"\",TRUE,3)": {"#VALUE!", "#VALUE!"},
+ // FV
+ "=FV()": {"#VALUE!", "FV requires at least 3 arguments"},
+ "=FV(0,0,0,0,0,0,0)": {"#VALUE!", "FV allows at most 5 arguments"},
+ "=FV(0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=FV(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FV(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FV(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FV(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FV(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // FVSCHEDULE
+ "=FVSCHEDULE()": {"#VALUE!", "FVSCHEDULE requires 2 arguments"},
+ "=FVSCHEDULE(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=FVSCHEDULE(0,\"x\")": {"#VALUE!", "strconv.ParseFloat: parsing \"x\": invalid syntax"},
+ // INTRATE
+ "=INTRATE()": {"#VALUE!", "INTRATE requires 4 or 5 arguments"},
+ "=INTRATE(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=INTRATE(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"},
+ "=INTRATE(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "INTRATE requires maturity > settlement"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "INTRATE requires investment > 0"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "INTRATE requires redemption > 0"},
+ "=INTRATE(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"},
+ // IPMT
+ "=IPMT()": {"#VALUE!", "IPMT requires at least 4 arguments"},
+ "=IPMT(0,0,0,0,0,0,0)": {"#VALUE!", "IPMT allows at most 6 arguments"},
+ "=IPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=IPMT(0,-1,0,0,0,0)": {"#N/A", "#N/A"},
+ "=IPMT(0,1,0,0,0,0)": {"#N/A", "#N/A"},
+ "=IPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ISPMT
+ "=ISPMT()": {"#VALUE!", "ISPMT requires 4 arguments"},
+ "=ISPMT(\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ISPMT(0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ISPMT(0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ISPMT(0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // MDURATION
+ "=MDURATION()": {"#VALUE!", "MDURATION requires 5 or 6 arguments"},
+ "=MDURATION(\"\",\"03/31/2025\",10%,8%,4)": {"#VALUE!", "#VALUE!"},
+ "=MDURATION(\"04/01/2015\",\"\",10%,8%,4)": {"#VALUE!", "#VALUE!"},
+ "=MDURATION(\"03/31/2025\",\"04/01/2015\",10%,8%,4)": {"#NUM!", "MDURATION requires maturity > settlement"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",-1,8%,4)": {"#NUM!", "MDURATION requires coupon >= 0"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,-1,4)": {"#NUM!", "MDURATION requires yld >= 0"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",\"\",8%,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,3)": {"#NUM!", "#NUM!"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,\"\")": {"#NUM!", "#NUM!"},
+ "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4,5)": {"#NUM!", "invalid basis"},
+ // NOMINAL
+ "=NOMINAL()": {"#VALUE!", "NOMINAL requires 2 arguments"},
+ "=NOMINAL(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NOMINAL(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NOMINAL(0,0)": {"#NUM!", "#NUM!"},
+ "=NOMINAL(1,0)": {"#NUM!", "#NUM!"},
+ // NPER
+ "=NPER()": {"#VALUE!", "NPER requires at least 3 arguments"},
+ "=NPER(0,0,0,0,0,0)": {"#VALUE!", "NPER allows at most 5 arguments"},
+ "=NPER(0,0,0)": {"#NUM!", "#NUM!"},
+ "=NPER(0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=NPER(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NPER(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NPER(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NPER(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=NPER(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // NPV
+ "=NPV()": {"#VALUE!", "NPV requires at least 2 arguments"},
+ "=NPV(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // ODDFPRICE
+ "=ODDFPRICE()": {"#VALUE!", "ODDFPRICE requires 8 or 9 arguments"},
+ "=ODDFPRICE(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFPRICE(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires settlement > issue"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires first_coupon > settlement"},
+ "=ODDFPRICE(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires maturity > first_coupon"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": {"#NUM!", "ODDFPRICE requires rate >= 0"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,-1,100,2)": {"#NUM!", "ODDFPRICE requires yld >= 0"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": {"#NUM!", "ODDFPRICE requires redemption > 0"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": {"#NUM!", "#NUM!"},
+ "=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"},
+ // ODDFYIELD
+ "=ODDFYIELD()": {"#VALUE!", "ODDFYIELD requires 8 or 9 arguments"},
+ "=ODDFYIELD(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFYIELD(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires settlement > issue"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires first_coupon > settlement"},
+ "=ODDFYIELD(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires maturity > first_coupon"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires rate >= 0"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,0,100,2)": {"#NUM!", "ODDFYIELD requires pr > 0"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": {"#NUM!", "ODDFYIELD requires redemption > 0"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": {"#NUM!", "#NUM!"},
+ "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"},
+ // ODDLPRICE
+ "=ODDLPRICE()": {"#VALUE!", "ODDLPRICE requires 7 or 8 arguments"},
+ "=ODDLPRICE(\"\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLPRICE(\"02/01/2017\",\"\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLPRICE requires settlement > last_interest"},
+ "=ODDLPRICE(\"06/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLPRICE requires maturity > settlement"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",-1,3.5%,100,2)": {"#NUM!", "ODDLPRICE requires rate >= 0"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,-1,100,2)": {"#NUM!", "ODDLPRICE requires yld >= 0"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,0,2)": {"#NUM!", "ODDLPRICE requires redemption > 0"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"},
+ "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"},
+ // ODDLYIELD
+ "=ODDLYIELD()": {"#VALUE!", "ODDLYIELD requires 7 or 8 arguments"},
+ "=ODDLYIELD(\"\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLYIELD(\"02/01/2017\",\"\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLYIELD requires settlement > last_interest"},
+ "=ODDLYIELD(\"06/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLYIELD requires maturity > settlement"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",-1,3.5%,100,2)": {"#NUM!", "ODDLYIELD requires rate >= 0"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,0,100,2)": {"#NUM!", "ODDLYIELD requires pr > 0"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,0,2)": {"#NUM!", "ODDLYIELD requires redemption > 0"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"},
+ "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"},
+ // PDURATION
+ "=PDURATION()": {"#VALUE!", "PDURATION requires 3 arguments"},
+ "=PDURATION(\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PDURATION(0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PDURATION(0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PDURATION(0,0,0)": {"#NUM!", "#NUM!"},
+ // PMT
+ "=PMT()": {"#VALUE!", "PMT requires at least 3 arguments"},
+ "=PMT(0,0,0,0,0,0)": {"#VALUE!", "PMT allows at most 5 arguments"},
+ "=PMT(0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=PMT(\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PMT(0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PMT(0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PMT(0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PMT(0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // PRICE
+ "=PRICE()": {"#VALUE!", "PRICE requires 6 or 7 arguments"},
+ "=PRICE(\"\",\"02/01/2020\",12%,10%,100,2,4)": {"#VALUE!", "#VALUE!"},
+ "=PRICE(\"04/01/2012\",\"\",12%,10%,100,2,4)": {"#VALUE!", "#VALUE!"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",\"\",10%,100,2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,\"\",100,2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,\"\",2,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",-1,10%,100,2,4)": {"#NUM!", "PRICE requires rate >= 0"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,-1,100,2,4)": {"#NUM!", "PRICE requires yld >= 0"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,0,2,4)": {"#NUM!", "PRICE requires redemption > 0"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,\"\")": {"#NUM!", "#NUM!"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,3,4)": {"#NUM!", "#NUM!"},
+ "=PRICE(\"04/01/2012\",\"02/01/2020\",12%,10%,100,2,5)": {"#NUM!", "invalid basis"},
+ // PPMT
+ "=PPMT()": {"#VALUE!", "PPMT requires at least 4 arguments"},
+ "=PPMT(0,0,0,0,0,0,0)": {"#VALUE!", "PPMT allows at most 6 arguments"},
+ "=PPMT(0,0,0,0,0,2)": {"#N/A", "#N/A"},
+ "=PPMT(0,-1,0,0,0,0)": {"#N/A", "#N/A"},
+ "=PPMT(0,1,0,0,0,0)": {"#N/A", "#N/A"},
+ "=PPMT(\"\",0,0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PPMT(0,\"\",0,0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PPMT(0,0,\"\",0,0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PPMT(0,0,0,\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PPMT(0,0,0,0,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PPMT(0,0,0,0,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // PRICEDISC
+ "=PRICEDISC()": {"#VALUE!", "PRICEDISC requires 4 or 5 arguments"},
+ "=PRICEDISC(\"\",\"03/31/2021\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=PRICEDISC(\"04/01/2016\",\"\",95,100)": {"#VALUE!", "#VALUE!"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",\"\",100)": {"#VALUE!", "#VALUE!"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,\"\")": {"#VALUE!", "#VALUE!"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,\"\")": {"#NUM!", "#NUM!"},
+ "=PRICEDISC(\"03/31/2021\",\"04/01/2016\",95,100)": {"#NUM!", "PRICEDISC requires maturity > settlement"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",0,100)": {"#NUM!", "PRICEDISC requires discount > 0"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,0)": {"#NUM!", "PRICEDISC requires redemption > 0"},
+ "=PRICEDISC(\"04/01/2016\",\"03/31/2021\",95,100,5)": {"#NUM!", "invalid basis"},
+ // PRICEMAT
+ "=PRICEMAT()": {"#VALUE!", "PRICEMAT requires 5 or 6 arguments"},
+ "=PRICEMAT(\"\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"},
+ "=PRICEMAT(\"04/01/2017\",\"\",\"01/01/2017\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"\",4.5%,2.5%)": {"#VALUE!", "#VALUE!"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,\"\")": {"#NUM!", "#NUM!"},
+ "=PRICEMAT(\"03/31/2021\",\"04/01/2017\",\"01/01/2017\",4.5%,2.5%)": {"#NUM!", "PRICEMAT requires maturity > settlement"},
+ "=PRICEMAT(\"01/01/2017\",\"03/31/2021\",\"04/01/2017\",4.5%,2.5%)": {"#NUM!", "PRICEMAT requires settlement > issue"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",-1,2.5%)": {"#NUM!", "PRICEMAT requires rate >= 0"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,-1)": {"#NUM!", "PRICEMAT requires yld >= 0"},
+ "=PRICEMAT(\"04/01/2017\",\"03/31/2021\",\"01/01/2017\",4.5%,2.5%,5)": {"#NUM!", "invalid basis"},
+ // PV
+ "=PV()": {"#VALUE!", "PV requires at least 3 arguments"},
+ "=PV(10%/4,16,2000,0,1,0)": {"#VALUE!", "PV allows at most 5 arguments"},
+ "=PV(\"\",16,2000,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PV(10%/4,\"\",2000,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PV(10%/4,16,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PV(10%/4,16,2000,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=PV(10%/4,16,2000,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // RATE
+ "=RATE()": {"#VALUE!", "RATE requires at least 3 arguments"},
+ "=RATE(48,-200,8000,3,1,0.5,0)": {"#VALUE!", "RATE allows at most 6 arguments"},
+ "=RATE(\"\",-200,8000,3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RATE(48,\"\",8000,3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RATE(48,-200,\"\",3,1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RATE(48,-200,8000,\"\",1,0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RATE(48,-200,8000,3,\"\",0.5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RATE(48,-200,8000,3,1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ // RECEIVED
+ "=RECEIVED()": {"#VALUE!", "RECEIVED requires at least 4 arguments"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,1,0)": {"#VALUE!", "RECEIVED allows at most 5 arguments"},
+ "=RECEIVED(\"\",\"03/31/2016\",1000,4.5%,1)": {"#VALUE!", "#VALUE!"},
+ "=RECEIVED(\"04/01/2011\",\"\",1000,4.5%,1)": {"#VALUE!", "#VALUE!"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",\"\",4.5%,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,\"\")": {"#NUM!", "#NUM!"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,0)": {"#NUM!", "RECEIVED requires discount > 0"},
+ "=RECEIVED(\"04/01/2011\",\"03/31/2016\",1000,4.5%,5)": {"#NUM!", "invalid basis"},
+ // RRI
+ "=RRI()": {"#VALUE!", "RRI requires 3 arguments"},
+ "=RRI(\"\",\"\",\"\")": {"#NUM!", "#NUM!"},
+ "=RRI(0,10000,15000)": {"#NUM!", "RRI requires nper argument to be > 0"},
+ "=RRI(10,0,15000)": {"#NUM!", "RRI requires pv argument to be > 0"},
+ "=RRI(10,10000,-1)": {"#NUM!", "RRI requires fv argument to be >= 0"},
+ // SLN
+ "=SLN()": {"#VALUE!", "SLN requires 3 arguments"},
+ "=SLN(\"\",\"\",\"\")": {"#NUM!", "#NUM!"},
+ "=SLN(10000,1000,0)": {"#NUM!", "SLN requires life argument to be > 0"},
+ // SYD
+ "=SYD()": {"#VALUE!", "SYD requires 4 arguments"},
+ "=SYD(\"\",\"\",\"\",\"\")": {"#NUM!", "#NUM!"},
+ "=SYD(10000,1000,0,1)": {"#NUM!", "SYD requires life argument to be > 0"},
+ "=SYD(10000,1000,5,0)": {"#NUM!", "SYD requires per argument to be > 0"},
+ "=SYD(10000,1000,1,5)": {"#NUM!", "#NUM!"},
+ // TBILLEQ
+ "=TBILLEQ()": {"#VALUE!", "TBILLEQ requires 3 arguments"},
+ "=TBILLEQ(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLEQ(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"},
+ "=TBILLEQ(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"},
+ "=TBILLEQ(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"},
+ // TBILLPRICE
+ "=TBILLPRICE()": {"#VALUE!", "TBILLPRICE requires 3 arguments"},
+ "=TBILLPRICE(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLPRICE(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=TBILLPRICE(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"},
+ "=TBILLPRICE(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"},
+ "=TBILLPRICE(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"},
+ // TBILLYIELD
+ "=TBILLYIELD()": {"#VALUE!", "TBILLYIELD requires 3 arguments"},
+ "=TBILLYIELD(\"\",\"06/30/2017\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLYIELD(\"01/01/2017\",\"\",2.5%)": {"#VALUE!", "#VALUE!"},
+ "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",\"\")": {"#VALUE!", "#VALUE!"},
+ "=TBILLYIELD(\"01/01/2017\",\"06/30/2017\",0)": {"#NUM!", "#NUM!"},
+ "=TBILLYIELD(\"01/01/2017\",\"06/30/2018\",2.5%)": {"#NUM!", "#NUM!"},
+ "=TBILLYIELD(\"06/30/2017\",\"01/01/2017\",2.5%)": {"#NUM!", "#NUM!"},
+ // VDB
+ "=VDB()": {"#VALUE!", "VDB requires 5 or 7 arguments"},
+ "=VDB(-1,1000,5,0,1)": {"#NUM!", "VDB requires cost >= 0"},
+ "=VDB(10000,-1,5,0,1)": {"#NUM!", "VDB requires salvage >= 0"},
+ "=VDB(10000,1000,0,0,1)": {"#NUM!", "VDB requires life > 0"},
+ "=VDB(10000,1000,5,-1,1)": {"#NUM!", "VDB requires start_period > 0"},
+ "=VDB(10000,1000,5,2,1)": {"#NUM!", "VDB requires start_period <= end_period"},
+ "=VDB(10000,1000,5,0,6)": {"#NUM!", "VDB requires end_period <= life"},
+ "=VDB(10000,1000,5,0,1,-0.2)": {"#VALUE!", "VDB requires factor >= 0"},
+ "=VDB(\"\",1000,5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VDB(10000,\"\",5,0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VDB(10000,1000,\"\",0,1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VDB(10000,1000,5,\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VDB(10000,1000,5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=VDB(10000,1000,5,0,1,\"\")": {"#NUM!", "#NUM!"},
+ "=VDB(10000,1000,5,0,1,0.2,\"\")": {"#NUM!", "#NUM!"},
+ // YIELD
+ "=YIELD()": {"#VALUE!", "YIELD requires 6 or 7 arguments"},
+ "=YIELD(\"\",\"06/30/2015\",10%,101,100,4)": {"#VALUE!", "#VALUE!"},
+ "=YIELD(\"01/01/2010\",\"\",10%,101,100,4)": {"#VALUE!", "#VALUE!"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",\"\",101,100,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,\"\",100,4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,\"\",4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,\"\")": {"#NUM!", "#NUM!"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,3)": {"#NUM!", "#NUM!"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,100,4,5)": {"#NUM!", "invalid basis"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",-1,101,100,4)": {"#NUM!", "YIELD requires rate >= 0"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,0,100,4)": {"#NUM!", "YIELD requires pr > 0"},
+ "=YIELD(\"01/01/2010\",\"06/30/2015\",10%,101,-1,4)": {"#NUM!", "YIELD requires redemption >= 0"},
+ // YIELDDISC
+ "=YIELDDISC()": {"#VALUE!", "YIELDDISC requires 4 or 5 arguments"},
+ "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDDISC(\"01/01/2017\",\"\",97,100,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",\"\",100,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,\"\",0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,\"\")": {"#NUM!", "#NUM!"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",0,100)": {"#NUM!", "YIELDDISC requires pr > 0"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,0)": {"#NUM!", "YIELDDISC requires redemption > 0"},
+ "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,5)": {"#NUM!", "invalid basis"},
+ // YIELDMAT
+ "=YIELDMAT()": {"#VALUE!", "YIELDMAT requires 5 or 6 arguments"},
+ "=YIELDMAT(\"\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDMAT(\"01/01/2017\",\"\",\"06/01/2014\",5.5%,101,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"\",5.5%,101,0)": {"#VALUE!", "#VALUE!"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",\"\",101,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5,5.5%,\"\")": {"#NUM!", "#NUM!"},
+ "=YIELDMAT(\"06/01/2014\",\"06/30/2018\",\"01/01/2017\",5.5%,101,0)": {"#NUM!", "YIELDMAT requires settlement > issue"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",-1,101,0)": {"#NUM!", "YIELDMAT requires rate >= 0"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",1,0,0)": {"#NUM!", "YIELDMAT requires pr > 0"},
+ "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,5)": {"#NUM!", "invalid basis"},
+ // DISPIMG
+ "=_xlfn.DISPIMG()": {"#VALUE!", "DISPIMG requires 2 numeric arguments"},
}
for formula, expected := range mathCalcError {
- f := prepareData()
+ f := prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
- assert.EqualError(t, err, expected)
- assert.Equal(t, "", result, formula)
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
}
referenceCalc := map[string]string{
@@ -731,89 +4656,1849 @@ func TestCalcCellValue(t *testing.T) {
"=MDETERM(A1:B2)": "-3",
// PRODUCT
"=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4",
+ // IMPRODUCT
+ "=IMPRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4",
// SUM
- "=A1/A3": "0.3333333333333333",
+ "=A1/A3": "0.333333333333333",
"=SUM(A1:A2)": "3",
+ "=SUM(Sheet1!A1:Sheet1!A2)": "3",
"=SUM(Sheet1!A1,A2)": "3",
"=(-2-SUM(-4+A2))*5": "0",
"=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5",
"=SUM(A1,A2,A3)*SUM(2,3)": "30",
- "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335",
- "=A1/A2/SUM(A1:A2:B1)": "0.041666666666666664",
+ "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.33333333333333",
+ "=A1/A2/SUM(A1:A2:B1)": "0.0416666666666667",
"=A1/A2/SUM(A1:A2:B1)*A3": "0.125",
+ "=SUM(B1:D1)": "4",
+ "=SUM(\"X\")": "0",
}
for formula, expected := range referenceCalc {
- f := prepareData()
+ f := prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
assert.NoError(t, err)
assert.Equal(t, expected, result, formula)
}
- referenceCalcError := map[string]string{
+ referenceCalcError := map[string][]string{
// MDETERM
- "=MDETERM(A1:B3)": "#VALUE!",
+ "=MDETERM(A1:B3)": {"#VALUE!", "#VALUE!"},
// SUM
- "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!",
+ "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": {"#VALUE!", "#DIV/0!"},
}
for formula, expected := range referenceCalcError {
- f := prepareData()
+ f := prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
result, err := f.CalcCellValue("Sheet1", "C1")
- assert.EqualError(t, err, expected)
- assert.Equal(t, "", result, formula)
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
}
volatileFuncs := []string{
+ "=NOW()",
"=RAND()",
"=RANDBETWEEN(1,2)",
+ "=TODAY()",
}
for _, formula := range volatileFuncs {
- f := prepareData()
+ f := prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
_, err := f.CalcCellValue("Sheet1", "C1")
assert.NoError(t, err)
}
- // Test get calculated cell value on not formula cell.
- f := prepareData()
+ // Test get calculated cell value on not formula cell
+ f := prepareCalcData(cellData)
result, err := f.CalcCellValue("Sheet1", "A1")
assert.NoError(t, err)
- assert.Equal(t, "", result)
- // Test get calculated cell value on not exists worksheet.
- f = prepareData()
+ assert.Equal(t, "1", result)
+ // Test get calculated cell value on not exists worksheet
+ f = prepareCalcData(cellData)
_, err = f.CalcCellValue("SheetN", "A1")
- assert.EqualError(t, err, "sheet SheetN is not exist")
- // Test get calculated cell value with not support formula.
- f = prepareData()
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get calculated cell value with invalid sheet name
+ _, err = f.CalcCellValue("Sheet:1", "A1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+ // Test get calculated cell value with not support formula
+ f = prepareCalcData(cellData)
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)"))
_, err = f.CalcCellValue("Sheet1", "A1")
assert.EqualError(t, err, "not support UNSUPPORT function")
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCalcCellValue.xlsx")))
-
}
-func TestCalcCellValueWithDefinedName(t *testing.T) {
+func TestCalcWithDefinedName(t *testing.T) {
cellData := [][]interface{}{
- {"A1 value", "B1 value", nil},
+ {"A1_as_string", "B1_as_string", 123, nil},
}
- prepareData := func() *File {
+ f := prepareCalcData(cellData)
+ assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"}))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"}))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name2", RefersTo: "Sheet1!C1", Scope: "Workbook"}))
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=defined_name1"))
+ result, err := f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err)
+ // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value
+ assert.Equal(t, "B1_as_string", result, "=defined_name1")
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=CONCATENATE(\"<\",defined_name1,\">\")"))
+ result, err = f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err)
+ assert.Equal(t, "", result, "=defined_name1")
+
+ // comparing numeric values
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=123=defined_name2"))
+ result, err = f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err)
+ assert.Equal(t, "TRUE", result, "=123=defined_name2")
+
+ // comparing text values
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=\"B1_as_string\"=defined_name1"))
+ result, err = f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err)
+ assert.Equal(t, "TRUE", result, "=\"B1_as_string\"=defined_name1")
+
+ // comparing text values
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=IF(\"B1_as_string\"=defined_name1,\"YES\",\"NO\")"))
+ result, err = f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err)
+ assert.Equal(t, "YES", result, "=IF(\"B1_as_string\"=defined_name1,\"YES\",\"NO\")")
+}
+
+func TestCalcISBLANK(t *testing.T) {
+ argsList := list.New()
+ argsList.PushBack(formulaArg{
+ Type: ArgUnknown,
+ })
+ fn := formulaFuncs{}
+ result := fn.ISBLANK(argsList)
+ assert.Equal(t, "TRUE", result.Value())
+ assert.Empty(t, result.Error)
+}
+
+func TestCalcAND(t *testing.T) {
+ argsList := list.New()
+ argsList.PushBack(formulaArg{
+ Type: ArgUnknown,
+ })
+ fn := formulaFuncs{}
+ result := fn.AND(argsList)
+ assert.Equal(t, result.String, "")
+ assert.Empty(t, result.Error)
+}
+
+func TestCalcOR(t *testing.T) {
+ argsList := list.New()
+ argsList.PushBack(formulaArg{
+ Type: ArgUnknown,
+ })
+ fn := formulaFuncs{}
+ result := fn.OR(argsList)
+ assert.Equal(t, result.Value(), "FALSE")
+ assert.Empty(t, result.Error)
+}
+
+func TestCalcDet(t *testing.T) {
+ assert.Equal(t, det([][]float64{
+ {1, 2, 3, 4},
+ {2, 3, 4, 5},
+ {3, 4, 5, 6},
+ {4, 5, 6, 7},
+ }), float64(0))
+}
+
+func TestCalcToBool(t *testing.T) {
+ b := newBoolFormulaArg(true).ToBool()
+ assert.Equal(t, b.Boolean, true)
+ assert.Equal(t, b.Number, 1.0)
+}
+
+func TestCalcToList(t *testing.T) {
+ assert.Equal(t, []formulaArg(nil), newEmptyFormulaArg().ToList())
+ formulaList := []formulaArg{newEmptyFormulaArg()}
+ assert.Equal(t, formulaList, newListFormulaArg(formulaList).ToList())
+}
+
+func TestCalcCompareFormulaArg(t *testing.T) {
+ assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), newNumberFormulaArg(matchModeMaxLess), false), criteriaEq)
+ lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()})
+ rhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg(), newEmptyFormulaArg()})
+ assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL)
+ assert.Equal(t, compareFormulaArg(rhs, lhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
+
+ lhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)})
+ rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)})
+ assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaEq)
+
+ lhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(1)})
+ rhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(0)})
+ assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
+
+ assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr)
+}
+
+func TestCalcCompareFormulaArgMatrix(t *testing.T) {
+ lhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}})
+ rhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}})
+ assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL)
+
+ lhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}})
+ rhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}})
+ assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
+
+ lhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}})
+ rhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(0)}})
+ assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG)
+}
+
+func TestCalcANCHORARRAY(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1))
+ assert.NoError(t, f.SetCellValue("Sheet1", "A2", 2))
+ formulaType, ref := STCellFormulaTypeArray, "B1:B2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "SUM(_xlfn.ANCHORARRAY($B$1))"))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err)
+ assert.Equal(t, "3", result)
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "SUM(_xlfn.ANCHORARRAY(\"\",\"\"))"))
+ result, err = f.CalcCellValue("Sheet1", "C1")
+ assert.EqualError(t, err, "ANCHORARRAY requires 1 numeric argument")
+ assert.Equal(t, "#VALUE!", result)
+
+ fn := &formulaFuncs{f: f, sheet: "SheetN"}
+ argsList := list.New()
+ argsList.PushBack(newStringFormulaArg("$B$1"))
+ formulaArg := fn.ANCHORARRAY(argsList)
+ assert.Equal(t, "sheet SheetN does not exist", formulaArg.Value())
+
+ fn.sheet = "Sheet1"
+ argsList = argsList.Init()
+ arg := newStringFormulaArg("$A$1")
+ arg.cellRefs = list.New()
+ arg.cellRefs.PushBack(cellRef{Row: 1, Col: 1})
+ argsList.PushBack(arg)
+ formulaArg = fn.ANCHORARRAY(argsList)
+ assert.Equal(t, ArgEmpty, formulaArg.Type)
+
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0].F = &xlsxF{}
+ formulaArg = fn.ANCHORARRAY(argsList)
+ assert.Equal(t, ArgError, formulaArg.Type)
+ assert.Equal(t, ErrParameterInvalid.Error(), formulaArg.Value())
+
+ argsList = argsList.Init()
+ arg = newStringFormulaArg("$B$1")
+ arg.cellRefs = list.New()
+ arg.cellRefs.PushBack(cellRef{Row: 1, Col: 1, Sheet: "SheetN"})
+ argsList.PushBack(arg)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0].F = &xlsxF{Ref: "A1:A1"}
+ formulaArg = fn.ANCHORARRAY(argsList)
+ assert.Equal(t, ArgError, formulaArg.Type)
+ assert.Equal(t, "sheet SheetN does not exist", formulaArg.Value())
+}
+
+func TestCalcArrayFormula(t *testing.T) {
+ t.Run("matrix_multiplication", func(t *testing.T) {
f := NewFile()
- for r, row := range cellData {
- for c, value := range row {
- cell, _ := CoordinatesToCellName(c+1, r+1)
- assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
- }
- }
- assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"}))
- assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
+ formulaType, ref := STCellFormulaTypeArray, "C1:C2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err)
+ assert.Equal(t, "2", result)
+ result, err = f.CalcCellValue("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "12", result)
+ })
+ t.Run("matrix_multiplication_with_defined_name", func(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "matrix",
+ RefersTo: "Sheet1!$A$1:$A$2",
+ }))
+ formulaType, ref := STCellFormulaTypeArray, "C1:C2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "matrix*B1:B2+\"1\"",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err)
+ assert.Equal(t, "3", result)
+ result, err = f.CalcCellValue("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "13", result)
+ })
+ t.Run("columm_multiplication", func(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
+ formulaType, ref := STCellFormulaTypeArray, "C1:C1048576"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A:A*B:B",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err)
+ assert.Equal(t, "2", result)
+ result, err = f.CalcCellValue("Sheet1", "C2")
+ assert.NoError(t, err)
+ assert.Equal(t, "12", result)
+ })
+ t.Run("row_multiplication", func(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
+ formulaType, ref := STCellFormulaTypeArray, "A3:XFD3"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "1:1*2:2",
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ result, err := f.CalcCellValue("Sheet1", "A3")
+ assert.NoError(t, err)
+ assert.Equal(t, "3", result)
+ result, err = f.CalcCellValue("Sheet1", "B3")
+ assert.NoError(t, err)
+ assert.Equal(t, "8", result)
+ })
+}
- return f
+func TestCalcTRANSPOSE(t *testing.T) {
+ cellData := [][]interface{}{
+ {"a", "d"},
+ {"b", "e"},
+ {"c", "f"},
}
- f := prepareData()
- assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=defined_name1"))
- result, err := f.CalcCellValue("Sheet1", "C1")
+ formula := "=TRANSPOSE(A1:A3)"
+ f := prepareCalcData(cellData)
+ formulaType, ref := STCellFormulaTypeArray, "D1:F2"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", formula,
+ FormulaOpts{Ref: &ref, Type: &formulaType}))
+ _, err := f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err, formula)
+}
+
+func TestCalcVLOOKUP(t *testing.T) {
+ cellData := [][]interface{}{
+ {nil, nil, nil, nil, nil, nil},
+ {nil, "Score", "Grade", nil, nil, nil},
+ {nil, 0, "F", nil, "Score", 85},
+ {nil, 60, "D", nil, "Grade"},
+ {nil, 70, "C", nil, nil, nil},
+ {nil, 80, "b", nil, nil, nil},
+ {nil, 90, "A", nil, nil, nil},
+ {nil, 85, "B", nil, nil, nil},
+ {nil, nil, nil, nil, nil, nil},
+ }
+ f := prepareCalcData(cellData)
+ calc := map[string]string{
+ "=VLOOKUP(F3,B3:C8,2)": "b",
+ "=VLOOKUP(F3,B3:C8,2,TRUE)": "b",
+ "=VLOOKUP(F3,B3:C8,2,FALSE)": "B",
+ }
+ for formula, expected := range calc {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "F4", formula))
+ result, err := f.CalcCellValue("Sheet1", "F4")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=VLOOKUP(INT(1),C3:C3,1,FALSE)": {"#N/A", "VLOOKUP no result found"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "F4", formula))
+ result, err := f.CalcCellValue("Sheet1", "F4")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcBoolean(t *testing.T) {
+ cellData := [][]interface{}{{0.5, "TRUE", -0.5, "FALSE", true}}
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=AVERAGEA(A1:C1)": "0.333333333333333",
+ "=MAX(0.5,B1)": "0.5",
+ "=MAX(A1:B1)": "0.5",
+ "=MAXA(A1:B1)": "0.5",
+ "=MAXA(A1:E1)": "1",
+ "=MAXA(0.5,B1)": "1",
+ "=MIN(-0.5,D1)": "-0.5",
+ "=MIN(C1:D1)": "-0.5",
+ "=MINA(C1:D1)": "-0.5",
+ "=MINA(-0.5,D1)": "-0.5",
+ "=STDEV(A1:C1)": "0.707106781186548",
+ "=STDEV(A1,B1,C1)": "0.707106781186548",
+ "=STDEVA(A1:C1,B1)": "0.707106781186548",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula))
+ result, err := f.CalcCellValue("Sheet1", "B10")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcMAXMIN(t *testing.T) {
+ cellData := [][]interface{}{{"1"}, {"2"}, {true}}
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=MAX(A1:A3)": "0",
+ "=MAXA(A1:A3)": "1",
+ "=MIN(A1:A3)": "0",
+ "=MINA(A1:A3)": "1",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcAVERAGEIF(t *testing.T) {
+ f := prepareCalcData([][]interface{}{
+ {"Monday", 500},
+ {"Tuesday", 50},
+ {"Thursday", 100},
+ {"Friday", 100},
+ {"Thursday", 200},
+ {5, 300},
+ {2, 200},
+ {3, 100},
+ {4, 50},
+ {5, 100},
+ {1, 50},
+ {true, 200},
+ {true, 250},
+ {false, 50},
+ })
+ for formula, expected := range map[string]string{
+ "=AVERAGEIF(A1:A14,\"Thursday\",B1:B14)": "150",
+ "=AVERAGEIF(A1:A14,5,B1:B14)": "200",
+ "=AVERAGEIF(A1:A14,\">2\",B1:B14)": "137.5",
+ "=AVERAGEIF(A1:A14,TRUE,B1:B14)": "225",
+ "=AVERAGEIF(A1:A14,\"<>TRUE\",B1:B14)": "150",
+ } {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcCOVAR(t *testing.T) {
+ cellData := [][]interface{}{
+ {"array1", "array2"},
+ {2, 22.9},
+ {7, 33.49},
+ {8, 34.5},
+ {3, 27.61},
+ {4, 19.5},
+ {1, 10.11},
+ {6, 37.9},
+ {5, 31.08},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=COVAR(A1:A9,B1:B9)": "16.633125",
+ "=COVAR(A2:A9,B2:B9)": "16.633125",
+ "=COVARIANCE.P(A1:A9,B1:B9)": "16.633125",
+ "=COVARIANCE.P(A2:A9,B2:B9)": "16.633125",
+ "=COVARIANCE.S(A1:A9,B1:B9)": "19.0092857142857",
+ "=COVARIANCE.S(A2:A9,B2:B9)": "19.0092857142857",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=COVAR()": {"#VALUE!", "COVAR requires 2 arguments"},
+ "=COVAR(A2:A9,B3:B3)": {"#N/A", "#N/A"},
+ "=COVARIANCE.P()": {"#VALUE!", "COVARIANCE.P requires 2 arguments"},
+ "=COVARIANCE.P(A2:A9,B3:B3)": {"#N/A", "#N/A"},
+ "=COVARIANCE.S()": {"#VALUE!", "COVARIANCE.S requires 2 arguments"},
+ "=COVARIANCE.S(A2:A9,B3:B3)": {"#N/A", "#N/A"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcDatabase(t *testing.T) {
+ cellData := [][]interface{}{
+ {"Tree", "Height", "Age", "Yield", "Profit", "Height"},
+ {nil, ">1000%", nil, nil, nil, "<16"},
+ {},
+ {"Tree", "Height", "Age", "Yield", "Profit"},
+ {"Apple", 18, 20, 14, 105},
+ {"Pear", 12, 12, 10, 96},
+ {"Cherry", 13, 14, 9, 105},
+ {"Apple", 14, nil, 10, 75},
+ {"Pear", 9, 8, 8, 77},
+ {"Apple", 12, 11, 6, 45},
+ }
+ f := prepareCalcData(cellData)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=\"=Apple\""))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=\"=Pear\""))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C8", "=NA()"))
+ formulaList := map[string]string{
+ "=DAVERAGE(A4:E10,\"Profit\",A1:F3)": "73.25",
+ "=DCOUNT(A4:E10,\"Age\",A1:F2)": "1",
+ "=DCOUNT(A4:E10,,A1:F2)": "2",
+ "=DCOUNT(A4:E10,\"Profit\",A1:F2)": "2",
+ "=DCOUNT(A4:E10,\"Tree\",A1:F2)": "0",
+ "=DCOUNT(A4:E10,\"Age\",A2:F3)": "0",
+ "=DCOUNTA(A4:E10,\"Age\",A1:F2)": "1",
+ "=DCOUNTA(A4:E10,,A1:F2)": "2",
+ "=DCOUNTA(A4:E10,\"Profit\",A1:F2)": "2",
+ "=DCOUNTA(A4:E10,\"Tree\",A1:F2)": "2",
+ "=DCOUNTA(A4:E10,\"Age\",A2:F3)": "0",
+ "=DGET(A4:E6,\"Profit\",A1:F3)": "96",
+ "=DMAX(A4:E10,\"Tree\",A1:F3)": "0",
+ "=DMAX(A4:E10,\"Profit\",A1:F3)": "96",
+ "=DMIN(A4:E10,\"Tree\",A1:F3)": "0",
+ "=DMIN(A4:E10,\"Profit\",A1:F3)": "45",
+ "=DPRODUCT(A4:E10,\"Profit\",A1:F3)": "24948000",
+ "=DSTDEV(A4:E10,\"Profit\",A1:F3)": "21.077238908358",
+ "=DSTDEVP(A4:E10,\"Profit\",A1:F3)": "18.2534243362718",
+ "=DSUM(A4:E10,\"Profit\",A1:F3)": "293",
+ "=DVAR(A4:E10,\"Profit\",A1:F3)": "444.25",
+ "=DVARP(A4:E10,\"Profit\",A1:F3)": "333.1875",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula))
+ result, err := f.CalcCellValue("Sheet1", "A11")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=DAVERAGE()": {"#VALUE!", "DAVERAGE requires 3 arguments"},
+ "=DAVERAGE(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DAVERAGE(A4:E10,\"Tree\",A1:F3)": {"#DIV/0!", "#DIV/0!"},
+ "=DCOUNT()": {"#VALUE!", "DCOUNT requires at least 2 arguments"},
+ "=DCOUNT(A4:E10,\"Age\",A1:F2,\"\")": {"#VALUE!", "DCOUNT allows at most 3 arguments"},
+ "=DCOUNT(A4,\"Age\",A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNT(A4:E10,NA(),A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNT(A4:E4,,A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNT(A4:E10,\"x\",A2:F3)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNTA()": {"#VALUE!", "DCOUNTA requires at least 2 arguments"},
+ "=DCOUNTA(A4:E10,\"Age\",A1:F2,\"\")": {"#VALUE!", "DCOUNTA allows at most 3 arguments"},
+ "=DCOUNTA(A4,\"Age\",A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNTA(A4:E10,NA(),A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNTA(A4:E4,,A1:F2)": {"#VALUE!", "#VALUE!"},
+ "=DCOUNTA(A4:E10,\"x\",A2:F3)": {"#VALUE!", "#VALUE!"},
+ "=DGET()": {"#VALUE!", "DGET requires 3 arguments"},
+ "=DGET(A4:E5,\"Profit\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DGET(A4:E10,\"Profit\",A1:F3)": {"#NUM!", "#NUM!"},
+ "=DMAX()": {"#VALUE!", "DMAX requires 3 arguments"},
+ "=DMAX(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DMIN()": {"#VALUE!", "DMIN requires 3 arguments"},
+ "=DMIN(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DPRODUCT()": {"#VALUE!", "DPRODUCT requires 3 arguments"},
+ "=DPRODUCT(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DSTDEV()": {"#VALUE!", "DSTDEV requires 3 arguments"},
+ "=DSTDEV(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DSTDEVP()": {"#VALUE!", "DSTDEVP requires 3 arguments"},
+ "=DSTDEVP(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DSUM()": {"#VALUE!", "DSUM requires 3 arguments"},
+ "=DSUM(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DVAR()": {"#VALUE!", "DVAR requires 3 arguments"},
+ "=DVAR(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ "=DVARP()": {"#VALUE!", "DVARP requires 3 arguments"},
+ "=DVARP(A4:E10,\"x\",A1:F3)": {"#VALUE!", "#VALUE!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A11", formula))
+ result, err := f.CalcCellValue("Sheet1", "A11")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcDBCS(t *testing.T) {
+ f := NewFile(Options{CultureInfo: CultureNameZhCN})
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=DBCS(\"`~·!@#$¥%…^&*()_-+=[]{}\\|;:'\"\"<,>.?/01234567890 abc ABC \uff65\uff9e\uff9f \uff74\uff78\uff7e\uff99\")"))
+ result, err := f.CalcCellValue("Sheet1", "A1")
assert.NoError(t, err)
- // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value
- assert.Equal(t, "B1 value", result, "=defined_name1")
+ assert.Equal(t, "\uff40\uff5e\u00b7\uff01\uff20\uff03\uff04\u00a5\uff05\u2026\uff3e\uff06\uff0a\uff08\uff09\uff3f\uff0d\uff0b\uff1d\uff3b\uff3d\uff5b\uff5d\uff3c\uff5c\uff1b\uff1a\uff07\uff02\uff1c\uff0c\uff1e\uff0e\uff1f\uff0f\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19\uff10\u3000\uff41\uff42\uff43\u3000\uff21\uff22\uff23\u3000\uff65\uff9e\uff9f\u3000\uff74\uff78\uff7e\uff99", result)
+}
+
+func TestCalcFORMULATEXT(t *testing.T) {
+ f, formulaText := NewFile(), "=SUM(B1:C1)"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formulaText))
+ for _, formula := range []string{"=FORMULATEXT(A1)", "=FORMULATEXT(A:A)", "=FORMULATEXT(A1:B1)"} {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", formula), formula)
+ result, err := f.CalcCellValue("Sheet1", "D1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, formulaText, result, formula)
+ }
+}
+
+func TestCalcGROWTHandTREND(t *testing.T) {
+ cellData := [][]interface{}{
+ {"known_x's", "known_y's", 0, -1},
+ {1, 10, 1},
+ {2, 20, 1},
+ {3, 40},
+ {4, 80},
+ {},
+ {"new_x's", "new_y's"},
+ {5},
+ {6},
+ {7},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=GROWTH(A2:B2)": "1",
+ "=GROWTH(B2:B5,A2:A5,A8:A10)": "160",
+ "=GROWTH(B2:B5,A2:A5,A8:A10,FALSE)": "467.842838114059",
+ "=GROWTH(A4:A5,A2:B3,A8:A10,FALSE)": "",
+ "=GROWTH(A3:A5,A2:B4,A2:B3)": "2",
+ "=GROWTH(A4:A5,A2:B3)": "",
+ "=GROWTH(A2:B2,A2:B3)": "",
+ "=GROWTH(A2:B2,A2:B3,A2:B3,FALSE)": "1.28402541668774",
+ "=GROWTH(A2:B2,A4:B5,A4:B5,FALSE)": "1",
+ "=GROWTH(A3:C3,A2:C3,A2:B3)": "2",
+ "=TREND(A2:B2)": "1",
+ "=TREND(B2:B5,A2:A5,A8:A10)": "95",
+ "=TREND(B2:B5,A2:A5,A8:A10,FALSE)": "81.6666666666667",
+ "=TREND(A4:A5,A2:B3,A8:A10,FALSE)": "",
+ "=TREND(A4:A5,A2:B3,A2:B3,FALSE)": "1.5",
+ "=TREND(A3:A5,A2:B4,A2:B3)": "2",
+ "=TREND(A4:A5,A2:B3)": "",
+ "=TREND(A2:B2,A2:B3)": "",
+ "=TREND(A2:B2,A2:B3,A2:B3,FALSE)": "1",
+ "=TREND(A2:B2,A4:B5,A4:B5,FALSE)": "1",
+ "=TREND(A3:C3,A2:C3,A2:B3)": "2",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=GROWTH()": {"#VALUE!", "GROWTH requires at least 1 argument"},
+ "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": {"#VALUE!", "GROWTH allows at most 4 arguments"},
+ "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=GROWTH(A2:B3,A4:B4)": {"#REF!", "#REF!"},
+ "=GROWTH(A4:B4,A2:A2)": {"#REF!", "#REF!"},
+ "=GROWTH(A2:A2,A4:A5)": {"#REF!", "#REF!"},
+ "=GROWTH(C1:C1,A2:A3)": {"#VALUE!", "#VALUE!"},
+ "=GROWTH(D1:D1,A2:A3)": {"#NUM!", "#NUM!"},
+ "=GROWTH(A2:A3,C1:C1)": {"#VALUE!", "#VALUE!"},
+ "=TREND()": {"#VALUE!", "TREND requires at least 1 argument"},
+ "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": {"#VALUE!", "TREND allows at most 4 arguments"},
+ "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": {"#VALUE!", "#VALUE!"},
+ "=TREND(B2:B5,A2:A5,A8:A10,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"},
+ "=TREND(A2:B3,A4:B4)": {"#REF!", "#REF!"},
+ "=TREND(A4:B4,A2:A2)": {"#REF!", "#REF!"},
+ "=TREND(A2:A2,A4:A5)": {"#REF!", "#REF!"},
+ "=TREND(C1:C1,A2:A3)": {"#VALUE!", "#VALUE!"},
+ "=TREND(D1:D1,A2:A3)": {"#REF!", "#REF!"},
+ "=TREND(A2:A3,C1:C1)": {"#VALUE!", "#VALUE!"},
+ "=TREND(C1:C1,C1:C1)": {"#VALUE!", "#VALUE!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcHLOOKUP(t *testing.T) {
+ cellData := [][]interface{}{
+ {"Example Result Table"},
+ {nil, "A", "B", "C", "E", "F"},
+ {"Math", .58, .9, .67, .76, .8},
+ {"French", .61, .71, .59, .59, .76},
+ {"Physics", .75, .45, .39, .52, .69},
+ {"Biology", .39, .55, .77, .61, .45},
+ {},
+ {"Individual Student Score"},
+ {"Student:", "Biology Score:"},
+ {"E"},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=HLOOKUP(A10,A2:F6,5,FALSE)": "0.61",
+ "=HLOOKUP(D3,D3:D3,1,TRUE)": "0.67",
+ "=HLOOKUP(F3,D3:F3,1,TRUE)": "0.8",
+ "=HLOOKUP(A5,A2:F2,1,TRUE)": "F",
+ "=HLOOKUP(\"D\",A2:F2,1,TRUE)": "C",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula))
+ result, err := f.CalcCellValue("Sheet1", "B10")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=HLOOKUP(INT(1),A3:A3,1,FALSE)": {"#N/A", "HLOOKUP no result found"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula))
+ result, err := f.CalcCellValue("Sheet1", "B10")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcCHITESTandCHISQdotTEST(t *testing.T) {
+ cellData := [][]interface{}{
+ {nil, "Observed Frequencies", nil, nil, "Expected Frequencies"},
+ {nil, "men", "women", nil, nil, "men", "women"},
+ {"answer a", 33, 39, nil, "answer a", 26.25, 31.5},
+ {"answer b", 62, 62, nil, "answer b", 57.75, 61.95},
+ {"answer c", 10, 4, nil, "answer c", 21, 11.55},
+ {nil, -1, 0},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=CHITEST(B3:C5,F3:G5)": "0.000699102758787672",
+ "=CHITEST(B3:C3,F3:G3)": "0.0605802098655177",
+ "=CHITEST(B3:B4,F3:F4)": "0.152357748933542",
+ "=CHITEST(B4:B6,F3:F5)": "7.07076951440726E-25",
+ "=CHISQ.TEST(B3:C5,F3:G5)": "0.000699102758787672",
+ "=CHISQ.TEST(B3:C3,F3:G3)": "0.0605802098655177",
+ "=CHISQ.TEST(B3:B4,F3:F4)": "0.152357748933542",
+ "=CHISQ.TEST(B4:B6,F3:F5)": "7.07076951440726E-25",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "I1", formula))
+ result, err := f.CalcCellValue("Sheet1", "I1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=CHITEST()": {"#VALUE!", "CHITEST requires 2 arguments"},
+ "=CHITEST(MUNIT(0),MUNIT(0))": {"#VALUE!", "#VALUE!"},
+ "=CHITEST(B3:C5,F3:F4)": {"#N/A", "#N/A"},
+ "=CHITEST(B3:B3,F3:F3)": {"#N/A", "#N/A"},
+ "=CHITEST(F3:F5,B4:B6)": {"#NUM!", "#NUM!"},
+ "=CHITEST(F3:F5,C4:C6)": {"#DIV/0!", "#DIV/0!"},
+ "=CHISQ.TEST()": {"#VALUE!", "CHISQ.TEST requires 2 arguments"},
+ "=CHISQ.TEST(B3:C5,F3:F4)": {"#N/A", "#N/A"},
+ "=CHISQ.TEST(B3:B3,F3:F3)": {"#N/A", "#N/A"},
+ "=CHISQ.TEST(F3:F5,B4:B6)": {"#NUM!", "#NUM!"},
+ "=CHISQ.TEST(F3:F5,C4:C6)": {"#DIV/0!", "#DIV/0!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "I1", formula))
+ result, err := f.CalcCellValue("Sheet1", "I1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcFTEST(t *testing.T) {
+ cellData := [][]interface{}{
+ {"Group 1", "Group 2"},
+ {3.5, 9.2},
+ {4.7, 8.2},
+ {6.2, 7.3},
+ {4.9, 6.1},
+ {3.8, 5.4},
+ {5.5, 7.8},
+ {7.1, 5.9},
+ {6.7, 8.4},
+ {3.9, 7.7},
+ {4.6, 6.6},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=FTEST(A2:A11,B2:B11)": "0.95403555939413",
+ "=F.TEST(A2:A11,B2:B11)": "0.95403555939413",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=FTEST()": {"#VALUE!", "FTEST requires 2 arguments"},
+ "=FTEST(A2:A2,B2:B2)": {"#DIV/0!", "#DIV/0!"},
+ "=FTEST(A12:A14,B2:B4)": {"#DIV/0!", "#DIV/0!"},
+ "=FTEST(A2:A4,B2:B2)": {"#DIV/0!", "#DIV/0!"},
+ "=FTEST(A2:A4,B12:B14)": {"#DIV/0!", "#DIV/0!"},
+ "=F.TEST()": {"#VALUE!", "F.TEST requires 2 arguments"},
+ "=F.TEST(A2:A2,B2:B2)": {"#DIV/0!", "#DIV/0!"},
+ "=F.TEST(A12:A14,B2:B4)": {"#DIV/0!", "#DIV/0!"},
+ "=F.TEST(A2:A4,B2:B2)": {"#DIV/0!", "#DIV/0!"},
+ "=F.TEST(A2:A4,B12:B14)": {"#DIV/0!", "#DIV/0!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcIRR(t *testing.T) {
+ cellData := [][]interface{}{{-1}, {0.2}, {0.24}, {0.288}, {0.3456}, {0.4147}}
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=IRR(A1:A4)": "-0.136189509034157",
+ "=IRR(A1:A6)": "0.130575760006905",
+ "=IRR(A1:A4,-0.1)": "-0.136189514994621",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=IRR()": {"#VALUE!", "IRR requires at least 1 argument"},
+ "=IRR(0,0,0)": {"#VALUE!", "IRR allows at most 2 arguments"},
+ "=IRR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=IRR(A2:A3)": {"#NUM!", "#NUM!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcMAXMINIFS(t *testing.T) {
+ f := NewFile()
+ for cell, row := range map[string][]interface{}{
+ "A1": {1, -math.MaxFloat64 - 1},
+ "A2": {2, -math.MaxFloat64 - 2},
+ "A3": {3, math.MaxFloat64 + 1},
+ "A4": {4, math.MaxFloat64 + 2},
+ } {
+ assert.NoError(t, f.SetSheetRow("Sheet1", cell, &row))
+ }
+ formulaList := map[string]string{
+ "=MAX(B1:B2)": "0",
+ "=MAXIFS(B1:B2,A1:A2,\">0\")": "0",
+ "=MIN(B3:B4)": "0",
+ "=MINIFS(B3:B4,A3:A4,\"<0\")": "0",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcMIRR(t *testing.T) {
+ cellData := [][]interface{}{{-100}, {18}, {22.5}, {28}, {35.5}, {45}}
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=MIRR(A1:A6,0.055,0.05)": "0.1000268752662",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=MIRR()": {"#VALUE!", "MIRR requires 3 arguments"},
+ "=MIRR(A1:A5,\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MIRR(A1:A5,0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=MIRR(B1:B5,0,0)": {"#DIV/0!", "#DIV/0!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) {
+ cellData := [][]interface{}{
+ {"Quarter", "Area", "Sales Rep.", "Sales"},
+ {1, "North", "Jeff", 223000},
+ {1, "North", "Chris", 125000},
+ {1, "South", "Carol", 456000},
+ {2, "North", "Jeff", 322000},
+ {2, "North", "Chris", 340000},
+ {2, "South", "Carol", 198000},
+ {3, "North", "Jeff", 310000},
+ {3, "North", "Chris", 250000},
+ {3, "South", "Carol", 460000},
+ {4, "North", "Jeff", 261000},
+ {4, "North", "Chris", 389000},
+ {4, "South", "Carol", 305000},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=AVERAGEIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "174000",
+ "=AVERAGEIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "285500",
+ "=SUMIFS(D2:D13,A2:A13,1,B2:B13,\"North\")": "348000",
+ "=SUMIFS(D2:D13,A2:A13,\">2\",C2:C13,\"Jeff\")": "571000",
+ "=SUMIFS(D2:D13,A2:A13,1,D2:D13,125000)": "125000",
+ "=SUMIFS(D2:D13,A2:A13,1,D2:D13,\">100000\",C2:C13,\"Chris\")": "125000",
+ "=SUMIFS(D2:D13,A2:A13,1,D2:D13,\"<40000\",C2:C13,\"Chris\")": "0",
+ "=SUMIFS(D2:D13,A2:A13,1,A2:A13,2)": "0",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula))
+ result, err := f.CalcCellValue("Sheet1", "E1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=AVERAGEIFS()": {"#VALUE!", "AVERAGEIFS requires at least 3 arguments"},
+ "=AVERAGEIFS(H1,\"\")": {"#VALUE!", "AVERAGEIFS requires at least 3 arguments"},
+ "=AVERAGEIFS(H1,\"\",TRUE,1)": {"#N/A", "#N/A"},
+ "=AVERAGEIFS(H1,\"\",TRUE)": {"#DIV/0!", "AVERAGEIF divide by zero"},
+ "=AVERAGEIFS(D2:D13,A2:A13,1,A2:A13,2)": {"#DIV/0!", "AVERAGEIF divide by zero"},
+ "=SUMIFS()": {"#VALUE!", "SUMIFS requires at least 3 arguments"},
+ "=SUMIFS(D2:D13,A2:A13,1,B2:B13)": {"#N/A", "#N/A"},
+ "=SUMIFS(D20:D23,A2:A13,\">2\",C2:C13,\"Jeff\")": {"#VALUE!", "#VALUE!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula))
+ result, err := f.CalcCellValue("Sheet1", "E1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcXIRR(t *testing.T) {
+ cellData := [][]interface{}{
+ {-100.00, 42370},
+ {20.00, 42461},
+ {40.00, 42644},
+ {25.00, 42767},
+ {8.00, 42795},
+ {15.00, 42887},
+ {-1e-10, 42979},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=XIRR(A1:A4,B1:B4)": "-0.196743861298328",
+ "=XIRR(A1:A6,B1:B6,0.5)": "0.0944390744445204",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=XIRR()": {"#VALUE!", "XIRR requires 2 or 3 arguments"},
+ "=XIRR(A1:A4,B1:B4,-1)": {"#VALUE!", "XIRR requires guess > -1"},
+ "=XIRR(\"\",B1:B4)": {"#VALUE!", "#VALUE!"},
+ "=XIRR(A1:A4,\"\")": {"#VALUE!", "#VALUE!"},
+ "=XIRR(A1:A4,B1:B4,\"\")": {"#NUM!", "#NUM!"},
+ "=XIRR(A2:A6,B2:B6)": {"#NUM!", "#NUM!"},
+ "=XIRR(A2:A7,B2:B7)": {"#NUM!", "#NUM!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcXLOOKUP(t *testing.T) {
+ cellData := [][]interface{}{
+ {},
+ {nil, nil, "Quarter", "Gross Profit", "Net profit", "Profit %"},
+ {nil, nil, "Qtr1", nil, 19342, 29.30},
+ {},
+ {nil, "Income Statement", "Qtr1", "Qtr2", "Qtr3", "Qtr4", "Total"},
+ {nil, "Total sales", 50000, 78200, 89500, 91250, 308.95},
+ {nil, "Cost of sales", -25000, -42050, -59450, -60450, -186950},
+ {nil, "Gross Profit", 25000, 36150, 30050, 30800, 122000},
+ {},
+ {nil, "Depreciation", -899, -791, -202, -412, -2304},
+ {nil, "Interest", -513, -853, -150, -956, -2472},
+ {nil, "Earnings before Tax", 23588, 34506, 29698, 29432, 117224},
+ {},
+ {nil, "Tax", -4246, -6211, -5346, -5298, 21100},
+ {},
+ {nil, "Net profit", 19342, 28295, 24352, 24134, 96124},
+ {nil, "Profit %", 0.293, 0.278, 0.234, 0.276, 0.269},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2))": "87272.293",
+ "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$G6,NA(),0,-2))": "309258.95",
+ "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,-2))": "87272.293",
+ "=SUM(XLOOKUP($C3,$C5:$G5,$C6:$G17,NA(),0,2))": "87272.293",
+ "=SUM(XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,2))": "244000",
+ "=XLOOKUP(D2,$B6:$B17,C6:C17)": "25000",
+ "=XLOOKUP(D2,$B6:$B17,XLOOKUP($C3,$C5:$G5,$C6:$G17))": "25000",
+ "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "25000",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula))
+ result, err := f.CalcCellValue("Sheet1", "D3")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=XLOOKUP()": {"#VALUE!", "XLOOKUP requires at least 3 arguments"},
+ "=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": {"#VALUE!", "XLOOKUP allows at most 6 arguments"},
+ "=XLOOKUP($C3,$C5,$C6,NA(),0,2)": {"#N/A", "#N/A"},
+ "=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP(D2,$B6:$B17,$C6:$G16,NA(),0,2)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),3,2)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,0)": {"#VALUE!", "#VALUE!"},
+ "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula))
+ result, err := f.CalcCellValue("Sheet1", "D3")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+
+ cellData = [][]interface{}{
+ {"Salesperson", "Item", "Amont"},
+ {"B", "Apples", 30, 25, 15, 50, 45, 18},
+ {"L", "Oranges", 25, "D3", "E3"},
+ {"C", "Grapes", 15},
+ {"L", "Lemons", 50},
+ {"L", "Oranges", 45},
+ {"C", "Peaches", 18},
+ {"B", "Pears", 40},
+ {"B", "Apples", 55},
+ }
+ f = prepareCalcData(cellData)
+ formulaList = map[string]string{
+ // Test match mode with partial match (wildcards)
+ "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "30",
+ // Test match mode with approximate match in vertical (next larger item)
+ "=XLOOKUP(32,B2:B9,C2:C9,NA(),1)": "30",
+ // Test match mode with approximate match in horizontal (next larger item)
+ "=XLOOKUP(30,C2:F2,C3:F3,NA(),1)": "25",
+ // Test match mode with approximate match in vertical (next smaller item)
+ "=XLOOKUP(40,C2:C9,B2:B9,NA(),-1)": "Pears",
+ // Test match mode with approximate match in horizontal (next smaller item)
+ "=XLOOKUP(29,C2:F2,C3:F3,NA(),-1)": "D3",
+ // Test search mode
+ "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,1)": "25",
+ "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-1)": "45",
+ "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,2)": "50",
+ "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-2)": "45",
+ // Test match mode and search mode
+ "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,-1)": "D3",
+ "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,1)": "D3",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D4", formula))
+ result, err := f.CalcCellValue("Sheet1", "D4")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError = map[string][]string{
+ // Test match mode with exact match
+ "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),0)": {"#N/A", "#N/A"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula))
+ result, err := f.CalcCellValue("Sheet1", "D3")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcXNPV(t *testing.T) {
+ cellData := [][]interface{}{
+ {nil, 0.05},
+ {42370, -10000, nil},
+ {42401, 2000},
+ {42491, 2400},
+ {42552, 2900},
+ {42675, 3500},
+ {42736, 4100},
+ {},
+ {42401},
+ {42370},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=XNPV(B1,B2:B7,A2:A7)": "4447.93800944052",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=XNPV()": {"#VALUE!", "XNPV requires 3 arguments"},
+ "=XNPV(\"\",B2:B7,A2:A7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=XNPV(0,B2:B7,A2:A7)": {"#VALUE!", "XNPV requires rate > 0"},
+ "=XNPV(B1,\"\",A2:A7)": {"#VALUE!", "#VALUE!"},
+ "=XNPV(B1,B2:B7,\"\")": {"#VALUE!", "#VALUE!"},
+ "=XNPV(B1,B2:B7,C2:C7)": {"#VALUE!", "#VALUE!"},
+ "=XNPV(B1,B2,A2)": {"#NUM!", "#NUM!"},
+ "=XNPV(B1,B2:B3,A2:A5)": {"#NUM!", "#NUM!"},
+ "=XNPV(B1,B2:B3,A9:A10)": {"#VALUE!", "#VALUE!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcMATCH(t *testing.T) {
+ f := NewFile()
+ for cell, row := range map[string][]interface{}{
+ "A1": {"cccc", 7, 4, 16},
+ "A2": {"dddd", 2, 6, 11},
+ "A3": {"aaaa", 4, 7, 10},
+ "A4": {"bbbb", 1, 10, 7},
+ "A5": {"eeee", 8, 11, 6},
+ "A6": {nil, 11, 16, 4},
+ } {
+ assert.NoError(t, f.SetSheetRow("Sheet1", cell, &row))
+ }
+ formulaList := map[string]string{
+ "=MATCH(\"aaaa\",A1:A6,0)": "3",
+ "=MATCH(\"*b\",A1:A5,0)": "4",
+ "=MATCH(\"?eee\",A1:A5,0)": "5",
+ "=MATCH(\"?*?e\",A1:A5,0)": "5",
+ "=MATCH(\"aaaa\",A1:A6,1)": "3",
+ "=MATCH(10,B1:B6)": "5",
+ "=MATCH(8,C1:C6,1)": "3",
+ "=MATCH(6,B1:B6,-1)": "1",
+ "=MATCH(10,D1:D6,-1)": "3",
+ "=MATCH(-10,D1:D6,-1)": "6",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula))
+ result, err := f.CalcCellValue("Sheet1", "E1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string]string{
+ "=MATCH(3,C1:C6,1)": "#N/A",
+ "=MATCH(5,C1:C6,-1)": "#N/A",
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula))
+ result, err := f.CalcCellValue("Sheet1", "E1")
+ assert.EqualError(t, err, expected, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ assert.Equal(t, newErrorFormulaArg(formulaErrorNA, formulaErrorNA), calcMatch(2, nil, []formulaArg{}))
+}
+
+func TestCalcISFORMULA(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=ISFORMULA(A1)"))
+ for _, formula := range []string{"=NA()", "=SUM(A1:A3)"} {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula))
+ result, err := f.CalcCellValue("Sheet1", "B1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, "TRUE", result, formula)
+ }
+}
+
+func TestCalcMODE(t *testing.T) {
+ cellData := [][]interface{}{
+ {1, 1},
+ {1, 1},
+ {2, 2},
+ {2, 2},
+ {3, 2},
+ {3},
+ {3},
+ {4},
+ {4},
+ {4},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=MODE(A1:A10)": "3",
+ "=MODE(B1:B6)": "2",
+ "=MODE.MULT(A1:A10)": "3",
+ "=MODE.SNGL(A1:A10)": "3",
+ "=MODE.SNGL(B1:B6)": "2",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=MODE()": {"#VALUE!", "MODE requires at least 1 argument"},
+ "=MODE(0,\"\")": {"#VALUE!", "#VALUE!"},
+ "=MODE(D1:D3)": {"#N/A", "#N/A"},
+ "=MODE.MULT()": {"#VALUE!", "MODE.MULT requires at least 1 argument"},
+ "=MODE.MULT(0,\"\")": {"#VALUE!", "#VALUE!"},
+ "=MODE.MULT(D1:D3)": {"#N/A", "#N/A"},
+ "=MODE.SNGL()": {"#VALUE!", "MODE.SNGL requires at least 1 argument"},
+ "=MODE.SNGL(0,\"\")": {"#VALUE!", "#VALUE!"},
+ "=MODE.SNGL(D1:D3)": {"#N/A", "#N/A"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcPEARSON(t *testing.T) {
+ cellData := [][]interface{}{
+ {"x", "y"},
+ {1, 10.11},
+ {2, 22.9},
+ {2, 27.61},
+ {3, 27.61},
+ {4, 11.15},
+ {5, 31.08},
+ {6, 37.9},
+ {7, 33.49},
+ {8, 21.05},
+ {9, 27.01},
+ {10, 45.78},
+ {11, 31.32},
+ {12, 50.57},
+ {13, 45.48},
+ {14, 40.94},
+ {15, 53.76},
+ {16, 36.18},
+ {17, 49.77},
+ {18, 55.66},
+ {19, 63.83},
+ {20, 63.6},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=PEARSON(A2:A22,B2:B22)": "0.864129542184994",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcPROB(t *testing.T) {
+ cellData := [][]interface{}{
+ {"x", "probability"},
+ {0, 0.1},
+ {1, 0.15},
+ {2, 0.17},
+ {3, 0.22},
+ {4, 0.21},
+ {5, 0.09},
+ {6, 0.05},
+ {7, 0.01},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=PROB(A2:A9,B2:B9,3)": "0.22",
+ "=PROB(A2:A9,B2:B9,3,5)": "0.52",
+ "=PROB(A2:A9,B2:B9,8,10)": "0",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=NA()"))
+ calcError := map[string][]string{
+ "=PROB(A2:A9,B2:B9,3)": {"#NUM!", "#NUM!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcRSQ(t *testing.T) {
+ cellData := [][]interface{}{
+ {"known_y's", "known_x's"},
+ {2, 22.9},
+ {7, 33.49},
+ {8, 34.5},
+ {3, 27.61},
+ {4, 19.5},
+ {1, 10.11},
+ {6, 37.9},
+ {5, 31.08},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=RSQ(A2:A9,B2:B9)": "0.711666290486784",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcSLOP(t *testing.T) {
+ cellData := [][]interface{}{
+ {"known_x's", "known_y's"},
+ {1, 3},
+ {2, 7},
+ {3, 17},
+ {4, 20},
+ {5, 20},
+ {6, 27},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=SLOPE(A2:A7,B2:B7)": "0.200826446280992",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcSHEET(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ formulaList := map[string]string{
+ "=SHEET(\"Sheet2\")": "2",
+ "=SHEET(Sheet2!A1)": "2",
+ "=SHEET(Sheet2!A1:A2)": "2",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula))
+ result, err := f.CalcCellValue("Sheet1", "A1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcSHEETS(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ formulaList := map[string]string{
+ "=SHEETS(Sheet1!A1:B1)": "1",
+ "=SHEETS(Sheet1!A1:Sheet1!B1)": "1",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula))
+ result, err := f.CalcCellValue("Sheet1", "A1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestCalcSTEY(t *testing.T) {
+ cellData := [][]interface{}{
+ {"known_x's", "known_y's"},
+ {1, 3},
+ {2, 7.9},
+ {3, 8},
+ {4, 9.2},
+ {4.5, 12},
+ {5, 10.5},
+ {6, 15},
+ {7, 15.5},
+ {8, 17},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=STEYX(B2:B11,A2:A11)": "1.20118634668221",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=STEYX()": {"#VALUE!", "STEYX requires 2 arguments"},
+ "=STEYX(B2:B11,A1:A9)": {"#N/A", "#N/A"},
+ "=STEYX(B2,A2)": {"#DIV/0!", "#DIV/0!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcTTEST(t *testing.T) {
+ cellData := [][]interface{}{
+ {4, 8, nil, 1, 1},
+ {5, 3, nil, 1, 1},
+ {2, 7},
+ {5, 3},
+ {8, 5},
+ {9, 2},
+ {3, 2},
+ {2, 7},
+ {3, 9},
+ {8, 4},
+ {9, 4},
+ {5, 7},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=TTEST(A1:A12,B1:B12,1,1)": "0.44907068944428",
+ "=TTEST(A1:A12,B1:B12,1,2)": "0.436717306029283",
+ "=TTEST(A1:A12,B1:B12,1,3)": "0.436722015384755",
+ "=TTEST(A1:A12,B1:B12,2,1)": "0.898141378888559",
+ "=TTEST(A1:A12,B1:B12,2,2)": "0.873434612058567",
+ "=TTEST(A1:A12,B1:B12,2,3)": "0.873444030769511",
+ "=T.TEST(A1:A12,B1:B12,1,1)": "0.44907068944428",
+ "=T.TEST(A1:A12,B1:B12,1,2)": "0.436717306029283",
+ "=T.TEST(A1:A12,B1:B12,1,3)": "0.436722015384755",
+ "=T.TEST(A1:A12,B1:B12,2,1)": "0.898141378888559",
+ "=T.TEST(A1:A12,B1:B12,2,2)": "0.873434612058567",
+ "=T.TEST(A1:A12,B1:B12,2,3)": "0.873444030769511",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=TTEST()": {"#VALUE!", "TTEST requires 4 arguments"},
+ "=TTEST(\"\",B1:B12,1,1)": {"#NUM!", "#NUM!"},
+ "=TTEST(A1:A12,\"\",1,1)": {"#NUM!", "#NUM!"},
+ "=TTEST(A1:A12,B1:B12,\"\",1)": {"#VALUE!", "#VALUE!"},
+ "=TTEST(A1:A12,B1:B12,1,\"\")": {"#VALUE!", "#VALUE!"},
+ "=TTEST(A1:A12,B1:B12,0,1)": {"#NUM!", "#NUM!"},
+ "=TTEST(A1:A12,B1:B12,1,0)": {"#NUM!", "#NUM!"},
+ "=TTEST(A1:A2,B1:B1,1,1)": {"#N/A", "#N/A"},
+ "=TTEST(A13:A14,B13:B14,1,1)": {"#NUM!", "#NUM!"},
+ "=TTEST(A12:A13,B12:B13,1,1)": {"#DIV/0!", "#DIV/0!"},
+ "=TTEST(A13:A14,B13:B14,1,2)": {"#NUM!", "#NUM!"},
+ "=TTEST(D1:D4,E1:E4,1,3)": {"#NUM!", "#NUM!"},
+ "=T.TEST()": {"#VALUE!", "T.TEST requires 4 arguments"},
+ "=T.TEST(\"\",B1:B12,1,1)": {"#NUM!", "#NUM!"},
+ "=T.TEST(A1:A12,\"\",1,1)": {"#NUM!", "#NUM!"},
+ "=T.TEST(A1:A12,B1:B12,\"\",1)": {"#VALUE!", "#VALUE!"},
+ "=T.TEST(A1:A12,B1:B12,1,\"\")": {"#VALUE!", "#VALUE!"},
+ "=T.TEST(A1:A12,B1:B12,0,1)": {"#NUM!", "#NUM!"},
+ "=T.TEST(A1:A12,B1:B12,1,0)": {"#NUM!", "#NUM!"},
+ "=T.TEST(A1:A2,B1:B1,1,1)": {"#N/A", "#N/A"},
+ "=T.TEST(A13:A14,B13:B14,1,1)": {"#NUM!", "#NUM!"},
+ "=T.TEST(A12:A13,B12:B13,1,1)": {"#DIV/0!", "#DIV/0!"},
+ "=T.TEST(A13:A14,B13:B14,1,2)": {"#NUM!", "#NUM!"},
+ "=T.TEST(D1:D4,E1:E4,1,3)": {"#NUM!", "#NUM!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) {
+ cellData := [][]interface{}{
+ {"05/01/2019", 43586, "text1"},
+ {"09/13/2019", 43721, "text2"},
+ {"10/01/2019", 43739},
+ {"12/25/2019", 43824},
+ {"01/01/2020", 43831},
+ {"01/01/2020", 43831},
+ {"01/24/2020", 43854},
+ {"04/04/2020", 43925},
+ {"05/01/2020", 43952},
+ {"06/25/2020", 44007},
+ }
+ f := prepareCalcData(cellData)
+ formulaList := map[string]string{
+ "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\")": "183",
+ "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2)": "183",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\")": "183",
+ "=NETWORKDAYS.INTL(\"09/12/2020\",\"01/01/2020\")": "-183",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1)": "183",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",2)": "184",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",3)": "184",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4)": "183",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",5)": "182",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",6)": "182",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",7)": "182",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",11)": "220",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",12)": "220",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",13)": "220",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",14)": "219",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",15)": "219",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",16)": "219",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",17)": "219",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,A1:A12)": "178",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,B1:B12)": "178",
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,C1:C2)": "183",
+ "=WORKDAY(\"12/01/2015\",25)": "42374",
+ "=WORKDAY(\"01/01/2020\",123,B1:B12)": "44006",
+ "=WORKDAY.INTL(\"12/01/2015\",0)": "42339",
+ "=WORKDAY.INTL(\"12/01/2015\",25)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",-25)": "42304",
+ "=WORKDAY.INTL(\"12/01/2015\",25,1)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",25,2)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",25,3)": "42372",
+ "=WORKDAY.INTL(\"12/01/2015\",25,4)": "42373",
+ "=WORKDAY.INTL(\"12/01/2015\",25,5)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",25,6)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",25,7)": "42374",
+ "=WORKDAY.INTL(\"12/01/2015\",25,11)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,12)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,13)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,14)": "42369",
+ "=WORKDAY.INTL(\"12/01/2015\",25,15)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,16)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,17)": "42368",
+ "=WORKDAY.INTL(\"12/01/2015\",25,\"0001100\")": "42374",
+ "=WORKDAY.INTL(\"01/01/2020\",-123,4)": "43659",
+ "=WORKDAY.INTL(\"01/01/2020\",123,4,44010)": "44002",
+ "=WORKDAY.INTL(\"01/01/2020\",-123,4,43640)": "43659",
+ "=WORKDAY.INTL(\"01/01/2020\",-123,4,43660)": "43658",
+ "=WORKDAY.INTL(\"01/01/2020\",-123,7,43660)": "43657",
+ "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12)": "44008",
+ "=WORKDAY.INTL(\"01/01/2020\",123,4,B1:B12)": "44008",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ calcError := map[string][]string{
+ "=NETWORKDAYS()": {"#VALUE!", "NETWORKDAYS requires at least 2 arguments"},
+ "=NETWORKDAYS(\"01/01/2020\",\"09/12/2020\",2,\"\")": {"#VALUE!", "NETWORKDAYS requires at most 3 arguments"},
+ "=NETWORKDAYS(\"\",\"09/12/2020\",2)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS(\"01/01/2020\",\"\",2)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL()": {"#VALUE!", "NETWORKDAYS.INTL requires at least 2 arguments"},
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",4,A1:A12,\"\")": {"#VALUE!", "NETWORKDAYS.INTL requires at most 4 arguments"},
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"January 25, 100\",4)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(\"\",123,4,B1:B12)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"000000x\")": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(\"01/01/2020\",123,\"0000002\")": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",8)": {"#VALUE!", "#VALUE!"},
+ "=NETWORKDAYS.INTL(-1,123)": {"#NUM!", "#NUM!"},
+ "=WORKDAY()": {"#VALUE!", "WORKDAY requires at least 2 arguments"},
+ "=WORKDAY(\"01/01/2020\",123,A1:A12,\"\")": {"#VALUE!", "WORKDAY requires at most 3 arguments"},
+ "=WORKDAY(\"01/01/2020\",\"\",B1:B12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=WORKDAY(\"\",123,B1:B12)": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY(-1,123)": {"#NUM!", "#NUM!"},
+ "=WORKDAY.INTL()": {"#VALUE!", "WORKDAY.INTL requires at least 2 arguments"},
+ "=WORKDAY.INTL(\"01/01/2020\",123,4,A1:A12,\"\")": {"#VALUE!", "WORKDAY.INTL requires at most 4 arguments"},
+ "=WORKDAY.INTL(\"01/01/2020\",\"\",4,B1:B12)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
+ "=WORKDAY.INTL(\"\",123,4,B1:B12)": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY.INTL(\"01/01/2020\",123,\"\",B1:B12)": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY.INTL(\"01/01/2020\",123,\"000000x\")": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY.INTL(\"01/01/2020\",123,\"0000002\")": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY.INTL(\"January 25, 100\",123)": {"#VALUE!", "#VALUE!"},
+ "=WORKDAY.INTL(-1,123)": {"#NUM!", "#NUM!"},
+ }
+ for formula, expected := range calcError {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula))
+ result, err := f.CalcCellValue("Sheet1", "C1")
+ assert.Equal(t, expected[0], result, formula)
+ assert.EqualError(t, err, expected[1], formula)
+ }
+}
+
+func TestCalcZTEST(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5}))
+ formulaList := map[string]string{
+ "=Z.TEST(A1:L1,5)": "0.371103278558538",
+ "=Z.TEST(A1:L1,6)": "0.838129187019751",
+ "=Z.TEST(A1:L1,5,1)": "0.193238115385616",
+ "=ZTEST(A1:L1,5)": "0.371103278558538",
+ "=ZTEST(A1:L1,6)": "0.838129187019751",
+ "=ZTEST(A1:L1,5,1)": "0.193238115385616",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "M1", formula))
+ result, err := f.CalcCellValue("Sheet1", "M1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestStrToDate(t *testing.T) {
+ _, _, _, _, err := strToDate("")
+ assert.Equal(t, formulaErrorVALUE, err.Error)
+}
+
+func TestGetYearDays(t *testing.T) {
+ for _, data := range [][]int{{2021, 0, 360}, {2000, 1, 366}, {2021, 1, 365}, {2000, 3, 365}} {
+ assert.Equal(t, data[2], getYearDays(data[0], data[1]))
+ }
+}
+
+func TestCalcGetBetaHelperContFrac(t *testing.T) {
+ assert.Equal(t, 1.0, getBetaHelperContFrac(1, 0, 1))
+}
+
+func TestCalcGetBetaDistPDF(t *testing.T) {
+ assert.Equal(t, 0.0, getBetaDistPDF(0.5, 2000, 3))
+ assert.Equal(t, 0.0, getBetaDistPDF(0, 1, 0))
+}
+
+func TestCalcD1mach(t *testing.T) {
+ assert.Equal(t, 0.0, d1mach(6))
+}
+
+func TestCalcChebyshevInit(t *testing.T) {
+ assert.Equal(t, 0, chebyshevInit(0, 0, nil))
+ assert.Equal(t, 0, chebyshevInit(1, 0, []float64{0}))
+}
+
+func TestCalcChebyshevEval(t *testing.T) {
+ assert.True(t, math.IsNaN(chebyshevEval(0, 0, nil)))
+}
+
+func TestCalcLgammacor(t *testing.T) {
+ assert.True(t, math.IsNaN(lgammacor(9)))
+ assert.Equal(t, 4.930380657631324e-32, lgammacor(3.7451940309632633e+306))
+ assert.Equal(t, 8.333333333333334e-10, lgammacor(10e+07))
+}
+
+func TestCalcLgammaerr(t *testing.T) {
+ assert.True(t, math.IsNaN(logrelerr(-2)))
+}
+
+func TestCalcLogBeta(t *testing.T) {
+ assert.True(t, math.IsNaN(logBeta(-1, -1)))
+ assert.Equal(t, math.MaxFloat64, logBeta(0, 0))
+}
+
+func TestCalcBetainvProbIterator(t *testing.T) {
+ assert.Equal(t, 1.0, betainvProbIterator(1, 1, 1, 1, 1, 1, 1, 1, 1))
+}
+
+func TestCalcRangeResolver(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=SUM(Sheet1!B:B)"))
+ cellRefs := list.New()
+ cellRanges := list.New()
+ // Test extract value from ranges on invalid ranges
+ cellRanges.PushBack(cellRange{
+ From: cellRef{Col: 1, Row: 1, Sheet: "SheetN"},
+ To: cellRef{Col: 1, Row: TotalRows, Sheet: "SheetN"},
+ })
+ _, err := f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
+ ws, err := f.workSheetReader("Sheet1")
+ ws.SheetData.Row = make([]xlsxRow, TotalRows+1)
+ ws.SheetData.Row[TotalRows].C = make([]xlsxC, 3)
+ assert.NoError(t, err)
+ cellRanges.Init()
+ cellRanges.PushBack(cellRange{
+ From: cellRef{Col: 3, Row: TotalRows, Sheet: "Sheet1"},
+ To: cellRef{Col: 3, Row: TotalRows + 1, Sheet: "Sheet1"},
+ })
+ _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
+ assert.Equal(t, ErrMaxRows, err)
+
+ // Test extract value from references with invalid references
+ cellRanges.Init()
+ cellRefs.PushBack(cellRef{Col: 1, Row: 1, Sheet: "SheetN"})
+ _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
+ cellRefs.Init()
+ cellRefs.PushBack(cellRef{Col: 1, Row: TotalRows + 1, Sheet: "SheetN"})
+ _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
+ assert.Equal(t, ErrMaxRows, err)
+}
+
+func TestNestedFunctionsWithOperators(t *testing.T) {
+ f := NewFile()
+ formulaList := map[string]string{
+ "=LEN(\"KEEP\")": "4",
+ "=LEN(\"REMOVEKEEP\") - LEN(\"REMOVE\")": "4",
+ "=RIGHT(\"REMOVEKEEP\", 4)": "KEEP",
+ "=RIGHT(\"REMOVEKEEP\", 10 - 6))": "KEEP",
+ "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - 6)": "KEEP",
+ "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - LEN(\"REMOV\") - 1)": "KEEP",
+ "=RIGHT(\"REMOVEKEEP\", 10 - LEN(\"REMOVE\"))": "KEEP",
+ "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - LEN(\"REMOVE\"))": "KEEP",
+ }
+ for formula, expected := range formulaList {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula))
+ result, err := f.CalcCellValue("Sheet1", "E1")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+}
+
+func TestFormulaRawCellValueOption(t *testing.T) {
+ f := NewFile()
+ rawTest := []struct {
+ value string
+ raw bool
+ expected string
+ }{
+ {"=VALUE(\"1.0E-07\")", false, "0.00"},
+ {"=VALUE(\"1.0E-07\")", true, "0.0000001"},
+ {"=\"text\"", false, "$text"},
+ {"=\"text\"", true, "text"},
+ {"=\"10e3\"", false, "$10e3"},
+ {"=\"10e3\"", true, "10e3"},
+ {"=\"10\" & \"e3\"", false, "$10e3"},
+ {"=\"10\" & \"e3\"", true, "10e3"},
+ {"=10e3", false, "10000.00"},
+ {"=10e3", true, "10000"},
+ {"=\"1111111111111111\"", false, "$1111111111111111"},
+ {"=\"1111111111111111\"", true, "1111111111111111"},
+ {"=1111111111111111", false, "1111111111111110.00"},
+ {"=1111111111111111", true, "1.11111111111111E+15"},
+ {"=1444.00000000003", false, "1444.00"},
+ {"=1444.00000000003", true, "1444.00000000003"},
+ {"=1444.000000000003", false, "1444.00"},
+ {"=1444.000000000003", true, "1444"},
+ {"=ROUND(1444.00000000000003,2)", false, "1444.00"},
+ {"=ROUND(1444.00000000000003,2)", true, "1444"},
+ }
+ exp := "0.00;0.00;;$@"
+ styleID, err := f.NewStyle(&Style{CustomNumFmt: &exp})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", styleID))
+ for _, test := range rawTest {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", test.value))
+ val, err := f.CalcCellValue("Sheet1", "A1", Options{RawCellValue: test.raw})
+ assert.NoError(t, err)
+ assert.Equal(t, test.expected, val)
+ }
+}
+
+func TestFormulaArgToToken(t *testing.T) {
+ assert.Equal(t,
+ efp.Token{
+ TType: efp.TokenTypeOperand,
+ TSubType: efp.TokenSubTypeLogical,
+ TValue: "TRUE",
+ },
+ formulaArgToToken(newBoolFormulaArg(true)),
+ )
+}
+
+func TestPrepareTrendGrowth(t *testing.T) {
+ assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxX([][]float64{{0, 0}, {0, 0}}))
+ assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxY(false, [][]float64{{0, 0}, {0, 0}}))
+ info, err := prepareTrendGrowth(false, [][]float64{{0, 0}, {0, 0}}, [][]float64{{0, 0}, {0, 0}})
+ assert.Nil(t, info)
+ assert.Equal(t, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM), err)
+}
+
+func TestCalcColRowQRDecomposition(t *testing.T) {
+ assert.False(t, calcRowQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0))
+ assert.False(t, calcColQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0))
+}
+
+func TestCalcCellResolver(t *testing.T) {
+ f := NewFile()
+ // Test reference a cell multiple times in a formula
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "VALUE1"))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=A1"))
+ for formula, expected := range map[string]string{
+ "=CONCATENATE(A1,\"_\",A1)": "VALUE1_VALUE1",
+ "=CONCATENATE(A1,\"_\",A2)": "VALUE1_VALUE1",
+ "=CONCATENATE(A2,\"_\",A2)": "VALUE1_VALUE1",
+ } {
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A3", formula))
+ result, err := f.CalcCellValue("Sheet1", "A3")
+ assert.NoError(t, err, formula)
+ assert.Equal(t, expected, result, formula)
+ }
+ // Test calculates formula that contains a nested argument function which returns a numeric result
+ f = NewFile()
+ for _, cell := range []string{"A1", "B2", "B3", "B4"} {
+ assert.NoError(t, f.SetCellValue("Sheet1", cell, "ABC"))
+ }
+ for cell, formula := range map[string]string{
+ "A2": "IF(B2<>\"\",MAX(A1:A1)+1,\"\")",
+ "A3": "IF(B3<>\"\",MAX(A1:A2)+1,\"\")",
+ "A4": "IF(B4<>\"\",MAX(A1:A3)+1,\"\")",
+ } {
+ assert.NoError(t, f.SetCellFormula("Sheet1", cell, formula))
+ }
+ for cell, expected := range map[string]string{"A2": "1", "A3": "2", "A4": "3"} {
+ result, err := f.CalcCellValue("Sheet1", cell)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result)
+ }
+ // Test calculates formula that reference date and error type cells
+ assert.NoError(t, f.SetCellValue("Sheet1", "C1", "20200208T080910.123"))
+ assert.NoError(t, f.SetCellValue("Sheet1", "C2", "2020-07-10 15:00:00.000"))
+ assert.NoError(t, f.SetCellValue("Sheet1", "C3", formulaErrorDIV))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[2].T = "d"
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[2].V = "20200208T080910.123"
+ ws.(*xlsxWorksheet).SheetData.Row[1].C[2].T = "d"
+ ws.(*xlsxWorksheet).SheetData.Row[1].C[2].V = "2020-07-10 15:00:00.000"
+ ws.(*xlsxWorksheet).SheetData.Row[2].C[2].T = "e"
+ ws.(*xlsxWorksheet).SheetData.Row[2].C[2].V = formulaErrorDIV
+ for _, tbl := range [][]string{
+ {"D1", "=SUM(C1,1)", "43870.3397004977"},
+ {"D2", "=LEN(C2)", "23"},
+ {"D3", "=IFERROR(C3,TRUE)", "TRUE"},
+ } {
+ assert.NoError(t, f.SetCellFormula("Sheet1", tbl[0], tbl[1]))
+ result, err := f.CalcCellValue("Sheet1", tbl[0])
+ assert.NoError(t, err)
+ assert.Equal(t, tbl[2], result)
+ }
+ // Test calculates formula that reference invalid cell
+ assert.NoError(t, f.SetCellValue("Sheet1", "E1", "E1"))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "F1", "=LEN(E1)"))
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, err := f.CalcCellValue("Sheet1", "F1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestEvalInfixExp(t *testing.T) {
+ f := NewFile()
+ arg, err := f.evalInfixExp(nil, "Sheet1", "A1", []efp.Token{
+ {TSubType: efp.TokenSubTypeRange, TValue: "1A"},
+ })
+ assert.Equal(t, arg, newEmptyFormulaArg())
+ assert.Equal(t, formulaErrorNAME, err.Error())
+}
+
+func TestParseToken(t *testing.T) {
+ f := NewFile()
+ assert.Equal(t, formulaErrorNAME, f.parseToken(nil, "Sheet1",
+ efp.Token{TSubType: efp.TokenSubTypeRange, TValue: "1A"}, nil, nil,
+ ).Error())
}
diff --git a/calcchain.go b/calcchain.go
index 03505079b0..edd917d371 100644
--- a/calcchain.go
+++ b/calcchain.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -15,23 +15,19 @@ import (
"bytes"
"encoding/xml"
"io"
- "log"
)
// calcChainReader provides a function to get the pointer to the structure
// after deserialization of xl/calcChain.xml.
-func (f *File) calcChainReader() *xlsxCalcChain {
- var err error
-
+func (f *File) calcChainReader() (*xlsxCalcChain, error) {
if f.CalcChain == nil {
f.CalcChain = new(xlsxCalcChain)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))).
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCalcChain)))).
Decode(f.CalcChain); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return f.CalcChain, err
}
}
-
- return f.CalcChain
+ return f.CalcChain, nil
}
// calcChainWriter provides a function to save xl/calcChain.xml after
@@ -39,29 +35,38 @@ func (f *File) calcChainReader() *xlsxCalcChain {
func (f *File) calcChainWriter() {
if f.CalcChain != nil && f.CalcChain.C != nil {
output, _ := xml.Marshal(f.CalcChain)
- f.saveFileList("xl/calcChain.xml", output)
+ f.saveFileList(defaultXMLPathCalcChain, output)
}
}
// deleteCalcChain provides a function to remove cell reference on the
// calculation chain.
-func (f *File) deleteCalcChain(index int, axis string) {
- calc := f.calcChainReader()
+func (f *File) deleteCalcChain(index int, cell string) error {
+ calc, err := f.calcChainReader()
+ if err != nil {
+ return err
+ }
if calc != nil {
calc.C = xlsxCalcChainCollection(calc.C).Filter(func(c xlsxCalcChainC) bool {
- return !((c.I == index && c.R == axis) || (c.I == index && axis == ""))
+ return !((c.I == index && c.R == cell) || (c.I == index && cell == "") || (c.I == 0 && c.R == cell))
})
}
if len(calc.C) == 0 {
f.CalcChain = nil
- delete(f.XLSX, "xl/calcChain.xml")
- content := f.contentTypesReader()
+ f.Pkg.Delete(defaultXMLPathCalcChain)
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
for k, v := range content.Overrides {
if v.PartName == "/xl/calcChain.xml" {
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
}
}
}
+ return err
}
type xlsxCalcChainCollection []xlsxCalcChainC
@@ -76,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa
}
return results
}
+
+// volatileDepsReader provides a function to get the pointer to the structure
+// after deserialization of xl/volatileDependencies.xml.
+func (f *File) volatileDepsReader() (*xlsxVolTypes, error) {
+ if f.VolatileDeps == nil {
+ volatileDeps, ok := f.Pkg.Load(defaultXMLPathVolatileDeps)
+ if !ok {
+ return f.VolatileDeps, nil
+ }
+ f.VolatileDeps = new(xlsxVolTypes)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(volatileDeps.([]byte)))).
+ Decode(f.VolatileDeps); err != nil && err != io.EOF {
+ return f.VolatileDeps, err
+ }
+ }
+ return f.VolatileDeps, nil
+}
+
+// volatileDepsWriter provides a function to save xl/volatileDependencies.xml
+// after serialize structure.
+func (f *File) volatileDepsWriter() {
+ if f.VolatileDeps != nil {
+ output, _ := xml.Marshal(f.VolatileDeps)
+ f.saveFileList(defaultXMLPathVolatileDeps, output)
+ }
+}
+
+// deleteVolTopicRef provides a function to remove cell reference on the
+// volatile dependencies topic.
+func (vt *xlsxVolTypes) deleteVolTopicRef(i1, i2, i3, i4 int) {
+ for i := range vt.VolType[i1].Main[i2].Tp[i3].Tr {
+ if i == i4 {
+ vt.VolType[i1].Main[i2].Tp[i3].Tr = append(vt.VolType[i1].Main[i2].Tp[i3].Tr[:i], vt.VolType[i1].Main[i2].Tp[i3].Tr[i+1:]...)
+ }
+ }
+}
diff --git a/calcchain_test.go b/calcchain_test.go
index 842dde16df..9256d179e7 100644
--- a/calcchain_test.go
+++ b/calcchain_test.go
@@ -1,12 +1,18 @@
package excelize
-import "testing"
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
func TestCalcChainReader(t *testing.T) {
f := NewFile()
+ // Test read calculation chain with unsupported charset
f.CalcChain = nil
- f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset
- f.calcChainReader()
+ f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset)
+ _, err := f.calcChainReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestDeleteCalcChain(t *testing.T) {
@@ -15,5 +21,26 @@ func TestDeleteCalcChain(t *testing.T) {
f.ContentTypes.Overrides = append(f.ContentTypes.Overrides, xlsxOverride{
PartName: "/xl/calcChain.xml",
})
- f.deleteCalcChain(1, "A1")
+ assert.NoError(t, f.deleteCalcChain(1, "A1"))
+
+ f.CalcChain = nil
+ f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteCalcChain(1, "A1"), "XML syntax error on line 1: invalid UTF-8")
+
+ f.CalcChain = nil
+ f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellFormula("Sheet1", "A1", ""), "XML syntax error on line 1: invalid UTF-8")
+
+ formulaType, ref := STCellFormulaTypeShared, "C1:C5"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+
+ // Test delete calculation chain with unsupported charset calculation chain
+ f.CalcChain = nil
+ f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellValue("Sheet1", "C1", true), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test delete calculation chain with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteCalcChain(1, "A1"), "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/cell.go b/cell.go
index 3293d19fe4..f2df30478c 100644
--- a/cell.go
+++ b/cell.go
@@ -1,25 +1,43 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"encoding/xml"
- "errors"
"fmt"
+ "math"
+ "os"
"reflect"
"strconv"
"strings"
- "sync"
"time"
+ "unicode/utf8"
+
+ "github.com/xuri/efp"
+)
+
+// CellType is the type of cell value type.
+type CellType byte
+
+// Cell value types enumeration.
+const (
+ CellTypeUnset CellType = iota
+ CellTypeBool
+ CellTypeDate
+ CellTypeError
+ CellTypeFormula
+ CellTypeInlineString
+ CellTypeNumber
+ CellTypeSharedString
)
const (
@@ -33,192 +51,323 @@ const (
STCellFormulaTypeShared = "shared"
)
-var rwMutex sync.RWMutex
+// cellTypes mapping the cell's data type and enumeration.
+var cellTypes = map[string]CellType{
+ "b": CellTypeBool,
+ "d": CellTypeDate,
+ "n": CellTypeNumber,
+ "e": CellTypeError,
+ "s": CellTypeSharedString,
+ "str": CellTypeFormula,
+ "inlineStr": CellTypeInlineString,
+}
// GetCellValue provides a function to get formatted value from cell by given
-// worksheet name and axis in XLSX file. If it is possible to apply a format
-// to the cell value, it will do so, if not then an error will be returned,
-// along with the raw value of the cell.
-func (f *File) GetCellValue(sheet, axis string) (string, error) {
- return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
- val, err := c.getValueFrom(f, f.sharedStringsReader())
+// worksheet name and cell reference in spreadsheet. The return value is
+// converted to the 'string' data type. This function is concurrency safe. If
+// the cell format can be applied to the value of a cell, the applied value
+// will be returned, otherwise the original value will be returned. All cells'
+// values will be the same in a merged range.
+func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) {
+ return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
+ sst, err := f.sharedStringsReader()
+ if err != nil {
+ return "", true, err
+ }
+ val, err := c.getValueFrom(f, sst, f.getOptions(opts...).RawCellValue)
return val, true, err
})
}
-// SetCellValue provides a function to set value of a cell. The specified
-// coordinates should not be in the first row of the table. The following
-// shows the supported data types:
-//
-// int
-// int8
-// int16
-// int32
-// int64
-// uint
-// uint8
-// uint16
-// uint32
-// uint64
-// float32
-// float64
-// string
-// []byte
-// time.Duration
-// time.Time
-// bool
-// nil
-//
-// Note that default date format is m/d/yy h:mm of time.Time type value. You can
-// set numbers format by SetCellStyle() method.
-func (f *File) SetCellValue(sheet, axis string, value interface{}) error {
+// GetCellType provides a function to get the cell's data type by given
+// worksheet name and cell reference in spreadsheet file.
+func (f *File) GetCellType(sheet, cell string) (CellType, error) {
+ var (
+ err error
+ cellTypeStr string
+ cellType CellType
+ )
+ if cellTypeStr, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
+ return c.T, true, nil
+ }); err != nil {
+ return CellTypeUnset, err
+ }
+ cellType = cellTypes[cellTypeStr]
+ return cellType, err
+}
+
+// SetCellValue provides a function to set the value of a cell. This function
+// is concurrency safe. The specified coordinates should not be in the first
+// row of the table, a complex number can be set with string text. The
+// following shows the supported data types:
+//
+// int
+// int8
+// int16
+// int32
+// int64
+// uint
+// uint8
+// uint16
+// uint32
+// uint64
+// float32
+// float64
+// string
+// []byte
+// time.Duration
+// time.Time
+// bool
+// nil
+//
+// Note that default date format is m/d/yy h:mm of time.Time type value. You
+// can set numbers format by the SetCellStyle function. If you need to set the
+// specialized date in Excel like January 0, 1900 or February 29, 1900, these
+// times can not representation in Go language time.Time data type. Please set
+// the cell value as number 0 or 60, then create and bind the date-time number
+// format style for the cell.
+func (f *File) SetCellValue(sheet, cell string, value interface{}) error {
var err error
switch v := value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
- err = f.setCellIntFunc(sheet, axis, v)
+ err = f.setCellIntFunc(sheet, cell, v)
case float32:
- err = f.SetCellFloat(sheet, axis, float64(v), -1, 32)
+ err = f.SetCellFloat(sheet, cell, float64(v), -1, 32)
case float64:
- err = f.SetCellFloat(sheet, axis, v, -1, 64)
+ err = f.SetCellFloat(sheet, cell, v, -1, 64)
case string:
- err = f.SetCellStr(sheet, axis, v)
+ err = f.SetCellStr(sheet, cell, v)
case []byte:
- err = f.SetCellStr(sheet, axis, string(v))
+ err = f.SetCellStr(sheet, cell, string(v))
case time.Duration:
_, d := setCellDuration(v)
- err = f.SetCellDefault(sheet, axis, d)
+ err = f.SetCellDefault(sheet, cell, d)
if err != nil {
return err
}
- err = f.setDefaultTimeStyle(sheet, axis, 21)
+ err = f.setDefaultTimeStyle(sheet, cell, getDurationNumFmt(v))
case time.Time:
- err = f.setCellTimeFunc(sheet, axis, v)
+ err = f.setCellTimeFunc(sheet, cell, v)
case bool:
- err = f.SetCellBool(sheet, axis, v)
+ err = f.SetCellBool(sheet, cell, v)
case nil:
- err = f.SetCellStr(sheet, axis, "")
+ err = f.SetCellDefault(sheet, cell, "")
default:
- err = f.SetCellStr(sheet, axis, fmt.Sprint(value))
+ err = f.SetCellStr(sheet, cell, fmt.Sprint(value))
}
return err
}
+// String extracts characters from a string item.
+func (x xlsxSI) String() string {
+ var value strings.Builder
+ if x.T != nil {
+ value.WriteString(x.T.Val)
+ }
+ for _, s := range x.R {
+ if s.T != nil {
+ value.WriteString(s.T.Val)
+ }
+ }
+ if value.Len() == 0 {
+ return ""
+ }
+ return bstrUnmarshal(value.String())
+}
+
+// hasValue determine if cell non-blank value.
+func (c *xlsxC) hasValue() bool {
+ return c.S != 0 || c.V != "" || c.F != nil || c.T != ""
+}
+
+// removeFormula delete formula for the cell.
+func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error {
+ if c.F != nil && c.Vm == nil {
+ sheetID := f.getSheetID(sheet)
+ if err := f.deleteCalcChain(sheetID, c.R); err != nil {
+ return err
+ }
+ if c.F.T == STCellFormulaTypeShared && c.F.Ref != "" {
+ si := c.F.Si
+ for r, row := range ws.SheetData.Row {
+ for col, cell := range row.C {
+ if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si {
+ ws.SheetData.Row[r].C[col].F = nil
+ _ = f.deleteCalcChain(sheetID, cell.R)
+ }
+ }
+ }
+ }
+ c.F = nil
+ }
+ return nil
+}
+
// setCellIntFunc is a wrapper of SetCellInt.
-func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error {
+func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error {
var err error
switch v := value.(type) {
case int:
- err = f.SetCellInt(sheet, axis, v)
+ err = f.SetCellInt(sheet, cell, int64(v))
case int8:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellInt(sheet, cell, int64(v))
case int16:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellInt(sheet, cell, int64(v))
case int32:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellInt(sheet, cell, int64(v))
case int64:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellInt(sheet, cell, v)
case uint:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellUint(sheet, cell, uint64(v))
case uint8:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellUint(sheet, cell, uint64(v))
case uint16:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellUint(sheet, cell, uint64(v))
case uint32:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellUint(sheet, cell, uint64(v))
case uint64:
- err = f.SetCellInt(sheet, axis, int(v))
+ err = f.SetCellUint(sheet, cell, v)
}
return err
}
// setCellTimeFunc provides a method to process time type of value for
// SetCellValue.
-func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error {
- xlsx, err := f.workSheetReader(sheet)
+func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
-
- var isNum bool
- cellData.T, cellData.V, isNum, err = setCellTime(value)
+ ws.mu.Lock()
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ ws.mu.Unlock()
+ var date1904, isNum bool
+ wb, err := f.workbookReader()
if err != nil {
return err
}
+ if wb != nil && wb.WorkbookPr != nil {
+ date1904 = wb.WorkbookPr.Date1904
+ }
+ if isNum, err = c.setCellTime(value, date1904); err != nil {
+ return err
+ }
if isNum {
- err = f.setDefaultTimeStyle(sheet, axis, 22)
- if err != nil {
- return err
- }
+ _ = f.setDefaultTimeStyle(sheet, cell, getTimeNumFmt(value))
}
return err
}
-func setCellTime(value time.Time) (t string, b string, isNum bool, err error) {
+// setCellTime prepares cell type and Excel time by given Go time.Time type
+// timestamp.
+func (c *xlsxC) setCellTime(value time.Time, date1904 bool) (isNum bool, err error) {
var excelTime float64
- excelTime, err = timeToExcelTime(value)
- if err != nil {
+ _, offset := value.In(value.Location()).Zone()
+ value = value.Add(time.Duration(offset) * time.Second)
+ if excelTime, err = timeToExcelTime(value, date1904); err != nil {
return
}
isNum = excelTime > 0
if isNum {
- t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64))
+ c.setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64))
} else {
- t, b = setCellDefault(value.Format(time.RFC3339Nano))
+ c.setCellDefault(value.Format(time.RFC3339Nano))
}
return
}
+// setCellDuration prepares cell type and value by given Go time.Duration type
+// time duration.
func setCellDuration(value time.Duration) (t string, v string) {
- v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32)
+ v = strconv.FormatFloat(value.Seconds()/86400, 'f', -1, 32)
return
}
// SetCellInt provides a function to set int type value of a cell by given
-// worksheet name, cell coordinates and cell value.
-func (f *File) SetCellInt(sheet, axis string, value int) error {
- rwMutex.Lock()
- defer rwMutex.Unlock()
- xlsx, err := f.workSheetReader(sheet)
+// worksheet name, cell reference and cell value.
+func (f *File) SetCellInt(sheet, cell string, value int64) error {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
- cellData.T, cellData.V = setCellInt(value)
- return err
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ c.T, c.V = setCellInt(value)
+ c.IS = nil
+ return f.removeFormula(c, ws, sheet)
+}
+
+// setCellInt prepares cell type and string type cell value by a given integer.
+func setCellInt(value int64) (t string, v string) {
+ v = strconv.FormatInt(value, 10)
+ return
+}
+
+// SetCellUint provides a function to set uint type value of a cell by given
+// worksheet name, cell reference and cell value.
+func (f *File) SetCellUint(sheet, cell string, value uint64) error {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ f.mu.Unlock()
+ return err
+ }
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
+ if err != nil {
+ return err
+ }
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ c.T, c.V = setCellUint(value)
+ c.IS = nil
+ return f.removeFormula(c, ws, sheet)
}
-func setCellInt(value int) (t string, v string) {
- v = strconv.Itoa(value)
+// setCellUint prepares cell type and string type cell value by a given unsigned
+// integer.
+func setCellUint(value uint64) (t string, v string) {
+ v = strconv.FormatUint(value, 10)
return
}
// SetCellBool provides a function to set bool type value of a cell by given
-// worksheet name, cell name and cell value.
-func (f *File) SetCellBool(sheet, axis string, value bool) error {
- rwMutex.Lock()
- defer rwMutex.Unlock()
- xlsx, err := f.workSheetReader(sheet)
+// worksheet name, cell reference and cell value.
+func (f *File) SetCellBool(sheet, cell string, value bool) error {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
- cellData.T, cellData.V = setCellBool(value)
- return err
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ c.T, c.V = setCellBool(value)
+ c.IS = nil
+ return f.removeFormula(c, ws, sheet)
}
+// setCellBool prepares cell type and string type cell value by a given boolean
+// value.
func setCellBool(value bool) (t string, v string) {
t = "b"
if value {
@@ -229,134 +378,318 @@ func setCellBool(value bool) (t string, v string) {
return
}
-// SetCellFloat sets a floating point value into a cell. The prec parameter
-// specifies how many places after the decimal will be shown while -1 is a
-// special value that will use as many decimal places as necessary to
-// represent the number. bitSize is 32 or 64 depending on if a float32 or
-// float64 was originally used for the value. For Example:
-//
-// var x float32 = 1.325
-// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32)
+// SetCellFloat sets a floating point value into a cell. The precision
+// parameter specifies how many places after the decimal will be shown
+// while -1 is a special value that will use as many decimal places as
+// necessary to represent the number. bitSize is 32 or 64 depending on if a
+// float32 or float64 was originally used for the value. For Example:
//
-func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error {
- rwMutex.Lock()
- defer rwMutex.Unlock()
- xlsx, err := f.workSheetReader(sheet)
+// var x float32 = 1.325
+// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32)
+func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error {
+ if math.IsNaN(value) || math.IsInf(value, 0) {
+ return f.SetCellStr(sheet, cell, fmt.Sprint(value))
+ }
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
- cellData.T, cellData.V = setCellFloat(value, prec, bitSize)
- return err
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ c.setCellFloat(value, precision, bitSize)
+ return f.removeFormula(c, ws, sheet)
}
-func setCellFloat(value float64, prec, bitSize int) (t string, v string) {
- v = strconv.FormatFloat(value, 'f', prec, bitSize)
- return
+// setCellFloat prepares cell type and string type cell value by a given float
+// value.
+func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) {
+ if math.IsNaN(value) || math.IsInf(value, 0) {
+ c.setInlineStr(fmt.Sprint(value))
+ return
+ }
+ c.T, c.V = "", strconv.FormatFloat(value, 'f', precision, bitSize)
+ c.IS = nil
}
// SetCellStr provides a function to set string type value of a cell. Total
// number of characters that a cell can contain 32767 characters.
-func (f *File) SetCellStr(sheet, axis, value string) error {
- rwMutex.Lock()
- defer rwMutex.Unlock()
- xlsx, err := f.workSheetReader(sheet)
+func (f *File) SetCellStr(sheet, cell, value string) error {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
- cellData.T, cellData.V = f.setCellString(value)
- return err
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ if c.T, c.V, err = f.setCellString(value); err != nil {
+ return err
+ }
+ c.IS = nil
+ return f.removeFormula(c, ws, sheet)
}
-// setCellString provides a function to set string type to shared string
-// table.
-func (f *File) setCellString(value string) (t string, v string) {
- if len(value) > TotalCellChars {
- value = value[0:TotalCellChars]
+// setCellString provides a function to set string type to shared string table.
+func (f *File) setCellString(value string) (t, v string, err error) {
+ if utf8.RuneCountInString(value) > TotalCellChars {
+ value = string([]rune(value)[:TotalCellChars])
}
t = "s"
- v = strconv.Itoa(f.setSharedString(value))
+ var si int
+ if si, err = f.setSharedString(value); err != nil {
+ return
+ }
+ v = strconv.Itoa(si)
+ return
+}
+
+// sharedStringsLoader load shared string table from system temporary file to
+// memory, and reset shared string table for reader.
+func (f *File) sharedStringsLoader() (err error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
+ f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings))
+ f.tempFiles.Delete(defaultXMLPathSharedStrings)
+ if err = os.Remove(path.(string)); err != nil {
+ return
+ }
+ f.SharedStrings = nil
+ }
+ if f.sharedStringTemp != nil {
+ if err := f.sharedStringTemp.Close(); err != nil {
+ return err
+ }
+ f.tempFiles.Delete(defaultTempFileSST)
+ f.sharedStringItem, err = nil, os.Remove(f.sharedStringTemp.Name())
+ f.sharedStringTemp = nil
+ }
return
}
// setSharedString provides a function to add string to the share string table.
-func (f *File) setSharedString(val string) int {
- sst := f.sharedStringsReader()
+func (f *File) setSharedString(val string) (int, error) {
+ if err := f.sharedStringsLoader(); err != nil {
+ return 0, err
+ }
+ sst, err := f.sharedStringsReader()
+ if err != nil {
+ return 0, err
+ }
+ f.mu.Lock()
+ defer f.mu.Unlock()
if i, ok := f.sharedStringsMap[val]; ok {
- return i
+ return i, nil
}
- sst.Count++
- sst.UniqueCount++
+ sst.mu.Lock()
+ defer sst.mu.Unlock()
t := xlsxT{Val: val}
- // Leading and ending space(s) character detection.
- if len(val) > 0 && (val[0] == 32 || val[len(val)-1] == 32) {
- ns := xml.Attr{
- Name: xml.Name{Space: NameSpaceXML, Local: "space"},
- Value: "preserve",
- }
- t.Space = ns
- }
+ val, t.Space = trimCellValue(val, false)
sst.SI = append(sst.SI, xlsxSI{T: &t})
+ sst.Count = len(sst.SI)
+ sst.UniqueCount = sst.Count
f.sharedStringsMap[val] = sst.UniqueCount - 1
- return sst.UniqueCount - 1
+ return sst.UniqueCount - 1, nil
}
-// setCellStr provides a function to set string type to cell.
-func setCellStr(value string) (t string, v string, ns xml.Attr) {
- if len(value) > TotalCellChars {
- value = value[0:TotalCellChars]
+// trimCellValue provides a function to set string type to cell.
+func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
+ if utf8.RuneCountInString(value) > TotalCellChars {
+ value = string([]rune(value)[:TotalCellChars])
}
- // Leading and ending space(s) character detection.
- if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) {
- ns = xml.Attr{
- Name: xml.Name{Space: NameSpaceXML, Local: "space"},
- Value: "preserve",
+ if value != "" {
+ prefix, suffix := value[0], value[len(value)-1]
+ for _, ascii := range []byte{9, 10, 13, 32} {
+ if prefix == ascii || suffix == ascii {
+ ns = xml.Attr{
+ Name: xml.Name{Space: NameSpaceXML, Local: "space"},
+ Value: "preserve",
+ }
+ break
+ }
+ }
+
+ if escape {
+ var buf strings.Builder
+ _ = xml.EscapeText(&buf, []byte(value))
+ value = strings.ReplaceAll(buf.String(), "
", "\n")
}
}
- t = "str"
- v = value
+ v = bstrMarshal(value)
return
}
+// setCellValue set cell data type and value for (inline) rich string cell or
+// formula cell.
+func (c *xlsxC) setCellValue(val string) {
+ if c.F != nil {
+ c.setStr(val)
+ return
+ }
+ c.setInlineStr(val)
+}
+
+// setInlineStr set cell data type and value which containing an (inline) rich
+// string.
+func (c *xlsxC) setInlineStr(val string) {
+ c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}}
+ c.IS.T.Val, c.IS.T.Space = trimCellValue(val, true)
+}
+
+// setStr set cell data type and value which containing a formula string.
+func (c *xlsxC) setStr(val string) {
+ c.T, c.IS = "str", nil
+ c.V, c.XMLSpace = trimCellValue(val, false)
+}
+
+// getCellBool parse cell value which containing a boolean.
+func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) {
+ if !raw {
+ if c.V == "1" {
+ return "TRUE", nil
+ }
+ if c.V == "0" {
+ return "FALSE", nil
+ }
+ }
+ return f.formattedValue(c, raw, CellTypeBool)
+}
+
+// setCellDefault prepares cell type and string type cell value by a given
+// string.
+func (c *xlsxC) setCellDefault(value string) {
+ if ok, _, _ := isNumeric(value); !ok {
+ if value != "" {
+ c.setInlineStr(value)
+ c.IS.T.Val = value
+ return
+ }
+ c.T, c.V, c.IS = value, value, nil
+ return
+ }
+ c.T, c.V = "", value
+}
+
+// getCellDate parse cell value which contains a date in the ISO 8601 format.
+func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
+ if !raw {
+ layout := "20060102T150405.999"
+ if strings.HasSuffix(c.V, "Z") {
+ layout = "20060102T150405Z"
+ if strings.Contains(c.V, "-") {
+ layout = "2006-01-02T15:04:05Z"
+ }
+ } else if strings.Contains(c.V, "-") {
+ layout = "2006-01-02 15:04:05Z"
+ }
+ if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil {
+ excelTime, _ := timeToExcelTime(timestamp, false)
+ c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
+ }
+ }
+ return f.formattedValue(c, raw, CellTypeDate)
+}
+
+// getValueFrom return a value from a column/row cell, this function is
+// intended to be used with for range on rows an argument with the spreadsheet
+// opened file.
+func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
+ switch c.T {
+ case "b":
+ return c.getCellBool(f, raw)
+ case "d":
+ return c.getCellDate(f, raw)
+ case "s":
+ if c.V != "" {
+ xlsxSI, _ := strconv.Atoi(strings.TrimSpace(c.V))
+ if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
+ return f.formattedValue(&xlsxC{S: c.S, V: f.getFromStringItem(xlsxSI)}, raw, CellTypeSharedString)
+ }
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ if len(d.SI) > xlsxSI {
+ return f.formattedValue(&xlsxC{S: c.S, V: d.SI[xlsxSI].String()}, raw, CellTypeSharedString)
+ }
+ }
+ return f.formattedValue(c, raw, CellTypeSharedString)
+ case "str":
+ return c.V, nil
+ case "inlineStr":
+ if c.IS != nil {
+ return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)
+ }
+ return f.formattedValue(c, raw, CellTypeInlineString)
+ default:
+ if isNum, precision, decimal := isNumeric(c.V); isNum && !raw {
+ if precision > 15 {
+ c.V = strconv.FormatFloat(decimal, 'G', 15, 64)
+ } else {
+ c.V = strconv.FormatFloat(decimal, 'f', -1, 64)
+ }
+ }
+ return f.formattedValue(c, raw, CellTypeNumber)
+ }
+}
+
// SetCellDefault provides a function to set string type value of a cell as
// default format without escaping the cell.
-func (f *File) SetCellDefault(sheet, axis, value string) error {
- xlsx, err := f.workSheetReader(sheet)
+func (f *File) SetCellDefault(sheet, cell, value string) error {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
- cellData.T, cellData.V = setCellDefault(value)
- return err
+ c.S = ws.prepareCellStyle(col, row, c.S)
+ c.setCellDefault(value)
+ return f.removeFormula(c, ws, sheet)
}
-func setCellDefault(value string) (t string, v string) {
- v = value
- return
+// GetCellFormula provides a function to get formula from cell by given
+// worksheet name and cell reference in spreadsheet.
+func (f *File) GetCellFormula(sheet, cell string) (string, error) {
+ return f.getCellFormula(sheet, cell, false)
}
-// GetCellFormula provides a function to get formula from cell by given
-// worksheet name and axis in XLSX file.
-func (f *File) GetCellFormula(sheet, axis string) (string, error) {
- return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
+// getCellFormula provides a function to get transformed formula from cell by
+// given worksheet name and cell reference in spreadsheet.
+func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, error) {
+ return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
+ if transformed && !f.formulaChecked {
+ if err := f.setArrayFormulaCells(); err != nil {
+ return "", false, err
+ }
+ f.formulaChecked = true
+ }
+ if transformed && c.f != "" {
+ return c.f, true, nil
+ }
if c.F == nil {
return "", false, nil
}
- if c.F.T == STCellFormulaTypeShared {
- return getSharedForumula(x, c.F.Si), true, nil
+ if c.F.T == STCellFormulaTypeShared && c.F.Si != nil {
+ return getSharedFormula(x, *c.F.Si, c.R), true, nil
}
return c.F.Content, true, nil
})
@@ -368,69 +701,252 @@ type FormulaOpts struct {
Ref *string // Shared formula ref
}
-// SetCellFormula provides a function to set cell formula by given string and
-// worksheet name.
-func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error {
- rwMutex.Lock()
- defer rwMutex.Unlock()
- xlsx, err := f.workSheetReader(sheet)
+// SetCellFormula provides a function to set formula on the cell is taken
+// according to the given worksheet name and cell formula settings. The result
+// of the formula cell can be calculated when the worksheet is opened by the
+// Office Excel application or can be using the "CalcCellValue" function also
+// can get the calculated cell value. If the Excel application doesn't
+// calculate the formula automatically when the workbook has been opened,
+// please call "UpdateLinkedValue" after setting the cell formula functions.
+//
+// Example 1, set normal formula "=SUM(A1,B1)" for the cell "A3" on "Sheet1":
+//
+// err := f.SetCellFormula("Sheet1", "A3", "=SUM(A1,B1)")
+//
+// Example 2, set one-dimensional vertical constant array (column array) formula
+// "1,2,3" for the cell "A3" on "Sheet1":
+//
+// err := f.SetCellFormula("Sheet1", "A3", "={1;2;3}")
+//
+// Example 3, set one-dimensional horizontal constant array (row array)
+// formula '"a","b","c"' for the cell "A3" on "Sheet1":
+//
+// err := f.SetCellFormula("Sheet1", "A3", "={\"a\",\"b\",\"c\"}")
+//
+// Example 4, set two-dimensional constant array formula '{1,2,"a","b"}' for
+// the cell "A3" on "Sheet1":
+//
+// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3"
+// err := f.SetCellFormula("Sheet1", "A3", "={1,2;\"a\",\"b\"}",
+// excelize.FormulaOpts{Ref: &ref, Type: &formulaType})
+//
+// Example 5, set range array formula "A1:A2" for the cell "A3" on "Sheet1":
+//
+// formulaType, ref := excelize.STCellFormulaTypeArray, "A3:A3"
+// err := f.SetCellFormula("Sheet1", "A3", "=A1:A2",
+// excelize.FormulaOpts{Ref: &ref, Type: &formulaType})
+//
+// Example 6, set shared formula "=A1+B1" for the cell "C1:C5"
+// on "Sheet1", "C1" is the master cell:
+//
+// formulaType, ref := excelize.STCellFormulaTypeShared, "C1:C5"
+// err := f.SetCellFormula("Sheet1", "C1", "=A1+B1",
+// excelize.FormulaOpts{Ref: &ref, Type: &formulaType})
+//
+// Example 7, set table formula "=SUM(Table1[[A]:[B]])" for the cell "C2"
+// on "Sheet1":
+//
+// package main
+//
+// import (
+// "fmt"
+//
+// "github.com/xuri/excelize/v2"
+// )
+//
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} {
+// if err := f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row); err != nil {
+// fmt.Println(err)
+// return
+// }
+// }
+// if err := f.AddTable("Sheet1", &excelize.Table{
+// Range: "A1:C2", Name: "Table1", StyleName: "TableStyleMedium2",
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// formulaType := excelize.STCellFormulaTypeDataTable
+// if err := f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])",
+// excelize.FormulaOpts{Type: &formulaType}); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
+func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- cellData, _, _, err := f.prepareCell(xlsx, sheet, axis)
+ c, _, _, err := ws.prepareCell(cell)
if err != nil {
return err
}
if formula == "" {
- cellData.F = nil
- f.deleteCalcChain(f.getSheetID(sheet), axis)
- return err
+ c.F = nil
+ return f.deleteCalcChain(f.getSheetID(sheet), cell)
}
- if cellData.F != nil {
- cellData.F.Content = formula
+ if c.F != nil {
+ c.F.Content = formula
} else {
- cellData.F = &xlsxF{Content: formula}
+ c.F = &xlsxF{Content: formula}
}
- for _, o := range opts {
- if o.Type != nil {
- cellData.F.T = *o.Type
+ for _, opt := range opts {
+ if opt.Type != nil {
+ if *opt.Type == STCellFormulaTypeDataTable {
+ return err
+ }
+ c.F.T = *opt.Type
+ if c.F.T == STCellFormulaTypeArray && opt.Ref != nil {
+ if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil {
+ return err
+ }
+ }
+ if c.F.T == STCellFormulaTypeShared {
+ if err = ws.setSharedFormula(*opt.Ref); err != nil {
+ return err
+ }
+ }
+ }
+ if opt.Ref != nil {
+ c.F.Ref = *opt.Ref
}
+ }
+ c.T, c.IS = "str", nil
+ return err
+}
- if o.Ref != nil {
- cellData.F.Ref = *o.Ref
+// setArrayFormula transform the array formula in an array formula range to the
+// normal formula and set cells in this range to the formula as the normal
+// formula.
+func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error {
+ if len(strings.Split(formula.Ref, ":")) < 2 {
+ return nil
+ }
+ coordinates, err := rangeRefToCoordinates(formula.Ref)
+ if err != nil {
+ return err
+ }
+ _ = sortCoordinates(coordinates)
+ tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames)
+ if err != nil {
+ return err
+ }
+ topLeftCol, topLeftRow := coordinates[0], coordinates[1]
+ for c := coordinates[0]; c <= coordinates[2]; c++ {
+ for r := coordinates[1]; r <= coordinates[3]; r++ {
+ colOffset, rowOffset := c-topLeftCol, r-topLeftRow
+ for i, af := range arrayFormulaOperandTokens {
+ colNum, rowNum := af.topLeftCol+colOffset, af.topLeftRow+rowOffset
+ if colNum <= af.bottomRightCol && rowNum <= af.bottomRightRow {
+ arrayFormulaOperandTokens[i].targetCellRef, _ = CoordinatesToCellName(colNum, rowNum)
+ }
+ }
+ ws.prepareSheetXML(c, r)
+ if cell := &ws.SheetData.Row[r-1].C[c-1]; cell.f == "" {
+ cell.f = transformArrayFormula(tokens, arrayFormulaOperandTokens)
+ }
+ }
+ }
+ return err
+}
+
+// setArrayFormulaCells transform the array formula in all worksheets to the
+// normal formula and set cells in the array formula reference range to the
+// formula as the normal formula.
+func (f *File) setArrayFormulaCells() error {
+ definedNames := f.GetDefinedName()
+ for _, sheetN := range f.GetSheetList() {
+ ws, err := f.workSheetReader(sheetN)
+ if err != nil {
+ if err.Error() == newNotWorksheetError(sheetN).Error() {
+ continue
+ }
+ return err
+ }
+ for _, row := range ws.SheetData.Row {
+ for _, cell := range row.C {
+ if cell.F != nil && cell.F.T == STCellFormulaTypeArray {
+ if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil {
+ return err
+ }
+ }
+ }
}
}
+ return nil
+}
+// setSharedFormula set shared formula for the cells.
+func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
+ coordinates, err := rangeRefToCoordinates(ref)
+ if err != nil {
+ return err
+ }
+ _ = sortCoordinates(coordinates)
+ cnt := ws.countSharedFormula()
+ for c := coordinates[0]; c <= coordinates[2]; c++ {
+ for r := coordinates[1]; r <= coordinates[3]; r++ {
+ ws.prepareSheetXML(c, r)
+ cell := &ws.SheetData.Row[r-1].C[c-1]
+ if cell.F == nil {
+ cell.F = &xlsxF{}
+ }
+ cell.F.T = STCellFormulaTypeShared
+ cell.F.Si = &cnt
+ }
+ }
return err
}
-// GetCellHyperLink provides a function to get cell hyperlink by given
-// worksheet name and axis. Boolean type value link will be ture if the cell
-// has a hyperlink and the target is the address of the hyperlink. Otherwise,
-// the value of link will be false and the value of the target will be a blank
-// string. For example get hyperlink of Sheet1!H6:
+// countSharedFormula count shared formula in the given worksheet.
+func (ws *xlsxWorksheet) countSharedFormula() (count int) {
+ for _, row := range ws.SheetData.Row {
+ for _, cell := range row.C {
+ if cell.F != nil && cell.F.Si != nil && *cell.F.Si+1 > count {
+ count = *cell.F.Si + 1
+ }
+ }
+ }
+ return
+}
+
+// GetCellHyperLink gets a cell hyperlink based on the given worksheet name and
+// cell reference. If the cell has a hyperlink, it will return 'true' and
+// the link address, otherwise it will return 'false' and an empty link
+// address.
//
-// link, target, err := f.GetCellHyperLink("Sheet1", "H6")
+// For example, get a hyperlink to a 'H6' cell on a worksheet named 'Sheet1':
//
-func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) {
+// link, target, err := f.GetCellHyperLink("Sheet1", "H6")
+func (f *File) GetCellHyperLink(sheet, cell string) (bool, string, error) {
// Check for correct cell name
- if _, _, err := SplitCellName(axis); err != nil {
- return false, "", err
- }
-
- xlsx, err := f.workSheetReader(sheet)
- if err != nil {
+ if _, _, err := SplitCellName(cell); err != nil {
return false, "", err
}
- axis, err = f.mergeCellsParser(xlsx, axis)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return false, "", err
}
- if xlsx.Hyperlinks != nil {
- for _, link := range xlsx.Hyperlinks.Hyperlink {
- if link.Ref == axis {
+ if ws.Hyperlinks != nil {
+ for _, link := range ws.Hyperlinks.Hyperlink {
+ ok, err := f.checkCellInRangeRef(cell, link.Ref)
+ if err != nil {
+ return false, "", err
+ }
+ if link.Ref == cell || ok {
if link.RID != "" {
return true, f.getSheetRelationshipsTargetByID(sheet, link.RID), err
}
@@ -441,250 +957,463 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) {
return false, "", err
}
+// HyperlinkOpts can be passed to SetCellHyperlink to set optional hyperlink
+// attributes (e.g. display value)
+type HyperlinkOpts struct {
+ Display *string
+ Tooltip *string
+}
+
+// removeHyperLink remove hyperlink for worksheet and delete relationships for
+// the worksheet by given sheet name and cell reference. Note that if the cell
+// in a range reference, the whole hyperlinks will be deleted.
+func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error {
+ for idx := 0; idx < len(ws.Hyperlinks.Hyperlink); idx++ {
+ link := ws.Hyperlinks.Hyperlink[idx]
+ ok, err := f.checkCellInRangeRef(cell, link.Ref)
+ if err != nil {
+ return err
+ }
+ if link.Ref == cell || ok {
+ ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:idx], ws.Hyperlinks.Hyperlink[idx+1:]...)
+ idx--
+ f.deleteSheetRelationships(sheet, link.RID)
+ }
+ }
+ if len(ws.Hyperlinks.Hyperlink) == 0 {
+ ws.Hyperlinks = nil
+ }
+ return nil
+}
+
// SetCellHyperLink provides a function to set cell hyperlink by given
-// worksheet name and link URL address. LinkType defines two types of
-// hyperlink "External" for web site or "Location" for moving to one of cell
-// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The
-// below is example for external link.
+// worksheet name and link URL address. LinkType defines three types of
+// hyperlink "External" for website or "Location" for moving to one of cell in
+// this workbook or "None" for remove hyperlink. Maximum limit hyperlinks in a
+// worksheet is 65530. This function is only used to set the hyperlink of the
+// cell and doesn't affect the value of the cell. If you need to set the value
+// of the cell, please use the other functions such as `SetCellStyle` or
+// `SetSheetRow`. The below is example for external link.
//
-// err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
-// // Set underline and font color style for the cell.
-// style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`)
-// err = f.SetCellStyle("Sheet1", "A3", "A3", style)
+// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub"
+// if err := f.SetCellHyperLink("Sheet1", "A3",
+// display, "External", excelize.HyperlinkOpts{
+// Display: &display,
+// Tooltip: &tooltip,
+// }); err != nil {
+// fmt.Println(err)
+// }
+// // Set underline and font color style for the cell.
+// style, err := f.NewStyle(&excelize.Style{
+// Font: &excelize.Font{Color: "1265BE", Underline: "single"},
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "A3", "A3", style)
//
-// A this is another example for "Location":
+// This is another example for "Location":
//
-// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location")
-//
-func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error {
+// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location")
+func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...HyperlinkOpts) error {
// Check for correct cell name
- if _, _, err := SplitCellName(axis); err != nil {
+ if _, _, err := SplitCellName(cell); err != nil {
return err
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- axis, err = f.mergeCellsParser(xlsx, axis)
- if err != nil {
+ if cell, err = ws.mergeCellsParser(cell); err != nil {
return err
}
var linkData xlsxHyperlink
-
- if xlsx.Hyperlinks == nil {
- xlsx.Hyperlinks = new(xlsxHyperlinks)
+ idx := -1
+ if ws.Hyperlinks == nil {
+ ws.Hyperlinks = new(xlsxHyperlinks)
+ }
+ for i, hyperlink := range ws.Hyperlinks.Hyperlink {
+ if hyperlink.Ref == cell {
+ idx = i
+ linkData = hyperlink
+ break
+ }
}
- if len(xlsx.Hyperlinks.Hyperlink) > TotalSheetHyperlinks {
- return errors.New("over maximum limit hyperlinks in a worksheet")
+ if len(ws.Hyperlinks.Hyperlink) > TotalSheetHyperlinks {
+ return ErrTotalSheetHyperlinks
}
switch linkType {
case "External":
+ sheetPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels"
+ rID := f.setRels(linkData.RID, sheetRels, SourceRelationshipHyperLink, link, linkType)
linkData = xlsxHyperlink{
- Ref: axis,
+ Ref: cell,
}
- sheetPath := f.sheetMap[trimSheetName(sheet)]
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels"
- rID := f.addRels(sheetRels, SourceRelationshipHyperLink, link, linkType)
linkData.RID = "rId" + strconv.Itoa(rID)
f.addSheetNameSpace(sheet, SourceRelationship)
case "Location":
linkData = xlsxHyperlink{
- Ref: axis,
+ Ref: cell,
Location: link,
}
+ case "None":
+ return f.removeHyperLink(ws, sheet, cell)
default:
- return fmt.Errorf("invalid link type %q", linkType)
+ return newInvalidLinkTypeError(linkType)
}
- xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData)
- return nil
+ for _, o := range opts {
+ if o.Display != nil {
+ linkData.Display = *o.Display
+ }
+ if o.Tooltip != nil {
+ linkData.Tooltip = *o.Tooltip
+ }
+ }
+ if idx == -1 {
+ ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink, linkData)
+ return err
+ }
+ ws.Hyperlinks.Hyperlink[idx] = linkData
+ return err
+}
+
+// getCellRichText returns rich text of cell by given string item.
+func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
+ if si.T != nil {
+ runs = append(runs, RichTextRun{Text: si.T.Val})
+ }
+ for _, v := range si.R {
+ run := RichTextRun{
+ Text: v.T.Val,
+ }
+ if v.RPr != nil {
+ run.Font = newFont(v.RPr)
+ }
+ runs = append(runs, run)
+ }
+ return
+}
+
+// GetCellRichText provides a function to get rich text of cell by given
+// worksheet and cell reference.
+func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err error) {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return
+ }
+ c, _, _, err := ws.prepareCell(cell)
+ if err != nil {
+ return
+ }
+ if c.T == "inlineStr" && c.IS != nil {
+ runs = getCellRichText(c.IS)
+ return
+ }
+ if c.T != "s" || c.V == "" {
+ return
+ }
+ siIdx, err := strconv.Atoi(c.V)
+ if err != nil {
+ return
+ }
+ sst, err := f.sharedStringsReader()
+ if err != nil {
+ return
+ }
+ if len(sst.SI) <= siIdx || siIdx < 0 {
+ return
+ }
+ runs = getCellRichText(&sst.SI[siIdx])
+ return
+}
+
+// newRpr create run properties for the rich text by given font format.
+func newRpr(fnt *Font) *xlsxRPr {
+ rpr := xlsxRPr{}
+ trueVal := ""
+ if fnt.Bold {
+ rpr.B = &trueVal
+ }
+ if fnt.Italic {
+ rpr.I = &trueVal
+ }
+ if fnt.Strike {
+ rpr.Strike = &trueVal
+ }
+ if fnt.Underline != "" {
+ rpr.U = &attrValString{Val: &fnt.Underline}
+ }
+ if fnt.Family != "" {
+ rpr.RFont = &attrValString{Val: &fnt.Family}
+ }
+ if inStrSlice([]string{"baseline", "superscript", "subscript"}, fnt.VertAlign, true) != -1 {
+ rpr.VertAlign = &attrValString{Val: &fnt.VertAlign}
+ }
+ if fnt.Size > 0 {
+ rpr.Sz = &attrValFloat{Val: &fnt.Size}
+ }
+ rpr.Color = newFontColor(fnt)
+ return &rpr
+}
+
+// newFont create font format by given run properties for the rich text.
+func newFont(rPr *xlsxRPr) *Font {
+ var font Font
+ font.Bold = rPr.B != nil
+ font.Italic = rPr.I != nil
+ if rPr.U != nil {
+ font.Underline = "single"
+ if rPr.U.Val != nil {
+ font.Underline = *rPr.U.Val
+ }
+ }
+ if rPr.RFont != nil && rPr.RFont.Val != nil {
+ font.Family = *rPr.RFont.Val
+ }
+ if rPr.Sz != nil && rPr.Sz.Val != nil {
+ font.Size = *rPr.Sz.Val
+ }
+ if rPr.VertAlign != nil && rPr.VertAlign.Val != nil {
+ font.VertAlign = *rPr.VertAlign.Val
+ }
+ font.Strike = rPr.Strike != nil
+ if rPr.Color != nil {
+ font.Color = strings.TrimPrefix(rPr.Color.RGB, "FF")
+ if rPr.Color.Theme != nil {
+ font.ColorTheme = rPr.Color.Theme
+ }
+ font.ColorIndexed = rPr.Color.Indexed
+ font.ColorTint = rPr.Color.Tint
+ }
+ return &font
+}
+
+// setRichText provides a function to set rich text of a cell.
+func setRichText(runs []RichTextRun) ([]xlsxR, error) {
+ var (
+ textRuns []xlsxR
+ totalCellChars int
+ )
+ for _, textRun := range runs {
+ totalCellChars += len(textRun.Text)
+ if totalCellChars > TotalCellChars {
+ return textRuns, ErrCellCharsLength
+ }
+ run := xlsxR{T: &xlsxT{}}
+ run.T.Val, run.T.Space = trimCellValue(textRun.Text, false)
+ fnt := textRun.Font
+ if fnt != nil {
+ run.RPr = newRpr(fnt)
+ }
+ textRuns = append(textRuns, run)
+ }
+ return textRuns, nil
}
// SetCellRichText provides a function to set cell with rich text by given
-// worksheet. For example, set rich text on the A1 cell of the worksheet named
-// Sheet1:
-//
-// package main
-//
-// import (
-// "fmt"
-//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
-//
-// func main() {
-// f := excelize.NewFile()
-// if err := f.SetRowHeight("Sheet1", 1, 35); err != nil {
-// fmt.Println(err)
-// return
-// }
-// if err := f.SetColWidth("Sheet1", "A", "A", 44); err != nil {
-// fmt.Println(err)
-// return
-// }
-// if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{
-// {
-// Text: "blod",
-// Font: &excelize.Font{
-// Bold: true,
-// Color: "2354e8",
-// Family: "Times New Roman",
-// },
-// },
-// {
-// Text: " and ",
-// Font: &excelize.Font{
-// Family: "Times New Roman",
-// },
-// },
-// {
-// Text: " italic",
-// Font: &excelize.Font{
-// Bold: true,
-// Color: "e83723",
-// Italic: true,
-// Family: "Times New Roman",
-// },
-// },
-// {
-// Text: "text with color and font-family,",
-// Font: &excelize.Font{
-// Bold: true,
-// Color: "2354e8",
-// Family: "Times New Roman",
-// },
-// },
-// {
-// Text: "\r\nlarge text with ",
-// Font: &excelize.Font{
-// Size: 14,
-// Color: "ad23e8",
-// },
-// },
-// {
-// Text: "strike",
-// Font: &excelize.Font{
-// Color: "e89923",
-// Strike: true,
-// },
-// },
-// {
-// Text: " and ",
-// Font: &excelize.Font{
-// Size: 14,
-// Color: "ad23e8",
-// },
-// },
-// {
-// Text: "underline.",
-// Font: &excelize.Font{
-// Color: "23e833",
-// Underline: "single",
-// },
-// },
-// }); err != nil {
-// fmt.Println(err)
-// return
-// }
-// style, err := f.NewStyle(&excelize.Style{
-// Alignment: &excelize.Alignment{
-// WrapText: true,
-// },
-// })
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil {
-// fmt.Println(err)
-// return
-// }
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
+// worksheet name, cell reference and rich text runs. For example, set rich text
+// on the A1 cell of the worksheet named Sheet1:
+//
+// package main
//
+// import (
+// "fmt"
+//
+// "github.com/xuri/excelize/v2"
+// )
+//
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// if err := f.SetRowHeight("Sheet1", 1, 35); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SetColWidth("Sheet1", "A", "A", 44); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{
+// {
+// Text: "bold",
+// Font: &excelize.Font{
+// Bold: true,
+// Color: "2354E8",
+// Family: "Times New Roman",
+// },
+// },
+// {
+// Text: " and ",
+// Font: &excelize.Font{
+// Family: "Times New Roman",
+// },
+// },
+// {
+// Text: "italic ",
+// Font: &excelize.Font{
+// Bold: true,
+// Color: "E83723",
+// Italic: true,
+// Family: "Times New Roman",
+// },
+// },
+// {
+// Text: "text with color and font-family,",
+// Font: &excelize.Font{
+// Bold: true,
+// Color: "2354E8",
+// Family: "Times New Roman",
+// },
+// },
+// {
+// Text: "\r\nlarge text with ",
+// Font: &excelize.Font{
+// Size: 14,
+// Color: "AD23E8",
+// },
+// },
+// {
+// Text: "strike",
+// Font: &excelize.Font{
+// Color: "E89923",
+// Strike: true,
+// },
+// },
+// {
+// Text: " superscript",
+// Font: &excelize.Font{
+// Color: "DBC21F",
+// VertAlign: "superscript",
+// },
+// },
+// {
+// Text: " and ",
+// Font: &excelize.Font{
+// Size: 14,
+// Color: "AD23E8",
+// VertAlign: "baseline",
+// },
+// },
+// {
+// Text: "underline",
+// Font: &excelize.Font{
+// Color: "23E833",
+// Underline: "single",
+// },
+// },
+// {
+// Text: " subscript.",
+// Font: &excelize.Font{
+// Color: "017505",
+// VertAlign: "subscript",
+// },
+// },
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// style, err := f.NewStyle(&excelize.Style{
+// Alignment: &excelize.Alignment{
+// WrapText: true,
+// },
+// })
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- cellData, col, _, err := f.prepareCell(ws, sheet, cell)
+ c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
- cellData.S = f.prepareCellStyle(ws, col, cellData.S)
+ if err := f.sharedStringsLoader(); err != nil {
+ return err
+ }
+ c.S = ws.prepareCellStyle(col, row, c.S)
si := xlsxSI{}
- sst := f.sharedStringsReader()
- textRuns := []xlsxR{}
- for _, textRun := range runs {
- run := xlsxR{T: &xlsxT{Val: textRun.Text}}
- if strings.ContainsAny(textRun.Text, "\r\n ") {
- run.T.Space = xml.Attr{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"}
- }
- fnt := textRun.Font
- if fnt != nil {
- rpr := xlsxRPr{}
- if fnt.Bold {
- rpr.B = " "
- }
- if fnt.Italic {
- rpr.I = " "
- }
- if fnt.Strike {
- rpr.Strike = " "
- }
- if fnt.Underline != "" {
- rpr.U = &attrValString{Val: &fnt.Underline}
- }
- if fnt.Family != "" {
- rpr.RFont = &attrValString{Val: &fnt.Family}
- }
- if fnt.Size > 0.0 {
- rpr.Sz = &attrValFloat{Val: &fnt.Size}
- }
- if fnt.Color != "" {
- rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)}
- }
- run.RPr = &rpr
+ sst, err := f.sharedStringsReader()
+ if err != nil {
+ return err
+ }
+ if si.R, err = setRichText(runs); err != nil {
+ return err
+ }
+ for idx, strItem := range sst.SI {
+ if reflect.DeepEqual(strItem, si) {
+ c.T, c.V = "s", strconv.Itoa(idx)
+ return err
}
- textRuns = append(textRuns, run)
}
- si.R = textRuns
sst.SI = append(sst.SI, si)
sst.Count++
sst.UniqueCount++
- cellData.T, cellData.V = "s", strconv.Itoa(len(sst.SI)-1)
+ c.T, c.V = "s", strconv.Itoa(len(sst.SI)-1)
return err
}
// SetSheetRow writes an array to row by given worksheet name, starting
-// coordinate and a pointer to array type 'slice'. For example, writes an
-// array to row 6 start with the cell B6 on Sheet1:
+// cell reference and a pointer to array type 'slice'. This function is
+// concurrency safe. For example, writes an array to row 6 start with the cell
+// B6 on Sheet1:
//
-// err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2})
+// err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2})
+func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error {
+ return f.setSheetCells(sheet, cell, slice, rows)
+}
+
+// SetSheetCol writes an array to column by given worksheet name, starting
+// cell reference and a pointer to array type 'slice'. For example, writes an
+// array to column B start with the cell B6 on Sheet1:
//
-func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error {
- col, row, err := CellNameToCoordinates(axis)
+// err := f.SetSheetCol("Sheet1", "B6", &[]interface{}{"1", nil, 2})
+func (f *File) SetSheetCol(sheet, cell string, slice interface{}) error {
+ return f.setSheetCells(sheet, cell, slice, columns)
+}
+
+// setSheetCells provides a function to set worksheet cells value.
+func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDirection) error {
+ col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
-
// Make sure 'slice' is a Ptr to Slice
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice {
- return errors.New("pointer to slice expected")
+ return ErrParameterInvalid
}
v = v.Elem()
-
for i := 0; i < v.Len(); i++ {
- cell, err := CoordinatesToCellName(col+i, row)
- // Error should never happens here. But keep checking to early detect regresions
- // if it will be introduced in future.
+ var cell string
+ var err error
+ if dir == rows {
+ cell, err = CoordinatesToCellName(col+i, row)
+ } else {
+ cell, err = CoordinatesToCellName(col, row+i)
+ }
+ // Error should never happen here. But keep checking to early detect regressions
+ // if it will be introduced in the future.
if err != nil {
return err
}
@@ -695,10 +1424,10 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error {
return err
}
-// getCellInfo does common preparation for all SetCell* methods.
-func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) {
+// getCellInfo does common preparation for all set cell value functions.
+func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) {
var err error
- cell, err = f.mergeCellsParser(xlsx, cell)
+ cell, err = ws.mergeCellsParser(cell)
if err != nil {
return nil, 0, 0, err
}
@@ -707,30 +1436,34 @@ func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int
return nil, 0, 0, err
}
- prepareSheetXML(xlsx, col, row)
-
- return &xlsx.SheetData.Row[row-1].C[col-1], col, row, err
+ ws.prepareSheetXML(col, row)
+ return &ws.SheetData.Row[row-1].C[col-1], col, row, err
}
-// getCellStringFunc does common value extraction workflow for all GetCell*
-// methods. Passed function implements specific part of required logic.
-func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) {
- xlsx, err := f.workSheetReader(sheet)
+// getCellStringFunc does common value extraction workflow for all get cell
+// value function. Passed function implements specific part of required
+// logic.
+func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return "", err
}
- axis, err = f.mergeCellsParser(xlsx, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ cell, err = ws.mergeCellsParser(cell)
if err != nil {
return "", err
}
- _, row, err := CellNameToCoordinates(axis)
+ _, row, err := CellNameToCoordinates(cell)
if err != nil {
return "", err
}
-
lastRowNum := 0
- if l := len(xlsx.SheetData.Row); l > 0 {
- lastRowNum = xlsx.SheetData.Row[l-1].R
+ if l := len(ws.SheetData.Row); l > 0 {
+ lastRowNum = ws.SheetData.Row[l-1].R
}
// keep in mind: row starts from 1
@@ -738,17 +1471,17 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c
return "", nil
}
- for rowIdx := range xlsx.SheetData.Row {
- rowData := &xlsx.SheetData.Row[rowIdx]
+ for rowIdx := range ws.SheetData.Row {
+ rowData := &ws.SheetData.Row[rowIdx]
if rowData.R != row {
continue
}
for colIdx := range rowData.C {
colData := &rowData.C[colIdx]
- if axis != colData.R {
+ if cell != colData.R {
continue
}
- val, ok, err := fn(xlsx, colData)
+ val, ok, err := fn(ws, colData)
if err != nil {
return "", err
}
@@ -763,25 +1496,72 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c
// formattedValue provides a function to returns a value after formatted. If
// it is possible to apply a format to the cell value, it will do so, if not
// then an error will be returned, along with the raw value of the cell.
-func (f *File) formattedValue(s int, v string) string {
- if s == 0 {
- return v
+func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, error) {
+ if raw || c.S == 0 {
+ return c.V, nil
+ }
+ styleSheet, err := f.stylesReader()
+ if err != nil {
+ return c.V, err
+ }
+ if styleSheet.CellXfs == nil {
+ return c.V, err
+ }
+ if c.S >= len(styleSheet.CellXfs.Xf) || c.S < 0 {
+ return c.V, err
+ }
+ var numFmtID int
+ if styleSheet.CellXfs.Xf[c.S].NumFmtID != nil {
+ numFmtID = *styleSheet.CellXfs.Xf[c.S].NumFmtID
+ }
+ date1904 := false
+ wb, err := f.workbookReader()
+ if err != nil {
+ return c.V, err
+ }
+ if wb != nil && wb.WorkbookPr != nil {
+ date1904 = wb.WorkbookPr.Date1904
+ }
+ if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
+ return format(c.V, fmtCode, date1904, cellType, f.options), err
}
- styleSheet := f.stylesReader()
- ok := builtInNumFmtFunc[*styleSheet.CellXfs.Xf[s].NumFmtID]
- if ok != nil {
- return ok(*styleSheet.CellXfs.Xf[s].NumFmtID, v)
+ if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
+ return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
}
- return v
+ return c.V, err
+}
+
+// getCustomNumFmtCode provides a function to returns custom number format code.
+func (ss *xlsxStyleSheet) getCustomNumFmtCode(numFmtID int) (string, bool) {
+ if ss.NumFmts == nil {
+ return "", false
+ }
+ for _, xlsxFmt := range ss.NumFmts.NumFmt {
+ if xlsxFmt.NumFmtID == numFmtID {
+ if xlsxFmt.FormatCode16 != "" {
+ return xlsxFmt.FormatCode16, true
+ }
+ return xlsxFmt.FormatCode, true
+ }
+ }
+ return "", false
}
// prepareCellStyle provides a function to prepare style index of cell in
// worksheet by given column index and style index.
-func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int {
- if xlsx.Cols != nil && style == 0 {
- for _, c := range xlsx.Cols.Col {
- if c.Min <= col && col <= c.Max {
- style = c.Style
+func (ws *xlsxWorksheet) prepareCellStyle(col, row, style int) int {
+ if style != 0 {
+ return style
+ }
+ if row <= len(ws.SheetData.Row) {
+ if styleID := ws.SheetData.Row[row-1].S; styleID != 0 {
+ return styleID
+ }
+ }
+ if ws.Cols != nil {
+ for _, c := range ws.Cols.Col {
+ if c.Min <= col && col <= c.Max && c.Style != 0 {
+ return c.Style
}
}
}
@@ -789,76 +1569,151 @@ func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int {
}
// mergeCellsParser provides a function to check merged cells in worksheet by
-// given axis.
-func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error) {
- axis = strings.ToUpper(axis)
- if xlsx.MergeCells != nil {
- for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
- ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref)
- if err != nil {
- return axis, err
+// given cell reference.
+func (ws *xlsxWorksheet) mergeCellsParser(cell string) (string, error) {
+ cell = strings.ToUpper(cell)
+ col, row, err := CellNameToCoordinates(cell)
+ if err != nil {
+ return cell, err
+ }
+ if ws.MergeCells != nil {
+ for i := 0; i < len(ws.MergeCells.Cells); i++ {
+ if ws.MergeCells.Cells[i] == nil {
+ ws.MergeCells.Cells = append(ws.MergeCells.Cells[:i], ws.MergeCells.Cells[i+1:]...)
+ i--
+ continue
}
- if ok {
- axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0]
+ if ref := ws.MergeCells.Cells[i].Ref; len(ws.MergeCells.Cells[i].rect) == 0 && ref != "" {
+ if strings.Count(ref, ":") != 1 {
+ ref += ":" + ref
+ }
+ rect, err := rangeRefToCoordinates(ref)
+ if err != nil {
+ return cell, err
+ }
+ _ = sortCoordinates(rect)
+ ws.MergeCells.Cells[i].rect = rect
+ }
+ if cellInRange([]int{col, row}, ws.MergeCells.Cells[i].rect) {
+ cell = strings.Split(ws.MergeCells.Cells[i].Ref, ":")[0]
+ break
}
}
}
- return axis, nil
+ return cell, nil
}
-// checkCellInArea provides a function to determine if a given coordinate is
-// within an area.
-func (f *File) checkCellInArea(cell, area string) (bool, error) {
+// checkCellInRangeRef provides a function to determine if a given cell reference
+// in a range.
+func (f *File) checkCellInRangeRef(cell, rangeRef string) (bool, error) {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return false, err
}
- rng := strings.Split(area, ":")
- if len(rng) != 2 {
+ if rng := strings.Split(rangeRef, ":"); len(rng) != 2 {
return false, err
}
- coordinates, err := f.areaRefToCoordinates(area)
+ coordinates, err := rangeRefToCoordinates(rangeRef)
if err != nil {
return false, err
}
- return cellInRef([]int{col, row}, coordinates), err
+ return cellInRange([]int{col, row}, coordinates), err
}
-// cellInRef provides a function to determine if a given range is within an
+// cellInRange provides a function to determine if a given range is within a
// range.
-func cellInRef(cell, ref []int) bool {
+func cellInRange(cell, ref []int) bool {
return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3]
}
// isOverlap find if the given two rectangles overlap or not.
func isOverlap(rect1, rect2 []int) bool {
- return cellInRef([]int{rect1[0], rect1[1]}, rect2) ||
- cellInRef([]int{rect1[2], rect1[1]}, rect2) ||
- cellInRef([]int{rect1[0], rect1[3]}, rect2) ||
- cellInRef([]int{rect1[2], rect1[3]}, rect2) ||
- cellInRef([]int{rect2[0], rect2[1]}, rect1) ||
- cellInRef([]int{rect2[2], rect2[1]}, rect1) ||
- cellInRef([]int{rect2[0], rect2[3]}, rect1) ||
- cellInRef([]int{rect2[2], rect2[3]}, rect1)
+ return cellInRange([]int{rect1[0], rect1[1]}, rect2) ||
+ cellInRange([]int{rect1[2], rect1[1]}, rect2) ||
+ cellInRange([]int{rect1[0], rect1[3]}, rect2) ||
+ cellInRange([]int{rect1[2], rect1[3]}, rect2) ||
+ cellInRange([]int{rect2[0], rect2[1]}, rect1) ||
+ cellInRange([]int{rect2[2], rect2[1]}, rect1) ||
+ cellInRange([]int{rect2[0], rect2[3]}, rect1) ||
+ cellInRange([]int{rect2[2], rect2[3]}, rect1)
+}
+
+// parseSharedFormula generate dynamic part of shared formula for target cell
+// by given column and rows distance and origin shared formula.
+func parseSharedFormula(dCol, dRow int, orig string) string {
+ ps := efp.ExcelParser()
+ tokens := ps.Parse(string(orig))
+ for i := 0; i < len(tokens); i++ {
+ token := tokens[i]
+ if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
+ tokens[i].TValue = shiftCell(token.TValue, dCol, dRow)
+ }
+ }
+ return ps.Render()
}
-// getSharedForumula find a cell contains the same formula as another cell,
+// getSharedFormula find a cell contains the same formula as another cell,
// the "shared" value can be used for the t attribute and the si attribute can
// be used to refer to the cell containing the formula. Two formulas are
// considered to be the same when their respective representations in
// R1C1-reference notation, are the same.
//
-// Note that this function not validate ref tag to check the cell if or not in
-// allow area, and always return origin shared formula.
-func getSharedForumula(xlsx *xlsxWorksheet, si string) string {
- for _, r := range xlsx.SheetData.Row {
- for _, c := range r.C {
- if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si == si {
- return c.F.Content
+// Note that this function not validate ref tag to check the cell whether in
+// allow range reference, and always return origin shared formula.
+func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
+ for row := 0; row < len(ws.SheetData.Row); row++ {
+ r := &ws.SheetData.Row[row]
+ for column := 0; column < len(r.C); column++ {
+ c := &r.C[column]
+ if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si {
+ col, row, _ := CellNameToCoordinates(cell)
+ sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)
+ dCol := col - sharedCol
+ dRow := row - sharedRow
+ return parseSharedFormula(dCol, dRow, c.F.Content)
}
}
}
return ""
}
+
+// shiftCell returns the cell shifted according to dCol and dRow taking into
+// consideration absolute references with dollar sign ($)
+func shiftCell(val string, dCol, dRow int) string {
+ parts := strings.Split(val, ":")
+ for j := 0; j < len(parts); j++ {
+ cell := parts[j]
+ trimmedCellName := strings.ReplaceAll(cell, "$", "")
+ c, r, err := CellNameToCoordinates(trimmedCellName)
+ if err == nil {
+ absCol := strings.Index(cell, "$") == 0
+ absRow := strings.LastIndex(cell, "$") > 0
+ if !absCol && !absRow {
+ parts[j], _ = CoordinatesToCellName(c+dCol, r+dRow)
+ }
+ if !absCol && absRow {
+ colName, _ := ColumnNumberToName(c + dCol)
+ parts[j] = colName + "$" + strconv.Itoa(r)
+ }
+ if absCol && !absRow {
+ colName, _ := ColumnNumberToName(c)
+ parts[j] = "$" + colName + strconv.Itoa(r+dRow)
+ }
+ continue
+ }
+ // Cell reference is a column name
+ c, err = ColumnNameToNumber(trimmedCellName)
+ if err == nil && !strings.HasPrefix(cell, "$") {
+ parts[j], _ = ColumnNumberToName(c + dCol)
+ continue
+ }
+ // Cell reference is a row number
+ r, err = strconv.Atoi(trimmedCellName)
+ if err == nil && !strings.HasPrefix(cell, "$") {
+ parts[j] = strconv.Itoa(r + dRow)
+ }
+ }
+ return strings.Join(parts, ":")
+}
diff --git a/cell_test.go b/cell_test.go
index fb30596f18..b9069a354e 100644
--- a/cell_test.go
+++ b/cell_test.go
@@ -2,52 +2,158 @@ package excelize
import (
"fmt"
+ _ "image/jpeg"
+ "math"
+ "os"
"path/filepath"
+ "reflect"
"strconv"
+ "strings"
+ "sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
-func TestCheckCellInArea(t *testing.T) {
+func TestConcurrency(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+ wg := new(sync.WaitGroup)
+ for i := 1; i <= 5; i++ {
+ wg.Add(1)
+ go func(val int, t *testing.T) {
+ // Concurrency set cell value
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val)))
+ // Concurrency get cell value
+ _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val))
+ assert.NoError(t, err)
+ // Concurrency set rows
+ assert.NoError(t, f.SetSheetRow("Sheet1", "B6", &[]interface{}{
+ " Hello",
+ []byte("World"), 42, int8(1<<8/2 - 1), int16(1<<16/2 - 1), int32(1<<32/2 - 1),
+ int64(1<<32/2 - 1), float32(42.65418), -42.65418, float32(42), float64(42),
+ uint(1<<32 - 1), uint8(1<<8 - 1), uint16(1<<16 - 1), uint32(1<<32 - 1),
+ uint64(1<<32 - 1), true, complex64(5 + 10i),
+ }))
+ // Concurrency create style
+ style, err := f.NewStyle(&Style{Font: &Font{Color: "1265BE", Underline: "single"}})
+ assert.NoError(t, err)
+ // Concurrency set cell style
+ assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style))
+ // Concurrency get cell style
+ _, err = f.GetCellStyle("Sheet1", "A3")
+ assert.NoError(t, err)
+ // Concurrency add picture
+ assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{
+ OffsetX: 10,
+ OffsetY: 10,
+ Hyperlink: "https://github.com/xuri/excelize",
+ HyperlinkType: "External",
+ Positioning: "oneCell",
+ },
+ ))
+ // Concurrency get cell picture
+ pics, err := f.GetPictures("Sheet1", "A1")
+ assert.Len(t, pics, 0)
+ assert.NoError(t, err)
+ // Concurrency iterate rows
+ rows, err := f.Rows("Sheet1")
+ assert.NoError(t, err)
+ for rows.Next() {
+ _, err := rows.Columns()
+ assert.NoError(t, err)
+ }
+ // Concurrency iterate columns
+ cols, err := f.Cols("Sheet1")
+ assert.NoError(t, err)
+ for cols.Next() {
+ _, err := cols.Rows()
+ assert.NoError(t, err)
+ }
+ // Concurrency set columns style
+ assert.NoError(t, f.SetColStyle("Sheet1", "C:E", style))
+ // Concurrency get columns style
+ styleID, err := f.GetColStyle("Sheet1", "D")
+ assert.NoError(t, err)
+ assert.Equal(t, style, styleID)
+ // Concurrency set columns width
+ assert.NoError(t, f.SetColWidth("Sheet1", "A", "B", 10))
+ // Concurrency get columns width
+ width, err := f.GetColWidth("Sheet1", "A")
+ assert.NoError(t, err)
+ assert.Equal(t, 10.0, width)
+ // Concurrency set columns visible
+ assert.NoError(t, f.SetColVisible("Sheet1", "A:B", true))
+ // Concurrency get columns visible
+ visible, err := f.GetColVisible("Sheet1", "A")
+ assert.NoError(t, err)
+ assert.Equal(t, true, visible)
+ // Concurrency add data validation
+ dv := NewDataValidation(true)
+ dv.Sqref = fmt.Sprintf("A%d:B%d", val, val)
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
+ dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ // Concurrency delete data validation with reference sequence
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", dv.Sqref))
+ wg.Done()
+ }(i, t)
+ }
+ wg.Wait()
+ val, err := f.GetCellValue("Sheet1", "A1")
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(t, "1", val)
+ // Test the length of data validation
+ dataValidations, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 0)
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx")))
+ assert.NoError(t, f.Close())
+}
+
+func TestCheckCellInRangeRef(t *testing.T) {
f := NewFile()
- expectedTrueCellInAreaList := [][2]string{
+ expectedTrueCellInRangeRefList := [][2]string{
{"c2", "A1:AAZ32"},
{"B9", "A1:B9"},
{"C2", "C2:C2"},
}
- for _, expectedTrueCellInArea := range expectedTrueCellInAreaList {
- cell := expectedTrueCellInArea[0]
- area := expectedTrueCellInArea[1]
- ok, err := f.checkCellInArea(cell, area)
+ for _, expectedTrueCellInRangeRef := range expectedTrueCellInRangeRefList {
+ cell := expectedTrueCellInRangeRef[0]
+ reference := expectedTrueCellInRangeRef[1]
+ ok, err := f.checkCellInRangeRef(cell, reference)
assert.NoError(t, err)
assert.Truef(t, ok,
- "Expected cell %v to be in area %v, got false\n", cell, area)
+ "Expected cell %v to be in range reference %v, got false\n", cell, reference)
}
- expectedFalseCellInAreaList := [][2]string{
+ expectedFalseCellInRangeRefList := [][2]string{
{"c2", "A4:AAZ32"},
{"C4", "D6:A1"}, // weird case, but you never know
{"AEF42", "BZ40:AEF41"},
}
- for _, expectedFalseCellInArea := range expectedFalseCellInAreaList {
- cell := expectedFalseCellInArea[0]
- area := expectedFalseCellInArea[1]
- ok, err := f.checkCellInArea(cell, area)
+ for _, expectedFalseCellInRangeRef := range expectedFalseCellInRangeRefList {
+ cell := expectedFalseCellInRangeRef[0]
+ reference := expectedFalseCellInRangeRef[1]
+ ok, err := f.checkCellInRangeRef(cell, reference)
assert.NoError(t, err)
assert.Falsef(t, ok,
- "Expected cell %v not to be inside of area %v, but got true\n", cell, area)
+ "Expected cell %v not to be inside of range reference %v, but got true\n", cell, reference)
}
- ok, err := f.checkCellInArea("A1", "A:B")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ ok, err := f.checkCellInRangeRef("A1", "A:B")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
assert.False(t, ok)
- ok, err = f.checkCellInArea("AA0", "Z0:AB1")
- assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`)
+ ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1")
+ assert.Equal(t, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")), err)
assert.False(t, ok)
}
@@ -62,7 +168,7 @@ func TestSetCellFloat(t *testing.T) {
assert.Equal(t, "123", val, "A1 should be 123")
val, err = f.GetCellValue(sheet, "A2")
assert.NoError(t, err)
- assert.Equal(t, "123.0", val, "A2 should be 123.0")
+ assert.Equal(t, "123", val, "A2 should be 123")
})
t.Run("with a decimal and precision limit", func(t *testing.T) {
@@ -81,26 +187,201 @@ func TestSetCellFloat(t *testing.T) {
assert.Equal(t, "123.42", val, "A1 should be 123.42")
})
f := NewFile()
- assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellFloat(sheet, "A", 123.42, -1, 64))
+ // Test set cell float data type value with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64))
+}
+
+func TestSetCellUint(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint8(math.MaxUint8)))
+ result, err := f.GetCellValue("Sheet1", "A1")
+ assert.Equal(t, "255", result)
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint16)))
+ result, err = f.GetCellValue("Sheet1", "A1")
+ assert.Equal(t, "65535", result)
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint32)))
+ result, err = f.GetCellValue("Sheet1", "A1")
+ assert.Equal(t, "4294967295", result)
+ assert.NoError(t, err)
+ // Test uint cell value not exists worksheet
+ assert.EqualError(t, f.SetCellUint("SheetN", "A1", 1), "sheet SheetN does not exist")
+ // Test uint cell value with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellUint("Sheet1", "A", 1))
+}
+
+func TestSetCellValuesMultiByte(t *testing.T) {
+ f := NewFile()
+ row := []interface{}{
+ // Test set cell value with multi byte characters value
+ strings.Repeat("\u4E00", TotalCellChars+1),
+ // Test set cell value with XML escape characters
+ strings.Repeat("<>", TotalCellChars/2),
+ strings.Repeat(">", TotalCellChars-1),
+ strings.Repeat(">", TotalCellChars+1),
+ }
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &row))
+ // Test set cell value with XML escape characters in stream writer
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ streamWriter, err := f.NewStreamWriter("Sheet2")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetRow("A1", row))
+ assert.NoError(t, streamWriter.Flush())
+ for _, sheetName := range []string{"Sheet1", "Sheet2"} {
+ for cell, expected := range map[string]int{
+ "A1": TotalCellChars,
+ "B1": TotalCellChars - 1,
+ "C1": TotalCellChars - 1,
+ "D1": TotalCellChars,
+ } {
+ result, err := f.GetCellValue(sheetName, cell)
+ assert.NoError(t, err)
+ assert.Len(t, []rune(result), expected)
+ }
+ }
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellValuesMultiByte.xlsx")))
}
func TestSetCellValue(t *testing.T) {
f := NewFile()
- assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Now().UTC()))
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Duration(1e13)))
+ // Test set cell value with column and row style inherit
+ style1, err := f.NewStyle(&Style{NumFmt: 2})
+ assert.NoError(t, err)
+ style2, err := f.NewStyle(&Style{NumFmt: 9})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetColStyle("Sheet1", "B", style1))
+ assert.NoError(t, f.SetRowStyle("Sheet1", 1, 1, style2))
+ assert.NoError(t, f.SetCellValue("Sheet1", "B1", 0.5))
+ assert.NoError(t, f.SetCellValue("Sheet1", "B2", 0.5))
+ B1, err := f.GetCellValue("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "50%", B1)
+ B2, err := f.GetCellValue("Sheet1", "B2")
+ assert.NoError(t, err)
+ assert.Equal(t, "0.50", B2)
+
+ // Test set cell value with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetCellValue("Sheet:1", "A1", "A1"))
+ // Test set cell value with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellValue("Sheet1", "A1", "A1"), "XML syntax error on line 1: invalid UTF-8")
+ // Test set cell value with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8")
+ // Test set cell value with the shared string table's count not equal with unique count
+ f = NewFile()
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`aa`, NameSpaceSpreadSheet.Value)))
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
+ SheetData: xlsxSheetData{Row: []xlsxRow{
+ {R: 1, C: []xlsxC{{R: "A1", T: "str", V: "1"}}},
+ }},
+ })
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "b"))
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "b", val)
+ assert.NoError(t, f.SetCellValue("Sheet1", "B1", "b"))
+ val, err = f.GetCellValue("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "b", val)
+
+ f = NewFile()
+ // Test set cell value with an IEEE 754 "not-a-number" value or infinity
+ for num, expected := range map[float64]string{
+ math.NaN(): "NaN",
+ math.Inf(0): "+Inf",
+ math.Inf(-1): "-Inf",
+ } {
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", num))
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, val)
+ }
+ // Test set cell value with time duration
+ for val, expected := range map[time.Duration]string{
+ time.Hour*21 + time.Minute*51 + time.Second*44: "21:51:44",
+ time.Hour*21 + time.Minute*50: "21:50",
+ time.Hour*24 + time.Minute*51 + time.Second*44: "24:51:44",
+ time.Hour*24 + time.Minute*50: "24:50:00",
+ } {
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, val)
+ }
+ // Test set cell value with time
+ for val, expected := range map[time.Time]string{
+ time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC): "Oct-24",
+ time.Date(2024, time.October, 10, 0, 0, 0, 0, time.UTC): "10-10-24",
+ time.Date(2024, time.October, 10, 12, 0, 0, 0, time.UTC): "10/10/24 12:00",
+ } {
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, val)
+ }
+}
+
+func TestSetCellValues(t *testing.T) {
+ f := NewFile()
+ err := f.SetCellValue("Sheet1", "A1", time.Date(2010, time.December, 31, 0, 0, 0, 0, time.UTC))
+ assert.NoError(t, err)
+
+ v, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, v, "12-31-10")
+
+ // Test date value lower than min date supported by Excel
+ err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC))
+ assert.NoError(t, err)
+
+ v, err = f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, v, "1600-12-31T00:00:00Z")
}
func TestSetCellBool(t *testing.T) {
f := NewFile()
- assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellBool("Sheet1", "A", true))
+ // Test set cell boolean data type value with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetCellBool("Sheet:1", "A1", true))
+}
+
+func TestSetCellTime(t *testing.T) {
+ date, err := time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z")
+ assert.NoError(t, err)
+ for location, expected := range map[string]string{
+ "America/New_York": "40127.75",
+ "Asia/Shanghai": "40128.291666666664",
+ "Europe/London": "40127.958333333336",
+ "UTC": "40127.958333333336",
+ } {
+ timezone, err := time.LoadLocation(location)
+ assert.NoError(t, err)
+ c := &xlsxC{}
+ isNum, err := c.setCellTime(date.In(timezone), false)
+ assert.NoError(t, err)
+ assert.Equal(t, true, isNum)
+ assert.Equal(t, expected, c.V)
+ }
}
func TestGetCellValue(t *testing.T) {
- // Test get cell value without r attribute of the row.
+ // Test get cell value without r attribute of the row
f := NewFile()
- delete(f.Sheet, "xl/worksheets/sheet1.xml")
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`A3
A4B4
A7B7
A8B8
`)
- f.checked = nil
+ sheetData := `%s`
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3
A4B4
A7B7
A8B8
`)))
+ f.checked = sync.Map{}
cells := []string{"A3", "A4", "B4", "A7", "B7"}
rows, err := f.GetRows("Sheet1")
assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
@@ -113,27 +394,258 @@ func TestGetCellValue(t *testing.T) {
cols, err := f.GetCols("Sheet1")
assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols)
assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2
B2
`)))
+ f.checked = sync.Map{}
+ cell, err := f.GetCellValue("Sheet1", "A2")
+ assert.Equal(t, "A2", cell)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2
B2
`)))
+ f.checked = sync.Map{}
+ rows, err = f.GetRows("Sheet1")
+ assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1
B1
`)))
+ f.checked = sync.Map{}
+ rows, err = f.GetRows("Sheet1")
+ assert.Equal(t, [][]string{{"A1", "B1"}}, rows)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3
A4B4
A7B7
A8B8
`)))
+ f.checked = sync.Map{}
+ rows, err = f.GetRows("Sheet1")
+ assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4
A6B6C6
100B3
`)))
+ f.checked = sync.Map{}
+ cell, err = f.GetCellValue("Sheet1", "H6")
+ assert.Equal(t, "H6", cell)
+ assert.NoError(t, err)
+ rows, err = f.GetRows("Sheet1")
+ assert.Equal(t, [][]string{
+ {"A6", "B6", "C6"},
+ nil,
+ {"100", "B3"},
+ {"", "", "", "", "", "F4"},
+ nil,
+ {"", "", "", "", "", "", "", "H6"},
+ }, rows)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1
|
A3
`)))
+ f.checked = sync.Map{}
+ rows, err = f.GetRows("Sheet1")
+ assert.Equal(t, [][]string{{"A1"}, nil, {"A3"}}, rows)
+ assert.NoError(t, err)
+ cell, err = f.GetCellValue("Sheet1", "A3")
+ assert.Equal(t, "A3", cell)
+ assert.NoError(t, err)
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
+ 2422.3000000000002
+ 2422.3000000000002
+ 12.4
+ 964
+ 1101.5999999999999
+ 275.39999999999998
+ 68.900000000000006
+ 44385.208333333336
+ 5.0999999999999996
+ 5.1100000000000003
+ 5.0999999999999996
+ 5.1109999999999998
+ 5.1111000000000004
+ 2422.012345678
+ 2422.0123456789
+ 12.012345678901
+ 964
+ 1101.5999999999999
+ 275.39999999999998
+ 68.900000000000006
+ 8.8880000000000001E-2
+ 4.0000000000000003e-5
+ 2422.3000000000002
+ 1101.5999999999999
+ 275.39999999999998
+ 68.900000000000006
+ 1.1000000000000001
+ 1234567890123_4
+ 123456789_0123_4
+ +0.0000000000000000002399999999999992E-4
+ 7.2399999999999992E-2
+ 20200208T080910.123
+ 20200208T080910,123
+ 20221022T150529Z
+ 2022-10-22T15:05:29Z
+ 2020-07-10 15:00:00.000
`)))
+ f.checked = sync.Map{}
+ rows, err = f.GetCols("Sheet1")
+ assert.Equal(t, []string{
+ "2422.3",
+ "2422.3",
+ "12.4",
+ "964",
+ "1101.6",
+ "275.4",
+ "68.9",
+ "44385.2083333333",
+ "5.1",
+ "5.11",
+ "5.1",
+ "5.111",
+ "5.1111",
+ "2422.012345678",
+ "2422.0123456789",
+ "12.012345678901",
+ "964",
+ "1101.6",
+ "275.4",
+ "68.9",
+ "0.08888",
+ "0.00004",
+ "2422.3",
+ "1101.6",
+ "275.4",
+ "68.9",
+ "1.1",
+ "1234567890123_4",
+ "123456789_0123_4",
+ "2.39999999999999E-23",
+ "0.0724",
+ "43869.3397004977",
+ "43869.3397004977",
+ "44856.6288078704",
+ "44856.6288078704",
+ "2020-07-10 15:00:00.000",
+ }, rows[0])
+ assert.NoError(t, err)
+
+ // Test get cell value with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, value := f.GetCellValue("Sheet1", "A1")
+ assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8")
+ // Test get cell value with invalid sheet name
+ _, err = f.GetCellValue("Sheet:1", "A1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+}
+
+func TestGetCellType(t *testing.T) {
+ f := NewFile()
+ cellType, err := f.GetCellType("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, CellTypeUnset, cellType)
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1"))
+ cellType, err = f.GetCellType("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, CellTypeSharedString, cellType)
+ _, err = f.GetCellType("Sheet1", "A")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
+ // Test get cell type with invalid sheet name
+ _, err = f.GetCellType("Sheet:1", "A1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+}
+
+func TestGetValueFrom(t *testing.T) {
+ f := NewFile()
+ c := xlsxC{T: "s"}
+ sst, err := f.sharedStringsReader()
+ assert.NoError(t, err)
+ value, err := c.getValueFrom(f, sst, false)
+ assert.NoError(t, err)
+ assert.Equal(t, "", value)
+
+ c = xlsxC{T: "s", V: " 1 "}
+ value, err = c.getValueFrom(f, &xlsxSST{Count: 1, SI: []xlsxSI{{}, {T: &xlsxT{Val: "s"}}}}, false)
+ assert.NoError(t, err)
+ assert.Equal(t, "s", value)
}
func TestGetCellFormula(t *testing.T) {
- // Test get cell formula on not exist worksheet.
+ // Test get cell formula on not exist worksheet
f := NewFile()
_, err := f.GetCellFormula("SheetN", "A1")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
- // Test get cell formula on no formula cell.
+ // Test get cell formula with invalid sheet name
+ _, err = f.GetCellFormula("Sheet:1", "A1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+
+ // Test get cell formula on no formula cell
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
_, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
+
+ // Test get cell shared formula
+ f = NewFile()
+ sheetData := `12*A1
2%s
3
4
5
6
7
`
+
+ for sharedFormula, expected := range map[string]string{
+ `2*A2`: `2*A3`,
+ `2*A1A`: `2*A1A`,
+ `2*$A$2+LEN("")`: `2*$A$2+LEN("")`,
+ `SUMIF(A:A,$B11, 5:5)`: `SUMIF(A:A,$B12,6:6)`,
+ `SUMIF(A:A,B$11, 5:5)`: `SUMIF(A:A,B$11,6:6)`,
+ } {
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, sharedFormula)))
+ formula, err := f.GetCellFormula("Sheet1", "B3")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, formula)
+ }
+
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`
`))
+ formula, err := f.GetCellFormula("Sheet1", "B2")
+ assert.NoError(t, err)
+ assert.Equal(t, "", formula)
+
+ // Test get array formula with invalid cell range reference
+ f = NewFile()
+ assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ formulaType, ref := STCellFormulaTypeArray, "B1:B2"
+ assert.NoError(t, f.SetCellFormula("Sheet2", "B1", "A1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet3.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[1].F.Ref = ":"
+ _, err = f.getCellFormula("Sheet2", "A1", true)
+ assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
+
+ // Test set formula for the cells in array formula range with unsupported charset
+ f = NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8")
}
func ExampleFile_SetCellFloat() {
f := NewFile()
- var x = 3.14159265
+ defer func() {
+ if err := f.Close(); err != nil {
+ fmt.Println(err)
+ }
+ }()
+ x := 3.14159265
if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil {
fmt.Println(err)
}
- val, _ := f.GetCellValue("Sheet1", "A1")
+ val, err := f.GetCellValue("Sheet1", "A1")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
fmt.Println(val)
// Output: 3.14
}
@@ -161,6 +673,180 @@ func TestOverflowNumericCell(t *testing.T) {
assert.NoError(t, err)
// GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648"
assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225")
+ assert.NoError(t, f.Close())
+}
+
+func TestSetCellFormula(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)"))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)"))
+
+ // Test set cell formula with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"))
+
+ // Test set cell formula with illegal rows number
+ assert.Equal(t, newCellNameToCoordinatesError("C", newInvalidCellNameError("C")), f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"))
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx")))
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ // Test remove cell formula
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", ""))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx")))
+ // Test remove all cell formula
+ assert.NoError(t, f.SetCellFormula("Sheet1", "B1", ""))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx")))
+ assert.NoError(t, f.Close())
+
+ // Test set shared formula for the cells
+ f = NewFile()
+ for r := 1; r <= 5; r++ {
+ assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", r), &[]interface{}{r, r + 1}))
+ }
+ formulaType, ref := STCellFormulaTypeShared, "C1:C5"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ sharedFormulaSpreadsheet := filepath.Join("test", "TestSetCellFormula4.xlsx")
+ assert.NoError(t, f.SaveAs(sharedFormulaSpreadsheet))
+
+ f, err = OpenFile(sharedFormulaSpreadsheet)
+ assert.NoError(t, err)
+ ref = "D1:D5"
+ assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ ref = ""
+ assert.Equal(t, ErrParameterInvalid, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx")))
+
+ // Test set table formula for the cells
+ f = NewFile()
+ for idx, row := range [][]interface{}{{"A", "B", "C"}, {1, 2}} {
+ assert.NoError(t, f.SetSheetRow("Sheet1", fmt.Sprintf("A%d", idx+1), &row))
+ }
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:C2", Name: "Table1", StyleName: "TableStyleMedium2"}))
+ formulaType = STCellFormulaTypeDataTable
+ assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType}))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx")))
+
+ // Test set array formula with invalid cell range reference
+ formulaType, ref = STCellFormulaTypeArray, ":"
+ assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &ref, Type: &formulaType}))
+
+ // Test set array formula with invalid cell reference
+ formulaType, ref = STCellFormulaTypeArray, "A1:A2"
+ assert.Equal(t, ErrColumnNumber, f.SetCellFormula("Sheet1", "A1", "SUM(XFE1:XFE2)", FormulaOpts{Ref: &ref, Type: &formulaType}))
+}
+
+func TestGetCellRichText(t *testing.T) {
+ f, theme := NewFile(), 1
+
+ runsSource := []RichTextRun{
+ {
+ Text: "a\n",
+ },
+ {
+ Text: "b",
+ Font: &Font{
+ Underline: "single",
+ Color: "ff0000",
+ ColorTheme: &theme,
+ ColorTint: 0.5,
+ Bold: true,
+ Italic: true,
+ Family: "Times New Roman",
+ Size: 100,
+ Strike: true,
+ },
+ },
+ }
+ assert.NoError(t, f.SetCellRichText("Sheet1", "A1", runsSource))
+ assert.NoError(t, f.SetCellValue("Sheet1", "A2", false))
+
+ runs, err := f.GetCellRichText("Sheet1", "A2")
+ assert.NoError(t, err)
+ assert.Equal(t, []RichTextRun(nil), runs)
+
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+
+ assert.Equal(t, runsSource[0].Text, runs[0].Text)
+ assert.Nil(t, runs[0].Font)
+ assert.NotNil(t, runs[1].Font)
+
+ runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color)
+ assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font")
+
+ // Test get cell rich text with inlineStr
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{
+ T: "inlineStr",
+ IS: &xlsxSI{
+ T: &xlsxT{Val: "A"},
+ R: []xlsxR{{T: &xlsxT{Val: "1"}}},
+ },
+ }
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, []RichTextRun{{Text: "A"}, {Text: "1"}}, runs)
+
+ // Test get cell rich text when string item index overflow
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "2", IS: &xlsxSI{}}
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(runs))
+ // Test get cell rich text when string item index is negative
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "-1", IS: &xlsxSI{}}
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(runs))
+ // Test get cell rich text when string item index is invalid
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "A", IS: &xlsxSI{}}
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
+ assert.Equal(t, 0, len(runs))
+ // Test get cell rich text on invalid string item index
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "x"}
+ runs, err = f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(runs))
+ // Test set cell rich text on not exists worksheet
+ _, err = f.GetCellRichText("SheetN", "A1")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test set cell rich text with illegal cell reference
+ _, err = f.GetCellRichText("Sheet1", "A")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
+ // Test set rich text color theme without tint
+ assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}}))
+ // Test set rich text color tint without theme
+ assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTint: 0.5}}}))
+
+ // Test set cell rich text with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", runsSource), "XML syntax error on line 1: invalid UTF-8")
+ // Test get cell rich text with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, err = f.GetCellRichText("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get cell rich text with invalid sheet name
+ _, err = f.GetCellRichText("Sheet:1", "A1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestSetCellRichText(t *testing.T) {
@@ -169,11 +855,12 @@ func TestSetCellRichText(t *testing.T) {
assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 44))
richTextRun := []RichTextRun{
{
- Text: "blod",
+ Text: "bold",
Font: &Font{
- Bold: true,
- Color: "2354e8",
- Family: "Times New Roman",
+ Bold: true,
+ Color: "2354E8",
+ ColorIndexed: 0,
+ Family: "Times New Roman",
},
},
{
@@ -186,16 +873,16 @@ func TestSetCellRichText(t *testing.T) {
Text: "italic ",
Font: &Font{
Bold: true,
- Color: "e83723",
+ Color: "E83723",
Italic: true,
Family: "Times New Roman",
},
},
{
- Text: "text with color and font-family,",
+ Text: "text with color and font-family, ",
Font: &Font{
Bold: true,
- Color: "2354e8",
+ Color: "2354E8",
Family: "Times New Roman",
},
},
@@ -203,30 +890,45 @@ func TestSetCellRichText(t *testing.T) {
Text: "\r\nlarge text with ",
Font: &Font{
Size: 14,
- Color: "ad23e8",
+ Color: "AD23E8",
},
},
{
Text: "strike",
Font: &Font{
- Color: "e89923",
+ Color: "E89923",
Strike: true,
},
},
+ {
+ Text: " superscript",
+ Font: &Font{
+ Color: "DBC21F",
+ VertAlign: "superscript",
+ },
+ },
{
Text: " and ",
Font: &Font{
- Size: 14,
- Color: "ad23e8",
+ Size: 14,
+ Color: "AD23E8",
+ VertAlign: "baseline",
},
},
{
- Text: "underline.",
+ Text: "underline",
Font: &Font{
- Color: "23e833",
+ Color: "23E833",
Underline: "single",
},
},
+ {
+ Text: " subscript.",
+ Font: &Font{
+ Color: "017505",
+ VertAlign: "subscript",
+ },
+ },
}
assert.NoError(t, f.SetCellRichText("Sheet1", "A1", richTextRun))
assert.NoError(t, f.SetCellRichText("Sheet1", "A2", richTextRun))
@@ -237,9 +939,250 @@ func TestSetCellRichText(t *testing.T) {
})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
+
+ runs, err := f.GetCellRichText("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, richTextRun, runs)
+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx")))
// Test set cell rich text on not exists worksheet
- assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist")
- // Test set cell rich text with illegal cell coordinates
- assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN does not exist")
+ // Test set cell rich text with invalid sheet name
+ assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error())
+ // Test set cell rich text with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellRichText("Sheet1", "A", richTextRun))
+ richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}}
+ // Test set cell rich text with characters over the maximum limit
+ assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error())
+}
+
+func TestFormattedValue(t *testing.T) {
+ f := NewFile()
+ result, err := f.formattedValue(&xlsxC{S: 0, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // S is too large
+ result, err = f.formattedValue(&xlsxC{S: 15, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // S is too small
+ result, err = f.formattedValue(&xlsxC{S: -15, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+ customNumFmt := "[$-409]MM/DD/YYYY"
+ _, err = f.NewStyle(&Style{
+ CustomNumFmt: &customNumFmt,
+ })
+ assert.NoError(t, err)
+ result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "03/04/2019", result)
+
+ // Test format value with no built-in number format ID
+ numFmtID := 5
+ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
+ NumFmtID: &numFmtID,
+ })
+ result, err = f.formattedValue(&xlsxC{S: 2, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // Test format value with invalid number format ID
+ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
+ NumFmtID: nil,
+ })
+ result, err = f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // Test format value with empty number format
+ f.Styles.NumFmts = nil
+ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
+ NumFmtID: &numFmtID,
+ })
+ result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // Test format numeric value with shared string data type
+ f.Styles.NumFmts, numFmtID = nil, 11
+ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
+ NumFmtID: &numFmtID,
+ })
+ result, err = f.formattedValue(&xlsxC{S: 5, V: "43528"}, false, CellTypeSharedString)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+
+ // Test format decimal value with build-in number format ID
+ styleID, err := f.NewStyle(&Style{
+ NumFmt: 1,
+ })
+ assert.NoError(t, err)
+ result, err = f.formattedValue(&xlsxC{S: styleID, V: "310.56"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "311", result)
+
+ assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber, nil))
+
+ // Test format value with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ // Test format value with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ assert.Equal(t, "text", format("text", "0", false, CellTypeNumber, nil))
+}
+
+func TestFormattedValueNilXfs(t *testing.T) {
+ // Set the CellXfs to nil and verify that the formattedValue function does not crash
+ f := NewFile()
+ f.Styles.CellXfs = nil
+ result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+}
+
+func TestFormattedValueNilNumFmts(t *testing.T) {
+ // Set the NumFmts value to nil and verify that the formattedValue function does not crash
+ f := NewFile()
+ f.Styles.NumFmts = nil
+ result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+}
+
+func TestFormattedValueNilWorkbook(t *testing.T) {
+ // Set the Workbook value to nil and verify that the formattedValue function does not crash
+ f := NewFile()
+ f.WorkBook = nil
+ result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+}
+
+func TestFormattedValueNilWorkbookPr(t *testing.T) {
+ // Set the WorkBook.WorkbookPr value to nil and verify that the formattedValue function does not
+ // crash.
+ f := NewFile()
+ f.WorkBook.WorkbookPr = nil
+ result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
+ assert.NoError(t, err)
+ assert.Equal(t, "43528", result)
+}
+
+func TestGetCustomNumFmtCode(t *testing.T) {
+ expected := "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"
+ styleSheet := &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{
+ {NumFmtID: 164, FormatCode16: expected},
+ }}}
+ numFmtCode, ok := styleSheet.getCustomNumFmtCode(164)
+ assert.Equal(t, expected, numFmtCode)
+ assert.True(t, ok)
+}
+
+func TestSharedStringsError(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
+ assert.NoError(t, err)
+ tempFile, ok := f.tempFiles.Load(defaultXMLPathSharedStrings)
+ assert.True(t, ok)
+ f.tempFiles.Store(defaultXMLPathSharedStrings, "")
+ assert.Equal(t, "1", f.getFromStringItem(1))
+ // Cleanup undelete temporary files
+ assert.NoError(t, os.Remove(tempFile.(string)))
+ // Test reload the file error on set cell value and rich text. The error message was different between macOS and Windows
+ err = f.SetCellValue("Sheet1", "A19", "A19")
+ assert.Error(t, err)
+
+ f.tempFiles.Store(defaultXMLPathSharedStrings, "")
+ err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{})
+ assert.Error(t, err)
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
+ assert.NoError(t, err)
+ rows, err := f.Rows("Sheet1")
+ assert.NoError(t, err)
+ const maxUint16 = 1<<16 - 1
+ currentRow := 0
+ for rows.Next() {
+ currentRow++
+ if currentRow == 19 {
+ _, err := rows.Columns()
+ assert.NoError(t, err)
+ // Test get cell value from string item with invalid offset
+ f.sharedStringItem[1] = []uint{maxUint16 - 1, maxUint16}
+ assert.Equal(t, "1", f.getFromStringItem(1))
+ break
+ }
+ }
+ assert.NoError(t, rows.Close())
+ // Test shared string item temporary files has been closed before close the workbook
+ assert.NoError(t, f.sharedStringTemp.Close())
+ assert.Error(t, f.Close())
+ // Cleanup undelete temporary files
+ f.tempFiles.Range(func(k, v interface{}) bool {
+ return assert.NoError(t, os.Remove(v.(string)))
+ })
+
+ f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
+ assert.NoError(t, err)
+ rows, err = f.Rows("Sheet1")
+ assert.NoError(t, err)
+ currentRow = 0
+ for rows.Next() {
+ currentRow++
+ if currentRow == 19 {
+ _, err := rows.Columns()
+ assert.NoError(t, err)
+ break
+ }
+ }
+ assert.NoError(t, rows.Close())
+ assert.NoError(t, f.sharedStringTemp.Close())
+ // Test shared string item temporary files has been closed before set the cell value
+ assert.Error(t, f.SetCellValue("Sheet1", "A1", "A1"))
+ assert.Error(t, f.Close())
+ // Cleanup undelete temporary files
+ f.tempFiles.Range(func(k, v interface{}) bool {
+ return assert.NoError(t, os.Remove(v.(string)))
+ })
+}
+
+func TestSetCellIntFunc(t *testing.T) {
+ cases := []struct {
+ val interface{}
+ target string
+ }{
+ {val: 128, target: "128"},
+ {val: int8(-128), target: "-128"},
+ {val: int16(-32768), target: "-32768"},
+ {val: int32(-2147483648), target: "-2147483648"},
+ {val: int64(-9223372036854775808), target: "-9223372036854775808"},
+ {val: uint(128), target: "128"},
+ {val: uint8(255), target: "255"},
+ {val: uint16(65535), target: "65535"},
+ {val: uint32(4294967295), target: "4294967295"},
+ {val: uint64(18446744073709551615), target: "18446744073709551615"},
+ }
+ for _, c := range cases {
+ cell := &xlsxC{}
+ setCellIntFunc(cell, c.val)
+ assert.Equal(t, c.target, cell.V)
+ }
+}
+
+func TestSIString(t *testing.T) {
+ assert.Empty(t, xlsxSI{}.String())
}
diff --git a/chart.go b/chart.go
index ae31f714ee..88df5c2f21 100644
--- a/chart.go
+++ b/chart.go
@@ -1,86 +1,112 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
- "encoding/json"
"encoding/xml"
- "errors"
"fmt"
"strconv"
"strings"
)
-// This section defines the currently supported chart types.
+// ChartType is the type of supported chart types.
+type ChartType byte
+
+// This section defines the currently supported chart types enumeration.
+const (
+ Area ChartType = iota
+ AreaStacked
+ AreaPercentStacked
+ Area3D
+ Area3DStacked
+ Area3DPercentStacked
+ Bar
+ BarStacked
+ BarPercentStacked
+ Bar3DClustered
+ Bar3DStacked
+ Bar3DPercentStacked
+ Bar3DConeClustered
+ Bar3DConeStacked
+ Bar3DConePercentStacked
+ Bar3DPyramidClustered
+ Bar3DPyramidStacked
+ Bar3DPyramidPercentStacked
+ Bar3DCylinderClustered
+ Bar3DCylinderStacked
+ Bar3DCylinderPercentStacked
+ Col
+ ColStacked
+ ColPercentStacked
+ Col3D
+ Col3DClustered
+ Col3DStacked
+ Col3DPercentStacked
+ Col3DCone
+ Col3DConeClustered
+ Col3DConeStacked
+ Col3DConePercentStacked
+ Col3DPyramid
+ Col3DPyramidClustered
+ Col3DPyramidStacked
+ Col3DPyramidPercentStacked
+ Col3DCylinder
+ Col3DCylinderClustered
+ Col3DCylinderStacked
+ Col3DCylinderPercentStacked
+ Doughnut
+ Line
+ Line3D
+ Pie
+ Pie3D
+ PieOfPie
+ BarOfPie
+ Radar
+ Scatter
+ Surface3D
+ WireframeSurface3D
+ Contour
+ WireframeContour
+ Bubble
+ Bubble3D
+)
+
+// ChartLineType is the type of supported chart line types.
+type ChartLineType byte
+
+// This section defines the currently supported chart line types enumeration.
const (
- Area = "area"
- AreaStacked = "areaStacked"
- AreaPercentStacked = "areaPercentStacked"
- Area3D = "area3D"
- Area3DStacked = "area3DStacked"
- Area3DPercentStacked = "area3DPercentStacked"
- Bar = "bar"
- BarStacked = "barStacked"
- BarPercentStacked = "barPercentStacked"
- Bar3DClustered = "bar3DClustered"
- Bar3DStacked = "bar3DStacked"
- Bar3DPercentStacked = "bar3DPercentStacked"
- Bar3DConeClustered = "bar3DConeClustered"
- Bar3DConeStacked = "bar3DConeStacked"
- Bar3DConePercentStacked = "bar3DConePercentStacked"
- Bar3DPyramidClustered = "bar3DPyramidClustered"
- Bar3DPyramidStacked = "bar3DPyramidStacked"
- Bar3DPyramidPercentStacked = "bar3DPyramidPercentStacked"
- Bar3DCylinderClustered = "bar3DCylinderClustered"
- Bar3DCylinderStacked = "bar3DCylinderStacked"
- Bar3DCylinderPercentStacked = "bar3DCylinderPercentStacked"
- Col = "col"
- ColStacked = "colStacked"
- ColPercentStacked = "colPercentStacked"
- Col3D = "col3D"
- Col3DClustered = "col3DClustered"
- Col3DStacked = "col3DStacked"
- Col3DPercentStacked = "col3DPercentStacked"
- Col3DCone = "col3DCone"
- Col3DConeClustered = "col3DConeClustered"
- Col3DConeStacked = "col3DConeStacked"
- Col3DConePercentStacked = "col3DConePercentStacked"
- Col3DPyramid = "col3DPyramid"
- Col3DPyramidClustered = "col3DPyramidClustered"
- Col3DPyramidStacked = "col3DPyramidStacked"
- Col3DPyramidPercentStacked = "col3DPyramidPercentStacked"
- Col3DCylinder = "col3DCylinder"
- Col3DCylinderClustered = "col3DCylinderClustered"
- Col3DCylinderStacked = "col3DCylinderStacked"
- Col3DCylinderPercentStacked = "col3DCylinderPercentStacked"
- Doughnut = "doughnut"
- Line = "line"
- Pie = "pie"
- Pie3D = "pie3D"
- PieOfPieChart = "pieOfPie"
- BarOfPieChart = "barOfPie"
- Radar = "radar"
- Scatter = "scatter"
- Surface3D = "surface3D"
- WireframeSurface3D = "wireframeSurface3D"
- Contour = "contour"
- WireframeContour = "wireframeContour"
- Bubble = "bubble"
- Bubble3D = "bubble3D"
+ ChartLineUnset ChartLineType = iota
+ ChartLineSolid
+ ChartLineNone
+ ChartLineAutomatic
+)
+
+// ChartTickLabelPositionType is the type of supported chart tick label position
+// types.
+type ChartTickLabelPositionType byte
+
+// This section defines the supported chart tick label position types
+// enumeration.
+const (
+ ChartTickLabelNextToAxis ChartTickLabelPositionType = iota
+ ChartTickLabelHigh
+ ChartTickLabelLow
+ ChartTickLabelNone
)
// This section defines the default value of chart properties.
var (
- chartView3DRotX = map[string]int{
+ chartView3DRotX = map[ChartType]int{
Area: 0,
AreaStacked: 0,
AreaPercentStacked: 0,
@@ -123,10 +149,11 @@ var (
Col3DCylinderPercentStacked: 15,
Doughnut: 0,
Line: 0,
+ Line3D: 20,
Pie: 0,
Pie3D: 30,
- PieOfPieChart: 0,
- BarOfPieChart: 0,
+ PieOfPie: 0,
+ BarOfPie: 0,
Radar: 0,
Scatter: 0,
Surface3D: 15,
@@ -134,7 +161,7 @@ var (
Contour: 90,
WireframeContour: 90,
}
- chartView3DRotY = map[string]int{
+ chartView3DRotY = map[ChartType]int{
Area: 0,
AreaStacked: 0,
AreaPercentStacked: 0,
@@ -177,10 +204,11 @@ var (
Col3DCylinderPercentStacked: 20,
Doughnut: 0,
Line: 0,
+ Line3D: 15,
Pie: 0,
Pie3D: 0,
- PieOfPieChart: 0,
- BarOfPieChart: 0,
+ PieOfPie: 0,
+ BarOfPie: 0,
Radar: 0,
Scatter: 0,
Surface3D: 20,
@@ -188,17 +216,18 @@ var (
Contour: 0,
WireframeContour: 0,
}
- plotAreaChartOverlap = map[string]int{
+ plotAreaChartOverlap = map[ChartType]int{
BarStacked: 100,
BarPercentStacked: 100,
ColStacked: 100,
ColPercentStacked: 100,
}
- chartView3DPerspective = map[string]int{
+ chartView3DPerspective = map[ChartType]int{
+ Line3D: 30,
Contour: 0,
WireframeContour: 0,
}
- chartView3DRAngAx = map[string]int{
+ chartView3DRAngAx = map[ChartType]int{
Area: 0,
AreaStacked: 0,
AreaPercentStacked: 0,
@@ -241,10 +270,11 @@ var (
Col3DCylinderPercentStacked: 1,
Doughnut: 0,
Line: 0,
+ Line3D: 0,
Pie: 0,
Pie3D: 0,
- PieOfPieChart: 0,
- BarOfPieChart: 0,
+ PieOfPie: 0,
+ BarOfPie: 0,
Radar: 0,
Scatter: 0,
Surface3D: 0,
@@ -260,7 +290,7 @@ var (
"top": "t",
"top_right": "tr",
}
- chartValAxNumFmtFormatCode = map[string]string{
+ chartValAxNumFmtFormatCode = map[ChartType]string{
Area: "General",
AreaStacked: "General",
AreaPercentStacked: "0%",
@@ -303,10 +333,11 @@ var (
Col3DCylinderPercentStacked: "0%",
Doughnut: "General",
Line: "General",
+ Line3D: "General",
Pie: "General",
Pie3D: "General",
- PieOfPieChart: "General",
- BarOfPieChart: "General",
+ PieOfPie: "General",
+ BarOfPie: "General",
Radar: "General",
Scatter: "General",
Surface3D: "General",
@@ -316,7 +347,7 @@ var (
Bubble: "General",
Bubble3D: "General",
}
- chartValAxCrossBetween = map[string]string{
+ chartValAxCrossBetween = map[ChartType]string{
Area: "midCat",
AreaStacked: "midCat",
AreaPercentStacked: "midCat",
@@ -359,10 +390,11 @@ var (
Col3DCylinderPercentStacked: "between",
Doughnut: "between",
Line: "between",
+ Line3D: "between",
Pie: "between",
Pie3D: "between",
- PieOfPieChart: "between",
- BarOfPieChart: "between",
+ PieOfPie: "between",
+ BarOfPie: "between",
Radar: "between",
Scatter: "between",
Surface3D: "midCat",
@@ -372,7 +404,7 @@ var (
Bubble: "midCat",
Bubble3D: "midCat",
}
- plotAreaChartGrouping = map[string]string{
+ plotAreaChartGrouping = map[ChartType]string{
Area: "standard",
AreaStacked: "stacked",
AreaPercentStacked: "percentStacked",
@@ -414,8 +446,9 @@ var (
Col3DCylinderStacked: "stacked",
Col3DCylinderPercentStacked: "percentStacked",
Line: "standard",
+ Line3D: "standard",
}
- plotAreaChartBarDir = map[string]string{
+ plotAreaChartBarDir = map[ChartType]string{
Bar: "bar",
BarStacked: "bar",
BarPercentStacked: "bar",
@@ -451,6 +484,43 @@ var (
Col3DCylinderStacked: "col",
Col3DCylinderPercentStacked: "col",
Line: "standard",
+ Line3D: "standard",
+ }
+ barColChartTypes = []ChartType{
+ Bar,
+ BarStacked,
+ BarPercentStacked,
+ Bar3DClustered,
+ Bar3DStacked,
+ Bar3DPercentStacked,
+ Bar3DConeClustered,
+ Bar3DConeStacked,
+ Bar3DConePercentStacked,
+ Bar3DPyramidClustered,
+ Bar3DPyramidStacked,
+ Bar3DPyramidPercentStacked,
+ Bar3DCylinderClustered,
+ Bar3DCylinderStacked,
+ Bar3DCylinderPercentStacked,
+ Col,
+ ColStacked,
+ ColPercentStacked,
+ Col3D,
+ Col3DClustered,
+ Col3DStacked,
+ Col3DPercentStacked,
+ Col3DCone,
+ Col3DConeStacked,
+ Col3DConeClustered,
+ Col3DConePercentStacked,
+ Col3DPyramid,
+ Col3DPyramidClustered,
+ Col3DPyramidStacked,
+ Col3DPyramidPercentStacked,
+ Col3DCylinder,
+ Col3DCylinderClustered,
+ Col3DCylinderStacked,
+ Col3DCylinderPercentStacked,
}
orientation = map[bool]string{
true: "maxMin",
@@ -464,40 +534,71 @@ var (
true: "r",
false: "l",
}
- valTickLblPos = map[string]string{
+ tickLblPosVal = map[ChartTickLabelPositionType]string{
+ ChartTickLabelNextToAxis: "nextTo",
+ ChartTickLabelHigh: "high",
+ ChartTickLabelLow: "low",
+ ChartTickLabelNone: "none",
+ }
+ tickLblPosNone = map[ChartType]string{
Contour: "none",
WireframeContour: "none",
}
)
-// parseFormatChartSet provides a function to parse the format settings of the
+// parseChartOptions provides a function to parse the format settings of the
// chart with default value.
-func parseFormatChartSet(formatSet string) (*formatChart, error) {
- format := formatChart{
- Dimension: formatChartDimension{
- Width: 480,
- Height: 290,
- },
- Format: formatPicture{
- FPrintsWithSheet: true,
- FLocksWithSheet: false,
- NoChangeAspect: false,
- OffsetX: 0,
- OffsetY: 0,
- XScale: 1.0,
- YScale: 1.0,
- },
- Legend: formatChartLegend{
- Position: "bottom",
- ShowLegendKey: false,
- },
- Title: formatChartTitle{
- Name: " ",
- },
- ShowBlanksAs: "gap",
+func parseChartOptions(opts *Chart) (*Chart, error) {
+ if opts == nil {
+ return nil, ErrParameterInvalid
+ }
+ if opts.Dimension.Width == 0 {
+ opts.Dimension.Width = defaultChartDimensionWidth
+ }
+ if opts.Dimension.Height == 0 {
+ opts.Dimension.Height = defaultChartDimensionHeight
+ }
+ if opts.Format.PrintObject == nil {
+ opts.Format.PrintObject = boolPtr(true)
+ }
+ if opts.Format.Locked == nil {
+ opts.Format.Locked = boolPtr(false)
+ }
+ if opts.Format.ScaleX == 0 {
+ opts.Format.ScaleX = defaultDrawingScale
+ }
+ if opts.Format.ScaleY == 0 {
+ opts.Format.ScaleY = defaultDrawingScale
+ }
+ if opts.Legend.Position == "" {
+ opts.Legend.Position = defaultChartLegendPosition
+ }
+ opts.parseTitle()
+ if opts.VaryColors == nil {
+ opts.VaryColors = boolPtr(true)
+ }
+ if opts.Border.Width == 0 {
+ opts.Border.Width = 0.75
+ }
+ if opts.ShowBlanksAs == "" {
+ opts.ShowBlanksAs = defaultChartShowBlanksAs
+ }
+ return opts, nil
+}
+
+// parseTitle parse the title settings of the chart with default value.
+func (opts *Chart) parseTitle() {
+ for i := range opts.Title {
+ if opts.Title[i].Font == nil {
+ opts.Title[i].Font = &Font{}
+ }
+ if opts.Title[i].Font.Color == "" {
+ opts.Title[i].Font.Color = "595959"
+ }
+ if opts.Title[i].Font.Size == 0 {
+ opts.Title[i].Font.Size = 14
+ }
}
- err := json.Unmarshal([]byte(formatSet), &format)
- return &format, err
}
// AddChart provides the method to add chart in a sheet by given chart format
@@ -505,239 +606,509 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) {
// properties set. For example, create 3D clustered column chart with data
// Sheet1!$E$1:$L$15:
//
-// package main
-//
-// import (
-// "fmt"
-//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
-//
-// func main() {
-// categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
-// values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
-// f := excelize.NewFile()
-// for k, v := range categories {
-// f.SetCellValue("Sheet1", k, v)
-// }
-// for k, v := range values {
-// f.SetCellValue("Sheet1", k, v)
-// }
-// if err := f.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil {
-// fmt.Println(err)
-// return
-// }
-// // Save xlsx file by the given path.
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
+// package main
+//
+// import (
+// "fmt"
+//
+// "github.com/xuri/excelize/v2"
+// )
+//
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// for idx, row := range [][]interface{}{
+// {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3},
+// {"Normal", 5, 2, 4}, {"Large", 6, 7, 8},
+// } {
+// cell, err := excelize.CoordinatesToCellName(1, idx+1)
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// f.SetSheetRow("Sheet1", cell, &row)
+// }
+// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
+// Type: excelize.Col3DClustered,
+// Series: []excelize.ChartSeries{
+// {
+// Name: "Sheet1!$A$2",
+// Categories: "Sheet1!$B$1:$D$1",
+// Values: "Sheet1!$B$2:$D$2",
+// },
+// {
+// Name: "Sheet1!$A$3",
+// Categories: "Sheet1!$B$1:$D$1",
+// Values: "Sheet1!$B$3:$D$3",
+// },
+// {
+// Name: "Sheet1!$A$4",
+// Categories: "Sheet1!$B$1:$D$1",
+// Values: "Sheet1!$B$4:$D$4",
+// },
+// },
+// Title: []excelize.RichTextRun{
+// {
+// Text: "Fruit 3D Clustered Column Chart",
+// },
+// },
+// Legend: excelize.ChartLegend{
+// ShowLegendKey: false,
+// },
+// PlotArea: excelize.ChartPlotArea{
+// ShowBubbleSize: true,
+// ShowCatName: false,
+// ShowLeaderLines: false,
+// ShowPercent: true,
+// ShowSerName: true,
+// ShowVal: true,
+// },
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// // Save spreadsheet by the given path.
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
//
// The following shows the type of chart supported by excelize:
//
-// Type | Chart
-// -----------------------------+------------------------------
-// area | 2D area chart
-// areaStacked | 2D stacked area chart
-// areaPercentStacked | 2D 100% stacked area chart
-// area3D | 3D area chart
-// area3DStacked | 3D stacked area chart
-// area3DPercentStacked | 3D 100% stacked area chart
-// bar | 2D clustered bar chart
-// barStacked | 2D stacked bar chart
-// barPercentStacked | 2D 100% stacked bar chart
-// bar3DClustered | 3D clustered bar chart
-// bar3DStacked | 3D stacked bar chart
-// bar3DPercentStacked | 3D 100% stacked bar chart
-// bar3DConeClustered | 3D cone clustered bar chart
-// bar3DConeStacked | 3D cone stacked bar chart
-// bar3DConePercentStacked | 3D cone percent bar chart
-// bar3DPyramidClustered | 3D pyramid clustered bar chart
-// bar3DPyramidStacked | 3D pyramid stacked bar chart
-// bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart
-// bar3DCylinderClustered | 3D cylinder clustered bar chart
-// bar3DCylinderStacked | 3D cylinder stacked bar chart
-// bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart
-// col | 2D clustered column chart
-// colStacked | 2D stacked column chart
-// colPercentStacked | 2D 100% stacked column chart
-// col3DClustered | 3D clustered column chart
-// col3D | 3D column chart
-// col3DStacked | 3D stacked column chart
-// col3DPercentStacked | 3D 100% stacked column chart
-// col3DCone | 3D cone column chart
-// col3DConeClustered | 3D cone clustered column chart
-// col3DConeStacked | 3D cone stacked column chart
-// col3DConePercentStacked | 3D cone percent stacked column chart
-// col3DPyramid | 3D pyramid column chart
-// col3DPyramidClustered | 3D pyramid clustered column chart
-// col3DPyramidStacked | 3D pyramid stacked column chart
-// col3DPyramidPercentStacked | 3D pyramid percent stacked column chart
-// col3DCylinder | 3D cylinder column chart
-// col3DCylinderClustered | 3D cylinder clustered column chart
-// col3DCylinderStacked | 3D cylinder stacked column chart
-// col3DCylinderPercentStacked | 3D cylinder percent stacked column chart
-// doughnut | doughnut chart
-// line | line chart
-// pie | pie chart
-// pie3D | 3D pie chart
-// pieOfPie | pie of pie chart
-// barOfPie | bar of pie chart
-// radar | radar chart
-// scatter | scatter chart
-// surface3D | 3D surface chart
-// wireframeSurface3D | 3D wireframe surface chart
-// contour | contour chart
-// wireframeContour | wireframe contour chart
-// bubble | bubble chart
-// bubble3D | 3D bubble chart
-//
-// In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting.
+// ID | Enumeration | Chart
+// ----+-----------------------------+------------------------------
+// 0 | Area | 2D area chart
+// 1 | AreaStacked | 2D stacked area chart
+// 2 | AreaPercentStacked | 2D 100% stacked area chart
+// 3 | Area3D | 3D area chart
+// 4 | Area3DStacked | 3D stacked area chart
+// 5 | Area3DPercentStacked | 3D 100% stacked area chart
+// 6 | Bar | 2D clustered bar chart
+// 7 | BarStacked | 2D stacked bar chart
+// 8 | BarPercentStacked | 2D 100% stacked bar chart
+// 9 | Bar3DClustered | 3D clustered bar chart
+// 10 | Bar3DStacked | 3D stacked bar chart
+// 11 | Bar3DPercentStacked | 3D 100% stacked bar chart
+// 12 | Bar3DConeClustered | 3D cone clustered bar chart
+// 13 | Bar3DConeStacked | 3D cone stacked bar chart
+// 14 | Bar3DConePercentStacked | 3D cone percent bar chart
+// 15 | Bar3DPyramidClustered | 3D pyramid clustered bar chart
+// 16 | Bar3DPyramidStacked | 3D pyramid stacked bar chart
+// 17 | Bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart
+// 18 | Bar3DCylinderClustered | 3D cylinder clustered bar chart
+// 19 | Bar3DCylinderStacked | 3D cylinder stacked bar chart
+// 20 | Bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart
+// 21 | Col | 2D clustered column chart
+// 22 | ColStacked | 2D stacked column chart
+// 23 | ColPercentStacked | 2D 100% stacked column chart
+// 24 | Col3DClustered | 3D clustered column chart
+// 25 | Col3D | 3D column chart
+// 26 | Col3DStacked | 3D stacked column chart
+// 27 | Col3DPercentStacked | 3D 100% stacked column chart
+// 28 | Col3DCone | 3D cone column chart
+// 29 | Col3DConeClustered | 3D cone clustered column chart
+// 30 | Col3DConeStacked | 3D cone stacked column chart
+// 31 | Col3DConePercentStacked | 3D cone percent stacked column chart
+// 32 | Col3DPyramid | 3D pyramid column chart
+// 33 | Col3DPyramidClustered | 3D pyramid clustered column chart
+// 34 | Col3DPyramidStacked | 3D pyramid stacked column chart
+// 35 | Col3DPyramidPercentStacked | 3D pyramid percent stacked column chart
+// 36 | Col3DCylinder | 3D cylinder column chart
+// 37 | Col3DCylinderClustered | 3D cylinder clustered column chart
+// 38 | Col3DCylinderStacked | 3D cylinder stacked column chart
+// 39 | Col3DCylinderPercentStacked | 3D cylinder percent stacked column chart
+// 40 | Doughnut | doughnut chart
+// 41 | Line | line chart
+// 42 | Line3D | 3D line chart
+// 43 | Pie | pie chart
+// 44 | Pie3D | 3D pie chart
+// 45 | PieOfPie | pie of pie chart
+// 46 | BarOfPie | bar of pie chart
+// 47 | Radar | radar chart
+// 48 | Scatter | scatter chart
+// 49 | Surface3D | 3D surface chart
+// 50 | WireframeSurface3D | 3D wireframe surface chart
+// 51 | Contour | contour chart
+// 52 | WireframeContour | wireframe contour chart
+// 53 | Bubble | bubble chart
+// 54 | Bubble3D | 3D bubble chart
+//
+// In Excel a chart series is a collection of information that defines which
+// data is plotted such as values, axis labels and formatting.
//
// The series options that can be set are:
//
-// name
-// categories
-// values
-// line
+// Name
+// Categories
+// Values
+// Fill
+// Line
+// Marker
+// DataLabel
+// DataLabelPosition
+//
+// Name: Set the name for the series. The name is displayed in the chart legend
+// and in the formula bar. The 'Name' property is optional and if it isn't
+// supplied it will default to Series 1..n. The name can also be a formula such
+// as Sheet1!$A$1
+//
+// Categories: This sets the chart category labels. The category is more or less
+// the same as the X axis. In most chart types the 'Categories' property is
+// optional and the chart will just assume a sequential series from 1..n.
+//
+// Values: This is the most important property of a series and is the only
+// mandatory option for every chart object. This option links the chart with
+// the worksheet data that it displays.
+//
+// Sizes: This sets the bubble size in a data series. The 'Sizes' property is
+// optional and the default value was same with 'Values'.
//
-// name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The name property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1
+// Fill: This set the format for the data series fill. The 'Fill' property is
+// optional
//
-// categories: This sets the chart category labels. The category is more or less the same as the X axis. In most chart types the categories property is optional and the chart will just assume a sequential series from 1..n.
+// Line: This sets the line format of the line chart. The 'Line' property is
+// optional and if it isn't supplied it will default style. The options that
+// can be set are width and color. The range of width is 0.25pt - 999pt. If the
+// value of width is outside the range, the default width of the line is 2pt.
//
-// values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays.
+// Marker: This sets the marker of the line chart and scatter chart. The range
+// of optional field 'Size' is 2-72 (default value is 5). The enumeration value
+// of optional field 'Symbol' are (default value is 'auto'):
//
-// line: This sets the line format of the line chart. The line property is optional and if it isn't supplied it will default style. The options that can be set is width. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt.
+// circle
+// dash
+// diamond
+// dot
+// none
+// picture
+// plus
+// square
+// star
+// triangle
+// x
+// auto
+//
+// DataLabel: This sets the format of the chart series data label.
+//
+// DataLabelPosition: This sets the position of the chart series data label.
//
// Set properties of the chart legend. The options that can be set are:
//
-// position
-// show_legend_key
+// Position
+// ShowLegendKey
//
-// position: Set the position of the chart legend. The default legend position is right. The available positions are:
+// Position: Set the position of the chart legend. The default legend position
+// is bottom. The available positions are:
//
-// top
-// bottom
-// left
-// right
-// top_right
+// none
+// top
+// bottom
+// left
+// right
+// top_right
//
-// show_legend_key: Set the legend keys shall be shown in data labels. The default value is false.
+// ShowLegendKey: Set the legend keys shall be shown in data labels. The default
+// value is false.
//
// Set properties of the chart title. The properties that can be set are:
//
-// title
+// Title
//
-// name: Set the name (title) for the chart. The name is displayed above the chart. The name can also be a formula such as Sheet1!$A$1 or a list with a sheetname. The name property is optional. The default is to have no chart title.
+// Title: Set the name (title) for the chart. The name is displayed above the
+// chart. The name can also be a formula such as Sheet1!$A$1 or a list with a
+// sheet name. The name property is optional. The default is to have no chart
+// title.
//
-// Specifies how blank cells are plotted on the chart by show_blanks_as. The default value is gap. The options that can be set are:
+// Specifies how blank cells are plotted on the chart by 'ShowBlanksAs'. The
+// default value is gap. The options that can be set are:
//
-// gap
-// span
-// zero
+// gap
+// span
+// zero
//
// gap: Specifies that blank values shall be left as a gap.
//
-// sapn: Specifies that blank values shall be spanned with a line.
+// span: Specifies that blank values shall be spanned with a line.
//
// zero: Specifies that blank values shall be treated as zero.
//
-// Set chart offset, scale, aspect ratio setting and print settings by format, same as function AddPicture.
+// Specifies that each data marker in the series has a different color by
+// 'VaryColors'. The default value is true.
+//
+// Set chart offset, scale, aspect ratio setting and print settings by 'Format',
+// same as function 'AddPicture'.
+//
+// Set the position of the chart plot area by 'PlotArea'. The properties that
+// can be set are:
+//
+// SecondPlotValues
+// ShowBubbleSize
+// ShowCatName
+// ShowLeaderLines
+// ShowPercent
+// ShowSerName
+// ShowVal
+// NumFmt
+//
+// SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and
+// 'barOfPie' chart.
+//
+// ShowBubbleSize: Specifies the bubble size shall be shown in a data label. The
+// 'ShowBubbleSize' property is optional. The default value is false.
+//
+// ShowCatName: Specifies that the category name shall be shown in the data
+// label. The 'ShowCatName' property is optional. The default value is true.
+//
+// ShowLeaderLines: Specifies leader lines shall be shown for data labels. The
+// 'ShowLeaderLines' property is optional. The default value is false.
+//
+// ShowPercent: Specifies that the percentage shall be shown in a data label.
+// The 'ShowPercent' property is optional. The default value is false.
+//
+// ShowSerName: Specifies that the series name shall be shown in a data label.
+// The 'ShowSerName' property is optional. The default value is false.
+//
+// ShowVal: Specifies that the value shall be shown in a data label.
+// The 'ShowVal' property is optional. The default value is false.
+//
+// NumFmt: Specifies that if linked to source and set custom number format code
+// for data labels. The 'NumFmt' property is optional. The default format code
+// is 'General'.
+//
+// Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'.
+// The properties of 'XAxis' that can be set are:
+//
+// None
+// MajorGridLines
+// MinorGridLines
+// TickLabelSkip
+// ReverseOrder
+// Maximum
+// Minimum
+// Alignment
+// Font
+// NumFmt
+// Title
+//
+// The properties of 'YAxis' that can be set are:
+//
+// None
+// MajorGridLines
+// MinorGridLines
+// MajorUnit
+// Secondary
+// ReverseOrder
+// Maximum
+// Minimum
+// Alignment
+// Font
+// LogBase
+// NumFmt
+// Title
+//
+// None: Disable axes.
+//
+// MajorGridLines: Specifies major grid lines.
+//
+// MinorGridLines: Specifies minor grid lines.
//
-// Set the position of the chart plot area by plotarea. The properties that can be set are:
+// MajorUnit: Specifies the distance between major ticks. Shall contain a
+// positive floating-point number. The 'MajorUnit' property is optional. The
+// default value is auto.
//
-// show_bubble_size
-// show_cat_name
-// show_leader_lines
-// show_percent
-// show_series_name
-// show_val
+// Secondary: Specifies the current series vertical axis as the secondary axis,
+// this only works for the second and later chart in the combo chart. The
+// default value is false.
//
-// show_bubble_size: Specifies the bubble size shall be shown in a data label. The show_bubble_size property is optional. The default value is false.
+// TickLabelSkip: Specifies how many tick labels to skip between label that is
+// drawn. The 'TickLabelSkip' property is optional. The default value is auto.
//
-// show_cat_name: Specifies that the category name shall be shown in the data label. The show_cat_name property is optional. The default value is true.
+// ReverseOrder: Specifies that the categories or values on reverse order
+// (orientation of the chart). The 'ReverseOrder' property is optional. The
+// default value is false.
//
-// show_leader_lines: Specifies leader lines shall be shown for data labels. The show_leader_lines property is optional. The default value is false.
+// Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property
+// is optional. The default value is auto.
//
-// show_percent: Specifies that the percentage shall be shown in a data label. The show_percent property is optional. The default value is false.
+// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property
+// is optional. The default value is auto.
//
-// show_series_name: Specifies that the series name shall be shown in a data label. The show_series_name property is optional. The default value is false.
+// Alignment: Specifies that the alignment of the horizontal and vertical axis.
+// The properties of alignment that can be set are:
//
-// show_val: Specifies that the value shall be shown in a data label. The show_val property is optional. The default value is false.
+// TextRotation
+// Vertical
//
-// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties of x_axis that can be set are:
+// The value of 'TextRotation' that can be set from -90 to 90.
//
-// major_grid_lines
-// minor_grid_lines
-// tick_label_skip
-// reverse_order
-// maximum
-// minimum
+// The value of 'Vertical' that can be set are:
//
-// The properties of y_axis that can be set are:
+// horz
+// vert
+// vert270
+// wordArtVert
+// eaVert
+// mongolianVert
+// wordArtVertRtl
//
-// major_grid_lines
-// minor_grid_lines
-// major_unit
-// reverse_order
-// maximum
-// minimum
+// Font: Specifies that the font of the horizontal and vertical axis. The
+// properties of font that can be set are:
//
-// major_grid_lines: Specifies major gridlines.
+// Bold
+// Italic
+// Underline
+// Family
+// Size
+// Strike
+// Color
+// VertAlign
//
-// minor_grid_lines: Specifies minor gridlines.
+// LogBase: Specifies logarithmic scale base number of the vertical axis.
//
-// major_unit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The major_unit property is optional. The default value is auto.
+// NumFmt: Specifies that if linked to source and set custom number format code
+// for axis. The 'NumFmt' property is optional. The default format code is
+// 'General'.
//
-// tick_label_skip: Specifies how many tick labels to skip between label that is drawn. The tick_label_skip property is optional. The default value is auto.
+// Title: Specifies that the primary horizontal or vertical axis title and
+// resize chart. The 'Title' property is optional.
//
-// reverse_order: Specifies that the categories or values on reverse order (orientation of the chart). The reverse_order property is optional. The default value is false.
+// Set chart size by 'Dimension' property. The 'Dimension' property is optional.
+// The default width is 480, and height is 260.
//
-// maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto.
+// Set the bubble size in all data series for the bubble chart or 3D bubble
+// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. The
+// default width is 100, and the value should be great than 0 and less or equal
+// than 300.
//
-// minimum: Specifies that the fixed minimum, 0 is auto. The minimum property is optional. The default value is auto.
+// Set the doughnut hole size in all data series for the doughnut chart by
+// 'HoleSize' property. The 'HoleSize' property is optional. The default width
+// is 75, and the value should be great than 0 and less or equal than 90.
//
-// Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290.
+// Set the gap with of the column and bar series chart by 'GapWidth' property.
+// The 'GapWidth' property is optional. The default width is 150, and the value
+// should be great or equal than 0 and less or equal than 500.
//
-// combo: Specifies the create a chart that combines two or more chart types
-// in a single chart. For example, create a clustered column - line chart with
+// Set series overlap of the column and bar series chart by 'Overlap' property.
+// The 'Overlap' property is optional. The default width is 0, and the value
+// should be great or equal than -100 and less or equal than 100.
+//
+// combo: Specifies the create a chart that combines two or more chart types in
+// a single chart. For example, create a clustered column - line chart with
// data Sheet1!$E$1:$L$15:
//
-// package main
-//
-// import (
-// "fmt"
-//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
-//
-// func main() {
-// categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
-// values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
-// f := excelize.NewFile()
-// for k, v := range categories {
-// f.SetCellValue("Sheet1", k, v)
-// }
-// for k, v := range values {
-// f.SetCellValue("Sheet1", k, v)
-// }
-// if err := f.AddChart("Sheet1", "E1", `{"type":"col","series":[{"name":"Sheet1!$A$2","categories":"","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`); err != nil {
-// fmt.Println(err)
-// return
-// }
-// // Save xlsx file by the given path.
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
-//
-func (f *File) AddChart(sheet, cell, format string, combo ...string) error {
- // Read sheet data.
- xlsx, err := f.workSheetReader(sheet)
+// package main
+//
+// import (
+// "fmt"
+//
+// "github.com/xuri/excelize/v2"
+// )
+//
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// for idx, row := range [][]interface{}{
+// {nil, "Apple", "Orange", "Pear"}, {"Small", 2, 3, 3},
+// {"Normal", 5, 2, 4}, {"Large", 6, 7, 8},
+// } {
+// cell, err := excelize.CoordinatesToCellName(1, idx+1)
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// f.SetSheetRow("Sheet1", cell, &row)
+// }
+// enable, disable := true, false
+// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
+// Type: excelize.Col,
+// Series: []excelize.ChartSeries{
+// {
+// Name: "Sheet1!$A$2",
+// Categories: "Sheet1!$B$1:$D$1",
+// Values: "Sheet1!$B$2:$D$2",
+// },
+// },
+// Format: excelize.GraphicOptions{
+// ScaleX: 1,
+// ScaleY: 1,
+// OffsetX: 15,
+// OffsetY: 10,
+// PrintObject: &enable,
+// LockAspectRatio: false,
+// Locked: &disable,
+// },
+// Title: []excelize.RichTextRun{
+// {
+// Text: "Clustered Column - Line Chart",
+// },
+// },
+// Legend: excelize.ChartLegend{
+// Position: "left",
+// ShowLegendKey: false,
+// },
+// PlotArea: excelize.ChartPlotArea{
+// ShowCatName: false,
+// ShowLeaderLines: false,
+// ShowPercent: true,
+// ShowSerName: true,
+// ShowVal: true,
+// },
+// }, &excelize.Chart{
+// Type: excelize.Line,
+// Series: []excelize.ChartSeries{
+// {
+// Name: "Sheet1!$A$4",
+// Categories: "Sheet1!$B$1:$D$1",
+// Values: "Sheet1!$B$4:$D$4",
+// Marker: excelize.ChartMarker{
+// Symbol: "none", Size: 10,
+// },
+// },
+// },
+// Format: excelize.GraphicOptions{
+// ScaleX: 1,
+// ScaleY: 1,
+// OffsetX: 15,
+// OffsetY: 10,
+// PrintObject: &enable,
+// LockAspectRatio: false,
+// Locked: &disable,
+// },
+// Legend: excelize.ChartLegend{
+// Position: "right",
+// ShowLegendKey: false,
+// },
+// PlotArea: excelize.ChartPlotArea{
+// ShowCatName: false,
+// ShowLeaderLines: false,
+// ShowPercent: true,
+// ShowSerName: true,
+// ShowVal: true,
+// },
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// // Save spreadsheet by the given path.
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
+func (f *File) AddChart(sheet, cell string, chart *Chart, combo ...*Chart) error {
+ // Read worksheet data
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- formatSet, comboCharts, err := f.getFormatChart(format, combo)
+ opts, comboCharts, err := f.getChartOptions(chart, combo)
if err != nil {
return err
}
@@ -745,16 +1116,18 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error {
drawingID := f.countDrawings() + 1
chartID := f.countCharts() + 1
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
- drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML)
+ drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "")
- err = f.addDrawingChart(sheet, drawingXML, cell, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format)
+ err = f.addDrawingChart(sheet, drawingXML, cell, int(opts.Dimension.Width), int(opts.Dimension.Height), drawingRID, &opts.Format)
if err != nil {
return err
}
- f.addChart(formatSet, comboCharts)
- f.addContentTypePart(chartID, "chart")
- f.addContentTypePart(drawingID, "drawings")
+ f.addChart(opts, comboCharts)
+ if err = f.addContentTypePart(chartID, "chart"); err != nil {
+ return err
+ }
+ _ = f.addContentTypePart(drawingID, "drawings")
f.addSheetNameSpace(sheet, SourceRelationship)
return err
}
@@ -763,22 +1136,26 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error {
// format set (such as offset, scale, aspect ratio setting and print settings)
// and properties set. In Excel a chartsheet is a worksheet that only contains
// a chart.
-func (f *File) AddChartSheet(sheet, format string, combo ...string) error {
+func (f *File) AddChartSheet(sheet string, chart *Chart, combo ...*Chart) error {
// Check if the worksheet already exists
- if f.GetSheetIndex(sheet) != -1 {
- return errors.New("the same name worksheet already exists")
+ idx, err := f.GetSheetIndex(sheet)
+ if err != nil {
+ return err
+ }
+ if idx != -1 {
+ return ErrExistsSheet
}
- formatSet, comboCharts, err := f.getFormatChart(format, combo)
+ opts, comboCharts, err := f.getChartOptions(chart, combo)
if err != nil {
return err
}
cs := xlsxChartsheet{
- SheetViews: []*xlsxChartsheetViews{{
- SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}},
+ SheetViews: &xlsxChartsheetViews{
+ SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}},
},
}
f.SheetCount++
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
sheetID := 0
for _, v := range wb.Sheets.Sheet {
if v.SheetID > sheetID {
@@ -787,22 +1164,26 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error {
}
sheetID++
path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml"
- f.sheetMap[trimSheetName(sheet)] = path
- f.Sheet[path] = nil
+ f.sheetMap[sheet] = path
+ f.Sheet.Store(path, nil)
drawingID := f.countDrawings() + 1
chartID := f.countCharts() + 1
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
f.prepareChartSheetDrawing(&cs, drawingID, sheet)
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "")
- f.addSheetDrawingChart(drawingXML, drawingRID, &formatSet.Format)
- f.addChart(formatSet, comboCharts)
- f.addContentTypePart(chartID, "chart")
- f.addContentTypePart(sheetID, "chartsheet")
- f.addContentTypePart(drawingID, "drawings")
- // Update xl/_rels/workbook.xml.rels
- rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "")
- // Update xl/workbook.xml
+ if err = f.addSheetDrawingChart(drawingXML, drawingRID, &opts.Format); err != nil {
+ return err
+ }
+ f.addChart(opts, comboCharts)
+ if err = f.addContentTypePart(chartID, "chart"); err != nil {
+ return err
+ }
+ _ = f.addContentTypePart(sheetID, "chartsheet")
+ _ = f.addContentTypePart(drawingID, "drawings")
+ // Update workbook.xml.rels
+ rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipChartsheet, fmt.Sprintf("/xl/chartsheets/sheet%d.xml", sheetID), "")
+ // Update workbook.xml
f.setWorkbook(sheet, sheetID, rID)
chartsheet, _ := xml.Marshal(cs)
f.addSheetNameSpace(sheet, NameSpaceSpreadSheet)
@@ -810,59 +1191,61 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error {
return err
}
-// getFormatChart provides a function to check format set of the chart and
+// getChartOptions provides a function to check format set of the chart and
// create chart format.
-func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*formatChart, error) {
- comboCharts := []*formatChart{}
- formatSet, err := parseFormatChartSet(format)
+func (f *File) getChartOptions(opts *Chart, combo []*Chart) (*Chart, []*Chart, error) {
+ var comboCharts []*Chart
+ options, err := parseChartOptions(opts)
if err != nil {
- return formatSet, comboCharts, err
+ return options, comboCharts, err
}
for _, comboFormat := range combo {
- comboChart, err := parseFormatChartSet(comboFormat)
+ comboChart, err := parseChartOptions(comboFormat)
if err != nil {
- return formatSet, comboCharts, err
+ return options, comboCharts, err
}
if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok {
- return formatSet, comboCharts, errors.New("unsupported chart type " + comboChart.Type)
+ return options, comboCharts, newUnsupportedChartType(comboChart.Type)
}
comboCharts = append(comboCharts, comboChart)
}
- if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok {
- return formatSet, comboCharts, errors.New("unsupported chart type " + formatSet.Type)
+ if _, ok := chartValAxNumFmtFormatCode[options.Type]; !ok {
+ return options, comboCharts, newUnsupportedChartType(options.Type)
}
- return formatSet, comboCharts, err
+ return options, comboCharts, err
}
-// DeleteChart provides a function to delete chart in XLSX by given worksheet
-// and cell name.
-func (f *File) DeleteChart(sheet, cell string) (err error) {
+// DeleteChart provides a function to delete chart in spreadsheet by given
+// worksheet name and cell reference.
+func (f *File) DeleteChart(sheet, cell string) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
- return
+ return err
}
col--
row--
ws, err := f.workSheetReader(sheet)
if err != nil {
- return
+ return err
}
if ws.Drawing == nil {
- return
+ return err
}
- drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1)
- return f.deleteDrawing(col, row, drawingXML, "Chart")
+ drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
+ _, err = f.deleteDrawing(col, row, drawingXML, "Chart")
+ return err
}
// countCharts provides a function to get chart files count storage in the
// folder xl/charts.
func (f *File) countCharts() int {
count := 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/charts/chart") {
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/charts/chart") {
count++
}
- }
+ return true
+ })
return count
}
diff --git a/chart_test.go b/chart_test.go
index 6ee5f7c6b2..42e2a65764 100644
--- a/chart_test.go
+++ b/chart_test.go
@@ -11,8 +11,8 @@ import (
)
func TestChartSize(t *testing.T) {
- xlsx := NewFile()
- sheet1 := xlsx.GetSheetName(0)
+ f := NewFile()
+ sheet1 := f.GetSheetName(0)
categories := map[string]string{
"A2": "Small",
@@ -23,7 +23,7 @@ func TestChartSize(t *testing.T) {
"D1": "Pear",
}
for cell, v := range categories {
- assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v))
+ assert.NoError(t, f.SetCellValue(sheet1, cell, v))
}
values := map[string]int{
@@ -38,19 +38,27 @@ func TestChartSize(t *testing.T) {
"D4": 8,
}
for cell, v := range values {
- assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v))
+ assert.NoError(t, f.SetCellValue(sheet1, cell, v))
}
- assert.NoError(t, xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+
- `"series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},`+
- `{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},`+
- `{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],`+
- `"title":{"name":"3D Clustered Column Chart"}}`))
+ assert.NoError(t, f.AddChart("Sheet1", "E4", &Chart{
+ Type: Col3DClustered,
+ Dimension: ChartDimension{
+ Width: 640,
+ Height: 480,
+ },
+ Series: []ChartSeries{
+ {Name: "Sheet1!$A$2", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$2:$D$2"},
+ {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
+ {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
+ },
+ Title: []RichTextRun{{Text: "3D Clustered Column Chart"}},
+ }))
var buffer bytes.Buffer
- // Save xlsx file by the given path.
- assert.NoError(t, xlsx.Write(&buffer))
+ // Save spreadsheet by the given path.
+ assert.NoError(t, f.Write(&buffer))
newFile, err := OpenReader(&buffer)
assert.NoError(t, err)
@@ -62,19 +70,19 @@ func TestChartSize(t *testing.T) {
var (
workdir decodeWsDr
- anchor decodeTwoCellAnchor
+ anchor decodeCellAnchor
)
- content, ok := newFile.XLSX["xl/drawings/drawing1.xml"]
+ content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml")
assert.True(t, ok, "Can't open the chart")
- err = xml.Unmarshal([]byte(content), &workdir)
+ err = xml.Unmarshal(content.([]byte), &workdir)
if !assert.NoError(t, err) {
t.FailNow()
}
- err = xml.Unmarshal([]byte(""+
- workdir.TwoCellAnchor[0].Content+""), &anchor)
+ err = xml.Unmarshal([]byte(""+
+ workdir.TwoCellAnchor[0].Content+""), &anchor)
if !assert.NoError(t, err) {
t.FailNow()
}
@@ -86,7 +94,7 @@ func TestChartSize(t *testing.T) {
}
if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") ||
- !assert.Equal(t, 27, anchor.To.Row, "Expected 'to' row 27") {
+ !assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29") {
t.FailNow()
}
@@ -94,7 +102,38 @@ func TestChartSize(t *testing.T) {
func TestAddDrawingChart(t *testing.T) {
f := NewFile()
- assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), `cannot convert cell "" to coordinates: invalid cell name ""`)
+ assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
+
+ path := "xl/drawings/drawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addDrawingChart("Sheet1", path, "A1", 0, 0, 0, &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestAddSheetDrawingChart(t *testing.T) {
+ f := NewFile()
+ path := "xl/drawings/drawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addSheetDrawingChart(path, 0, &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestDeleteDrawing(t *testing.T) {
+ f := NewFile()
+ path := "xl/drawings/drawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err := f.deleteDrawing(0, 0, path, "Chart")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+ f.Drawings.Store(path, &xlsxWsDr{OneCellAnchor: []*xdrCellAnchor{{
+ GraphicFrame: string(MacintoshCyrillicCharset),
+ }}})
+ _, err = f.deleteDrawing(0, 0, path, "Chart")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ f.Drawings.Store(path, &xlsxWsDr{TwoCellAnchor: []*xdrCellAnchor{{
+ GraphicFrame: string(MacintoshCyrillicCharset),
+ }}})
+ _, err = f.deleteDrawing(0, 0, path, "Chart")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddChart(t *testing.T) {
@@ -111,101 +150,175 @@ func TestAddChart(t *testing.T) {
for k, v := range values {
assert.NoError(t, f.SetCellValue("Sheet1", k, v))
}
- assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input")
-
- // Test add chart on not exists worksheet.
- assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist")
-
- assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Clustered Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AF1", `{"type":"col3DConeStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AF16", `{"type":"col3DConeClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AF30", `{"type":"col3DConePercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AF45", `{"type":"col3DCone","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cone Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AN1", `{"type":"col3DPyramidStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AN16", `{"type":"col3DPyramidClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AN30", `{"type":"col3DPyramidPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AN45", `{"type":"col3DPyramid","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AV1", `{"type":"col3DCylinderStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AV16", `{"type":"col3DCylinderClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AV30", `{"type":"col3DCylinderPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "AV45", `{"type":"col3DCylinder","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "P1", `{"type":"radar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`))
- assert.NoError(t, f.AddChart("Sheet2", "X1", `{"type":"scatter","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Scatter Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top","show_legend_key":false},"title":{"name":"Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`))
- assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`))
- assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked 100% Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Clustered Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"maximum":7.5,"minimum":0.5}}`))
- assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`))
- // area series charts
- assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D 100% Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Area Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- // cylinder series chart
- assert.NoError(t, f.AddChart("Sheet2", "AF48", `{"type":"bar3DCylinderStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AF64", `{"type":"bar3DCylinderClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AF80", `{"type":"bar3DCylinderPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- // cone series chart
- assert.NoError(t, f.AddChart("Sheet2", "AN48", `{"type":"bar3DConeStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AN64", `{"type":"bar3DConeClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AN80", `{"type":"bar3DConePercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AV48", `{"type":"bar3DPyramidStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AV64", `{"type":"bar3DPyramidClustered","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Clustered Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "AV80", `{"type":"bar3DPyramidPercentStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Percent Stacked Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- // surface series chart
- assert.NoError(t, f.AddChart("Sheet2", "AV1", `{"type":"surface3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Surface Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"major_grid_lines":true}}`))
- assert.NoError(t, f.AddChart("Sheet2", "AV16", `{"type":"wireframeSurface3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"3D Wireframe Surface Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","y_axis":{"major_grid_lines":true}}`))
- assert.NoError(t, f.AddChart("Sheet2", "AV32", `{"type":"contour","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Contour Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "BD1", `{"type":"wireframeContour","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Wireframe Contour Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- // bubble chart
- assert.NoError(t, f.AddChart("Sheet2", "BD16", `{"type":"bubble","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
- assert.NoError(t, f.AddChart("Sheet2", "BD32", `{"type":"bubble3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble 3D Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`))
- // pie of pie chart
- assert.NoError(t, f.AddChart("Sheet2", "BD48", `{"type":"pieOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Pie of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`))
- // bar of pie chart
- assert.NoError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`))
+ assert.EqualError(t, f.AddChart("Sheet1", "P1", nil), ErrParameterInvalid.Error())
+
+ // Test add chart on not exists worksheet
+ assert.EqualError(t, f.AddChart("SheetN", "P1", nil), "sheet SheetN does not exist")
+ maximum, minimum, zero := 7.5, 0.5, .0
+ series := []ChartSeries{
+ {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"},
+ {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"},
+ {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"},
+ {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"},
+ {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"},
+ {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"},
+ {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"},
+ {
+ Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37",
+ Marker: ChartMarker{
+ Fill: Fill{Type: "pattern", Color: []string{"FFFF00"}, Pattern: 1},
+ },
+ },
+ }
+ series2 := []ChartSeries{
+ {
+ Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30",
+ Fill: Fill{Type: "pattern", Color: []string{"000000"}, Pattern: 1},
+ Marker: ChartMarker{Symbol: "none", Size: 10},
+ },
+ {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"},
+ {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"},
+ {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"},
+ {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"},
+ {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"},
+ {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"},
+ {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Line: ChartLine{Width: 0.25}},
+ }
+ series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}
+ series4 := []ChartSeries{
+ {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30", DataLabelPosition: ChartDataLabelsPositionAbove},
+ {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft},
+ {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit},
+ {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter},
+ {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase},
+ {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd},
+ {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd},
+ {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight},
+ }
+ format := GraphicOptions{
+ ScaleX: defaultDrawingScale,
+ ScaleY: defaultDrawingScale,
+ OffsetX: 15,
+ OffsetY: 10,
+ PrintObject: boolPtr(true),
+ LockAspectRatio: false,
+ Locked: boolPtr(false),
+ }
+ legend := ChartLegend{Position: "left", ShowLegendKey: false}
+ plotArea := ChartPlotArea{
+ SecondPlotValues: 3,
+ ShowBubbleSize: true,
+ ShowCatName: true,
+ ShowLeaderLines: false,
+ ShowPercent: true,
+ ShowSerName: true,
+ ShowVal: true,
+ Fill: Fill{Type: "pattern", Pattern: 1},
+ }
+ for _, c := range []struct {
+ sheetName, cell string
+ opts *Chart
+ }{
+ {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart", Font: &Font{Size: 11, Family: "Calibri"}}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}},
+ {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero", GapWidth: uintPtr(10), Overlap: intPtr(100)}},
+ {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}},
+ {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}},
+ {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}},
+ {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert270", TextRotation: 0}}}},
+ {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
+ {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Scatter Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
+ {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, TickLabelPosition: ChartTickLabelLow}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
+ {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Pie Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}},
+ // bar series chart
+ {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked 100% Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}},
+ {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}},
+ // area series chart
+ {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ // cylinder series chart
+ {sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ // cone series chart
+ {sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ // surface series chart
+ {sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
+ {sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Wireframe Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
+ {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Wireframe Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
+ // bubble chart
+ {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", BubbleSize: 75}},
+ {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
+ // pie of pie chart
+ {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Pie of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
+ // bar of pie chart
+ {sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
+ } {
+ assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts))
+ }
// combo chart
- f.NewSheet("Combo Charts")
- clusteredColumnCombo := map[string][]string{
- "A1": {"line", "Clustered Column - Line Chart"},
- "I1": {"bubble", "Clustered Column - Bubble Chart"},
- "Q1": {"bubble3D", "Clustered Column - Bubble 3D Chart"},
- "Y1": {"doughnut", "Clustered Column - Doughnut Chart"},
- }
- for axis, props := range clusteredColumnCombo {
- assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0])))
- }
- stackedAreaCombo := map[string][]string{
- "A16": {"line", "Stacked Area - Line Chart"},
- "I16": {"bubble", "Stacked Area - Bubble Chart"},
- "Q16": {"bubble3D", "Stacked Area - Bubble 3D Chart"},
- "Y16": {"doughnut", "Stacked Area - Doughnut Chart"},
+ _, err = f.NewSheet("Combo Charts")
+ assert.NoError(t, err)
+ clusteredColumnCombo := [][]interface{}{
+ {"A1", Line, "Clustered Column - Line Chart"},
+ {"I1", Doughnut, "Clustered Column - Doughnut Chart"},
+ }
+ for _, props := range clusteredColumnCombo {
+ assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[2].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}, YAxis: ChartAxis{Secondary: true}}))
+ }
+ stackedAreaCombo := map[string][]interface{}{
+ "A16": {Line, "Stacked Area - Line Chart"},
+ "I16": {Doughnut, "Stacked Area - Doughnut Chart"},
}
for axis, props := range stackedAreaCombo {
- assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0])))
+ assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[1].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}))
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx")))
- // Test with illegal cell coordinates
- assert.EqualError(t, f.AddChart("Sheet2", "A", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ // Test with invalid sheet name
+ assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error())
+ // Test with illegal cell reference
+ assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
// Test with unsupported chart type
- assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bubble 3D Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`), "unsupported chart type unknown")
+ assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error())
// Test add combo chart with invalid format set
- assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`, ""), "unexpected end of JSON input")
+ assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error())
// Test add combo chart with unsupported chart type
- assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Bar of Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown")
+ assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error())
+ assert.NoError(t, f.Close())
+
+ // Test add chart with unsupported charset content types.
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
}
func TestAddChartSheet(t *testing.T) {
@@ -218,7 +331,12 @@ func TestAddChartSheet(t *testing.T) {
for k, v := range values {
assert.NoError(t, f.SetCellValue("Sheet1", k, v))
}
- assert.NoError(t, f.AddChartSheet("Chart1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`))
+ series := []ChartSeries{
+ {Name: "Sheet1!$A$2", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$2:$D$2"},
+ {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
+ {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
+ }
+ assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}))
// Test set the chartsheet as active sheet
var sheetIdx int
for idx, sheetName := range f.GetSheetList() {
@@ -230,34 +348,75 @@ func TestAddChartSheet(t *testing.T) {
f.SetActiveSheet(sheetIdx)
// Test cell value on chartsheet
- assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet")
+ assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet")
// Test add chartsheet on already existing name sheet
- assert.EqualError(t, f.AddChartSheet("Sheet1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), "the same name worksheet already exists")
+
+ assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrExistsSheet.Error())
+ // Test add chartsheet with invalid sheet name
+ assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrSheetNameInvalid.Error())
// Test with unsupported chart type
- assert.EqualError(t, f.AddChartSheet("Chart2", `{"type":"unknown","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`), "unsupported chart type unknown")
+ assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x37).Error())
+
+ assert.NoError(t, f.UpdateLinkedValue())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx")))
+ // Test add chart sheet with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
}
func TestDeleteChart(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.DeleteChart("Sheet1", "A1"))
- assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"},{"name":"Sheet1!$A$31","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$31:$D$31"},{"name":"Sheet1!$A$32","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$32:$D$32"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"left","show_legend_key":false},"title":{"name":"2D Column Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`))
+ series := []ChartSeries{
+ {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"},
+ {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31"},
+ {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32"},
+ {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33"},
+ {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"},
+ {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"},
+ {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"},
+ {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"},
+ }
+ format := GraphicOptions{
+ ScaleX: defaultDrawingScale,
+ ScaleY: defaultDrawingScale,
+ OffsetX: 15,
+ OffsetY: 10,
+ PrintObject: boolPtr(true),
+ LockAspectRatio: false,
+ Locked: boolPtr(false),
+ }
+ legend := ChartLegend{Position: "left", ShowLegendKey: false}
+ plotArea := ChartPlotArea{
+ ShowBubbleSize: true,
+ ShowCatName: true,
+ ShowLeaderLines: false,
+ ShowPercent: true,
+ ShowSerName: true,
+ ShowVal: true,
+ }
+ assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}))
assert.NoError(t, f.DeleteChart("Sheet1", "P1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx")))
- // Test delete chart on not exists worksheet.
- assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist")
- // Test delete chart with invalid coordinates.
- assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`)
- // Test delete chart on no chart worksheet.
+ // Test delete chart with invalid sheet name
+ assert.EqualError(t, f.DeleteChart("Sheet:1", "P1"), ErrSheetNameInvalid.Error())
+ // Test delete chart on not exists worksheet
+ assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN does not exist")
+ // Test delete chart with invalid coordinates
+ assert.EqualError(t, f.DeleteChart("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
+ // Test delete chart on no chart worksheet
assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1"))
+ assert.NoError(t, f.Close())
}
func TestChartWithLogarithmicBase(t *testing.T) {
- // Create test XLSX file with data
- xlsx := NewFile()
- sheet1 := xlsx.GetSheetName(0)
+ // Create test workbook with data
+ f := NewFile()
+ sheet1 := f.GetSheetName(0)
categories := map[string]float64{
"A1": 1,
"A2": 2,
@@ -281,48 +440,33 @@ func TestChartWithLogarithmicBase(t *testing.T) {
"B10": 5000,
}
for cell, v := range categories {
- assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v))
- }
-
- // Add two chart, one without and one with log scaling
- assert.NoError(t, xlsx.AddChart(sheet1, "C1",
- `{"type":"line","dimension":{"width":640, "height":480},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"title":{"name":"Line chart without log scaling"}}`))
- assert.NoError(t, xlsx.AddChart(sheet1, "M1",
- `{"type":"line","dimension":{"width":640, "height":480},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"y_axis":{"logbase":10.5},`+
- `"title":{"name":"Line chart with log 10 scaling"}}`))
- assert.NoError(t, xlsx.AddChart(sheet1, "A25",
- `{"type":"line","dimension":{"width":320, "height":240},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"y_axis":{"logbase":1.9},`+
- `"title":{"name":"Line chart with log 1.9 scaling"}}`))
- assert.NoError(t, xlsx.AddChart(sheet1, "F25",
- `{"type":"line","dimension":{"width":320, "height":240},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"y_axis":{"logbase":2},`+
- `"title":{"name":"Line chart with log 2 scaling"}}`))
- assert.NoError(t, xlsx.AddChart(sheet1, "K25",
- `{"type":"line","dimension":{"width":320, "height":240},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"y_axis":{"logbase":1000.1},`+
- `"title":{"name":"Line chart with log 1000.1 scaling"}}`))
- assert.NoError(t, xlsx.AddChart(sheet1, "P25",
- `{"type":"line","dimension":{"width":320, "height":240},`+
- `"series":[{"name":"value","categories":"Sheet1!$A$1:$A$19","values":"Sheet1!$B$1:$B$10"}],`+
- `"y_axis":{"logbase":1000},`+
- `"title":{"name":"Line chart with log 1000 scaling"}}`))
-
- // Export XLSX file for human confirmation
- assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx")))
-
- // Write the XLSX file to a buffer
+ assert.NoError(t, f.SetCellValue(sheet1, cell, v))
+ }
+ series := []ChartSeries{{Name: "value", Categories: "Sheet1!$A$1:$A$19", Values: "Sheet1!$B$1:$B$10"}}
+ dimension := []uint{640, 480, 320, 240}
+ for _, c := range []struct {
+ cell string
+ opts *Chart
+ }{
+ {cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart without log scaling"}}}},
+ {cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 10.5 scaling"}}, YAxis: ChartAxis{LogBase: 10.5}}},
+ {cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1.9 scaling"}}, YAxis: ChartAxis{LogBase: 1.9}}},
+ {cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 2 scaling"}}, YAxis: ChartAxis{LogBase: 2}}},
+ {cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000.1 scaling"}}, YAxis: ChartAxis{LogBase: 1000.1}}},
+ {cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000 scaling"}}, YAxis: ChartAxis{LogBase: 1000}}},
+ } {
+ // Add two chart, one without and one with log scaling
+ assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts))
+ }
+
+ // Export workbook for human confirmation
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx")))
+
+ // Write the workbook to a buffer
var buffer bytes.Buffer
- assert.NoError(t, xlsx.Write(&buffer))
+ assert.NoError(t, f.Write(&buffer))
- // Read back the XLSX file from the buffer
+ // Read back the workbook from the buffer
newFile, err := OpenReader(&buffer)
assert.NoError(t, err)
@@ -338,14 +482,18 @@ func TestChartWithLogarithmicBase(t *testing.T) {
type xmlChartContent []byte
xmlCharts := make([]xmlChartContent, expectedChartsCount)
expectedChartsLogBase := []float64{0, 10.5, 0, 2, 0, 1000}
- var ok bool
-
+ var (
+ drawingML interface{}
+ ok bool
+ )
for i := 0; i < expectedChartsCount; i++ {
chartPath := fmt.Sprintf("xl/charts/chart%d.xml", i+1)
- xmlCharts[i], ok = newFile.XLSX[chartPath]
+ if drawingML, ok = newFile.Pkg.Load(chartPath); ok {
+ xmlCharts[i] = drawingML.([]byte)
+ }
assert.True(t, ok, "Can't open the %s", chartPath)
- err = xml.Unmarshal([]byte(xmlCharts[i]), &chartSpaces[i])
+ err = xml.Unmarshal(xmlCharts[i], &chartSpaces[i])
if !assert.NoError(t, err) {
t.FailNow()
}
diff --git a/col.go b/col.go
index 5a8299ea9c..6608048a83 100644
--- a/col.go
+++ b/col.go
@@ -1,67 +1,75 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
"encoding/xml"
- "errors"
"math"
"strconv"
"strings"
- "github.com/mohae/deepcopy"
+ "github.com/tiendc/go-deepcopy"
)
// Define the default cell size and EMU unit of measurement.
const (
+ defaultColWidth float64 = 9.140625
defaultColWidthPixels float64 = 64
+ defaultRowHeight float64 = 15
defaultRowHeightPixels float64 = 20
EMU int = 9525
)
// Cols defines an iterator to a sheet
type Cols struct {
- err error
- curCol, totalCol, stashCol, totalRow int
- sheet string
- cols []xlsxCols
- f *File
- sheetXML []byte
+ err error
+ curCol, totalCols, totalRows, stashCol int
+ rawCellValue bool
+ sheet string
+ f *File
+ sheetXML []byte
+ sst *xlsxSST
}
-// GetCols return all the columns in a sheet by given worksheet name (case
-// sensitive). For example:
-//
-// cols, err := f.GetCols("Sheet1")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// for _, col := range cols {
-// for _, rowCell := range col {
-// fmt.Print(rowCell, "\t")
-// }
-// fmt.Println()
-// }
-//
-func (f *File) GetCols(sheet string) ([][]string, error) {
+// GetCols gets the value of all cells by columns on the worksheet based on the
+// given worksheet name, returned as a two-dimensional array, where the value
+// of the cell is converted to the `string` type. If the cell format can be
+// applied to the value of the cell, the applied value will be used, otherwise
+// the original value will be used.
+//
+// For example, get and traverse the value of all cells by columns on a
+// worksheet named
+// 'Sheet1':
+//
+// cols, err := f.GetCols("Sheet1")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// for _, col := range cols {
+// for _, rowCell := range col {
+// fmt.Print(rowCell, "\t")
+// }
+// fmt.Println()
+// }
+func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) {
cols, err := f.Cols(sheet)
if err != nil {
return nil, err
}
results := make([][]string, 0, 64)
for cols.Next() {
- col, _ := cols.Rows()
+ col, _ := cols.Rows(opts...)
results = append(results, col)
}
return results, nil
@@ -70,7 +78,7 @@ func (f *File) GetCols(sheet string) ([][]string, error) {
// Next will return true if the next column is found.
func (cols *Cols) Next() bool {
cols.curCol++
- return cols.curCol <= cols.totalCol
+ return cols.curCol <= cols.totalCols
}
// Error will return an error when the error occurs.
@@ -79,160 +87,191 @@ func (cols *Cols) Error() error {
}
// Rows return the current column's row values.
-func (cols *Cols) Rows() ([]string, error) {
- var (
- err error
- inElement string
- cellCol, cellRow int
- rows []string
- )
+func (cols *Cols) Rows(opts ...Options) ([]string, error) {
+ var rowIterator rowXMLIterator
if cols.stashCol >= cols.curCol {
- return rows, err
+ return rowIterator.cells, rowIterator.err
+ }
+ cols.rawCellValue = cols.f.getOptions(opts...).RawCellValue
+ if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil {
+ return rowIterator.cells, rowIterator.err
}
- d := cols.f.sharedStringsReader()
decoder := cols.f.xmlNewDecoder(bytes.NewReader(cols.sheetXML))
for {
token, _ := decoder.Token()
if token == nil {
break
}
- switch startElement := token.(type) {
+ switch xmlElement := token.(type) {
case xml.StartElement:
- inElement = startElement.Name.Local
- if inElement == "row" {
- cellCol = 0
- cellRow++
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- cellRow, _ = strconv.Atoi(attr.Value)
- }
+ rowIterator.inElement = xmlElement.Name.Local
+ if rowIterator.inElement == "row" {
+ rowIterator.cellCol = 0
+ rowIterator.cellRow++
+ attrR, _ := attrValToInt("r", xmlElement.Attr)
+ if attrR != 0 {
+ rowIterator.cellRow = attrR
}
}
- if inElement == "c" {
- cellCol++
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- if cellCol, cellRow, err = CellNameToCoordinates(attr.Value); err != nil {
- return rows, err
- }
- }
+ if cols.rowXMLHandler(&rowIterator, &xmlElement, decoder); rowIterator.err != nil {
+ return rowIterator.cells, rowIterator.err
+ }
+ case xml.EndElement:
+ if xmlElement.Name.Local == "sheetData" {
+ return rowIterator.cells, rowIterator.err
+ }
+ }
+ }
+ return rowIterator.cells, rowIterator.err
+}
+
+// columnXMLIterator defined runtime use field for the worksheet column SAX parser.
+type columnXMLIterator struct {
+ err error
+ cols Cols
+ cellCol, curRow, row int
+}
+
+// columnXMLHandler parse the column XML element of the worksheet.
+func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartElement) {
+ colIterator.err = nil
+ inElement := xmlElement.Name.Local
+ if inElement == "row" {
+ colIterator.row++
+ for _, attr := range xmlElement.Attr {
+ if attr.Name.Local == "r" {
+ if colIterator.curRow, colIterator.err = strconv.Atoi(attr.Value); colIterator.err != nil {
+ return
}
- blank := cellRow - len(rows)
- for i := 1; i < blank; i++ {
- rows = append(rows, "")
+ colIterator.row = colIterator.curRow
+ }
+ }
+ colIterator.cols.totalRows = colIterator.row
+ colIterator.cellCol = 0
+ }
+ if inElement == "c" {
+ colIterator.cellCol++
+ for _, attr := range xmlElement.Attr {
+ if attr.Name.Local == "r" {
+ if colIterator.cellCol, _, colIterator.err = CellNameToCoordinates(attr.Value); colIterator.err != nil {
+ return
}
- if cellCol == cols.curCol {
- colCell := xlsxC{}
- _ = decoder.DecodeElement(&colCell, &startElement)
- val, _ := colCell.getValueFrom(cols.f, d)
- rows = append(rows, val)
+ }
+ }
+ if colIterator.cellCol > colIterator.cols.totalCols {
+ colIterator.cols.totalCols = colIterator.cellCol
+ }
+ }
+}
+
+// rowXMLHandler parse the row XML element of the worksheet.
+func (cols *Cols) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, decoder *xml.Decoder) {
+ if rowIterator.inElement == "c" {
+ rowIterator.cellCol++
+ for _, attr := range xmlElement.Attr {
+ if attr.Name.Local == "r" {
+ if rowIterator.cellCol, rowIterator.cellRow, rowIterator.err = CellNameToCoordinates(attr.Value); rowIterator.err != nil {
+ return
}
}
}
+ blank := rowIterator.cellRow - len(rowIterator.cells)
+ for i := 1; i < blank; i++ {
+ rowIterator.cells = append(rowIterator.cells, "")
+ }
+ if rowIterator.cellCol == cols.curCol {
+ colCell := xlsxC{}
+ _ = decoder.DecodeElement(&colCell, xmlElement)
+ val, _ := colCell.getValueFrom(cols.f, cols.sst, cols.rawCellValue)
+ rowIterator.cells = append(rowIterator.cells, val)
+ }
}
- return rows, nil
}
// Cols returns a columns iterator, used for streaming reading data for a
-// worksheet with a large data. For example:
-//
-// cols, err := f.Cols("Sheet1")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// for cols.Next() {
-// col, err := cols.Rows()
-// if err != nil {
-// fmt.Println(err)
-// }
-// for _, rowCell := range col {
-// fmt.Print(rowCell, "\t")
-// }
-// fmt.Println()
-// }
-//
+// worksheet with a large data. This function is concurrency safe. For
+// example:
+//
+// cols, err := f.Cols("Sheet1")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// for cols.Next() {
+// col, err := cols.Rows()
+// if err != nil {
+// fmt.Println(err)
+// }
+// for _, rowCell := range col {
+// fmt.Print(rowCell, "\t")
+// }
+// fmt.Println()
+// }
func (f *File) Cols(sheet string) (*Cols, error) {
- name, ok := f.sheetMap[trimSheetName(sheet)]
+ if err := checkSheetName(sheet); err != nil {
+ return nil, err
+ }
+ name, ok := f.getSheetXMLPath(sheet)
if !ok {
return nil, ErrSheetNotExist{sheet}
}
- if f.Sheet[name] != nil {
- output, _ := xml.Marshal(f.Sheet[name])
+ if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
+ ws := worksheet.(*xlsxWorksheet)
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ output, _ := xml.Marshal(ws)
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
}
- var (
- inElement string
- cols Cols
- cellCol, curRow, row int
- err error
- )
- cols.sheetXML = f.readXML(name)
- decoder := f.xmlNewDecoder(bytes.NewReader(cols.sheetXML))
+ var colIterator columnXMLIterator
+ colIterator.cols.sheetXML = f.readBytes(name)
+ decoder := f.xmlNewDecoder(bytes.NewReader(colIterator.cols.sheetXML))
for {
token, _ := decoder.Token()
if token == nil {
break
}
- switch startElement := token.(type) {
+ switch xmlElement := token.(type) {
case xml.StartElement:
- inElement = startElement.Name.Local
- if inElement == "row" {
- row++
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- if curRow, err = strconv.Atoi(attr.Value); err != nil {
- return &cols, err
- }
- row = curRow
- }
- }
- cols.totalRow = row
- cellCol = 0
+ columnXMLHandler(&colIterator, &xmlElement)
+ if colIterator.err != nil {
+ return &colIterator.cols, colIterator.err
}
- if inElement == "c" {
- cellCol++
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- if cellCol, _, err = CellNameToCoordinates(attr.Value); err != nil {
- return &cols, err
- }
- }
- }
- if cellCol > cols.totalCol {
- cols.totalCol = cellCol
- }
+ case xml.EndElement:
+ if xmlElement.Name.Local == "sheetData" {
+ colIterator.cols.f = f
+ colIterator.cols.sheet = sheet
+ return &colIterator.cols, nil
}
}
}
- cols.f = f
- cols.sheet = trimSheetName(sheet)
- return &cols, nil
+ return &colIterator.cols, nil
}
// GetColVisible provides a function to get visible of a single column by given
-// worksheet name and column name. For example, get visible state of column D
-// in Sheet1:
-//
-// visible, err := f.GetColVisible("Sheet1", "D")
+// worksheet name and column name. This function is concurrency safe. For
+// example, get visible state of column D in Sheet1:
//
+// visible, err := f.GetColVisible("Sheet1", "D")
func (f *File) GetColVisible(sheet, col string) (bool, error) {
- visible := true
colNum, err := ColumnNameToNumber(col)
if err != nil {
- return visible, err
+ return true, err
}
-
- xlsx, err := f.workSheetReader(sheet)
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return false, err
}
- if xlsx.Cols == nil {
- return visible, err
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ if ws.Cols == nil {
+ return true, err
}
-
- for c := range xlsx.Cols.Col {
- colData := &xlsx.Cols.Col[c]
+ visible := true
+ for c := range ws.Cols.Col {
+ colData := &ws.Cols.Col[c]
if colData.Min <= colNum && colNum <= colData.Max {
visible = !colData.Hidden
}
@@ -241,53 +280,40 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
}
// SetColVisible provides a function to set visible columns by given worksheet
-// name, columns range and visibility.
+// name, columns range and visibility. This function is concurrency safe.
//
// For example hide column D on Sheet1:
//
-// err := f.SetColVisible("Sheet1", "D", false)
+// err := f.SetColVisible("Sheet1", "D", false)
//
// Hide the columns from D to F (included):
//
-// err := f.SetColVisible("Sheet1", "D:F", false)
-//
+// err := f.SetColVisible("Sheet1", "D:F", false)
func (f *File) SetColVisible(sheet, columns string, visible bool) error {
- var max int
-
- colsTab := strings.Split(columns, ":")
- min, err := ColumnNameToNumber(colsTab[0])
+ minVal, maxVal, err := f.parseColRange(columns)
if err != nil {
return err
}
- if len(colsTab) == 2 {
- max, err = ColumnNameToNumber(colsTab[1])
- if err != nil {
- return err
- }
- } else {
- max = min
- }
- if max < min {
- min, max = max, min
- }
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
colData := xlsxCol{
- Min: min,
- Max: max,
- Width: 9, // default width
+ Min: minVal,
+ Max: maxVal,
+ Width: float64Ptr(defaultColWidth),
Hidden: !visible,
CustomWidth: true,
}
- if xlsx.Cols == nil {
+ if ws.Cols == nil {
cols := xlsxCols{}
cols.Col = append(cols.Col, colData)
- xlsx.Cols = &cols
+ ws.Cols = &cols
return nil
}
- xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
+ ws.Cols.Col = flatCols(colData, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
@@ -304,23 +330,22 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error {
// column by given worksheet name and column name. For example, get outline
// level of column D in Sheet1:
//
-// level, err := f.GetColOutlineLevel("Sheet1", "D")
-//
+// level, err := f.GetColOutlineLevel("Sheet1", "D")
func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) {
level := uint8(0)
colNum, err := ColumnNameToNumber(col)
if err != nil {
return level, err
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return 0, err
}
- if xlsx.Cols == nil {
+ if ws.Cols == nil {
return level, err
}
- for c := range xlsx.Cols.Col {
- colData := &xlsx.Cols.Col[c]
+ for c := range ws.Cols.Col {
+ colData := &ws.Cols.Col[c]
if colData.Min <= colNum && colNum <= colData.Max {
level = colData.OutlineLevel
}
@@ -328,15 +353,33 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) {
return level, err
}
+// parseColRange parse and convert column range with column name to the column number.
+func (f *File) parseColRange(columns string) (minVal, maxVal int, err error) {
+ colsTab := strings.Split(columns, ":")
+ minVal, err = ColumnNameToNumber(colsTab[0])
+ if err != nil {
+ return
+ }
+ maxVal = minVal
+ if len(colsTab) == 2 {
+ if maxVal, err = ColumnNameToNumber(colsTab[1]); err != nil {
+ return
+ }
+ }
+ if maxVal < minVal {
+ minVal, maxVal = maxVal, minVal
+ }
+ return
+}
+
// SetColOutlineLevel provides a function to set outline level of a single
// column by given worksheet name and column name. The value of parameter
// 'level' is 1-7. For example, set outline level of column D in Sheet1 to 2:
//
-// err := f.SetColOutlineLevel("Sheet1", "D", 2)
-//
+// err := f.SetColOutlineLevel("Sheet1", "D", 2)
func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
if level > 7 || level < 1 {
- return errors.New("invalid outline level")
+ return ErrOutlineLevel
}
colNum, err := ColumnNameToNumber(col)
if err != nil {
@@ -348,17 +391,17 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
OutlineLevel: level,
CustomWidth: true,
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if xlsx.Cols == nil {
+ if ws.Cols == nil {
cols := xlsxCols{}
cols.Col = append(cols.Col, colData)
- xlsx.Cols = &cols
+ ws.Cols = &cols
return err
}
- xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
+ ws.Cols.Col = flatCols(colData, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
@@ -372,50 +415,69 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
}
// SetColStyle provides a function to set style of columns by given worksheet
-// name, columns range and style ID.
+// name, columns range and style ID. This function is concurrency safe. Note
+// that this will overwrite the existing styles for the columns, it won't
+// append or merge style with existing styles.
//
// For example set style of column H on Sheet1:
//
-// err = f.SetColStyle("Sheet1", "H", style)
+// err = f.SetColStyle("Sheet1", "H", style)
//
// Set style of columns C:F on Sheet1:
//
-// err = f.SetColStyle("Sheet1", "C:F", style)
-//
+// err = f.SetColStyle("Sheet1", "C:F", style)
func (f *File) SetColStyle(sheet, columns string, styleID int) error {
- xlsx, err := f.workSheetReader(sheet)
+ minVal, maxVal, err := f.parseColRange(columns)
+ if err != nil {
+ return err
+ }
+ f.mu.Lock()
+ s, err := f.stylesReader()
if err != nil {
+ f.mu.Unlock()
return err
}
- var c1, c2 string
- var min, max int
- cols := strings.Split(columns, ":")
- c1 = cols[0]
- min, err = ColumnNameToNumber(c1)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
- if len(cols) == 2 {
- c2 = cols[1]
- max, err = ColumnNameToNumber(c2)
- if err != nil {
- return err
+ f.mu.Unlock()
+ s.mu.Lock()
+ if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
+ s.mu.Unlock()
+ return newInvalidStyleID(styleID)
+ }
+ s.mu.Unlock()
+ ws.mu.Lock()
+ ws.setColStyle(minVal, maxVal, styleID)
+ ws.mu.Unlock()
+ if rows := len(ws.SheetData.Row); rows > 0 {
+ for col := minVal; col <= maxVal; col++ {
+ from, _ := CoordinatesToCellName(col, 1)
+ to, _ := CoordinatesToCellName(col, rows)
+ err = f.SetCellStyle(sheet, from, to, styleID)
}
- } else {
- max = min
}
- if max < min {
- min, max = max, min
+ return err
+}
+
+// setColStyle provides a function to set the style of a single column or
+// multiple columns.
+func (ws *xlsxWorksheet) setColStyle(minVal, maxVal, styleID int) {
+ if ws.Cols == nil {
+ ws.Cols = &xlsxCols{}
}
- if xlsx.Cols == nil {
- xlsx.Cols = &xlsxCols{}
+ width := defaultColWidth
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
+ width = ws.SheetFormatPr.DefaultColWidth
}
- xlsx.Cols.Col = flatCols(xlsxCol{
- Min: min,
- Max: max,
- Width: 9,
+ ws.Cols.Col = flatCols(xlsxCol{
+ Min: minVal,
+ Max: maxVal,
+ Width: float64Ptr(width),
Style: styleID,
- }, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
+ }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.CustomWidth = c.CustomWidth
@@ -425,45 +487,49 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
fc.Width = c.Width
return fc
})
- return nil
}
// SetColWidth provides a function to set the width of a single column or
-// multiple columns. For example:
+// multiple columns. This function is concurrency safe. For example:
//
-// f := excelize.NewFile()
-// err := f.SetColWidth("Sheet1", "A", "H", 20)
-//
-func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error {
- min, err := ColumnNameToNumber(startcol)
+// err := f.SetColWidth("Sheet1", "A", "H", 20)
+func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error {
+ minVal, maxVal, err := f.parseColRange(startCol + ":" + endCol)
if err != nil {
return err
}
- max, err := ColumnNameToNumber(endcol)
- if err != nil {
- return err
+ if width > MaxColumnWidth {
+ return ErrColumnWidth
}
- if min > max {
- min, max = max, min
- }
-
- xlsx, err := f.workSheetReader(sheet)
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ ws.setColWidth(minVal, maxVal, width)
+ return err
+}
+
+// setColWidth provides a function to set the width of a single column or
+// multiple columns.
+func (ws *xlsxWorksheet) setColWidth(minVal, maxVal int, width float64) {
col := xlsxCol{
- Min: min,
- Max: max,
- Width: width,
+ Min: minVal,
+ Max: maxVal,
+ Width: float64Ptr(width),
CustomWidth: true,
}
- if xlsx.Cols == nil {
+ if ws.Cols == nil {
cols := xlsxCols{}
cols.Col = append(cols.Col, col)
- xlsx.Cols = &cols
- return err
+ ws.Cols = &cols
+ return
}
- xlsx.Cols.Col = flatCols(col, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol {
+ ws.Cols.Col = flatCols(col, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
fc.Collapsed = c.Collapsed
fc.Hidden = c.Hidden
@@ -472,15 +538,15 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error
fc.Style = c.Style
return fc
})
- return err
}
// flatCols provides a method for the column's operation functions to flatten
// and check the worksheet columns.
func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol {
- fc := []xlsxCol{}
+ var fc []xlsxCol
for i := col.Min; i <= col.Max; i++ {
- c := deepcopy.Copy(col).(xlsxCol)
+ var c xlsxCol
+ deepcopy.Copy(&c, col)
c.Min, c.Max = i, i
fc = append(fc, c)
}
@@ -498,7 +564,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
fc[idx] = replacer(fc[idx], column)
continue
}
- c := deepcopy.Copy(column).(xlsxCol)
+ var c xlsxCol
+ deepcopy.Copy(&c, column)
c.Min, c.Max = i, i
fc = append(fc, c)
}
@@ -509,25 +576,25 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
// positionObjectPixels calculate the vertices that define the position of a
// graphical object within the worksheet in pixels.
//
-// +------------+------------+
-// | A | B |
-// +-----+------------+------------+
-// | |(x1,y1) | |
-// | 1 |(A1)._______|______ |
-// | | | | |
-// | | | | |
-// +-----+----| OBJECT |-----+
-// | | | | |
-// | 2 | |______________. |
-// | | | (B2)|
-// | | | (x2,y2)|
-// +-----+------------+------------+
-//
-// Example of an object that covers some of the area from cell A1 to B2.
+// +------------+------------+
+// | A | B |
+// +-----+------------+------------+
+// | |(x1,y1) | |
+// | 1 |(A1)._______|______ |
+// | | | | |
+// | | | | |
+// +-----+----| OBJECT |-----+
+// | | | | |
+// | 2 | |______________. |
+// | | | (B2)|
+// | | | (x2,y2)|
+// +-----+------------+------------+
+//
+// Example of an object that covers some range reference from cell A1 to B2.
//
// Based on the width and height of the object we need to calculate 8 vars:
//
-// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2.
+// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2.
//
// We also calculate the absolute x and y position of the top left vertex of
// the object. This is required for images.
@@ -540,56 +607,36 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
// subtracting the width and height of the object from the width and
// height of the underlying cells.
//
-// colStart # Col containing upper left corner of object.
-// x1 # Distance to left side of object.
-//
-// rowStart # Row containing top left corner of object.
-// y1 # Distance to top of object.
-//
-// colEnd # Col containing lower right corner of object.
-// x2 # Distance to right side of object.
+// colStart # Col containing upper left corner of object.
+// x1 # Distance to left side of object.
//
-// rowEnd # Row containing bottom right corner of object.
-// y2 # Distance to bottom of object.
+// rowStart # Row containing top left corner of object.
+// y1 # Distance to top of object.
//
-// width # Width of object frame.
-// height # Height of object frame.
+// colEnd # Col containing lower right corner of object.
+// x2 # Distance to right side of object.
//
-// xAbs # Absolute distance to left side of object.
-// yAbs # Absolute distance to top side of object.
+// rowEnd # Row containing bottom right corner of object.
+// y2 # Distance to bottom of object.
//
-func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int, int, int) {
- xAbs := 0
- yAbs := 0
-
- // Calculate the absolute x offset of the top-left vertex.
- for colID := 1; colID <= col; colID++ {
- xAbs += f.getColWidth(sheet, colID)
- }
- xAbs += x1
-
- // Calculate the absolute y offset of the top-left vertex.
- // Store the column change to allow optimisations.
- for rowID := 1; rowID <= row; rowID++ {
- yAbs += f.getRowHeight(sheet, rowID)
- }
- yAbs += y1
-
+// width # Width of object frame.
+// height # Height of object frame.
+func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) {
+ colIdx, rowIdx := col-1, row-1
// Adjust start column for offsets that are greater than the col width.
- for x1 >= f.getColWidth(sheet, col) {
- x1 -= f.getColWidth(sheet, col)
- col++
+ for x1 >= f.getColWidth(sheet, colIdx+1) {
+ colIdx++
+ x1 -= f.getColWidth(sheet, colIdx)
}
// Adjust start row for offsets that are greater than the row height.
- for y1 >= f.getRowHeight(sheet, row) {
- y1 -= f.getRowHeight(sheet, row)
- row++
+ for y1 >= f.getRowHeight(sheet, rowIdx+1) {
+ rowIdx++
+ y1 -= f.getRowHeight(sheet, rowIdx)
}
- // Initialise end cell to the same as the start cell.
- colEnd := col
- rowEnd := row
+ // Initialized end cell to the same as the start cell.
+ colEnd, rowEnd := colIdx, rowIdx
width += x1
height += y1
@@ -601,79 +648,125 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh
}
// Subtract the underlying cell heights to find end cell of the object.
- for height >= f.getRowHeight(sheet, rowEnd) {
- height -= f.getRowHeight(sheet, rowEnd)
+ for height >= f.getRowHeight(sheet, rowEnd+1) {
rowEnd++
+ height -= f.getRowHeight(sheet, rowEnd)
}
// The end vertices are whatever is left from the width and height.
- x2 := width
- y2 := height
- return col, row, xAbs, yAbs, colEnd, rowEnd, x2, y2
+ return colIdx, rowIdx, colEnd, rowEnd, width, height
}
// getColWidth provides a function to get column width in pixels by given
-// sheet name and column index.
+// sheet name and column number.
func (f *File) getColWidth(sheet string, col int) int {
- xlsx, _ := f.workSheetReader(sheet)
- if xlsx.Cols != nil {
+ ws, _ := f.workSheetReader(sheet)
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ if ws.Cols != nil {
var width float64
- for _, v := range xlsx.Cols.Col {
- if v.Min <= col && col <= v.Max {
- width = v.Width
+ for _, v := range ws.Cols.Col {
+ if v.Min <= col && col <= v.Max && v.Width != nil {
+ width = *v.Width
}
}
if width != 0 {
return int(convertColWidthToPixels(width))
}
}
- // Optimisation for when the column widths haven't changed.
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
+ return int(convertColWidthToPixels(ws.SheetFormatPr.DefaultColWidth))
+ }
+ // Optimization for when the column widths haven't changed.
return int(defaultColWidthPixels)
}
+// GetColStyle provides a function to get column style ID by given worksheet
+// name and column name. This function is concurrency safe.
+func (f *File) GetColStyle(sheet, col string) (int, error) {
+ var styleID int
+ colNum, err := ColumnNameToNumber(col)
+ if err != nil {
+ return styleID, err
+ }
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ f.mu.Unlock()
+ return styleID, err
+ }
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ if ws.Cols != nil {
+ for _, v := range ws.Cols.Col {
+ if v.Min <= colNum && colNum <= v.Max {
+ styleID = v.Style
+ }
+ }
+ }
+ return styleID, err
+}
+
// GetColWidth provides a function to get column width by given worksheet name
-// and column index.
+// and column name. This function is concurrency safe.
func (f *File) GetColWidth(sheet, col string) (float64, error) {
colNum, err := ColumnNameToNumber(col)
if err != nil {
- return defaultColWidthPixels, err
+ return defaultColWidth, err
}
- xlsx, err := f.workSheetReader(sheet)
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
- return defaultColWidthPixels, err
+ f.mu.Unlock()
+ return defaultColWidth, err
}
- if xlsx.Cols != nil {
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ if ws.Cols != nil {
var width float64
- for _, v := range xlsx.Cols.Col {
- if v.Min <= colNum && colNum <= v.Max {
- width = v.Width
+ for _, v := range ws.Cols.Col {
+ if v.Min <= colNum && colNum <= v.Max && v.Width != nil {
+ width = *v.Width
}
}
if width != 0 {
return width, err
}
}
- // Optimisation for when the column widths haven't changed.
- return defaultColWidthPixels, err
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
+ return ws.SheetFormatPr.DefaultColWidth, err
+ }
+ // Optimization for when the column widths haven't changed.
+ return defaultColWidth, err
}
-// InsertCol provides a function to insert a new column before given column
-// index. For example, create a new column before column C in Sheet1:
+// InsertCols provides a function to insert new columns before the given column
+// name and number of columns. For example, create two columns before column
+// C in Sheet1:
//
-// err := f.InsertCol("Sheet1", "C")
+// err := f.InsertCols("Sheet1", "C", 2)
//
-func (f *File) InsertCol(sheet, col string) error {
+// Use this method with caution, which will affect changes in references such
+// as formulas, charts, and so on. If there is any referenced value of the
+// worksheet, it will cause a file error when you open it. The excelize only
+// partially updates these references currently.
+func (f *File) InsertCols(sheet, col string, n int) error {
num, err := ColumnNameToNumber(col)
if err != nil {
return err
}
- return f.adjustHelper(sheet, columns, num, 1)
+ if n < 1 || n > MaxColumns {
+ return ErrColumnNumber
+ }
+ return f.adjustHelper(sheet, columns, num, n)
}
// RemoveCol provides a function to remove single column by given worksheet
// name and column index. For example, remove column C in Sheet1:
//
-// err := f.RemoveCol("Sheet1", "C")
+// err := f.RemoveCol("Sheet1", "C")
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
@@ -685,12 +778,12 @@ func (f *File) RemoveCol(sheet, col string) error {
return err
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- for rowIdx := range xlsx.SheetData.Row {
- rowData := &xlsx.SheetData.Row[rowIdx]
+ for rowIdx := range ws.SheetData.Row {
+ rowData := &ws.SheetData.Row[rowIdx]
for colIdx := range rowData.C {
colName, _, _ := SplitCellName(rowData.C[colIdx].R)
if colName == col {
@@ -702,7 +795,7 @@ func (f *File) RemoveCol(sheet, col string) error {
return f.adjustHelper(sheet, columns, num, -1)
}
-// convertColWidthToPixels provieds function to convert the width of a cell
+// convertColWidthToPixels provides function to convert the width of a cell
// from user's units to pixels. Excel rounds the column width to the nearest
// pixel. If the width hasn't been set by the user we use the default value.
// If the column is hidden it has a value of zero.
diff --git a/col_test.go b/col_test.go
index e6e7e29b8e..2e7aeb80c7 100644
--- a/col_test.go
+++ b/col_test.go
@@ -1,7 +1,9 @@
package excelize
import (
+ "fmt"
"path/filepath"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -39,6 +41,7 @@ func TestCols(t *testing.T) {
if !assert.Equal(t, collectedRows, returnedColumns) {
t.FailNow()
}
+ assert.NoError(t, f.Close())
f = NewFile()
cells := []string{"C2", "C3", "C4"}
@@ -48,48 +51,69 @@ func TestCols(t *testing.T) {
_, err = f.Rows("Sheet1")
assert.NoError(t, err)
- f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
Dimension: &xlsxDimension{
Ref: "C2:C4",
},
- }
+ })
_, err = f.Rows("Sheet1")
assert.NoError(t, err)
+
+ // Test columns iterator with invalid sheet name
+ _, err = f.Cols("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test get columns cells with invalid sheet name
+ _, err = f.GetCols("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test columns iterator with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ cols, err = f.Cols("Sheet1")
+ assert.NoError(t, err)
+ cols.Next()
+ _, err = cols.Rows()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ f = NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B
`))
+ f.checked = sync.Map{}
+ _, err = f.Cols("Sheet1")
+ assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
+
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B
`))
+ _, err = f.Cols("Sheet1")
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
}
func TestColumnsIterator(t *testing.T) {
- const (
- sheet2 = "Sheet2"
- expectedNumCol = 9
- )
-
+ sheetName, colCount, expectedNumCol := "Sheet2", 0, 9
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
require.NoError(t, err)
- cols, err := f.Cols(sheet2)
+ cols, err := f.Cols(sheetName)
require.NoError(t, err)
- var colCount int
for cols.Next() {
colCount++
require.True(t, colCount <= expectedNumCol, "colCount is greater than expected")
}
assert.Equal(t, expectedNumCol, colCount)
+ assert.NoError(t, f.Close())
- f = NewFile()
+ f, sheetName, colCount, expectedNumCol = NewFile(), "Sheet1", 0, 4
cells := []string{"C2", "C3", "C4", "D2", "D3", "D4"}
for _, cell := range cells {
- assert.NoError(t, f.SetCellValue("Sheet1", cell, 1))
+ assert.NoError(t, f.SetCellValue(sheetName, cell, 1))
}
- cols, err = f.Cols("Sheet1")
+ cols, err = f.Cols(sheetName)
require.NoError(t, err)
- colCount = 0
for cols.Next() {
colCount++
require.True(t, colCount <= 4, "colCount is greater than expected")
}
- assert.Equal(t, 4, colCount)
+ assert.Equal(t, expectedNumCol, colCount)
}
func TestColsError(t *testing.T) {
@@ -98,7 +122,8 @@ func TestColsError(t *testing.T) {
t.FailNow()
}
_, err = f.Cols("SheetN")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
}
func TestGetColsError(t *testing.T) {
@@ -107,51 +132,63 @@ func TestGetColsError(t *testing.T) {
t.FailNow()
}
_, err = f.GetCols("SheetN")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
f = NewFile()
- delete(f.Sheet, "xl/worksheets/sheet1.xml")
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B
`)
- f.checked = nil
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`B
`, NameSpaceSpreadSheet.Value)))
+ f.checked = sync.Map{}
_, err = f.GetCols("Sheet1")
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
- f = NewFile()
- delete(f.Sheet, "xl/worksheets/sheet1.xml")
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B
`)
- f.checked = nil
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`B
`, NameSpaceSpreadSheet.Value)))
_, err = f.GetCols("Sheet1")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
f = NewFile()
cols, err := f.Cols("Sheet1")
assert.NoError(t, err)
- cols.totalRow = 2
- cols.totalCol = 2
+ cols.totalRows = 2
+ cols.totalCols = 2
cols.curCol = 1
- cols.sheetXML = []byte(`A
`)
+ cols.sheetXML = []byte(fmt.Sprintf(`A
`, NameSpaceSpreadSheet.Value))
_, err = cols.Rows()
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+
+ f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
+ f.Sheet.Store("xl/worksheets/sheet1.xml", nil)
+ _, err = f.Cols("Sheet1")
+ assert.NoError(t, err)
}
func TestColsRows(t *testing.T) {
f := NewFile()
- f.NewSheet("Sheet1")
- cols, err := f.Cols("Sheet1")
+ _, err := f.Cols("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1))
- f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
Dimension: &xlsxDimension{
Ref: "A1:A1",
},
- }
+ })
- cols.stashCol, cols.curCol = 0, 1
- cols, err = f.Cols("Sheet1")
+ f = NewFile()
+ f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
+ _, err = f.Cols("Sheet1")
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ f = NewFile()
+ cols, err := f.Cols("Sheet1")
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ _, err = cols.Rows()
assert.NoError(t, err)
-
+ cols.stashCol, cols.curCol = 0, 1
// Test if token is nil
cols.sheetXML = nil
_, err = cols.Rows()
@@ -193,19 +230,24 @@ func TestColumnVisibility(t *testing.T) {
assert.Equal(t, true, visible)
assert.NoError(t, err)
- // Test get column visible on an inexistent worksheet.
+ // Test get column visible on not exists worksheet
_, err = f.GetColVisible("SheetN", "F")
- assert.EqualError(t, err, "sheet SheetN is not exist")
-
- // Test get column visible with illegal cell coordinates.
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get column visible with invalid sheet name
+ _, err = f.GetColVisible("Sheet:1", "F")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test get column visible with illegal cell reference
_, err = f.GetColVisible("Sheet1", "*")
- assert.EqualError(t, err, `invalid column name "*"`)
- assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`)
+ assert.EqualError(t, err, newInvalidColumnNameError("*").Error())
+ assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), newInvalidColumnNameError("*").Error())
+ // Test set column visible with invalid sheet name
+ assert.EqualError(t, f.SetColVisible("Sheet:1", "A", false), ErrSheetNameInvalid.Error())
- f.NewSheet("Sheet3")
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
assert.NoError(t, f.SetColVisible("Sheet3", "E", false))
- assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), "invalid column name \"-1\"")
- assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist")
+ assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), newInvalidColumnNameError("-1").Error())
+ assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN does not exist")
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx")))
})
@@ -224,43 +266,56 @@ func TestOutlineLevel(t *testing.T) {
assert.Equal(t, uint8(0), level)
assert.NoError(t, err)
- f.NewSheet("Sheet2")
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4))
level, err = f.GetColOutlineLevel("Sheet1", "D")
assert.Equal(t, uint8(4), level)
assert.NoError(t, err)
- level, err = f.GetColOutlineLevel("Shee2", "A")
+ level, err = f.GetColOutlineLevel("SheetN", "A")
assert.Equal(t, uint8(0), level)
- assert.EqualError(t, err, "sheet Shee2 is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
+ // Test column outline level with invalid sheet name
+ _, err = f.GetColOutlineLevel("Sheet:1", "A")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13))
+ assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), ErrColumnWidth.Error())
+ // Test set column width with invalid sheet name
+ assert.EqualError(t, f.SetColWidth("Sheet:1", "A", "D", 13), ErrSheetNameInvalid.Error())
+
assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2))
assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7))
- assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level")
- assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level")
- // Test set row outline level on not exists worksheet.
- assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist")
- // Test get row outline level on not exists worksheet.
+ assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), ErrOutlineLevel.Error())
+ assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), ErrOutlineLevel.Error())
+ // Test set row outline level on not exists worksheet
+ assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN does not exist")
+ // Test set row outline level with invalid sheet name
+ assert.EqualError(t, f.SetRowOutlineLevel("Sheet:1", 1, 4), ErrSheetNameInvalid.Error())
+ // Test get row outline level on not exists worksheet
_, err = f.GetRowOutlineLevel("SheetN", 1)
- assert.EqualError(t, err, "sheet SheetN is not exist")
-
- // Test set and get column outline level with illegal cell coordinates.
- assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get row outline level with invalid sheet name
+ _, err = f.GetRowOutlineLevel("Sheet:1", 1)
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test set and get column outline level with illegal cell reference
+ assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), newInvalidColumnNameError("*").Error())
_, err = f.GetColOutlineLevel("Sheet1", "*")
- assert.EqualError(t, err, `invalid column name "*"`)
+ assert.EqualError(t, err, newInvalidColumnNameError("*").Error())
- // Test set column outline level on not exists worksheet.
- assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist")
+ // Test set column outline level on not exists worksheet
+ assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN does not exist")
- assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0")
+ assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), newInvalidRowNumberError(0).Error())
level, err = f.GetRowOutlineLevel("Sheet1", 2)
assert.NoError(t, err)
assert.Equal(t, uint8(7), level)
_, err = f.GetRowOutlineLevel("Sheet1", 0)
- assert.EqualError(t, err, `invalid row number 0`)
+ assert.EqualError(t, err, newInvalidRowNumberError(0).Error())
level, err = f.GetRowOutlineLevel("Sheet1", 10)
assert.NoError(t, err)
@@ -271,23 +326,56 @@ func TestOutlineLevel(t *testing.T) {
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2))
+ assert.NoError(t, f.Close())
}
func TestSetColStyle(t *testing.T) {
f := NewFile()
- style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`)
+ assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello"))
+
+ styleID, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"94D3A2"}, Pattern: 1}})
+ assert.NoError(t, err)
+ // Test set column style on not exists worksheet
+ assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN does not exist")
+ // Test set column style with illegal column name
+ assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error())
+ assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error())
+ // Test set column style with invalid style ID
+ assert.EqualError(t, f.SetColStyle("Sheet1", "B", -1), newInvalidStyleID(-1).Error())
+ // Test set column style with not exists style ID
+ assert.EqualError(t, f.SetColStyle("Sheet1", "B", 10), newInvalidStyleID(10).Error())
+ // Test set column style with invalid sheet name
+ assert.EqualError(t, f.SetColStyle("Sheet:1", "A", 0), ErrSheetNameInvalid.Error())
+
+ assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID))
+ style, err := f.GetColStyle("Sheet1", "B")
+ assert.NoError(t, err)
+ assert.Equal(t, styleID, style)
+
+ // Test set column style with already exists column with style
+ assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID))
+ assert.NoError(t, f.SetColStyle("Sheet1", "D:C", styleID))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetData.Row[1].C[2].S = 0
+ cellStyleID, err := f.GetCellStyle("Sheet1", "C2")
assert.NoError(t, err)
- // Test set column style on not exists worksheet.
- assert.EqualError(t, f.SetColStyle("SheetN", "E", style), "sheet SheetN is not exist")
- // Test set column style with illegal cell coordinates.
- assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), `invalid column name "*"`)
- assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), `invalid column name "*"`)
-
- assert.NoError(t, f.SetColStyle("Sheet1", "B", style))
- // Test set column style with already exists column with style.
- assert.NoError(t, f.SetColStyle("Sheet1", "B", style))
- assert.NoError(t, f.SetColStyle("Sheet1", "D:C", style))
+ assert.Equal(t, styleID, cellStyleID)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx")))
+ // Test set column style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test set column style with worksheet properties columns default width settings
+ f = NewFile()
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{DefaultColWidth: float64Ptr(20)}))
+ style, err = f.NewStyle(&Style{Alignment: &Alignment{Vertical: "center"}})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetColStyle("Sheet1", "A:Z", style))
+ width, err := f.GetColWidth("Sheet1", "B")
+ assert.NoError(t, err)
+ assert.Equal(t, 20.0, width)
}
func TestColWidth(t *testing.T) {
@@ -298,52 +386,85 @@ func TestColWidth(t *testing.T) {
assert.Equal(t, float64(12), width)
assert.NoError(t, err)
width, err = f.GetColWidth("Sheet1", "C")
- assert.Equal(t, float64(64), width)
+ assert.Equal(t, defaultColWidth, width)
assert.NoError(t, err)
- // Test set and get column width with illegal cell coordinates.
- width, err = f.GetColWidth("Sheet1", "*")
- assert.Equal(t, float64(64), width)
- assert.EqualError(t, err, `invalid column name "*"`)
- assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`)
- assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`)
-
- // Test set column width on not exists worksheet.
- assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN is not exist")
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetFormatPr = &xlsxSheetFormatPr{DefaultColWidth: 10}
+ ws.(*xlsxWorksheet).Cols = nil
+ width, err = f.GetColWidth("Sheet1", "A")
+ assert.NoError(t, err)
+ assert.Equal(t, 10.0, width)
+ assert.Equal(t, 76, f.getColWidth("Sheet1", 1))
- // Test get column width on not exists worksheet.
+ // Test set and get column width with illegal cell reference
+ width, err = f.GetColWidth("Sheet1", "*")
+ assert.Equal(t, defaultColWidth, width)
+ assert.EqualError(t, err, newInvalidColumnNameError("*").Error())
+ assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), newInvalidColumnNameError("*").Error())
+ assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), newInvalidColumnNameError("*").Error())
+
+ // Test set column width on not exists worksheet
+ assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN does not exist")
+ // Test get column width on not exists worksheet
_, err = f.GetColWidth("SheetN", "A")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get column width invalid sheet name
+ _, err = f.GetColWidth("Sheet:1", "A")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColWidth.xlsx")))
convertRowHeightToPixels(0)
}
-func TestInsertCol(t *testing.T) {
+func TestGetColStyle(t *testing.T) {
+ f := NewFile()
+ styleID, err := f.GetColStyle("Sheet1", "A")
+ assert.NoError(t, err)
+ assert.Equal(t, styleID, 0)
+
+ // Test get column style on not exists worksheet
+ _, err = f.GetColStyle("SheetN", "A")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get column style with illegal column name
+ _, err = f.GetColStyle("Sheet1", "*")
+ assert.EqualError(t, err, newInvalidColumnNameError("*").Error())
+ // Test get column style with invalid sheet name
+ _, err = f.GetColStyle("Sheet:1", "A")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+}
+
+func TestInsertCols(t *testing.T) {
f := NewFile()
sheet1 := f.GetSheetName(0)
- fillCells(f, sheet1, 10, 10)
+ assert.NoError(t, fillCells(f, sheet1, 10, 10))
- assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
+ assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))
- assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`))
- assert.NoError(t, f.InsertCol(sheet1, "A"))
+ assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", []AutoFilterOptions{{Column: "B", Expression: "x != blanks"}}))
+ assert.NoError(t, f.InsertCols(sheet1, "A", 1))
- // Test insert column with illegal cell coordinates.
- assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`)
+ // Test insert column with illegal cell reference
+ assert.EqualError(t, f.InsertCols(sheet1, "*", 1), newInvalidColumnNameError("*").Error())
+ // Test insert column with invalid sheet name
+ assert.EqualError(t, f.InsertCols("Sheet:1", "A", 1), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, f.InsertCols(sheet1, "A", 0), ErrColumnNumber.Error())
+ assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns), ErrColumnNumber.Error())
+ assert.EqualError(t, f.InsertCols(sheet1, "A", MaxColumns-10), ErrColumnNumber.Error())
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx")))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCols.xlsx")))
}
func TestRemoveCol(t *testing.T) {
f := NewFile()
sheet1 := f.GetSheetName(0)
- fillCells(f, sheet1, 10, 15)
+ assert.NoError(t, fillCells(f, sheet1, 10, 15))
- assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
+ assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "B1"))
@@ -352,11 +473,16 @@ func TestRemoveCol(t *testing.T) {
assert.NoError(t, f.RemoveCol(sheet1, "A"))
assert.NoError(t, f.RemoveCol(sheet1, "A"))
- // Test remove column with illegal cell coordinates.
- assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`)
-
- // Test remove column on not exists worksheet.
- assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist")
+ // Test remove column with illegal cell reference
+ assert.EqualError(t, f.RemoveCol("Sheet1", "*"), newInvalidColumnNameError("*").Error())
+ // Test remove column on not exists worksheet
+ assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN does not exist")
+ // Test remove column with invalid sheet name
+ assert.EqualError(t, f.RemoveCol("Sheet:1", "A"), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx")))
}
+
+func TestConvertColWidthToPixels(t *testing.T) {
+ assert.Equal(t, -11.0, convertColWidthToPixels(-1))
+}
diff --git a/comment.go b/comment.go
deleted file mode 100644
index 6010891318..0000000000
--- a/comment.go
+++ /dev/null
@@ -1,367 +0,0 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
-// this source code is governed by a BSD-style license that can be found in
-// the LICENSE file.
-//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
-
-package excelize
-
-import (
- "bytes"
- "encoding/json"
- "encoding/xml"
- "fmt"
- "io"
- "log"
- "path/filepath"
- "strconv"
- "strings"
-)
-
-// parseFormatCommentsSet provides a function to parse the format settings of
-// the comment with default value.
-func parseFormatCommentsSet(formatSet string) (*formatComment, error) {
- format := formatComment{
- Author: "Author:",
- Text: " ",
- }
- err := json.Unmarshal([]byte(formatSet), &format)
- return &format, err
-}
-
-// GetComments retrieves all comments and returns a map of worksheet name to
-// the worksheet comments.
-func (f *File) GetComments() (comments map[string][]Comment) {
- comments = map[string][]Comment{}
- for n, path := range f.sheetMap {
- if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(filepath.Base(path)), "..")); d != nil {
- sheetComments := []Comment{}
- for _, comment := range d.CommentList.Comment {
- sheetComment := Comment{}
- if comment.AuthorID < len(d.Authors) {
- sheetComment.Author = d.Authors[comment.AuthorID].Author
- }
- sheetComment.Ref = comment.Ref
- sheetComment.AuthorID = comment.AuthorID
- if comment.Text.T != nil {
- sheetComment.Text += *comment.Text.T
- }
- for _, text := range comment.Text.R {
- if text.T != nil {
- sheetComment.Text += text.T.Val
- }
- }
- sheetComments = append(sheetComments, sheetComment)
- }
- comments[n] = sheetComments
- }
- }
- return
-}
-
-// getSheetComments provides the method to get the target comment reference by
-// given worksheet file path.
-func (f *File) getSheetComments(sheetFile string) string {
- var rels = "xl/worksheets/_rels/" + sheetFile + ".rels"
- if sheetRels := f.relsReader(rels); sheetRels != nil {
- for _, v := range sheetRels.Relationships {
- if v.Type == SourceRelationshipComments {
- return v.Target
- }
- }
- }
- return ""
-}
-
-// AddComment provides the method to add comment in a sheet by given worksheet
-// index, cell and format set (such as author and text). Note that the max
-// author length is 255 and the max text length is 32512. For example, add a
-// comment in Sheet1!$A$30:
-//
-// err := f.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
-//
-func (f *File) AddComment(sheet, cell, format string) error {
- formatSet, err := parseFormatCommentsSet(format)
- if err != nil {
- return err
- }
- // Read sheet data.
- xlsx, err := f.workSheetReader(sheet)
- if err != nil {
- return err
- }
- commentID := f.countComments() + 1
- drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
- sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
- sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
- if xlsx.LegacyDrawing != nil {
- // The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
- sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID)
- commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
- drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
- } else {
- // Add first comment for given sheet.
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
- rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
- f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
- f.addSheetNameSpace(sheet, SourceRelationship)
- f.addSheetLegacyDrawing(sheet, rID)
- }
- commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
- var colCount int
- for i, l := range strings.Split(formatSet.Text, "\n") {
- if ll := len(l); ll > colCount {
- if i == 0 {
- ll += len(formatSet.Author)
- }
- colCount = ll
- }
- }
- err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
- if err != nil {
- return err
- }
- f.addComment(commentsXML, cell, formatSet)
- f.addContentTypePart(commentID, "comments")
- return err
-}
-
-// addDrawingVML provides a function to create comment as
-// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
-func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
- col, row, err := CellNameToCoordinates(cell)
- if err != nil {
- return err
- }
- yAxis := col - 1
- xAxis := row - 1
- vml := f.VMLDrawing[drawingVML]
- if vml == nil {
- vml = &vmlDrawing{
- XMLNSv: "urn:schemas-microsoft-com:vml",
- XMLNSo: "urn:schemas-microsoft-com:office:office",
- XMLNSx: "urn:schemas-microsoft-com:office:excel",
- XMLNSmv: "http://macVmlSchemaUri",
- Shapelayout: &xlsxShapelayout{
- Ext: "edit",
- IDmap: &xlsxIDmap{
- Ext: "edit",
- Data: commentID,
- },
- },
- Shapetype: &xlsxShapetype{
- ID: "_x0000_t202",
- Coordsize: "21600,21600",
- Spt: 202,
- Path: "m0,0l0,21600,21600,21600,21600,0xe",
- Stroke: &xlsxStroke{
- Joinstyle: "miter",
- },
- VPath: &vPath{
- Gradientshapeok: "t",
- Connecttype: "rect",
- },
- },
- }
- }
- sp := encodeShape{
- Fill: &vFill{
- Color2: "#fbfe82",
- Angle: -180,
- Type: "gradient",
- Fill: &oFill{
- Ext: "view",
- Type: "gradientUnscaled",
- },
- },
- Shadow: &vShadow{
- On: "t",
- Color: "black",
- Obscured: "t",
- },
- Path: &vPath{
- Connecttype: "none",
- },
- Textbox: &vTextbox{
- Style: "mso-direction-alt:auto",
- Div: &xlsxDiv{
- Style: "text-align:left",
- },
- },
- ClientData: &xClientData{
- ObjectType: "Note",
- Anchor: fmt.Sprintf(
- "%d, 23, %d, 0, %d, %d, %d, 5",
- 1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
- AutoFill: "True",
- Row: xAxis,
- Column: yAxis,
- },
- }
- s, _ := xml.Marshal(sp)
- shape := xlsxShape{
- ID: "_x0000_s1025",
- Type: "#_x0000_t202",
- Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
- Fillcolor: "#fbf6d6",
- Strokecolor: "#edeaa1",
- Val: string(s[13 : len(s)-14]),
- }
- d := f.decodeVMLDrawingReader(drawingVML)
- if d != nil {
- for _, v := range d.Shape {
- s := xlsxShape{
- ID: "_x0000_s1025",
- Type: "#_x0000_t202",
- Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
- Fillcolor: "#fbf6d6",
- Strokecolor: "#edeaa1",
- Val: v.Val,
- }
- vml.Shape = append(vml.Shape, s)
- }
- }
- vml.Shape = append(vml.Shape, shape)
- f.VMLDrawing[drawingVML] = vml
- return err
-}
-
-// addComment provides a function to create chart as xl/comments%d.xml by
-// given cell and format sets.
-func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
- a := formatSet.Author
- t := formatSet.Text
- if len(a) > 255 {
- a = a[0:255]
- }
- if len(t) > 32512 {
- t = t[0:32512]
- }
- comments := f.commentsReader(commentsXML)
- if comments == nil {
- comments = &xlsxComments{
- Authors: []xlsxAuthor{
- {
- Author: formatSet.Author,
- },
- },
- }
- }
- defaultFont := f.GetDefaultFont()
- cmt := xlsxComment{
- Ref: cell,
- AuthorID: 0,
- Text: xlsxText{
- R: []xlsxR{
- {
- RPr: &xlsxRPr{
- B: " ",
- Sz: &attrValFloat{Val: float64Ptr(9)},
- Color: &xlsxColor{
- Indexed: 81,
- },
- RFont: &attrValString{Val: stringPtr(defaultFont)},
- Family: &attrValInt{Val: intPtr(2)},
- },
- T: &xlsxT{Val: a},
- },
- {
- RPr: &xlsxRPr{
- Sz: &attrValFloat{Val: float64Ptr(9)},
- Color: &xlsxColor{
- Indexed: 81,
- },
- RFont: &attrValString{Val: stringPtr(defaultFont)},
- Family: &attrValInt{Val: intPtr(2)},
- },
- T: &xlsxT{Val: t},
- },
- },
- },
- }
- comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
- f.Comments[commentsXML] = comments
-}
-
-// countComments provides a function to get comments files count storage in
-// the folder xl.
-func (f *File) countComments() int {
- c1, c2 := 0, 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/comments") {
- c1++
- }
- }
- for rel := range f.Comments {
- if strings.Contains(rel, "xl/comments") {
- c2++
- }
- }
- if c1 < c2 {
- return c2
- }
- return c1
-}
-
-// decodeVMLDrawingReader provides a function to get the pointer to the
-// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
-func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing {
- var err error
-
- if f.DecodeVMLDrawing[path] == nil {
- c, ok := f.XLSX[path]
- if ok {
- f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))).
- Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
- }
- }
- }
- return f.DecodeVMLDrawing[path]
-}
-
-// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
-// after serialize structure.
-func (f *File) vmlDrawingWriter() {
- for path, vml := range f.VMLDrawing {
- if vml != nil {
- v, _ := xml.Marshal(vml)
- f.XLSX[path] = v
- }
- }
-}
-
-// commentsReader provides a function to get the pointer to the structure
-// after deserialization of xl/comments%d.xml.
-func (f *File) commentsReader(path string) *xlsxComments {
- var err error
-
- if f.Comments[path] == nil {
- content, ok := f.XLSX[path]
- if ok {
- f.Comments[path] = new(xlsxComments)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))).
- Decode(f.Comments[path]); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
- }
- }
- }
- return f.Comments[path]
-}
-
-// commentsWriter provides a function to save xl/comments%d.xml after
-// serialize structure.
-func (f *File) commentsWriter() {
- for path, c := range f.Comments {
- if c != nil {
- v, _ := xml.Marshal(c)
- f.saveFileList(path, v)
- }
- }
-}
diff --git a/comment_test.go b/comment_test.go
deleted file mode 100644
index 955d4e87c4..0000000000
--- a/comment_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
-// this source code is governed by a BSD-style license that can be found in
-// the LICENSE file.
-//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX files. Support reads and writes XLSX file generated by
-// Microsoft Excel™ 2007 and later. Support save file without losing original
-// charts of XLSX. This library needs Go version 1.10 or later.
-
-package excelize
-
-import (
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestAddComments(t *testing.T) {
- f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- s := strings.Repeat("c", 32768)
- assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`))
- assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`))
-
- // Test add comment on not exists worksheet.
- assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist")
- // Test add comment on with illegal cell coordinates
- assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) {
- assert.Len(t, f.GetComments(), 2)
- }
-
- f.Comments["xl/comments2.xml"] = nil
- f.XLSX["xl/comments2.xml"] = []byte(`Excelize: Excelize: `)
- comments := f.GetComments()
- assert.EqualValues(t, 2, len(comments["Sheet1"]))
- assert.EqualValues(t, 1, len(comments["Sheet2"]))
-}
-
-func TestDecodeVMLDrawingReader(t *testing.T) {
- f := NewFile()
- path := "xl/drawings/vmlDrawing1.xml"
- f.XLSX[path] = MacintoshCyrillicCharset
- f.decodeVMLDrawingReader(path)
-}
-
-func TestCommentsReader(t *testing.T) {
- f := NewFile()
- path := "xl/comments1.xml"
- f.XLSX[path] = MacintoshCyrillicCharset
- f.commentsReader(path)
-}
-
-func TestCountComments(t *testing.T) {
- f := NewFile()
- f.Comments["xl/comments1.xml"] = nil
- assert.Equal(t, f.countComments(), 1)
-}
diff --git a/crypt.go b/crypt.go
new file mode 100644
index 0000000000..27f5c8502d
--- /dev/null
+++ b/crypt.go
@@ -0,0 +1,1018 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/md5"
+ "crypto/rand"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/xml"
+ "hash"
+ "math"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/richardlehane/mscfb"
+ "golang.org/x/crypto/md4"
+ "golang.org/x/crypto/ripemd160"
+ "golang.org/x/text/encoding/unicode"
+)
+
+var (
+ blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption
+ oleIdentifier = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
+ headerCLSID = make([]byte, 16)
+ difSect = -4
+ endOfChain = -2
+ fatSect = -3
+ iterCount = 50000
+ packageEncryptionChunkSize = 4096
+ packageOffset = 8 // First 8 bytes are the size of the stream
+ sheetProtectionSpinCount = 1e5
+ workbookProtectionSpinCount = 1e5
+)
+
+// Encryption specifies the encryption structure, streams, and storages are
+// required when encrypting ECMA-376 documents.
+type Encryption struct {
+ XMLName xml.Name `xml:"encryption"`
+ KeyData KeyData `xml:"keyData"`
+ DataIntegrity DataIntegrity `xml:"dataIntegrity"`
+ KeyEncryptors KeyEncryptors `xml:"keyEncryptors"`
+}
+
+// KeyData specifies the cryptographic attributes used to encrypt the data.
+type KeyData struct {
+ SaltSize int `xml:"saltSize,attr"`
+ BlockSize int `xml:"blockSize,attr"`
+ KeyBits int `xml:"keyBits,attr"`
+ HashSize int `xml:"hashSize,attr"`
+ CipherAlgorithm string `xml:"cipherAlgorithm,attr"`
+ CipherChaining string `xml:"cipherChaining,attr"`
+ HashAlgorithm string `xml:"hashAlgorithm,attr"`
+ SaltValue string `xml:"saltValue,attr"`
+}
+
+// DataIntegrity specifies the encrypted copies of the salt and hash values
+// used to help ensure that the integrity of the encrypted data has not been
+// compromised.
+type DataIntegrity struct {
+ EncryptedHmacKey string `xml:"encryptedHmacKey,attr"`
+ EncryptedHmacValue string `xml:"encryptedHmacValue,attr"`
+}
+
+// KeyEncryptors specifies the key encryptors used to encrypt the data.
+type KeyEncryptors struct {
+ KeyEncryptor []KeyEncryptor `xml:"keyEncryptor"`
+}
+
+// KeyEncryptor specifies that the schema used by this encryptor is the schema
+// specified for password-based encryptors.
+type KeyEncryptor struct {
+ XMLName xml.Name `xml:"keyEncryptor"`
+ URI string `xml:"uri,attr"`
+ EncryptedKey EncryptedKey `xml:"encryptedKey"`
+}
+
+// EncryptedKey used to generate the encrypting key.
+type EncryptedKey struct {
+ XMLName xml.Name `xml:"http://schemas.microsoft.com/office/2006/keyEncryptor/password encryptedKey"`
+ SpinCount int `xml:"spinCount,attr"`
+ EncryptedVerifierHashInput string `xml:"encryptedVerifierHashInput,attr"`
+ EncryptedVerifierHashValue string `xml:"encryptedVerifierHashValue,attr"`
+ EncryptedKeyValue string `xml:"encryptedKeyValue,attr"`
+ KeyData
+}
+
+// StandardEncryptionHeader structure is used by ECMA-376 document encryption
+// [ECMA-376] and Office binary document RC4 CryptoAPI encryption, to specify
+// encryption properties for an encrypted stream.
+type StandardEncryptionHeader struct {
+ Flags uint32
+ SizeExtra uint32
+ AlgID uint32
+ AlgIDHash uint32
+ KeySize uint32
+ ProviderType uint32
+ Reserved1 uint32
+ Reserved2 uint32
+ CspName string
+}
+
+// StandardEncryptionVerifier structure is used by Office Binary Document RC4
+// CryptoAPI Encryption and ECMA-376 Document Encryption. Every usage of this
+// structure MUST specify the hashing algorithm and encryption algorithm used
+// in the EncryptionVerifier structure.
+type StandardEncryptionVerifier struct {
+ SaltSize uint32
+ Salt []byte
+ EncryptedVerifier []byte
+ VerifierHashSize uint32
+ EncryptedVerifierHash []byte
+}
+
+// encryptionInfo structure is used for standard encryption with SHA1
+// cryptographic algorithm.
+type encryption struct {
+ BlockSize, SaltSize int
+ EncryptedKeyValue, EncryptedVerifierHashInput, EncryptedVerifierHashValue, SaltValue []byte
+ KeyBits uint32
+}
+
+// Decrypt API decrypts the CFB file format with ECMA-376 agile encryption and
+// standard encryption. Support cryptographic algorithm: MD4, MD5, RIPEMD-160,
+// SHA1, SHA256, SHA384 and SHA512 currently.
+func Decrypt(raw []byte, opts *Options) (packageBuf []byte, err error) {
+ doc, err := mscfb.New(bytes.NewReader(raw))
+ if err != nil {
+ return
+ }
+ encryptionInfoBuf, encryptedPackageBuf := extractPart(doc)
+ mechanism, err := encryptionMechanism(encryptionInfoBuf)
+ if err != nil || mechanism == "extensible" {
+ return
+ }
+ if mechanism == "agile" {
+ return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opts)
+ }
+ return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opts)
+}
+
+// Encrypt API encrypt data with the password.
+func Encrypt(raw []byte, opts *Options) ([]byte, error) {
+ encryptor := encryption{
+ EncryptedVerifierHashInput: make([]byte, 16),
+ EncryptedVerifierHashValue: make([]byte, 32),
+ SaltValue: make([]byte, 16),
+ BlockSize: 16,
+ KeyBits: 128,
+ SaltSize: 16,
+ }
+ // Key Encryption
+ encryptionInfoBuffer, err := encryptor.standardKeyEncryption(opts.Password)
+ if err != nil {
+ return nil, err
+ }
+ // Package Encryption
+ encryptedPackage := make([]byte, 8)
+ binary.LittleEndian.PutUint64(encryptedPackage, uint64(len(raw)))
+ encryptedPackage = append(encryptedPackage, encryptor.encrypt(raw)...)
+ // Create a new CFB
+ compoundFile := &cfb{
+ paths: []string{"Root Entry/"},
+ sectors: []sector{{name: "Root Entry", typeID: 5}},
+ }
+ compoundFile.put("EncryptionInfo", encryptionInfoBuffer)
+ compoundFile.put("EncryptedPackage", encryptedPackage)
+ return compoundFile.write(), nil
+}
+
+// extractPart extract data from storage by specified part name.
+func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []byte) {
+ for entry, err := doc.Next(); err == nil; entry, err = doc.Next() {
+ switch entry.Name {
+ case "EncryptionInfo":
+ buf := make([]byte, entry.Size)
+ i, _ := doc.Read(buf)
+ if i > 0 {
+ encryptionInfoBuf = buf
+ }
+ case "EncryptedPackage":
+ buf := make([]byte, entry.Size)
+ i, _ := doc.Read(buf)
+ if i > 0 {
+ encryptedPackageBuf = buf
+ }
+ }
+ }
+ return
+}
+
+// encryptionMechanism parse password-protected documents created mechanism.
+func encryptionMechanism(buffer []byte) (mechanism string, err error) {
+ if len(buffer) < 4 {
+ err = ErrUnknownEncryptMechanism
+ return
+ }
+ versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[:2]), binary.LittleEndian.Uint16(buffer[2:4])
+ if versionMajor == 4 && versionMinor == 4 {
+ mechanism = "agile"
+ return
+ } else if (2 <= versionMajor && versionMajor <= 4) && versionMinor == 2 {
+ mechanism = "standard"
+ return
+ } else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 {
+ mechanism = "extensible"
+ }
+ err = ErrUnsupportedEncryptMechanism
+ return
+}
+
+// ECMA-376 Standard Encryption
+
+// standardDecrypt decrypt the CFB file format with ECMA-376 standard encryption.
+func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opts *Options) ([]byte, error) {
+ encryptionHeaderSize := binary.LittleEndian.Uint32(encryptionInfoBuf[8:12])
+ block := encryptionInfoBuf[12 : 12+encryptionHeaderSize]
+ header := StandardEncryptionHeader{
+ Flags: binary.LittleEndian.Uint32(block[:4]),
+ SizeExtra: binary.LittleEndian.Uint32(block[4:8]),
+ AlgID: binary.LittleEndian.Uint32(block[8:12]),
+ AlgIDHash: binary.LittleEndian.Uint32(block[12:16]),
+ KeySize: binary.LittleEndian.Uint32(block[16:20]),
+ ProviderType: binary.LittleEndian.Uint32(block[20:24]),
+ Reserved1: binary.LittleEndian.Uint32(block[24:28]),
+ Reserved2: binary.LittleEndian.Uint32(block[28:32]),
+ CspName: string(block[32:]),
+ }
+ block = encryptionInfoBuf[12+encryptionHeaderSize:]
+ algIDMap := map[uint32]string{
+ 0x0000660E: "AES-128",
+ 0x0000660F: "AES-192",
+ 0x00006610: "AES-256",
+ }
+ algorithm := "AES"
+ _, ok := algIDMap[header.AlgID]
+ if !ok {
+ algorithm = "RC4"
+ }
+ verifier := standardEncryptionVerifier(algorithm, block)
+ secretKey, err := standardConvertPasswdToKey(header, verifier, opts)
+ if err != nil {
+ return nil, err
+ }
+ // decrypted data
+ x := encryptedPackageBuf[8:]
+ blob, err := aes.NewCipher(secretKey)
+ if err != nil {
+ return nil, err
+ }
+ decrypted := make([]byte, len(x))
+ size := 16
+ for bs, be := 0, size; bs < len(x); bs, be = bs+size, be+size {
+ blob.Decrypt(decrypted[bs:be], x[bs:be])
+ }
+ return decrypted, err
+}
+
+// standardEncryptionVerifier extract ECMA-376 standard encryption verifier.
+func standardEncryptionVerifier(algorithm string, blob []byte) StandardEncryptionVerifier {
+ verifier := StandardEncryptionVerifier{
+ SaltSize: binary.LittleEndian.Uint32(blob[:4]),
+ Salt: blob[4:20],
+ EncryptedVerifier: blob[20:36],
+ VerifierHashSize: binary.LittleEndian.Uint32(blob[36:40]),
+ }
+ if algorithm == "RC4" {
+ verifier.EncryptedVerifierHash = blob[40:60]
+ } else if algorithm == "AES" {
+ verifier.EncryptedVerifierHash = blob[40:72]
+ }
+ return verifier
+}
+
+// standardConvertPasswdToKey generate intermediate key from given password.
+func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier StandardEncryptionVerifier, opts *Options) ([]byte, error) {
+ encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
+ passwordBuffer, err := encoder.Bytes([]byte(opts.Password))
+ if err != nil {
+ return nil, err
+ }
+ key := hashing("sha1", verifier.Salt, passwordBuffer)
+ for i := 0; i < iterCount; i++ {
+ iterator := createUInt32LEBuffer(i, 4)
+ key = hashing("sha1", iterator, key)
+ }
+ var block int
+ hFinal := hashing("sha1", key, createUInt32LEBuffer(block, 4))
+ cbRequiredKeyLength := int(header.KeySize) / 8
+ cbHash := sha1.Size
+ buf1 := bytes.Repeat([]byte{0x36}, 64)
+ buf1 = append(standardXORBytes(hFinal, buf1[:cbHash]), buf1[cbHash:]...)
+ x1 := hashing("sha1", buf1)
+ buf2 := bytes.Repeat([]byte{0x5c}, 64)
+ buf2 = append(standardXORBytes(hFinal, buf2[:cbHash]), buf2[cbHash:]...)
+ x2 := hashing("sha1", buf2)
+ x3 := append(x1, x2...)
+ keyDerived := x3[:cbRequiredKeyLength]
+ return keyDerived, err
+}
+
+// standardXORBytes perform XOR operations for two bytes slice.
+func standardXORBytes(a, b []byte) []byte {
+ r := make([][2]byte, len(a))
+ for i, e := range a {
+ r[i] = [2]byte{e, b[i]}
+ }
+ buf := make([]byte, len(a))
+ for p, q := range r {
+ buf[p] = q[0] ^ q[1]
+ }
+ return buf
+}
+
+// encrypt provides a function to encrypt given value with AES cryptographic
+// algorithm.
+func (e *encryption) encrypt(input []byte) []byte {
+ inputBytes := len(input)
+ if pad := inputBytes % e.BlockSize; pad != 0 {
+ inputBytes += e.BlockSize - pad
+ }
+ var output, chunk []byte
+ encryptedChunk := make([]byte, e.BlockSize)
+ for i := 0; i < inputBytes; i += e.BlockSize {
+ if i+e.BlockSize <= len(input) {
+ chunk = input[i : i+e.BlockSize]
+ } else {
+ chunk = input[i:]
+ }
+ chunk = append(chunk, make([]byte, e.BlockSize-len(chunk))...)
+ c, _ := aes.NewCipher(e.EncryptedKeyValue)
+ c.Encrypt(encryptedChunk, chunk)
+ output = append(output, encryptedChunk...)
+ }
+ return output
+}
+
+// standardKeyEncryption encrypt convert the password to an encryption key.
+func (e *encryption) standardKeyEncryption(password string) ([]byte, error) {
+ if len(password) == 0 || len(password) > MaxFieldLength {
+ return nil, ErrPasswordLengthInvalid
+ }
+ var storage cfb
+ storage.writeUint16(0x0003)
+ storage.writeUint16(0x0002)
+ storage.writeUint32(0x24)
+ storage.writeUint32(0xA4)
+ storage.writeUint32(0x24)
+ storage.writeUint32(0x00)
+ storage.writeUint32(0x660E)
+ storage.writeUint32(0x8004)
+ storage.writeUint32(0x80)
+ storage.writeUint32(0x18)
+ storage.writeUint64(0x00)
+ providerName := "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)"
+ storage.writeStrings(providerName)
+ storage.writeUint16(0x00)
+ storage.writeUint32(0x10)
+ keyDataSaltValue, _ := randomBytes(16)
+ verifierHashInput, _ := randomBytes(16)
+ e.SaltValue = keyDataSaltValue
+ e.EncryptedKeyValue, _ = standardConvertPasswdToKey(
+ StandardEncryptionHeader{KeySize: e.KeyBits},
+ StandardEncryptionVerifier{Salt: e.SaltValue},
+ &Options{Password: password})
+ verifierHashInputKey := hashing("sha1", verifierHashInput)
+ e.EncryptedVerifierHashInput = e.encrypt(verifierHashInput)
+ e.EncryptedVerifierHashValue = e.encrypt(verifierHashInputKey)
+ storage.writeBytes(e.SaltValue)
+ storage.writeBytes(e.EncryptedVerifierHashInput)
+ storage.writeUint32(0x14)
+ storage.writeBytes(e.EncryptedVerifierHashValue)
+ storage.position = 0
+ return storage.stream, nil
+}
+
+// ECMA-376 Agile Encryption
+
+// agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption.
+// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256,
+// SHA384 and SHA512.
+func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opts *Options) (packageBuf []byte, err error) {
+ var encryptionInfo Encryption
+ if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil {
+ return
+ }
+ // Convert the password into an encryption key.
+ key, err := convertPasswdToKey(opts.Password, blockKey, encryptionInfo)
+ if err != nil {
+ return
+ }
+ // Use the key to decrypt the package key.
+ encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey
+ saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue)
+ if err != nil {
+ return
+ }
+ encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue)
+ if err != nil {
+ return
+ }
+ packageKey, _ := decrypt(key, saltValue, encryptedKeyValue)
+ // Use the package key to decrypt the package.
+ return decryptPackage(packageKey, encryptedPackageBuf, encryptionInfo)
+}
+
+// convertPasswdToKey convert the password into an encryption key.
+func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) (key []byte, err error) {
+ var b bytes.Buffer
+ saltValue, err := base64.StdEncoding.DecodeString(encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SaltValue)
+ if err != nil {
+ return
+ }
+ b.Write(saltValue)
+ encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
+ passwordBuffer, err := encoder.Bytes([]byte(passwd))
+ if err != nil {
+ return
+ }
+ b.Write(passwordBuffer)
+ // Generate the initial hash.
+ key = hashing(encryption.KeyData.HashAlgorithm, b.Bytes())
+ // Now regenerate until spin count.
+ for i := 0; i < encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SpinCount; i++ {
+ iterator := createUInt32LEBuffer(i, 4)
+ key = hashing(encryption.KeyData.HashAlgorithm, iterator, key)
+ }
+ // Now generate the final hash.
+ key = hashing(encryption.KeyData.HashAlgorithm, key, blockKey)
+ // Truncate or pad as needed to get to length of keyBits.
+ keyBytes := encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.KeyBits / 8
+ if len(key) < keyBytes {
+ tmp := make([]byte, 0x36)
+ key = append(key, tmp...)
+ } else if len(key) > keyBytes {
+ key = key[:keyBytes]
+ }
+ return
+}
+
+// hashing data by specified hash algorithm.
+func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) {
+ hashMap := map[string]hash.Hash{
+ "md4": md4.New(),
+ "md5": md5.New(),
+ "ripemd-160": ripemd160.New(),
+ "sha1": sha1.New(),
+ "sha256": sha256.New(),
+ "sha384": sha512.New384(),
+ "sha512": sha512.New(),
+ }
+ handler, ok := hashMap[strings.ToLower(hashAlgorithm)]
+ if !ok {
+ return key
+ }
+ for _, buf := range buffer {
+ _, _ = handler.Write(buf)
+ }
+ key = handler.Sum(nil)
+ return key
+}
+
+// createUInt32LEBuffer create buffer with little endian 32-bit unsigned
+// integer.
+func createUInt32LEBuffer(value int, bufferSize int) []byte {
+ buf := make([]byte, bufferSize)
+ binary.LittleEndian.PutUint32(buf, uint32(value))
+ return buf
+}
+
+// parseEncryptionInfo parse the encryption info XML into an object.
+func parseEncryptionInfo(encryptionInfo []byte) (encryption Encryption, err error) {
+ err = xml.Unmarshal(encryptionInfo, &encryption)
+ return
+}
+
+// decrypt provides a function to decrypt input by given cipher algorithm,
+// cipher chaining, key and initialization vector.
+func decrypt(key, iv, input []byte) (packageKey []byte, err error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return input, err
+ }
+ cipher.NewCBCDecrypter(block, iv).CryptBlocks(input, input)
+ return input, nil
+}
+
+// decryptPackage decrypt package by given packageKey and encryption
+// info.
+func decryptPackage(packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) {
+ encryptedKey, offset := encryption.KeyData, packageOffset
+ var i, start, end int
+ var iv, outputChunk []byte
+ for end < len(input) {
+ start = end
+ end = start + packageEncryptionChunkSize
+
+ if end > len(input) {
+ end = len(input)
+ }
+ // Grab the next chunk
+ var inputChunk []byte
+ if (end + offset) < len(input) {
+ inputChunk = input[start+offset : end+offset]
+ } else {
+ inputChunk = input[start+offset : end]
+ }
+
+ // Pad the chunk if it is not an integer multiple of the block size
+ remainder := len(inputChunk) % encryptedKey.BlockSize
+ if remainder != 0 {
+ inputChunk = append(inputChunk, make([]byte, encryptedKey.BlockSize-remainder)...)
+ }
+ // Create the initialization vector
+ iv, err = createIV(i, encryption)
+ if err != nil {
+ return
+ }
+ // Decrypt the chunk and add it to the array
+ outputChunk, err = decrypt(packageKey, iv, inputChunk)
+ if err != nil {
+ return
+ }
+ outputChunks = append(outputChunks, outputChunk...)
+ i++
+ }
+ return
+}
+
+// createIV create an initialization vector (IV).
+func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) {
+ encryptedKey := encryption.KeyData
+ // Create the block key from the current index
+ var blockKeyBuf []byte
+ if reflect.TypeOf(blockKey).Kind() == reflect.Int {
+ blockKeyBuf = createUInt32LEBuffer(blockKey.(int), 4)
+ } else {
+ blockKeyBuf = blockKey.([]byte)
+ }
+ saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue)
+ if err != nil {
+ return nil, err
+ }
+ // Create the initialization vector by hashing the salt with the block key.
+ // Truncate or pad as needed to meet the block size.
+ iv := hashing(encryptedKey.HashAlgorithm, append(saltValue, blockKeyBuf...))
+ if len(iv) < encryptedKey.BlockSize {
+ tmp := make([]byte, 0x36)
+ iv = append(iv, tmp...)
+ } else if len(iv) > encryptedKey.BlockSize {
+ iv = iv[:encryptedKey.BlockSize]
+ }
+ return iv, nil
+}
+
+// randomBytes returns securely generated random bytes. It will return an
+// error if the system's secure random number generator fails to function
+// correctly, in which case the caller should not continue.
+func randomBytes(n int) ([]byte, error) {
+ b := make([]byte, n)
+ _, err := rand.Read(b)
+ return b, err
+}
+
+// ISO Write Protection Method
+
+// genISOPasswdHash implements the ISO password hashing algorithm by given
+// plaintext password, name of the cryptographic hash algorithm, salt value
+// and spin count.
+func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashValue, saltValue string, err error) {
+ if len(passwd) < 1 || len(passwd) > MaxFieldLength {
+ err = ErrPasswordLengthInvalid
+ return
+ }
+ algorithmName, ok := map[string]string{
+ "MD4": "md4",
+ "MD5": "md5",
+ "SHA-1": "sha1",
+ "SHA-256": "sha256",
+ "SHA-384": "sha384",
+ "SHA-512": "sha512",
+ }[hashAlgorithm]
+ if !ok {
+ err = ErrUnsupportedHashAlgorithm
+ return
+ }
+ var b bytes.Buffer
+ s, _ := randomBytes(16)
+ if salt != "" {
+ if s, err = base64.StdEncoding.DecodeString(salt); err != nil {
+ return
+ }
+ }
+ b.Write(s)
+ encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
+ passwordBuffer, _ := encoder.Bytes([]byte(passwd))
+ b.Write(passwordBuffer)
+ // Generate the initial hash.
+ key := hashing(algorithmName, b.Bytes())
+ // Now regenerate until spin count.
+ for i := 0; i < spinCount; i++ {
+ iterator := createUInt32LEBuffer(i, 4)
+ key = hashing(algorithmName, key, iterator)
+ }
+ hashValue, saltValue = base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(s)
+ return
+}
+
+// Compound File Binary Implements
+
+// cfb structure is used for the compound file binary (CFB) file format writer.
+type cfb struct {
+ stream []byte
+ position int
+ paths []string
+ sectors []sector
+}
+
+// sector structure used for FAT, directory, miniFAT, and miniStream sectors.
+type sector struct {
+ clsID, content []byte
+ name string
+ C, L, R, color, size, start, state, typeID int
+}
+
+// writeBytes write bytes in the stream by a given value with an offset.
+func (c *cfb) writeBytes(value []byte) {
+ pos := c.position
+ for i := 0; i < len(value); i++ {
+ for j := len(c.stream); j <= i+pos; j++ {
+ c.stream = append(c.stream, 0)
+ }
+ c.stream[i+pos] = value[i]
+ }
+ c.position = pos + len(value)
+}
+
+// writeUint16 write an uint16 data type bytes in the stream by a given value
+// with an offset.
+func (c *cfb) writeUint16(value int) {
+ buf := make([]byte, 2)
+ binary.LittleEndian.PutUint16(buf, uint16(value))
+ c.writeBytes(buf)
+}
+
+// writeUint32 write an uint32 data type bytes in the stream by a given value
+// with an offset.
+func (c *cfb) writeUint32(value int) {
+ buf := make([]byte, 4)
+ binary.LittleEndian.PutUint32(buf, uint32(value))
+ c.writeBytes(buf)
+}
+
+// writeUint64 write an uint64 data type bytes in the stream by a given value
+// with an offset.
+func (c *cfb) writeUint64(value int) {
+ buf := make([]byte, 8)
+ binary.LittleEndian.PutUint64(buf, uint64(value))
+ c.writeBytes(buf)
+}
+
+// writeStrings write strings in the stream by a given value with an offset.
+func (c *cfb) writeStrings(value string) {
+ encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
+ buffer, err := encoder.Bytes([]byte(value))
+ if err != nil {
+ return
+ }
+ c.writeBytes(buffer)
+}
+
+// put provides a function to add an entry to compound file by given entry name
+// and raw bytes.
+func (c *cfb) put(name string, content []byte) {
+ path := c.paths[0]
+ if len(path) <= len(name) && name[:len(path)] == path {
+ path = name
+ } else {
+ if len(path) > 0 && string(path[len(path)-1]) != "/" {
+ path += "/"
+ }
+ path = strings.ReplaceAll(path+name, "//", "/")
+ }
+ file := sector{name: path, typeID: 2, content: content, size: len(content)}
+ c.sectors = append(c.sectors, file)
+ c.paths = append(c.paths, path)
+}
+
+// compare provides a function to compare object path, each set of sibling
+// objects in one level of the containment hierarchy (all child objects under
+// a storage object) is represented as a red-black tree. The parent object of
+// this set of siblings will have a pointer to the top of this tree.
+func (c *cfb) compare(left, right string) int {
+ L, R, i, j := strings.Split(left, "/"), strings.Split(right, "/"), 0, 0
+ for Z := int(math.Min(float64(len(L)), float64(len(R)))); i < Z; i++ {
+ if j = len(L[i]) - len(R[i]); j != 0 {
+ return j
+ }
+ if L[i] != R[i] {
+ if L[i] < R[i] {
+ return -1
+ }
+ return 1
+ }
+ }
+ return len(L) - len(R)
+}
+
+// prepare provides a function to prepare object before write stream.
+func (c *cfb) prepare() {
+ type object struct {
+ path string
+ sector sector
+ }
+ var objects []object
+ for i := 0; i < len(c.paths); i++ {
+ if c.sectors[i].typeID == 0 {
+ continue
+ }
+ objects = append(objects, object{path: c.paths[i], sector: c.sectors[i]})
+ }
+ sort.Slice(objects, func(i, j int) bool {
+ return c.compare(objects[i].path, objects[j].path) == 0
+ })
+ c.paths, c.sectors = []string{}, []sector{}
+ for i := 0; i < len(objects); i++ {
+ c.paths = append(c.paths, objects[i].path)
+ c.sectors = append(c.sectors, objects[i].sector)
+ }
+ for i := 0; i < len(objects); i++ {
+ sector, path := &c.sectors[i], c.paths[i]
+ sector.name, sector.color = filepath.Base(path), 1
+ sector.L, sector.R, sector.C = -1, -1, -1
+ sector.size, sector.start = len(sector.content), 0
+ if len(sector.clsID) == 0 {
+ sector.clsID = headerCLSID
+ }
+ if i == 0 {
+ sector.C = -1
+ if len(objects) > 1 {
+ sector.C = 1
+ }
+ sector.size, sector.typeID = 0, 5
+ } else {
+ if len(c.paths) > i+1 && filepath.Dir(c.paths[i+1]) == filepath.Dir(path) {
+ sector.R = i + 1
+ }
+ sector.typeID = 2
+ }
+ }
+}
+
+// locate provides a function to locate sectors location and size of the
+// compound file.
+func (c *cfb) locate() []int {
+ var miniStreamSectorSize, FATSectorSize int
+ for i := 0; i < len(c.sectors); i++ {
+ sector := c.sectors[i]
+ if len(sector.content) == 0 {
+ continue
+ }
+ size := len(sector.content)
+ if size > 0 {
+ if size < 0x1000 {
+ miniStreamSectorSize += (size + 0x3F) >> 6
+ } else {
+ FATSectorSize += (size + 0x01FF) >> 9
+ }
+ }
+ }
+ directorySectors := (len(c.paths) + 3) >> 2
+ miniStreamSectors := (miniStreamSectorSize + 7) >> 3
+ miniFATSectors := (miniStreamSectorSize + 0x7F) >> 7
+ sectors := miniStreamSectors + FATSectorSize + directorySectors + miniFATSectors
+ FATSectors := (sectors + 0x7F) >> 7
+ DIFATSectors := 0
+ if FATSectors > 109 {
+ DIFATSectors = int(math.Ceil((float64(FATSectors) - 109) / 0x7F))
+ }
+ for ((sectors + FATSectors + DIFATSectors + 0x7F) >> 7) > FATSectors {
+ FATSectors++
+ if FATSectors <= 109 {
+ DIFATSectors = 0
+ } else {
+ DIFATSectors = int(math.Ceil((float64(FATSectors) - 109) / 0x7F))
+ }
+ }
+ location := []int{1, DIFATSectors, FATSectors, miniFATSectors, directorySectors, FATSectorSize, miniStreamSectorSize, 0}
+ c.sectors[0].size = miniStreamSectorSize << 6
+ c.sectors[0].start = location[0] + location[1] + location[2] + location[3] + location[4] + location[5]
+ location[7] = c.sectors[0].start + ((location[6] + 7) >> 3)
+ return location
+}
+
+// writeMSAT provides a function to write compound file master sector allocation
+// table.
+func (c *cfb) writeMSAT(location []int) {
+ var i, offset int
+ for i = 0; i < 109; i++ {
+ if i < location[2] {
+ c.writeUint32(location[1] + i)
+ } else {
+ c.writeUint32(-1)
+ }
+ }
+ if location[1] != 0 {
+ for offset = 0; offset < location[1]; offset++ {
+ for ; i < 236+offset*127; i++ {
+ if i < location[2] {
+ c.writeUint32(location[1] + i)
+ } else {
+ c.writeUint32(-1)
+ }
+ }
+ if offset == location[1]-1 {
+ c.writeUint32(endOfChain)
+ } else {
+ c.writeUint32(offset + 1)
+ }
+ }
+ }
+}
+
+// writeDirectoryEntry provides a function to write compound file directory
+// entries. The directory entry array is an array of directory entries that
+// are grouped into a directory sector. Each storage object or stream object
+// within a compound file is represented by a single directory entry. The
+// space for the directory sectors that are holding the array is allocated
+// from the FAT.
+func (c *cfb) writeDirectoryEntry(location []int) {
+ var sector sector
+ var j, sectorSize int
+ for i := 0; i < location[4]<<2; i++ {
+ var path string
+ if i < len(c.paths) {
+ path = c.paths[i]
+ }
+ if i >= len(c.paths) || len(path) == 0 {
+ for j = 0; j < 17; j++ {
+ c.writeUint32(0)
+ }
+ for j = 0; j < 3; j++ {
+ c.writeUint32(-1)
+ }
+ for j = 0; j < 12; j++ {
+ c.writeUint32(0)
+ }
+ continue
+ }
+ sector = c.sectors[i]
+ if i == 0 {
+ if sector.size > 0 {
+ sector.start = sector.start - 1
+ } else {
+ sector.start = endOfChain
+ }
+ }
+ name := sector.name
+ sectorSize = 2 * (len(name) + 1)
+ c.writeStrings(name)
+ c.position += 64 - 2*(len(name))
+ c.writeUint16(sectorSize)
+ c.writeBytes([]byte(string(rune(sector.typeID))))
+ c.writeBytes([]byte(string(rune(sector.color))))
+ c.writeUint32(sector.L)
+ c.writeUint32(sector.R)
+ c.writeUint32(sector.C)
+ if len(sector.clsID) == 0 {
+ for j = 0; j < 4; j++ {
+ c.writeUint32(0)
+ }
+ } else {
+ c.writeBytes(sector.clsID)
+ }
+ c.writeUint32(sector.state)
+ c.writeUint32(0)
+ c.writeUint32(0)
+ c.writeUint32(0)
+ c.writeUint32(0)
+ c.writeUint32(sector.start)
+ c.writeUint32(sector.size)
+ c.writeUint32(0)
+ }
+}
+
+// writeSectorChains provides a function to write compound file sector chains.
+func (c *cfb) writeSectorChains(location []int) sector {
+ var i, j, offset, sectorSize int
+ writeSectorChain := func(head, offset int) int {
+ for offset += head; i < offset-1; i++ {
+ c.writeUint32(i + 1)
+ }
+ if head != 0 {
+ i++
+ c.writeUint32(endOfChain)
+ }
+ return offset
+ }
+ for offset += location[1]; i < offset; i++ {
+ c.writeUint32(difSect)
+ }
+ for offset += location[2]; i < offset; i++ {
+ c.writeUint32(fatSect)
+ }
+ offset = writeSectorChain(location[3], offset)
+ offset = writeSectorChain(location[4], offset)
+ sector := c.sectors[0]
+ for ; j < len(c.sectors); j++ {
+ if sector = c.sectors[j]; len(sector.content) == 0 {
+ continue
+ }
+ if sectorSize = len(sector.content); sectorSize < 0x1000 {
+ continue
+ }
+ c.sectors[j].start = offset
+ offset = writeSectorChain((sectorSize+0x01FF)>>9, offset)
+ }
+ writeSectorChain((location[6]+7)>>3, offset)
+ for c.position&0x1FF != 0 {
+ c.writeUint32(endOfChain)
+ }
+ i, offset = 0, 0
+ for j = 0; j < len(c.sectors); j++ {
+ if sector = c.sectors[j]; len(sector.content) == 0 {
+ continue
+ }
+ if sectorSize = len(sector.content); sectorSize == 0 || sectorSize >= 0x1000 {
+ continue
+ }
+ sector.start = offset
+ offset = writeSectorChain((sectorSize+0x3F)>>6, offset)
+ }
+ for c.position&0x1FF != 0 {
+ c.writeUint32(endOfChain)
+ }
+ return sector
+}
+
+// write provides a function to create compound file package stream.
+func (c *cfb) write() []byte {
+ c.prepare()
+ location := c.locate()
+ c.stream = make([]byte, location[7]<<9)
+ var i, j int
+ for i = 0; i < 8; i++ {
+ c.writeBytes([]byte{oleIdentifier[i]})
+ }
+ c.writeBytes(make([]byte, 16))
+ c.writeUint16(0x003E)
+ c.writeUint16(0x0003)
+ c.writeUint16(0xFFFE)
+ c.writeUint16(0x0009)
+ c.writeUint16(0x0006)
+ c.writeBytes(make([]byte, 10))
+ c.writeUint32(location[2])
+ c.writeUint32(location[0] + location[1] + location[2] + location[3] - 1)
+ c.writeUint32(0)
+ c.writeUint32(1 << 12)
+ if location[3] != 0 {
+ c.writeUint32(location[0] + location[1] + location[2] - 1)
+ } else {
+ c.writeUint32(endOfChain)
+ }
+ c.writeUint32(location[3])
+ if location[1] != 0 {
+ c.writeUint32(location[0] - 1)
+ } else {
+ c.writeUint32(endOfChain)
+ }
+ c.writeUint32(location[1])
+ c.writeMSAT(location)
+ sector := c.writeSectorChains(location)
+ c.writeDirectoryEntry(location)
+ for i = 1; i < len(c.sectors); i++ {
+ sector = c.sectors[i]
+ if sector.size >= 0x1000 {
+ c.position = (sector.start + 1) << 9
+ for j = 0; j < sector.size; j++ {
+ c.writeBytes([]byte{sector.content[j]})
+ }
+ for ; j&0x1FF != 0; j++ {
+ c.writeBytes([]byte{0})
+ }
+ }
+ }
+ for i = 1; i < len(c.sectors); i++ {
+ sector = c.sectors[i]
+ if sector.size > 0 && sector.size < 0x1000 {
+ for j = 0; j < sector.size; j++ {
+ c.writeBytes([]byte{sector.content[j]})
+ }
+ for ; j&0x3F != 0; j++ {
+ c.writeBytes([]byte{0})
+ }
+ }
+ }
+ for c.position < len(c.stream) {
+ c.writeBytes([]byte{0})
+ }
+ return c.stream
+}
diff --git a/crypt_test.go b/crypt_test.go
new file mode 100644
index 0000000000..1e57f608ed
--- /dev/null
+++ b/crypt_test.go
@@ -0,0 +1,104 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "bytes"
+ "encoding/binary"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/richardlehane/mscfb"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEncrypt(t *testing.T) {
+ // Test decrypt spreadsheet with incorrect password
+ _, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "passwd"})
+ assert.EqualError(t, err, ErrWorkbookPassword.Error())
+ // Test decrypt spreadsheet with password
+ f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"})
+ assert.NoError(t, err)
+ cell, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SECRET", cell)
+ assert.NoError(t, f.Close())
+ // Test decrypt spreadsheet with unsupported encrypt mechanism
+ raw, err := os.ReadFile(filepath.Join("test", "encryptAES.xlsx"))
+ assert.NoError(t, err)
+ raw[2050] = 3
+ _, err = Decrypt(raw, &Options{Password: "password"})
+ assert.Equal(t, ErrUnsupportedEncryptMechanism, err)
+
+ // Test encrypt spreadsheet with invalid password
+ assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error())
+ // Test encrypt spreadsheet with new password
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"}))
+ assert.NoError(t, f.Close())
+ f, err = OpenFile(filepath.Join("test", "Encryption.xlsx"), Options{Password: "passwd"})
+ assert.NoError(t, err)
+ cell, err = f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SECRET", cell)
+ // Test remove password by save workbook with options
+ assert.NoError(t, f.Save(Options{Password: ""}))
+ assert.NoError(t, f.Close())
+
+ doc, err := mscfb.New(bytes.NewReader(raw))
+ assert.NoError(t, err)
+ encryptionInfoBuf, encryptedPackageBuf := extractPart(doc)
+ binary.LittleEndian.PutUint64(encryptionInfoBuf[20:32], uint64(0))
+ _, err = standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, &Options{Password: "password"})
+ assert.NoError(t, err)
+ _, err = decrypt(nil, nil, nil)
+ assert.EqualError(t, err, "crypto/aes: invalid key size 0")
+ _, err = agileDecrypt(encryptionInfoBuf, MacintoshCyrillicCharset, &Options{Password: "password"})
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid character entity &0 (no semicolon)")
+ _, err = convertPasswdToKey("password", nil, Encryption{
+ KeyEncryptors: KeyEncryptors{KeyEncryptor: []KeyEncryptor{
+ {EncryptedKey: EncryptedKey{KeyData: KeyData{SaltValue: "=="}}},
+ }},
+ })
+ assert.EqualError(t, err, "illegal base64 data at input byte 0")
+ _, err = createIV([]byte{0}, Encryption{KeyData: KeyData{SaltValue: "=="}})
+ assert.EqualError(t, err, "illegal base64 data at input byte 0")
+}
+
+func TestEncryptionMechanism(t *testing.T) {
+ mechanism, err := encryptionMechanism([]byte{3, 0, 3, 0})
+ assert.Equal(t, mechanism, "extensible")
+ assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error())
+ _, err = encryptionMechanism([]byte{})
+ assert.EqualError(t, err, ErrUnknownEncryptMechanism.Error())
+}
+
+func TestHashing(t *testing.T) {
+ assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []byte(nil))
+}
+
+func TestGenISOPasswdHash(t *testing.T) {
+ for hashAlgorithm, expected := range map[string][]string{
+ "MD4": {"2lZQZUubVHLm/t6KsuHX4w==", "TTHjJdU70B/6Zq83XGhHVA=="},
+ "MD5": {"HWbqyd4dKKCjk1fEhk2kuQ==", "8ADyorkumWCayIukRhlVKQ=="},
+ "SHA-1": {"XErQIV3Ol+nhXkyCxrLTEQm+mSc=", "I3nDtyf59ASaNX1l6KpFnA=="},
+ "SHA-256": {"7oqMFyfED+mPrzRIBQ+KpKT4SClMHEPOZldliP15xAA=", "ru1R/w3P3Jna2Qo+EE8QiA=="},
+ "SHA-384": {"nMODLlxsC8vr0btcq0kp/jksg5FaI3az5Sjo1yZk+/x4bFzsuIvpDKUhJGAk/fzo", "Zjq9/jHlgOY6MzFDSlVNZg=="},
+ "SHA-512": {"YZ6jrGOFQgVKK3rDK/0SHGGgxEmFJglQIIRamZc2PkxVtUBp54fQn96+jVXEOqo6dtCSanqksXGcm/h3KaiR4Q==", "p5s/bybHBPtusI7EydTIrg=="},
+ } {
+ hashValue, saltValue, err := genISOPasswdHash("password", hashAlgorithm, expected[1], int(sheetProtectionSpinCount))
+ assert.NoError(t, err)
+ assert.Equal(t, expected[0], hashValue)
+ assert.Equal(t, expected[1], saltValue)
+ }
+}
diff --git a/datavalidation.go b/datavalidation.go
index f76f9b3cc4..ab61931625 100644
--- a/datavalidation.go
+++ b/datavalidation.go
@@ -1,47 +1,42 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"fmt"
+ "io"
+ "math"
"strings"
+ "unicode/utf16"
)
// DataValidationType defined the type of data validation.
-type DataValidationType int
+type DataValidationType byte
// Data validation types.
const (
- _DataValidationType = iota
- typeNone // inline use
+ _ DataValidationType = iota
+ DataValidationTypeNone
DataValidationTypeCustom
DataValidationTypeDate
DataValidationTypeDecimal
- typeList // inline use
- DataValidationTypeTextLeng
+ DataValidationTypeList
+ DataValidationTypeTextLength
DataValidationTypeTime
- // DataValidationTypeWhole Integer
DataValidationTypeWhole
)
-const (
- // dataValidationFormulaStrLen 255 characters+ 2 quotes
- dataValidationFormulaStrLen = 257
- // dataValidationFormulaStrLenErr
- dataValidationFormulaStrLenErr = "data validation must be 0-255 characters"
-)
-
// DataValidationErrorStyle defined the style of data validation error alert.
-type DataValidationErrorStyle int
+type DataValidationErrorStyle byte
// Data validation error styles.
const (
@@ -59,11 +54,11 @@ const (
)
// DataValidationOperator operator enum.
-type DataValidationOperator int
+type DataValidationOperator byte
// Data validation operators.
const (
- _DataValidationOperator = iota
+ _ DataValidationOperator = iota
DataValidationOperatorBetween
DataValidationOperatorEqual
DataValidationOperatorGreaterThan
@@ -74,6 +69,43 @@ const (
DataValidationOperatorNotEqual
)
+var (
+ // formulaEscaper mimics the Excel escaping rules for data validation,
+ // which converts `"` to `""` instead of `"`.
+ formulaEscaper = strings.NewReplacer(
+ `&`, `&`,
+ `<`, `<`,
+ `>`, `>`,
+ )
+ formulaUnescaper = strings.NewReplacer(
+ `&`, `&`,
+ `<`, `<`,
+ `>`, `>`,
+ )
+ // dataValidationTypeMap defined supported data validation types.
+ dataValidationTypeMap = map[DataValidationType]string{
+ DataValidationTypeNone: "none",
+ DataValidationTypeCustom: "custom",
+ DataValidationTypeDate: "date",
+ DataValidationTypeDecimal: "decimal",
+ DataValidationTypeList: "list",
+ DataValidationTypeTextLength: "textLength",
+ DataValidationTypeTime: "time",
+ DataValidationTypeWhole: "whole",
+ }
+ // dataValidationOperatorMap defined supported data validation operators.
+ dataValidationOperatorMap = map[DataValidationOperator]string{
+ DataValidationOperatorBetween: "between",
+ DataValidationOperatorEqual: "equal",
+ DataValidationOperatorGreaterThan: "greaterThan",
+ DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
+ DataValidationOperatorLessThan: "lessThan",
+ DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
+ DataValidationOperatorNotBetween: "notBetween",
+ DataValidationOperatorNotEqual: "notEqual",
+ }
+)
+
// NewDataValidation return data validation struct.
func NewDataValidation(allowBlank bool) *DataValidation {
return &DataValidation{
@@ -84,9 +116,9 @@ func NewDataValidation(allowBlank bool) *DataValidation {
}
// SetError set error notice.
-func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) {
- dd.Error = &msg
- dd.ErrorTitle = &title
+func (dv *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) {
+ dv.Error = &msg
+ dv.ErrorTitle = &title
strStyle := styleStop
switch style {
case DataValidationErrorStyleStop:
@@ -97,164 +129,279 @@ func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg st
strStyle = styleInformation
}
- dd.ShowErrorMessage = true
- dd.ErrorStyle = &strStyle
+ dv.ShowErrorMessage = true
+ dv.ErrorStyle = &strStyle
}
// SetInput set prompt notice.
-func (dd *DataValidation) SetInput(title, msg string) {
- dd.ShowInputMessage = true
- dd.PromptTitle = &title
- dd.Prompt = &msg
+func (dv *DataValidation) SetInput(title, msg string) {
+ dv.ShowInputMessage = true
+ dv.PromptTitle = &title
+ dv.Prompt = &msg
}
-// SetDropList data validation list.
-func (dd *DataValidation) SetDropList(keys []string) error {
- formula := "\"" + strings.Join(keys, ",") + "\""
- if dataValidationFormulaStrLen < len(formula) {
- return fmt.Errorf(dataValidationFormulaStrLenErr)
+// SetDropList data validation list. If you type the items into the data
+// validation dialog box (a delimited list), the limit is 255 characters,
+// including the separators. If your data validation list source formula is
+// over the maximum length limit, please set the allowed values in the
+// worksheet cells, and use the SetSqrefDropList function to set the reference
+// for their cells.
+func (dv *DataValidation) SetDropList(keys []string) error {
+ formula := strings.Join(keys, ",")
+ if MaxFieldLength < len(utf16.Encode([]rune(formula))) {
+ return ErrDataValidationFormulaLength
}
- dd.Formula1 = fmt.Sprintf("%s", formula)
- dd.Type = convDataValidationType(typeList)
+ dv.Type = dataValidationTypeMap[DataValidationTypeList]
+ if strings.HasPrefix(formula, "=") {
+ dv.Formula1 = formulaEscaper.Replace(formula)
+ return nil
+ }
+ dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula)))
return nil
}
-// SetRange provides function to set data validation range in drop list.
-func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error {
- formula1 := fmt.Sprintf("%d", f1)
- formula2 := fmt.Sprintf("%d", f2)
- if dataValidationFormulaStrLen+21 < len(dd.Formula1) || dataValidationFormulaStrLen+21 < len(dd.Formula2) {
- return fmt.Errorf(dataValidationFormulaStrLenErr)
+// SetRange provides function to set data validation range in drop list, only
+// accepts int, float64, string or []string data type formula argument.
+func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error {
+ genFormula := func(val interface{}) (string, error) {
+ var formula string
+ switch v := val.(type) {
+ case int:
+ formula = fmt.Sprintf("%d", v)
+ case float64:
+ if math.Abs(v) > math.MaxFloat32 {
+ return formula, ErrDataValidationRange
+ }
+ formula = fmt.Sprintf("%.17g", v)
+ case string:
+ formula = v
+ default:
+ return formula, ErrParameterInvalid
+ }
+ return formula, nil
}
-
- dd.Formula1 = fmt.Sprintf("%s", formula1)
- dd.Formula2 = fmt.Sprintf("%s", formula2)
- dd.Type = convDataValidationType(t)
- dd.Operator = convDataValidationOperatior(o)
- return nil
+ formula1, err := genFormula(f1)
+ if err != nil {
+ return err
+ }
+ formula2, err := genFormula(f2)
+ if err != nil {
+ return err
+ }
+ dv.Formula1, dv.Formula2 = formula1, formula2
+ dv.Type = dataValidationTypeMap[t]
+ dv.Operator = dataValidationOperatorMap[o]
+ return err
}
// SetSqrefDropList provides set data validation on a range with source
// reference range of the worksheet by given data validation object and
// worksheet name. The data validation object can be created by
-// NewDataValidation function. For example, set data validation on
-// Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create
-// in-cell dropdown by allowing list source:
-//
-// dvRange := excelize.NewDataValidation(true)
-// dvRange.Sqref = "A7:B8"
-// dvRange.SetSqrefDropList("$E$1:$E$3", true)
-// f.AddDataValidation("Sheet1", dvRange)
+// NewDataValidation function. There are limits to the number of items that
+// will show in a data validation drop down list: The list can show up to show
+// 32768 items from a list on the worksheet. If you need more items than that,
+// you could create a dependent drop down list, broken down by category. For
+// example, set data validation on Sheet1!A7:B8 with validation criteria source
+// Sheet1!E1:E3 settings, create in-cell dropdown by allowing list source:
//
-func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error {
- if isCurrentSheet {
- dd.Formula1 = fmt.Sprintf("%s", sqref)
- dd.Type = convDataValidationType(typeList)
- return nil
- }
- return fmt.Errorf("cross-sheet sqref cell are not supported")
+// dv := excelize.NewDataValidation(true)
+// dv.Sqref = "A7:B8"
+// dv.SetSqrefDropList("$E$1:$E$3")
+// err := f.AddDataValidation("Sheet1", dv)
+func (dv *DataValidation) SetSqrefDropList(sqref string) {
+ dv.Formula1 = sqref
+ dv.Type = dataValidationTypeMap[DataValidationTypeList]
}
// SetSqref provides function to set data validation range in drop list.
-func (dd *DataValidation) SetSqref(sqref string) {
- if dd.Sqref == "" {
- dd.Sqref = sqref
- } else {
- dd.Sqref = fmt.Sprintf("%s %s", dd.Sqref, sqref)
- }
-}
-
-// convDataValidationType get excel data validation type.
-func convDataValidationType(t DataValidationType) string {
- typeMap := map[DataValidationType]string{
- typeNone: "none",
- DataValidationTypeCustom: "custom",
- DataValidationTypeDate: "date",
- DataValidationTypeDecimal: "decimal",
- typeList: "list",
- DataValidationTypeTextLeng: "textLength",
- DataValidationTypeTime: "time",
- DataValidationTypeWhole: "whole",
+func (dv *DataValidation) SetSqref(sqref string) {
+ if dv.Sqref == "" {
+ dv.Sqref = sqref
+ return
}
-
- return typeMap[t]
-
-}
-
-// convDataValidationOperatior get excel data validation operator.
-func convDataValidationOperatior(o DataValidationOperator) string {
- typeMap := map[DataValidationOperator]string{
- DataValidationOperatorBetween: "between",
- DataValidationOperatorEqual: "equal",
- DataValidationOperatorGreaterThan: "greaterThan",
- DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
- DataValidationOperatorLessThan: "lessThan",
- DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
- DataValidationOperatorNotBetween: "notBetween",
- DataValidationOperatorNotEqual: "notEqual",
- }
-
- return typeMap[o]
-
+ dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref)
}
// AddDataValidation provides set data validation on a range of the worksheet
-// by given data validation object and worksheet name. The data validation
-// object can be created by NewDataValidation function.
+// by given data validation object and worksheet name. This function is
+// concurrency safe. The data validation object can be created by
+// NewDataValidation function.
//
// Example 1, set data validation on Sheet1!A1:B2 with validation criteria
// settings, show error alert after invalid data is entered with "Stop" style
// and custom title "error body":
//
-// dvRange := excelize.NewDataValidation(true)
-// dvRange.Sqref = "A1:B2"
-// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween)
-// dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body")
-// err := f.AddDataValidation("Sheet1", dvRange)
+// dv := excelize.NewDataValidation(true)
+// dv.Sqref = "A1:B2"
+// dv.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween)
+// dv.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body")
+// err := f.AddDataValidation("Sheet1", dv)
//
// Example 2, set data validation on Sheet1!A3:B4 with validation criteria
// settings, and show input message when cell is selected:
//
-// dvRange = excelize.NewDataValidation(true)
-// dvRange.Sqref = "A3:B4"
-// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan)
-// dvRange.SetInput("input title", "input body")
-// err = f.AddDataValidation("Sheet1", dvRange)
+// dv = excelize.NewDataValidation(true)
+// dv.Sqref = "A3:B4"
+// dv.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan)
+// dv.SetInput("input title", "input body")
+// err = f.AddDataValidation("Sheet1", dv)
//
// Example 3, set data validation on Sheet1!A5:B6 with validation criteria
// settings, create in-cell dropdown by allowing list source:
//
-// dvRange = excelize.NewDataValidation(true)
-// dvRange.Sqref = "A5:B6"
-// dvRange.SetDropList([]string{"1", "2", "3"})
-// err = f.AddDataValidation("Sheet1", dvRange)
-//
+// dv = excelize.NewDataValidation(true)
+// dv.Sqref = "A5:B6"
+// dv.SetDropList([]string{"1", "2", "3"})
+// err = f.AddDataValidation("Sheet1", dv)
func (f *File) AddDataValidation(sheet string, dv *DataValidation) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
if nil == ws.DataValidations {
ws.DataValidations = new(xlsxDataValidations)
}
- ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv)
+ dataValidation := &xlsxDataValidation{
+ AllowBlank: dv.AllowBlank,
+ Error: dv.Error,
+ ErrorStyle: dv.ErrorStyle,
+ ErrorTitle: dv.ErrorTitle,
+ Operator: dv.Operator,
+ Prompt: dv.Prompt,
+ PromptTitle: dv.PromptTitle,
+ ShowDropDown: dv.ShowDropDown,
+ ShowErrorMessage: dv.ShowErrorMessage,
+ ShowInputMessage: dv.ShowInputMessage,
+ Sqref: dv.Sqref,
+ Type: dv.Type,
+ }
+ if dv.Formula1 != "" {
+ dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1}
+ }
+ if dv.Formula2 != "" {
+ dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2}
+ }
+ ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
return err
}
+// GetDataValidations returns data validations list by given worksheet name.
+func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return nil, err
+ }
+ var (
+ dataValidations []*DataValidation
+ decodeExtLst = new(decodeExtLst)
+ decodeDataValidations *xlsxDataValidations
+ ext *xlsxExt
+ )
+ if ws.DataValidations != nil {
+ dataValidations = append(dataValidations, getDataValidations(ws.DataValidations)...)
+ }
+ if ws.ExtLst != nil {
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return dataValidations, err
+ }
+ for _, ext = range decodeExtLst.Ext {
+ if ext.URI == ExtURIDataValidations {
+ decodeDataValidations = new(xlsxDataValidations)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations)
+ dataValidations = append(dataValidations, getDataValidations(decodeDataValidations)...)
+ }
+ }
+ }
+ return dataValidations, err
+}
+
+// getDataValidations returns data validations list by given worksheet data
+// validations.
+func getDataValidations(dvs *xlsxDataValidations) []*DataValidation {
+ if dvs == nil {
+ return nil
+ }
+ var dataValidations []*DataValidation
+ for _, dv := range dvs.DataValidation {
+ if dv == nil {
+ continue
+ }
+ dataValidation := &DataValidation{
+ AllowBlank: dv.AllowBlank,
+ Error: dv.Error,
+ ErrorStyle: dv.ErrorStyle,
+ ErrorTitle: dv.ErrorTitle,
+ Operator: dv.Operator,
+ Prompt: dv.Prompt,
+ PromptTitle: dv.PromptTitle,
+ ShowDropDown: dv.ShowDropDown,
+ ShowErrorMessage: dv.ShowErrorMessage,
+ ShowInputMessage: dv.ShowInputMessage,
+ Sqref: dv.Sqref,
+ Type: dv.Type,
+ }
+ if dv.Formula1 != nil {
+ dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content)
+ }
+ if dv.Formula2 != nil {
+ dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content)
+ }
+ if dv.XMSqref != "" {
+ dataValidation.Sqref = dv.XMSqref
+ dataValidation.Formula1 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula1, ""), "")
+ dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, ""), "")
+ }
+ dataValidations = append(dataValidations, dataValidation)
+ }
+ return dataValidations
+}
+
// DeleteDataValidation delete data validation by given worksheet name and
-// reference sequence.
-func (f *File) DeleteDataValidation(sheet, sqref string) error {
+// reference sequence. This function is concurrency safe.
+// All data validations in the worksheet will be deleted
+// if not specify reference sequence parameter.
+func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
if ws.DataValidations == nil {
return nil
}
+ if sqref == nil {
+ ws.DataValidations = nil
+ return nil
+ }
+ delCells, err := flatSqref(sqref[0])
+ if err != nil {
+ return err
+ }
dv := ws.DataValidations
for i := 0; i < len(dv.DataValidation); i++ {
- if dv.DataValidation[i].Sqref == sqref {
+ var applySqref []string
+ colCells, err := flatSqref(dv.DataValidation[i].Sqref)
+ if err != nil {
+ return err
+ }
+ for col, cells := range delCells {
+ for _, cell := range cells {
+ idx := inCoordinates(colCells[col], cell)
+ if idx != -1 {
+ colCells[col] = append(colCells[col][:idx], colCells[col][idx+1:]...)
+ }
+ }
+ }
+ for _, col := range colCells {
+ applySqref = append(applySqref, squashSqref(col)...)
+ }
+ dv.DataValidation[i].Sqref = strings.Join(applySqref, " ")
+ if len(applySqref) == 0 {
dv.DataValidation = append(dv.DataValidation[:i], dv.DataValidation[i+1:]...)
i--
}
@@ -265,3 +412,45 @@ func (f *File) DeleteDataValidation(sheet, sqref string) error {
}
return nil
}
+
+// squashSqref generates cell reference sequence by given cells coordinates list.
+func squashSqref(cells [][]int) []string {
+ if len(cells) == 1 {
+ cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1])
+ return []string{cell}
+ } else if len(cells) == 0 {
+ return []string{}
+ }
+ var refs []string
+ l, r := 0, 0
+ for i := 1; i < len(cells); i++ {
+ if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 {
+ ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
+ if l == r {
+ ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
+ }
+ refs = append(refs, ref)
+ l, r = i, i
+ } else {
+ r++
+ }
+ }
+ ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
+ if l == r {
+ ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
+ }
+ return append(refs, ref)
+}
+
+// isFormulaDataValidation returns whether the data validation rule is a formula.
+func (dv *xlsxInnerXML) isFormula() bool {
+ return dv != nil && !(strings.HasPrefix(dv.Content, """) && strings.HasSuffix(dv.Content, """))
+}
+
+// unescapeDataValidationFormula returns unescaped data validation formula.
+func unescapeDataValidationFormula(val string) string {
+ if strings.HasPrefix(val, "\"") { // Text detection
+ return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val))
+ }
+ return formulaUnescaper.Replace(val)
+}
diff --git a/datavalidation_test.go b/datavalidation_test.go
index d70b874885..a5d2becaf3 100644
--- a/datavalidation_test.go
+++ b/datavalidation_test.go
@@ -1,15 +1,19 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX files. Support reads and writes XLSX file generated by
-// Microsoft Excel™ 2007 and later. Support save file without losing original
-// charts of XLSX. This library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
+ "fmt"
+ "math"
"path/filepath"
"strings"
"testing"
@@ -22,27 +26,110 @@ func TestDataValidation(t *testing.T) {
f := NewFile()
- dvRange := NewDataValidation(true)
- dvRange.Sqref = "A1:B2"
- assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
- dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body")
- dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body")
- dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body")
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
+ dv := NewDataValidation(true)
+ dv.Sqref = "A1:B2"
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
+ dv.SetError(DataValidationErrorStyleWarning, "error title", "error body")
+ dv.SetError(DataValidationErrorStyleInformation, "error title", "error body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+
+ dataValidations, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 1)
+
assert.NoError(t, f.SaveAs(resultFile))
- dvRange = NewDataValidation(true)
- dvRange.Sqref = "A3:B4"
- assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
- dvRange.SetInput("input title", "input body")
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
+ dv = NewDataValidation(true)
+ dv.Sqref = "A3:B4"
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
+ dv.SetInput("input title", "input body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+
+ dataValidations, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 2)
+
assert.NoError(t, f.SaveAs(resultFile))
- dvRange = NewDataValidation(true)
- dvRange.Sqref = "A5:B6"
- assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"}))
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetSheetRow("Sheet2", "A2", &[]interface{}{"B2", 1}))
+ assert.NoError(t, f.SetSheetRow("Sheet2", "A3", &[]interface{}{"B3", 3}))
+ dv = NewDataValidation(true)
+ dv.Sqref = "A1:B1"
+ assert.NoError(t, dv.SetRange("INDIRECT($A$2)", "INDIRECT($A$3)", DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
+ assert.NoError(t, f.AddDataValidation("Sheet2", dv))
+ dataValidations, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 2)
+ dataValidations, err = f.GetDataValidations("Sheet2")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 1)
+
+ dv = NewDataValidation(true)
+ dv.Sqref = "A5:B6"
+ for _, listValid := range [][]string{
+ {"1", "2", "3"},
+ {"=A1"},
+ {strings.Repeat("&", MaxFieldLength)},
+ {strings.Repeat("\u4E00", MaxFieldLength)},
+ {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
+ {`A<`, `B>`, `C"`, "D\t", `E'`, `F`},
+ } {
+ dv.Formula1 = ""
+ assert.NoError(t, dv.SetDropList(listValid),
+ "SetDropList failed for valid input %v", listValid)
+ assert.NotEqual(t, "", dv.Formula1,
+ "Formula1 should not be empty for valid input %v", listValid)
+ }
+ assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1)
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+
+ dataValidations, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dataValidations, 3)
+
+ // Test get data validation on no exists worksheet
+ _, err = f.GetDataValidations("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get data validation with invalid sheet name
+ _, err = f.GetDataValidations("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+
assert.NoError(t, f.SaveAs(resultFile))
+
+ // Test get data validation on a worksheet without data validation settings
+ f = NewFile()
+ dataValidations, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []*DataValidation(nil), dataValidations)
+
+ // Test get data validations which storage in the extension lists
+ f = NewFile()
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`Sheet1!$B$1:$B$5A7:B8`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
+ dataValidations, err = f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []*DataValidation{
+ {
+ AllowBlank: true,
+ Type: "list",
+ Formula1: "Sheet1!$B$1:$B$5",
+ Sqref: "A7:B8",
+ },
+ }, dataValidations)
+
+ // Test get data validations with invalid extension list characters
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
+ _, err = f.GetDataValidations("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: element closed by ")
+
+ // Test get validations without validations
+ assert.Nil(t, getDataValidations(nil))
+ assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}}))
}
func TestDataValidationError(t *testing.T) {
@@ -53,52 +140,106 @@ func TestDataValidationError(t *testing.T) {
assert.NoError(t, f.SetCellStr("Sheet1", "E2", "E2"))
assert.NoError(t, f.SetCellStr("Sheet1", "E3", "E3"))
- dvRange := NewDataValidation(true)
- dvRange.SetSqref("A7:B8")
- dvRange.SetSqref("A7:B8")
- assert.NoError(t, dvRange.SetSqrefDropList("$E$1:$E$3", true))
-
- err := dvRange.SetSqrefDropList("$E$1:$E$3", false)
- assert.EqualError(t, err, "cross-sheet sqref cell are not supported")
+ dv := NewDataValidation(true)
+ dv.SetSqref("A7:B8")
+ dv.SetSqref("A7:B8")
+ dv.SetSqrefDropList("$E$1:$E$3")
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
- assert.NoError(t, f.SaveAs(resultFile))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
- dvRange = NewDataValidation(true)
- err = dvRange.SetDropList(make([]string, 258))
- if dvRange.Formula1 != "" {
+ dv = NewDataValidation(true)
+ err := dv.SetDropList(make([]string, 258))
+ if dv.Formula1 != "" {
t.Errorf("data validation error. Formula1 must be empty!")
return
}
- assert.EqualError(t, err, "data validation must be 0-255 characters")
- assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
- dvRange.SetSqref("A9:B10")
-
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
+ assert.EqualError(t, err, ErrDataValidationFormulaLength.Error())
+ assert.EqualError(t, dv.SetRange(nil, 20, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error())
+ assert.EqualError(t, dv.SetRange(10, nil, DataValidationTypeWhole, DataValidationOperatorBetween), ErrParameterInvalid.Error())
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
+ dv.SetSqref("A9:B10")
+
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+
+ // Test width invalid data validation formula
+ prevFormula1 := dv.Formula1
+ for _, keys := range [][]string{
+ make([]string, 257),
+ {strings.Repeat("s", 256)},
+ {strings.Repeat("\u4E00", 256)},
+ {strings.Repeat("\U0001F600", 128)},
+ {strings.Repeat("\U0001F600", 127), "s"},
+ } {
+ err = dv.SetDropList(keys)
+ assert.Equal(t, prevFormula1, dv.Formula1,
+ "Formula1 should be unchanged for invalid input %v", keys)
+ assert.EqualError(t, err, ErrDataValidationFormulaLength.Error())
+ }
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, dv.SetRange(
+ -math.MaxFloat32, math.MaxFloat32,
+ DataValidationTypeWhole, DataValidationOperatorGreaterThan))
+ assert.EqualError(t, dv.SetRange(
+ -math.MaxFloat64, math.MaxFloat32,
+ DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error())
+ assert.EqualError(t, dv.SetRange(
+ math.SmallestNonzeroFloat64, math.MaxFloat64,
+ DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error())
assert.NoError(t, f.SaveAs(resultFile))
- // Test width invalid data validation formula.
- dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22)
- assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters")
+ // Test add data validation on no exists worksheet
+ f = NewFile()
+ assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN does not exist")
- // Test add data validation on no exists worksheet.
+ // Test add data validation with invalid sheet name
f = NewFile()
- assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist")
+ assert.EqualError(t, f.AddDataValidation("Sheet:1", nil), ErrSheetNameInvalid.Error())
}
func TestDeleteDataValidation(t *testing.T) {
f := NewFile()
assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2"))
- dvRange := NewDataValidation(true)
- dvRange.Sqref = "A1:B2"
- assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
- dvRange.SetInput("input title", "input body")
- assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
-
+ dv := NewDataValidation(true)
+ dv.Sqref = "A1:B2"
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetInput("input title", "input body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2"))
+
+ dv.Sqref = "A1"
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "B1"))
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1"))
+
+ dv.Sqref = "C2:C5"
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "C4"))
+
+ dv = NewDataValidation(true)
+ dv.Sqref = "D2:D2 D3 D4"
+ assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
+ dv.SetInput("input title", "input body")
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.NoError(t, f.DeleteDataValidation("Sheet1", "D3"))
+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteDataValidation.xlsx")))
- // Test delete data validation on no exists worksheet.
- assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist")
+ dv.Sqref = "A"
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+
+ assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1:A"
+ assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:B2"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+
+ // Test delete data validation on no exists worksheet
+ assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN does not exist")
+ // Test delete all data validation with invalid sheet name
+ assert.EqualError(t, f.DeleteDataValidation("Sheet:1"), ErrSheetNameInvalid.Error())
+ // Test delete all data validations in the worksheet
+ assert.NoError(t, f.DeleteDataValidation("Sheet1"))
+ assert.Nil(t, ws.(*xlsxWorksheet).DataValidations)
}
diff --git a/date.go b/date.go
index 34c8989cb5..7d8757f1ee 100644
--- a/date.go
+++ b/date.go
@@ -1,55 +1,50 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
- "errors"
"math"
"time"
)
const (
+ nanosInADay = float64((24 * time.Hour) / time.Nanosecond)
dayNanoseconds = 24 * time.Hour
maxDuration = 290 * 364 * dayNanoseconds
+ roundEpsilon = 1e-9
)
var (
+ daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+ excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)
+ excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC)
excelBuggyPeriodStart = time.Date(1900, time.March, 1, 0, 0, 0, 0, time.UTC).Add(-time.Nanosecond)
)
// timeToExcelTime provides a function to convert time to Excel time.
-func timeToExcelTime(t time.Time) (float64, error) {
- // TODO in future this should probably also handle date1904 and like TimeFromExcelTime
-
- // Force user to explicit convet passed value to UTC time.
- // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT
- // probably due to daylight saving.
- if t.Location() != time.UTC {
- return 0.0, errors.New("only UTC time expected")
+func timeToExcelTime(t time.Time, date1904 bool) (float64, error) {
+ date := excelMinTime1900
+ if date1904 {
+ date = excel1904Epoc
}
-
- if t.Before(excelMinTime1900) {
- return 0.0, nil
+ if t.Before(date) {
+ return 0, nil
}
-
- tt := t
- diff := t.Sub(excelMinTime1900)
- result := float64(0)
-
+ tt, diff, result := t, t.Sub(date), 0.0
for diff >= maxDuration {
result += float64(maxDuration / dayNanoseconds)
tt = tt.Add(-maxDuration)
- diff = tt.Sub(excelMinTime1900)
+ diff = tt.Sub(date)
}
rem := diff % dayNanoseconds
@@ -60,8 +55,8 @@ func timeToExcelTime(t time.Time) (float64, error) {
// Microsoft intentionally included this bug in Excel so that it would remain compatible with the spreadsheet
// program that had the majority market share at the time; Lotus 1-2-3.
// https://www.myonlinetraininghub.com/excel-date-and-time
- if t.After(excelBuggyPeriodStart) {
- result += 1.0
+ if !date1904 && t.After(excelBuggyPeriodStart) {
+ result++
}
return result, nil
}
@@ -85,7 +80,6 @@ func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) {
// minutes, seconds and nanoseconds that comprised a given fraction of a day.
// values would round to 1 us.
func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) {
-
const (
c1us = 1e3
c1s = 1e9
@@ -120,7 +114,7 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time {
// "Communications of the ACM" in 1968 (published in CACM, volume 11, number
// 10, October 1968, p.657). None of those programmers seems to have found it
// necessary to explain the constants or variable names set out by Henry F.
-// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and
+// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that journal and
// expand an explanation here - that day is not today.
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
l := jd + 68569
@@ -139,12 +133,11 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
// timeFromExcelTime provides a function to convert an excelTime
// representation (stored as a floating point number) to a time.Time.
func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
- const MDD int64 = 106750 // Max time.Duration Days, aprox. 290 years
var date time.Time
- var intPart = int64(excelTime)
+ wholeDaysPart := int(excelTime)
// Excel uses Julian dates prior to March 1st 1900, and Gregorian
// thereafter.
- if intPart <= 61 {
+ if wholeDaysPart <= 61 {
const OFFSET1900 = 15018.0
const OFFSET1904 = 16480.0
const MJD0 float64 = 2400000.5
@@ -156,29 +149,96 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
}
return date
}
- var floatPart = excelTime - float64(intPart)
- var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000
+ floatPart := excelTime - float64(wholeDaysPart) + roundEpsilon
if date1904 {
- date = time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)
+ date = excel1904Epoc
} else {
- date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
+ date = excel1900Epoc
}
-
- // Duration is limited to aprox. 290 years
- for intPart > MDD {
- durationDays := time.Duration(MDD) * time.Hour * 24
- date = date.Add(durationDays)
- intPart = intPart - MDD
+ durationPart := time.Duration(nanosInADay * floatPart)
+ date = date.AddDate(0, 0, wholeDaysPart).Add(durationPart)
+ if date.Nanosecond()/1e6 > 500 {
+ return date.Round(time.Second)
}
- durationDays := time.Duration(intPart) * time.Hour * 24
- durationPart := time.Duration(dayNanoSeconds * floatPart)
- return date.Add(durationDays).Add(durationPart)
+ return date.Truncate(time.Second)
}
-// ExcelDateToTime converts a float-based excel date representation to a time.Time.
+// ExcelDateToTime converts a float-based Excel date representation to a time.Time.
func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) {
if excelDate < 0 {
return time.Time{}, newInvalidExcelDateError(excelDate)
}
return timeFromExcelTime(excelDate, use1904Format), nil
}
+
+// isLeapYear determine if leap year for a given year.
+func isLeapYear(y int) bool {
+ if y == y/400*400 {
+ return true
+ }
+ if y == y/100*100 {
+ return false
+ }
+ return y == y/4*4
+}
+
+// getDaysInMonth provides a function to get the days by a given year and
+// month number.
+func getDaysInMonth(y, m int) int {
+ if m == 2 && isLeapYear(y) {
+ return 29
+ }
+ return daysInMonth[m-1]
+}
+
+// validateDate provides a function to validate if a valid date by a given
+// year, month, and day number.
+func validateDate(y, m, d int) bool {
+ if m < 1 || m > 12 {
+ return false
+ }
+ if d < 1 {
+ return false
+ }
+ return d <= getDaysInMonth(y, m)
+}
+
+// formatYear converts the given year number into a 4-digit format.
+func formatYear(y int) int {
+ if y < 1900 {
+ if y < 30 {
+ y += 2000
+ } else {
+ y += 1900
+ }
+ }
+ return y
+}
+
+// getDurationNumFmt returns most simplify numbers format code for time
+// duration type cell value by given worksheet name, cell reference and number.
+func getDurationNumFmt(d time.Duration) int {
+ if d >= time.Hour*24 {
+ return 46
+ }
+ // Whole minutes
+ if d.Minutes() == float64(int(d.Minutes())) {
+ return 20
+ }
+ return 21
+}
+
+// getTimeNumFmt returns most simplify numbers format code for time type cell
+// value by given worksheet name, cell reference and number.
+func getTimeNumFmt(t time.Time) int {
+ nextMonth := t.AddDate(0, 1, 0)
+ // Whole months
+ if t.Day() == 1 && nextMonth.Day() == 1 {
+ return 17
+ }
+ // Whole days
+ if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 {
+ return 14
+ }
+ return 22
+}
diff --git a/date_test.go b/date_test.go
index ee01356efc..11011cf3b5 100644
--- a/date_test.go
+++ b/date_test.go
@@ -17,7 +17,7 @@ var trueExpectedDateList = []dateTest{
{0.0000000000000000, time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)},
{25569.000000000000, time.Unix(0, 0).UTC()},
- // Expected values extracted from real xlsx file
+ // Expected values extracted from real spreadsheet
{1.0000000000000000, time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)},
{1.0000115740740740, time.Date(1900, time.January, 1, 0, 0, 1, 0, time.UTC)},
{1.0006944444444446, time.Date(1900, time.January, 1, 0, 1, 0, 0, time.UTC)},
@@ -33,13 +33,14 @@ var excelTimeInputList = []dateTest{
{60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)},
{61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)},
{41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)},
+ {44450.3333333333, time.Date(2021, time.September, 11, 8, 0, 0, 0, time.UTC)},
{401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)},
}
func TestTimeToExcelTime(t *testing.T) {
for i, test := range trueExpectedDateList {
t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) {
- excelTime, err := timeToExcelTime(test.GoValue)
+ excelTime, err := timeToExcelTime(test.GoValue, false)
assert.NoError(t, err)
assert.Equalf(t, test.ExcelValue, excelTime,
"Time: %s", test.GoValue.String())
@@ -54,8 +55,8 @@ func TestTimeToExcelTime_Timezone(t *testing.T) {
}
for i, test := range trueExpectedDateList {
t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) {
- _, err := timeToExcelTime(test.GoValue.In(location))
- assert.EqualError(t, err, "only UTC time expected")
+ _, err := timeToExcelTime(test.GoValue.In(location), false)
+ assert.NoError(t, err)
})
}
}
@@ -66,12 +67,38 @@ func TestTimeFromExcelTime(t *testing.T) {
assert.Equal(t, test.GoValue, timeFromExcelTime(test.ExcelValue, false))
})
}
+ for hour := 0; hour < 24; hour++ {
+ for minVal := 0; minVal < 60; minVal++ {
+ for sec := 0; sec < 60; sec++ {
+ date := time.Date(2021, time.December, 30, hour, minVal, sec, 0, time.UTC)
+ // Test use 1900 date system
+ excel1900Time, err := timeToExcelTime(date, false)
+ assert.NoError(t, err)
+ date1900Out := timeFromExcelTime(excel1900Time, false)
+ assert.EqualValues(t, hour, date1900Out.Hour())
+ assert.EqualValues(t, minVal, date1900Out.Minute())
+ assert.EqualValues(t, sec, date1900Out.Second())
+ // Test use 1904 date system
+ excel1904Time, err := timeToExcelTime(date, true)
+ assert.NoError(t, err)
+ date1904Out := timeFromExcelTime(excel1904Time, true)
+ assert.EqualValues(t, hour, date1904Out.Hour())
+ assert.EqualValues(t, minVal, date1904Out.Minute())
+ assert.EqualValues(t, sec, date1904Out.Second())
+ }
+ }
+ }
}
func TestTimeFromExcelTime_1904(t *testing.T) {
- _, _ = shiftJulianToNoon(1, -0.6)
- timeFromExcelTime(61, true)
- timeFromExcelTime(62, true)
+ julianDays, julianFraction := shiftJulianToNoon(1, -0.6)
+ assert.Equal(t, julianDays, 0.0)
+ assert.Equal(t, julianFraction, 0.9)
+ julianDays, julianFraction = shiftJulianToNoon(1, 0.1)
+ assert.Equal(t, julianDays, 1.0)
+ assert.Equal(t, julianFraction, 0.6)
+ assert.Equal(t, timeFromExcelTime(61, true), time.Date(1904, time.March, 2, 0, 0, 0, 0, time.UTC))
+ assert.Equal(t, timeFromExcelTime(62, true), time.Date(1904, time.March, 3, 0, 0, 0, 0, time.UTC))
}
func TestExcelDateToTime(t *testing.T) {
@@ -85,5 +112,5 @@ func TestExcelDateToTime(t *testing.T) {
}
// Check error case
_, err := ExcelDateToTime(-1, false)
- assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported supported")
+ assert.EqualError(t, err, newInvalidExcelDateError(-1).Error())
}
diff --git a/docProps.go b/docProps.go
index 03604c9348..975f51b0a2 100644
--- a/docProps.go
+++ b/docProps.go
@@ -1,95 +1,175 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
"encoding/xml"
- "fmt"
"io"
"reflect"
)
-// SetDocProps provides a function to set document core properties. The
+// SetAppProps provides a function to set document application properties. The
// properties that can be set are:
//
-// Property | Description
-// ----------------+-----------------------------------------------------------------------------
-// Title | The name given to the resource.
-// |
-// Subject | The topic of the content of the resource.
-// |
-// Creator | An entity primarily responsible for making the content of the resource.
-// |
-// Keywords | A delimited set of keywords to support searching and indexing. This is
-// | typically a list of terms that are not available elsewhere in the properties.
-// |
-// Description | An explanation of the content of the resource.
-// |
-// LastModifiedBy | The user who performed the last modification. The identification is
-// | environment-specific.
-// |
-// Language | The language of the intellectual content of the resource.
-// |
-// Identifier | An unambiguous reference to the resource within a given context.
-// |
-// Revision | The topic of the content of the resource.
-// |
-// ContentStatus | The status of the content. For example: Values might include "Draft",
-// | "Reviewed" and "Final"
-// |
-// Category | A categorization of the content of this package.
-// |
-// Version | The version number. This value is set by the user or by the application.
+// Property | Description
+// -------------------+--------------------------------------------------------------------------
+// Application | The name of the application that created this document.
+// |
+// ScaleCrop | Indicates the display mode of the document thumbnail. Set this element
+// | to 'true' to enable scaling of the document thumbnail to the display. Set
+// | this element to 'false' to enable cropping of the document thumbnail to
+// | show only sections that will fit the display.
+// |
+// DocSecurity | Security level of a document as a numeric value. Document security is
+// | defined as:
+// | 1 - Document is password protected.
+// | 2 - Document is recommended to be opened as read-only.
+// | 3 - Document is enforced to be opened as read-only.
+// | 4 - Document is locked for annotation.
+// |
+// Company | The name of a company associated with the document.
+// |
+// LinksUpToDate | Indicates whether hyperlinks in a document are up-to-date. Set this
+// | element to 'true' to indicate that hyperlinks are updated. Set this
+// | element to 'false' to indicate that hyperlinks are outdated.
+// |
+// HyperlinksChanged | Specifies that one or more hyperlinks in this part were updated
+// | exclusively in this part by a producer. The next producer to open this
+// | document shall update the hyperlink relationships with the new
+// | hyperlinks specified in this part.
+// |
+// AppVersion | Specifies the version of the application which produced this document.
+// | The content of this element shall be of the form XX.YYYY where X and Y
+// | represent numerical values, or the document shall be considered
+// | non-conformant.
//
// For example:
//
-// err := f.SetDocProps(&excelize.DocProperties{
-// Category: "category",
-// ContentStatus: "Draft",
-// Created: "2019-06-04T22:00:10Z",
-// Creator: "Go Excelize",
-// Description: "This file created by Go Excelize",
-// Identifier: "xlsx",
-// Keywords: "Spreadsheet",
-// LastModifiedBy: "Go Author",
-// Modified: "2019-06-04T22:00:10Z",
-// Revision: "0",
-// Subject: "Test Subject",
-// Title: "Test Title",
-// Language: "en-US",
-// Version: "1.0.0",
-// })
-//
-func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
- var (
- core *decodeCoreProperties
- newProps *xlsxCoreProperties
- fields []string
- output []byte
- immutable, mutable reflect.Value
- field, val string
- )
+// err := f.SetAppProps(&excelize.AppProperties{
+// Application: "Microsoft Excel",
+// ScaleCrop: true,
+// DocSecurity: 3,
+// Company: "Company Name",
+// LinksUpToDate: true,
+// HyperlinksChanged: true,
+// AppVersion: "16.0000",
+// })
+func (f *File) SetAppProps(appProperties *AppProperties) error {
+ app := new(xlsxProperties)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))).
+ Decode(app); err != nil && err != io.EOF {
+ return err
+ }
+ setNoPtrFieldsVal([]string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"},
+ reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem())
+ app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value
+ output, err := xml.Marshal(app)
+ f.saveFileList(defaultXMLPathDocPropsApp, output)
+ return err
+}
- core = new(decodeCoreProperties)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
- Decode(core); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
+// GetAppProps provides a function to get document application properties.
+func (f *File) GetAppProps() (ret *AppProperties, err error) {
+ app := new(xlsxProperties)
+ if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))).
+ Decode(app); err != nil && err != io.EOF {
return
}
- newProps, err = &xlsxCoreProperties{
+ ret, err = &AppProperties{
+ Application: app.Application,
+ ScaleCrop: app.ScaleCrop,
+ DocSecurity: app.DocSecurity,
+ Company: app.Company,
+ LinksUpToDate: app.LinksUpToDate,
+ HyperlinksChanged: app.HyperlinksChanged,
+ AppVersion: app.AppVersion,
+ }, nil
+ return
+}
+
+// SetDocProps provides a function to set document core properties. The
+// properties that can be set are:
+//
+// Property | Description
+// ----------------+-----------------------------------------------------------
+// Title | The name given to the resource.
+// |
+// Subject | The topic of the content of the resource.
+// |
+// Creator | An entity primarily responsible for making the content of
+// | the resource.
+// |
+// Keywords | A delimited set of keywords to support searching and
+// | indexing. This is typically a list of terms that are not
+// | available elsewhere in the properties.
+// |
+// Description | An explanation of the content of the resource.
+// |
+// LastModifiedBy | The user who performed the last modification. The
+// | identification is environment-specific.
+// |
+// Language | The language of the intellectual content of the resource.
+// |
+// Identifier | An unambiguous reference to the resource within a given
+// | context.
+// |
+// Revision | The topic of the content of the resource.
+// |
+// ContentStatus | The status of the content. For example: Values might
+// | include "Draft", "Reviewed" and "Final"
+// |
+// Category | A categorization of the content of this package.
+// |
+// Version | The version number. This value is set by the user or by
+// | the application.
+// |
+// Created | The created time of the content of the resource which
+// | represent in ISO 8601 UTC format, for example
+// | "2019-06-04T22:00:10Z".
+// |
+// Modified | The modified time of the content of the resource which
+// | represent in ISO 8601 UTC format, for example
+// | "2019-06-04T22:00:10Z".
+// |
+//
+// For example:
+//
+// err := f.SetDocProps(&excelize.DocProperties{
+// Category: "category",
+// ContentStatus: "Draft",
+// Created: "2019-06-04T22:00:10Z",
+// Creator: "Go Excelize",
+// Description: "This file created by Go Excelize",
+// Identifier: "xlsx",
+// Keywords: "Spreadsheet",
+// LastModifiedBy: "Go Author",
+// Modified: "2019-06-04T22:00:10Z",
+// Revision: "0",
+// Subject: "Test Subject",
+// Title: "Test Title",
+// Language: "en-US",
+// Version: "1.0.0",
+// })
+func (f *File) SetDocProps(docProperties *DocProperties) error {
+ core := new(decodeCoreProperties)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))).
+ Decode(core); err != nil && err != io.EOF {
+ return err
+ }
+ newProps := &xlsxCoreProperties{
Dc: NameSpaceDublinCore,
Dcterms: NameSpaceDublinCoreTerms,
- Dcmitype: NameSpaceDublinCoreMetadataIntiative,
+ Dcmitype: NameSpaceDublinCoreMetadataInitiative,
XSI: NameSpaceXMLSchemaInstance,
Title: core.Title,
Subject: core.Subject,
@@ -103,56 +183,56 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) {
ContentStatus: core.ContentStatus,
Category: core.Category,
Version: core.Version,
- }, nil
- newProps.Created.Text, newProps.Created.Type, newProps.Modified.Text, newProps.Modified.Type =
- core.Created.Text, core.Created.Type, core.Modified.Text, core.Modified.Type
- fields = []string{
- "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords",
- "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version",
}
- immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem()
- for _, field = range fields {
- if val = immutable.FieldByName(field).String(); val != "" {
- mutable.FieldByName(field).SetString(val)
- }
+ if core.Created != nil {
+ newProps.Created = &xlsxDcTerms{Type: core.Created.Type, Text: core.Created.Text}
+ }
+ if core.Modified != nil {
+ newProps.Modified = &xlsxDcTerms{Type: core.Modified.Type, Text: core.Modified.Text}
}
+ setNoPtrFieldsVal([]string{
+ "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords",
+ "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version",
+ }, reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem())
if docProperties.Created != "" {
- newProps.Created.Text = docProperties.Created
+ newProps.Created = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Created}
}
if docProperties.Modified != "" {
- newProps.Modified.Text = docProperties.Modified
+ newProps.Modified = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Modified}
}
- output, err = xml.Marshal(newProps)
- f.saveFileList("docProps/core.xml", output)
+ output, err := xml.Marshal(newProps)
+ f.saveFileList(defaultXMLPathDocPropsCore, output)
- return
+ return err
}
// GetDocProps provides a function to get document core properties.
func (f *File) GetDocProps() (ret *DocProperties, err error) {
- var core = new(decodeCoreProperties)
+ core := new(decodeCoreProperties)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))).
+ if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))).
Decode(core); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
return
}
ret, err = &DocProperties{
Category: core.Category,
ContentStatus: core.ContentStatus,
- Created: core.Created.Text,
Creator: core.Creator,
Description: core.Description,
Identifier: core.Identifier,
Keywords: core.Keywords,
LastModifiedBy: core.LastModifiedBy,
- Modified: core.Modified.Text,
Revision: core.Revision,
Subject: core.Subject,
Title: core.Title,
Language: core.Language,
Version: core.Version,
}, nil
-
+ if core.Created != nil {
+ ret.Created = core.Created.Text
+ }
+ if core.Modified != nil {
+ ret.Modified = core.Modified.Text
+ }
return
}
diff --git a/docProps_test.go b/docProps_test.go
index ef930aefc4..dc26e2b285 100644
--- a/docProps_test.go
+++ b/docProps_test.go
@@ -1,11 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX files. Support reads and writes XLSX file generated by
-// Microsoft Excel™ 2007 and later. Support save file without losing original
-// charts of XLSX. This library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -18,6 +20,51 @@ import (
var MacintoshCyrillicCharset = []byte{0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0}
+func TestSetAppProps(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ assert.NoError(t, f.SetAppProps(&AppProperties{
+ Application: "Microsoft Excel",
+ ScaleCrop: true,
+ DocSecurity: 3,
+ Company: "Company Name",
+ LinksUpToDate: true,
+ HyperlinksChanged: true,
+ AppVersion: "16.0000",
+ }))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx")))
+ f.Pkg.Store(defaultXMLPathDocPropsApp, nil)
+ assert.NoError(t, f.SetAppProps(&AppProperties{}))
+ assert.NoError(t, f.Close())
+
+ // Test unsupported charset
+ f = NewFile()
+ f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetAppProps(&AppProperties{}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestGetAppProps(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ props, err := f.GetAppProps()
+ assert.NoError(t, err)
+ assert.Equal(t, props.Application, "Microsoft Macintosh Excel")
+ f.Pkg.Store(defaultXMLPathDocPropsApp, nil)
+ _, err = f.GetAppProps()
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+
+ // Test get application properties with unsupported charset
+ f = NewFile()
+ f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset)
+ _, err = f.GetAppProps()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
func TestSetDocProps(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
@@ -40,13 +87,14 @@ func TestSetDocProps(t *testing.T) {
Version: "1.0.0",
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
- f.XLSX["docProps/core.xml"] = nil
+ f.Pkg.Store(defaultXMLPathDocPropsCore, nil)
assert.NoError(t, f.SetDocProps(&DocProperties{}))
+ assert.NoError(t, f.Close())
- // Test unsupport charset
+ // Test unsupported charset
f = NewFile()
- f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
- assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
+ f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetDocProps(&DocProperties{}), "XML syntax error on line 1: invalid UTF-8")
}
func TestGetDocProps(t *testing.T) {
@@ -57,13 +105,14 @@ func TestGetDocProps(t *testing.T) {
props, err := f.GetDocProps()
assert.NoError(t, err)
assert.Equal(t, props.Creator, "Microsoft Office User")
- f.XLSX["docProps/core.xml"] = nil
+ f.Pkg.Store(defaultXMLPathDocPropsCore, nil)
_, err = f.GetDocProps()
assert.NoError(t, err)
+ assert.NoError(t, f.Close())
- // Test unsupport charset
+ // Test get workbook properties with unsupported charset
f = NewFile()
- f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
+ f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset)
_, err = f.GetDocProps()
- assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/drawing.go b/drawing.go
index ced747d708..c029fdf7d3 100644
--- a/drawing.go
+++ b/drawing.go
@@ -1,22 +1,20 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
"encoding/xml"
- "fmt"
"io"
- "log"
"reflect"
"strconv"
"strings"
@@ -24,16 +22,18 @@ import (
// prepareDrawing provides a function to prepare drawing ID and XML by given
// drawingID, worksheet name and default drawingXML.
-func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) {
+func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
- if xlsx.Drawing != nil {
- // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
- sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
+ if ws.Drawing != nil {
+ // The worksheet already has a picture or chart relationships, use the
+ // relationships drawing ../drawings/drawing%d.xml or /xl/drawings/drawing%d.xml.
+ sheetRelationshipsDrawingXML = strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "/xl/drawings/", "../drawings/")
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
- drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1)
+ drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl")
} else {
// Add first picture for given sheet.
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetDrawing(sheet, rID)
}
@@ -42,95 +42,34 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing
// prepareChartSheetDrawing provides a function to prepare drawing ID and XML
// by given drawingID, worksheet name and default drawingXML.
-func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet string) {
+func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
// Only allow one chart in a chartsheet.
- sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/chartsheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetNameSpace(sheet, SourceRelationship)
- xlsx.Drawing = &xlsxDrawing{
+ cs.Drawing = &xlsxDrawing{
RID: "rId" + strconv.Itoa(rID),
}
- return
}
// addChart provides a function to create chart as xl/charts/chart%d.xml by
// given format sets.
-func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
+func (f *File) addChart(opts *Chart, comboCharts []*Chart) {
count := f.countCharts()
xlsxChartSpace := xlsxChartSpace{
- XMLNSc: NameSpaceDrawingMLChart,
- XMLNSa: NameSpaceDrawingML,
- XMLNSr: SourceRelationship.Value,
- XMLNSc16r2: SourceRelationshipChart201506,
+ XMLNSa: NameSpaceDrawingML.Value,
Date1904: &attrValBool{Val: boolPtr(false)},
Lang: &attrValString{Val: stringPtr("en-US")},
RoundedCorners: &attrValBool{Val: boolPtr(false)},
Chart: cChart{
- Title: &cTitle{
- Tx: cTx{
- Rich: &cRich{
- P: aP{
- PPr: &aPPr{
- DefRPr: aRPr{
- Kern: 1200,
- Strike: "noStrike",
- U: "none",
- Sz: 1400,
- SolidFill: &aSolidFill{
- SchemeClr: &aSchemeClr{
- Val: "tx1",
- LumMod: &attrValInt{
- Val: intPtr(65000),
- },
- LumOff: &attrValInt{
- Val: intPtr(35000),
- },
- },
- },
- Ea: &aEa{
- Typeface: "+mn-ea",
- },
- Cs: &aCs{
- Typeface: "+mn-cs",
- },
- Latin: &aLatin{
- Typeface: "+mn-lt",
- },
- },
- },
- R: &aR{
- RPr: aRPr{
- Lang: "en-US",
- AltLang: "en-US",
- },
- T: formatSet.Title.Name,
- },
- },
- },
- },
- TxPr: cTxPr{
- P: aP{
- PPr: &aPPr{
- DefRPr: aRPr{
- Kern: 1200,
- U: "none",
- Sz: 14000,
- Strike: "noStrike",
- },
- },
- EndParaRPr: &aEndParaRPr{
- Lang: "en-US",
- },
- },
- },
- Overlay: &attrValBool{Val: boolPtr(false)},
- },
+ Title: f.drawPlotAreaTitles(opts.Title, ""),
View3D: &cView3D{
- RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])},
- RotY: &attrValInt{Val: intPtr(chartView3DRotY[formatSet.Type])},
- Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[formatSet.Type])},
- RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[formatSet.Type])},
+ RotX: &attrValInt{Val: intPtr(chartView3DRotX[opts.Type])},
+ RotY: &attrValInt{Val: intPtr(chartView3DRotY[opts.Type])},
+ Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[opts.Type])},
+ RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[opts.Type])},
},
Floor: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
@@ -143,34 +82,19 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
},
PlotArea: &cPlotArea{},
Legend: &cLegend{
- LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])},
+ LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])},
Overlay: &attrValBool{Val: boolPtr(false)},
},
PlotVisOnly: &attrValBool{Val: boolPtr(false)},
- DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)},
+ DispBlanksAs: &attrValString{Val: stringPtr(opts.ShowBlanksAs)},
ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)},
},
SpPr: &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "bg1"},
},
- Ln: &aLn{
- W: 9525,
- Cap: "flat",
- Cmpd: "sng",
- Algn: "ctr",
- SolidFill: &aSolidFill{
- SchemeClr: &aSchemeClr{Val: "tx1",
- LumMod: &attrValInt{
- Val: intPtr(15000),
- },
- LumOff: &attrValInt{
- Val: intPtr(85000),
- },
- },
- },
- },
+ Ln: f.drawChartLn(&opts.Border),
},
PrintSettings: &cPrintSettings{
PageMargins: &cPageMargins{
@@ -183,7 +107,8 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
},
},
}
- plotAreaFunc := map[string]func(*formatChart) *cPlotArea{
+ xlsxChartSpace.SpPr = f.drawShapeFill(opts.Fill, xlsxChartSpace.SpPr)
+ plotAreaFunc := map[ChartType]func(pa *cPlotArea, opts *Chart) *cPlotArea{
Area: f.drawBaseChart,
AreaStacked: f.drawBaseChart,
AreaPercentStacked: f.drawBaseChart,
@@ -226,19 +151,24 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
Col3DCylinderPercentStacked: f.drawBaseChart,
Doughnut: f.drawDoughnutChart,
Line: f.drawLineChart,
- Pie3D: f.drawPie3DChart,
+ Line3D: f.drawLine3DChart,
Pie: f.drawPieChart,
- PieOfPieChart: f.drawPieOfPieChart,
- BarOfPieChart: f.drawBarOfPieChart,
+ Pie3D: f.drawPie3DChart,
+ PieOfPie: f.drawPieOfPieChart,
+ BarOfPie: f.drawBarOfPieChart,
Radar: f.drawRadarChart,
Scatter: f.drawScatterChart,
Surface3D: f.drawSurface3DChart,
WireframeSurface3D: f.drawSurface3DChart,
Contour: f.drawSurfaceChart,
WireframeContour: f.drawSurfaceChart,
- Bubble: f.drawBaseChart,
- Bubble3D: f.drawBaseChart,
+ Bubble: f.drawBubbleChart,
+ Bubble3D: f.drawBubbleChart,
+ }
+ if opts.Legend.Position == "none" {
+ xlsxChartSpace.Chart.Legend = nil
}
+ xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr)
addChart := func(c, p *cPlotArea) {
immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem()
for i := 0; i < mutable.NumField(); i++ {
@@ -246,14 +176,19 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
if field.IsNil() {
continue
}
- immutable.FieldByName(mutable.Type().Field(i).Name).Set(field)
+ fld := immutable.FieldByName(mutable.Type().Field(i).Name)
+ if field.Kind() == reflect.Slice && i < 16 { // All []*cCharts type fields
+ fld.Set(reflect.Append(fld, field.Index(0)))
+ continue
+ }
+ fld.Set(field)
}
}
- addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet))
- order := len(formatSet.Series)
+ addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](xlsxChartSpace.Chart.PlotArea, opts))
+ order := len(opts.Series)
for idx := range comboCharts {
comboCharts[idx].order = order
- addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx]))
+ addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](xlsxChartSpace.Chart.PlotArea, comboCharts[idx]))
order += len(comboCharts[idx].Series)
}
chart, _ := xml.Marshal(xlsxChartSpace)
@@ -263,447 +198,536 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) {
// drawBaseChart provides a function to draw the c:plotArea element for bar,
// and column series charts by given format sets.
-func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea {
- c := cCharts{
- BarDir: &attrValString{
- Val: stringPtr("col"),
- },
- Grouping: &attrValString{
- Val: stringPtr("clustered"),
- },
- VaryColors: &attrValBool{
- Val: boolPtr(true),
- },
- Ser: f.drawChartSeries(formatSet),
- Shape: f.drawChartShape(formatSet),
- DLbls: f.drawChartDLbls(formatSet),
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
+func (f *File) drawBaseChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ c := []*cCharts{
+ {
+ BarDir: &attrValString{
+ Val: stringPtr("col"),
+ },
+ Grouping: &attrValString{
+ Val: stringPtr(plotAreaChartGrouping[opts.Type]),
+ },
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
+ Shape: f.drawChartShape(opts),
+ DLbls: f.drawChartDLbls(opts),
+ GapWidth: f.drawChartGapWidth(opts),
+ AxID: f.genAxID(opts),
+ Overlap: f.drawChartOverlap(opts),
},
- Overlap: &attrValInt{Val: intPtr(100)},
}
var ok bool
- if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok {
- c.BarDir = nil
- }
- if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok {
- c.Grouping = nil
- }
- if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok {
- c.Overlap = nil
- }
- catAx := f.drawPlotAreaCatAx(formatSet)
- valAx := f.drawPlotAreaValAx(formatSet)
- charts := map[string]*cPlotArea{
- "area": {
- AreaChart: &c,
+ if *c[0].BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok {
+ c[0].BarDir = nil
+ }
+ catAx := f.drawPlotAreaCatAx(pa, opts)
+ valAx := f.drawPlotAreaValAx(pa, opts)
+ charts := map[ChartType]*cPlotArea{
+ Area: {
+ AreaChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "areaStacked": {
- AreaChart: &c,
+ AreaStacked: {
+ AreaChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "areaPercentStacked": {
- AreaChart: &c,
+ AreaPercentStacked: {
+ AreaChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "area3D": {
- Area3DChart: &c,
+ Area3D: {
+ Area3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "area3DStacked": {
- Area3DChart: &c,
+ Area3DStacked: {
+ Area3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "area3DPercentStacked": {
- Area3DChart: &c,
+ Area3DPercentStacked: {
+ Area3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar": {
- BarChart: &c,
+ Bar: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "barStacked": {
- BarChart: &c,
+ BarStacked: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "barPercentStacked": {
- BarChart: &c,
+ BarPercentStacked: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DClustered": {
- Bar3DChart: &c,
+ Bar3DClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DStacked": {
- Bar3DChart: &c,
+ Bar3DStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DPercentStacked": {
- Bar3DChart: &c,
+ Bar3DPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DConeClustered": {
- Bar3DChart: &c,
+ Bar3DConeClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DConeStacked": {
- Bar3DChart: &c,
+ Bar3DConeStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DConePercentStacked": {
- Bar3DChart: &c,
+ Bar3DConePercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DPyramidClustered": {
- Bar3DChart: &c,
+ Bar3DPyramidClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DPyramidStacked": {
- Bar3DChart: &c,
+ Bar3DPyramidStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DPyramidPercentStacked": {
- Bar3DChart: &c,
+ Bar3DPyramidPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DCylinderClustered": {
- Bar3DChart: &c,
+ Bar3DCylinderClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DCylinderStacked": {
- Bar3DChart: &c,
+ Bar3DCylinderStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bar3DCylinderPercentStacked": {
- Bar3DChart: &c,
+ Bar3DCylinderPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col": {
- BarChart: &c,
+ Col: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "colStacked": {
- BarChart: &c,
+ ColStacked: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "colPercentStacked": {
- BarChart: &c,
+ ColPercentStacked: {
+ BarChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3D": {
- Bar3DChart: &c,
+ Col3D: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DClustered": {
- Bar3DChart: &c,
+ Col3DClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DStacked": {
- Bar3DChart: &c,
+ Col3DStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DPercentStacked": {
- Bar3DChart: &c,
+ Col3DPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DCone": {
- Bar3DChart: &c,
+ Col3DCone: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DConeClustered": {
- Bar3DChart: &c,
+ Col3DConeClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DConeStacked": {
- Bar3DChart: &c,
+ Col3DConeStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DConePercentStacked": {
- Bar3DChart: &c,
+ Col3DConePercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DPyramid": {
- Bar3DChart: &c,
+ Col3DPyramid: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DPyramidClustered": {
- Bar3DChart: &c,
+ Col3DPyramidClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DPyramidStacked": {
- Bar3DChart: &c,
+ Col3DPyramidStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DPyramidPercentStacked": {
- Bar3DChart: &c,
+ Col3DPyramidPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DCylinder": {
- Bar3DChart: &c,
+ Col3DCylinder: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DCylinderClustered": {
- Bar3DChart: &c,
+ Col3DCylinderClustered: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DCylinderStacked": {
- Bar3DChart: &c,
+ Col3DCylinderStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "col3DCylinderPercentStacked": {
- Bar3DChart: &c,
+ Col3DCylinderPercentStacked: {
+ Bar3DChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bubble": {
- BubbleChart: &c,
+ Bubble: {
+ BubbleChart: c,
CatAx: catAx,
ValAx: valAx,
},
- "bubble3D": {
- BubbleChart: &c,
+ Bubble3D: {
+ BubbleChart: c,
CatAx: catAx,
ValAx: valAx,
},
}
- return charts[formatSet.Type]
+ return charts[opts.Type]
}
// drawDoughnutChart provides a function to draw the c:plotArea element for
// doughnut chart by given format sets.
-func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawDoughnutChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ holeSize := 75
+ if opts.HoleSize > 0 && opts.HoleSize <= 90 {
+ holeSize = opts.HoleSize
+ }
+
return &cPlotArea{
- DoughnutChart: &cCharts{
- VaryColors: &attrValBool{
- Val: boolPtr(true),
+ DoughnutChart: []*cCharts{
+ {
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
+ HoleSize: &attrValInt{Val: intPtr(holeSize)},
},
- Ser: f.drawChartSeries(formatSet),
- HoleSize: &attrValInt{Val: intPtr(75)},
},
}
}
// drawLineChart provides a function to draw the c:plotArea element for line
// chart by given format sets.
-func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawLineChart(pa *cPlotArea, opts *Chart) *cPlotArea {
return &cPlotArea{
- LineChart: &cCharts{
- Grouping: &attrValString{
- Val: stringPtr(plotAreaChartGrouping[formatSet.Type]),
- },
- VaryColors: &attrValBool{
- Val: boolPtr(false),
- },
- Ser: f.drawChartSeries(formatSet),
- DLbls: f.drawChartDLbls(formatSet),
- Smooth: &attrValBool{
- Val: boolPtr(false),
+ LineChart: []*cCharts{
+ {
+ Grouping: &attrValString{
+ Val: stringPtr(plotAreaChartGrouping[opts.Type]),
+ },
+ VaryColors: &attrValBool{
+ Val: boolPtr(false),
+ },
+ Ser: f.drawChartSeries(opts),
+ DLbls: f.drawChartDLbls(opts),
+ AxID: f.genAxID(opts),
},
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
+ },
+ CatAx: f.drawPlotAreaCatAx(pa, opts),
+ ValAx: f.drawPlotAreaValAx(pa, opts),
+ }
+}
+
+// drawLine3DChart provides a function to draw the c:plotArea element for line
+// chart by given format sets.
+func (f *File) drawLine3DChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ return &cPlotArea{
+ Line3DChart: []*cCharts{
+ {
+ Grouping: &attrValString{
+ Val: stringPtr(plotAreaChartGrouping[opts.Type]),
+ },
+ VaryColors: &attrValBool{
+ Val: boolPtr(false),
+ },
+ Ser: f.drawChartSeries(opts),
+ DLbls: f.drawChartDLbls(opts),
+ AxID: f.genAxID(opts),
},
},
- CatAx: f.drawPlotAreaCatAx(formatSet),
- ValAx: f.drawPlotAreaValAx(formatSet),
+ CatAx: f.drawPlotAreaCatAx(pa, opts),
+ ValAx: f.drawPlotAreaValAx(pa, opts),
}
}
// drawPieChart provides a function to draw the c:plotArea element for pie
// chart by given format sets.
-func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawPieChart(pa *cPlotArea, opts *Chart) *cPlotArea {
return &cPlotArea{
- PieChart: &cCharts{
- VaryColors: &attrValBool{
- Val: boolPtr(true),
+ PieChart: []*cCharts{
+ {
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
},
- Ser: f.drawChartSeries(formatSet),
},
}
}
// drawPie3DChart provides a function to draw the c:plotArea element for 3D
// pie chart by given format sets.
-func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawPie3DChart(pa *cPlotArea, opts *Chart) *cPlotArea {
return &cPlotArea{
- Pie3DChart: &cCharts{
- VaryColors: &attrValBool{
- Val: boolPtr(true),
+ Pie3DChart: []*cCharts{
+ {
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
},
- Ser: f.drawChartSeries(formatSet),
},
}
}
// drawPieOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
-func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawPieOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ var splitPos *attrValInt
+ if opts.PlotArea.SecondPlotValues > 0 {
+ splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)}
+ }
return &cPlotArea{
- OfPieChart: &cCharts{
- OfPieType: &attrValString{
- Val: stringPtr("pie"),
- },
- VaryColors: &attrValBool{
- Val: boolPtr(true),
+ OfPieChart: []*cCharts{
+ {
+ OfPieType: &attrValString{
+ Val: stringPtr("pie"),
+ },
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
+ SplitPos: splitPos,
+ SerLines: &attrValString{},
},
- Ser: f.drawChartSeries(formatSet),
- SerLines: &attrValString{},
},
}
}
// drawBarOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
-func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawBarOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ var splitPos *attrValInt
+ if opts.PlotArea.SecondPlotValues > 0 {
+ splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)}
+ }
return &cPlotArea{
- OfPieChart: &cCharts{
- OfPieType: &attrValString{
- Val: stringPtr("bar"),
- },
- VaryColors: &attrValBool{
- Val: boolPtr(true),
+ OfPieChart: []*cCharts{
+ {
+ OfPieType: &attrValString{
+ Val: stringPtr("bar"),
+ },
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ SplitPos: splitPos,
+ Ser: f.drawChartSeries(opts),
+ SerLines: &attrValString{},
},
- Ser: f.drawChartSeries(formatSet),
- SerLines: &attrValString{},
},
}
}
// drawRadarChart provides a function to draw the c:plotArea element for radar
// chart by given format sets.
-func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawRadarChart(pa *cPlotArea, opts *Chart) *cPlotArea {
return &cPlotArea{
- RadarChart: &cCharts{
- RadarStyle: &attrValString{
- Val: stringPtr("marker"),
- },
- VaryColors: &attrValBool{
- Val: boolPtr(false),
- },
- Ser: f.drawChartSeries(formatSet),
- DLbls: f.drawChartDLbls(formatSet),
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
+ RadarChart: []*cCharts{
+ {
+ RadarStyle: &attrValString{
+ Val: stringPtr("marker"),
+ },
+ VaryColors: &attrValBool{
+ Val: boolPtr(false),
+ },
+ Ser: f.drawChartSeries(opts),
+ DLbls: f.drawChartDLbls(opts),
+ AxID: f.genAxID(opts),
},
},
- CatAx: f.drawPlotAreaCatAx(formatSet),
- ValAx: f.drawPlotAreaValAx(formatSet),
+ CatAx: f.drawPlotAreaCatAx(pa, opts),
+ ValAx: f.drawPlotAreaValAx(pa, opts),
}
}
// drawScatterChart provides a function to draw the c:plotArea element for
// scatter chart by given format sets.
-func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawScatterChart(pa *cPlotArea, opts *Chart) *cPlotArea {
return &cPlotArea{
- ScatterChart: &cCharts{
- ScatterStyle: &attrValString{
- Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker
- },
- VaryColors: &attrValBool{
- Val: boolPtr(false),
- },
- Ser: f.drawChartSeries(formatSet),
- DLbls: f.drawChartDLbls(formatSet),
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
+ ScatterChart: []*cCharts{
+ {
+ ScatterStyle: &attrValString{
+ Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker
+ },
+ VaryColors: &attrValBool{
+ Val: boolPtr(false),
+ },
+ Ser: f.drawChartSeries(opts),
+ DLbls: f.drawChartDLbls(opts),
+ AxID: f.genAxID(opts),
},
},
- CatAx: f.drawPlotAreaCatAx(formatSet),
- ValAx: f.drawPlotAreaValAx(formatSet),
+ ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...),
}
}
// drawSurface3DChart provides a function to draw the c:surface3DChart element by
// given format sets.
-func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawSurface3DChart(pa *cPlotArea, opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
- Surface3DChart: &cCharts{
- Ser: f.drawChartSeries(formatSet),
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
- {Val: intPtr(832256642)},
+ Surface3DChart: []*cCharts{
+ {
+ Ser: f.drawChartSeries(opts),
+ AxID: []*attrValInt{
+ {Val: intPtr(100000000)},
+ {Val: intPtr(100000001)},
+ {Val: intPtr(100000005)},
+ },
},
},
- CatAx: f.drawPlotAreaCatAx(formatSet),
- ValAx: f.drawPlotAreaValAx(formatSet),
- SerAx: f.drawPlotAreaSerAx(formatSet),
+ CatAx: f.drawPlotAreaCatAx(pa, opts),
+ ValAx: f.drawPlotAreaValAx(pa, opts),
+ SerAx: f.drawPlotAreaSerAx(opts),
}
- if formatSet.Type == WireframeSurface3D {
- plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)}
+ if opts.Type == WireframeSurface3D {
+ plotArea.Surface3DChart[0].Wireframe = &attrValBool{Val: boolPtr(true)}
}
return plotArea
}
// drawSurfaceChart provides a function to draw the c:surfaceChart element by
// given format sets.
-func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea {
+func (f *File) drawSurfaceChart(pa *cPlotArea, opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
- SurfaceChart: &cCharts{
- Ser: f.drawChartSeries(formatSet),
- AxID: []*attrValInt{
- {Val: intPtr(754001152)},
- {Val: intPtr(753999904)},
- {Val: intPtr(832256642)},
+ SurfaceChart: []*cCharts{
+ {
+ Ser: f.drawChartSeries(opts),
+ AxID: []*attrValInt{
+ {Val: intPtr(100000000)},
+ {Val: intPtr(100000001)},
+ {Val: intPtr(100000005)},
+ },
+ },
+ },
+ CatAx: f.drawPlotAreaCatAx(pa, opts),
+ ValAx: f.drawPlotAreaValAx(pa, opts),
+ SerAx: f.drawPlotAreaSerAx(opts),
+ }
+ if opts.Type == WireframeContour {
+ plotArea.SurfaceChart[0].Wireframe = &attrValBool{Val: boolPtr(true)}
+ }
+ return plotArea
+}
+
+// drawBubbleChart provides a function to draw the c:bubbleChart element by
+// given format sets.
+func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea {
+ plotArea := &cPlotArea{
+ BubbleChart: []*cCharts{
+ {
+ VaryColors: &attrValBool{
+ Val: opts.VaryColors,
+ },
+ Ser: f.drawChartSeries(opts),
+ DLbls: f.drawChartDLbls(opts),
+ AxID: f.genAxID(opts),
},
},
- CatAx: f.drawPlotAreaCatAx(formatSet),
- ValAx: f.drawPlotAreaValAx(formatSet),
- SerAx: f.drawPlotAreaSerAx(formatSet),
+ ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...),
}
- if formatSet.Type == WireframeContour {
- plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)}
+ if opts.BubbleSize > 0 && opts.BubbleSize <= 300 {
+ plotArea.BubbleChart[0].BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))}
}
return plotArea
}
+// drawChartGapWidth provides a function to draw the c:gapWidth element by given
+// format sets.
+func (f *File) drawChartGapWidth(opts *Chart) *attrValInt {
+ for _, t := range barColChartTypes {
+ if t == opts.Type && opts.GapWidth != nil && *opts.GapWidth != 150 && *opts.GapWidth <= 500 {
+ return &attrValInt{intPtr(int(*opts.GapWidth))}
+ }
+ }
+ return nil
+}
+
+// drawChartOverlap provides a function to draw the c:overlap element by given
+// format sets.
+func (f *File) drawChartOverlap(opts *Chart) *attrValInt {
+ var val *attrValInt
+ if _, ok := plotAreaChartOverlap[opts.Type]; ok {
+ val = &attrValInt{intPtr(100)}
+ }
+ if opts.Overlap != nil && -100 <= *opts.Overlap && *opts.Overlap <= 100 {
+ val = &attrValInt{intPtr(*opts.Overlap)}
+ }
+ for _, t := range barColChartTypes {
+ if t == opts.Type {
+ return val
+ }
+ }
+ return nil
+}
+
// drawChartShape provides a function to draw the c:shape element by given
// format sets.
-func (f *File) drawChartShape(formatSet *formatChart) *attrValString {
- shapes := map[string]string{
+func (f *File) drawChartShape(opts *Chart) *attrValString {
+ shapes := map[ChartType]string{
Bar3DConeClustered: "cone",
Bar3DConeStacked: "cone",
Bar3DConePercentStacked: "cone",
@@ -726,7 +750,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString {
Col3DCylinderStacked: "cylinder",
Col3DCylinderPercentStacked: "cylinder",
}
- if shape, ok := shapes[formatSet.Type]; ok {
+ if shape, ok := shapes[opts.Type]; ok {
return &attrValString{Val: stringPtr(shape)}
}
return nil
@@ -734,59 +758,79 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString {
// drawChartSeries provides a function to draw the c:ser element by given
// format sets.
-func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer {
- ser := []cSer{}
- for k := range formatSet.Series {
+func (f *File) drawChartSeries(opts *Chart) *[]cSer {
+ var ser []cSer
+ for k := range opts.Series {
ser = append(ser, cSer{
- IDx: &attrValInt{Val: intPtr(k + formatSet.order)},
- Order: &attrValInt{Val: intPtr(k + formatSet.order)},
+ IDx: &attrValInt{Val: intPtr(k + opts.order)},
+ Order: &attrValInt{Val: intPtr(k + opts.order)},
Tx: &cTx{
StrRef: &cStrRef{
- F: formatSet.Series[k].Name,
+ F: opts.Series[k].Name,
},
},
- SpPr: f.drawChartSeriesSpPr(k, formatSet),
- Marker: f.drawChartSeriesMarker(k, formatSet),
- DPt: f.drawChartSeriesDPt(k, formatSet),
- DLbls: f.drawChartSeriesDLbls(formatSet),
- Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet),
- Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet),
- XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet),
- YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet),
- BubbleSize: f.drawCharSeriesBubbleSize(formatSet.Series[k], formatSet),
- Bubble3D: f.drawCharSeriesBubble3D(formatSet),
+ SpPr: f.drawChartSeriesSpPr(k, opts),
+ Marker: f.drawChartSeriesMarker(k, opts),
+ DPt: f.drawChartSeriesDPt(k, opts),
+ DLbls: f.drawChartSeriesDLbls(k, opts),
+ InvertIfNegative: &attrValBool{Val: boolPtr(false)},
+ Cat: f.drawChartSeriesCat(opts.Series[k], opts),
+ Smooth: &attrValBool{Val: boolPtr(opts.Series[k].Line.Smooth)},
+ Val: f.drawChartSeriesVal(opts.Series[k], opts),
+ XVal: f.drawChartSeriesXVal(opts.Series[k], opts),
+ YVal: f.drawChartSeriesYVal(opts.Series[k], opts),
+ BubbleSize: f.drawCharSeriesBubbleSize(opts.Series[k], opts),
+ Bubble3D: f.drawCharSeriesBubble3D(opts),
})
}
return &ser
}
+// drawShapeFill provides a function to draw the a:solidFill element by given
+// fill format sets.
+func (f *File) drawShapeFill(fill Fill, spPr *cSpPr) *cSpPr {
+ if fill.Type == "pattern" && fill.Pattern == 1 {
+ if spPr == nil {
+ spPr = &cSpPr{}
+ }
+ if len(fill.Color) == 1 {
+ spPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(strings.TrimPrefix(fill.Color[0], "#"))}}
+ return spPr
+ }
+ spPr.SolidFill = nil
+ spPr.NoFill = stringPtr("")
+ }
+ return spPr
+}
+
// drawChartSeriesSpPr provides a function to draw the c:spPr element by given
// format sets.
-func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr {
- spPrScatter := &cSpPr{
+func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr {
+ spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)}}}
+ spPr = f.drawShapeFill(opts.Series[i].Fill, spPr)
+ solid := &cSpPr{
Ln: &aLn{
- W: 25400,
- NoFill: " ",
+ W: f.ptToEMUs(opts.Series[i].Line.Width),
+ Cap: "rnd", // rnd, sq, flat
+ SolidFill: spPr.SolidFill,
},
}
- spPrLine := &cSpPr{
- Ln: &aLn{
- W: f.ptToEMUs(formatSet.Series[i].Line.Width),
- Cap: "rnd", // rnd, sq, flat
- },
+ noLn := &cSpPr{Ln: &aLn{NoFill: &attrValString{}}}
+ if chartSeriesSpPr, ok := map[ChartType]map[ChartLineType]*cSpPr{
+ Line: {ChartLineUnset: solid, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: solid},
+ Scatter: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn},
+ }[opts.Type]; ok {
+ return chartSeriesSpPr[opts.Series[i].Line.Type]
}
- if i+formatSet.order < 6 {
- spPrLine.Ln.SolidFill = &aSolidFill{
- SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)},
- }
+ if spPr.SolidFill.SrgbClr != nil {
+ return spPr
}
- chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter}
- return chartSeriesSpPr[formatSet.Type]
+ return nil
}
// drawChartSeriesDPt provides a function to draw the c:dPt element by given
// data index and format sets.
-func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt {
+func (f *File) drawChartSeriesDPt(i int, opts *Chart) []*cDPt {
dpt := []*cDPt{{
IDx: &attrValInt{Val: intPtr(i)},
Bubble3D: &attrValBool{Val: boolPtr(false)},
@@ -809,20 +853,20 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt {
},
},
}}
- chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt}
- return chartSeriesDPt[formatSet.Type]
+ chartSeriesDPt := map[ChartType][]*cDPt{Pie: dpt, Pie3D: dpt}
+ return chartSeriesDPt[opts.Type]
}
// drawChartSeriesCat provides a function to draw the c:cat element by given
// chart series and format sets.
-func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) *cCat {
+func (f *File) drawChartSeriesCat(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
- chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil}
- if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" {
+ chartSeriesCat := map[ChartType]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil}
+ if _, ok := chartSeriesCat[opts.Type]; ok || v.Categories == "" {
return nil
}
return cat
@@ -830,14 +874,14 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) *
// drawChartSeriesVal provides a function to draw the c:val element by given
// chart series and format sets.
-func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) *cVal {
+func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
- chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil}
- if _, ok := chartSeriesVal[formatSet.Type]; ok {
+ chartSeriesVal := map[ChartType]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil}
+ if _, ok := chartSeriesVal[opts.Type]; ok {
return nil
}
return val
@@ -845,11 +889,18 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) *
// drawChartSeriesMarker provides a function to draw the c:marker element by
// given data index and format sets.
-func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker {
+func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker {
+ defaultSymbol := map[ChartType]*attrValString{Scatter: {Val: stringPtr("circle")}}
marker := &cMarker{
- Symbol: &attrValString{Val: stringPtr("circle")},
+ Symbol: defaultSymbol[opts.Type],
Size: &attrValInt{Val: intPtr(5)},
}
+ if symbol := stringPtr(opts.Series[i].Marker.Symbol); *symbol != "" {
+ marker.Symbol = &attrValString{Val: symbol}
+ }
+ if size := intPtr(opts.Series[i].Marker.Size); *size != 0 {
+ marker.Size = &attrValInt{Val: size}
+ }
if i < 6 {
marker.SpPr = &cSpPr{
SolidFill: &aSolidFill{
@@ -867,210 +918,318 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker {
},
}
}
- chartSeriesMarker := map[string]*cMarker{Scatter: marker}
- return chartSeriesMarker[formatSet.Type]
+ marker.SpPr = f.drawShapeFill(opts.Series[i].Marker.Fill, marker.SpPr)
+ chartSeriesMarker := map[ChartType]*cMarker{Scatter: marker, Line: marker}
+ return chartSeriesMarker[opts.Type]
}
// drawChartSeriesXVal provides a function to draw the c:xVal element by given
// chart series and format sets.
-func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) *cCat {
+func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
- chartSeriesXVal := map[string]*cCat{Scatter: cat}
- return chartSeriesXVal[formatSet.Type]
+ chartSeriesXVal := map[ChartType]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat}
+ return chartSeriesXVal[opts.Type]
}
// drawChartSeriesYVal provides a function to draw the c:yVal element by given
// chart series and format sets.
-func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) *cVal {
+func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
- chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val}
- return chartSeriesYVal[formatSet.Type]
+ chartSeriesYVal := map[ChartType]*cVal{Scatter: val, Bubble: val, Bubble3D: val}
+ return chartSeriesYVal[opts.Type]
}
// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize
// element by given chart series and format sets.
-func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatChart) *cVal {
- if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[formatSet.Type]; !ok {
+func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal {
+ if _, ok := map[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok {
return nil
}
+ fVal := v.Values
+ if v.Sizes != "" {
+ fVal = v.Sizes
+ }
return &cVal{
NumRef: &cNumRef{
- F: v.Values,
+ F: fVal,
},
}
}
// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element
// by given format sets.
-func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool {
- if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok {
+func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool {
+ if _, ok := map[ChartType]bool{Bubble3D: true}[opts.Type]; !ok {
return nil
}
return &attrValBool{Val: boolPtr(true)}
}
+// drawChartNumFmt provides a function to draw the c:numFmt element by given
+// data labels format sets.
+func (f *File) drawChartNumFmt(labels ChartNumFmt) *cNumFmt {
+ var numFmt *cNumFmt
+ if labels.CustomNumFmt != "" || labels.SourceLinked {
+ numFmt = &cNumFmt{
+ FormatCode: labels.CustomNumFmt,
+ SourceLinked: labels.SourceLinked,
+ }
+ }
+ return numFmt
+}
+
// drawChartDLbls provides a function to draw the c:dLbls element by given
// format sets.
-func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls {
+func (f *File) drawChartDLbls(opts *Chart) *cDLbls {
return &cDLbls{
- ShowLegendKey: &attrValBool{Val: boolPtr(formatSet.Legend.ShowLegendKey)},
- ShowVal: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowVal)},
- ShowCatName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowCatName)},
- ShowSerName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowSerName)},
- ShowBubbleSize: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowBubbleSize)},
- ShowPercent: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowPercent)},
- ShowLeaderLines: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowLeaderLines)},
+ NumFmt: f.drawChartNumFmt(opts.PlotArea.NumFmt),
+ ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)},
+ ShowVal: &attrValBool{Val: boolPtr(opts.PlotArea.ShowVal)},
+ ShowCatName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowCatName)},
+ ShowSerName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowSerName)},
+ ShowBubbleSize: &attrValBool{Val: boolPtr(opts.PlotArea.ShowBubbleSize)},
+ ShowPercent: &attrValBool{Val: boolPtr(opts.PlotArea.ShowPercent)},
+ ShowLeaderLines: &attrValBool{Val: boolPtr(opts.PlotArea.ShowLeaderLines)},
+ }
+}
+
+// inSupportedChartDataLabelsPositionType provides a method to check if an
+// element is present in an array, and return the index of its location,
+// otherwise return -1.
+func inSupportedChartDataLabelsPositionType(a []ChartDataLabelPositionType, x ChartDataLabelPositionType) int {
+ for idx, n := range a {
+ if x == n {
+ return idx
+ }
}
+ return -1
}
// drawChartSeriesDLbls provides a function to draw the c:dLbls element by
// given format sets.
-func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls {
- dLbls := f.drawChartDLbls(formatSet)
- chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil}
- if _, ok := chartSeriesDLbls[formatSet.Type]; ok {
+func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls {
+ dLbls := f.drawChartDLbls(opts)
+ chartSeriesDLbls := map[ChartType]*cDLbls{
+ Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil,
+ }
+ if _, ok := chartSeriesDLbls[opts.Type]; ok {
return nil
}
+ if types, ok := supportedChartDataLabelsPosition[opts.Type]; ok && opts.Series[i].DataLabelPosition != ChartDataLabelsPositionUnset {
+ if inSupportedChartDataLabelsPositionType(types, opts.Series[i].DataLabelPosition) != -1 {
+ dLbls.DLblPos = &attrValString{Val: stringPtr(chartDataLabelsPositionTypes[opts.Series[i].DataLabelPosition])}
+ }
+ }
+ dLbl := opts.Series[i].DataLabel
+ dLbls.SpPr = f.drawShapeFill(dLbl.Fill, dLbls.SpPr)
+ dLbls.TxPr = &cTxPr{BodyPr: aBodyPr{}, P: aP{PPr: &aPPr{DefRPr: aRPr{}}}}
+ drawChartFont(&dLbl.Font, &dLbls.TxPr.P.PPr.DefRPr)
return dLbls
}
// drawPlotAreaCatAx provides a function to draw the c:catAx element.
-func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs {
- min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)}
- max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)}
- if formatSet.XAxis.Minimum == 0 {
- min = nil
- }
- if formatSet.XAxis.Maximum == 0 {
- max = nil
- }
- axs := []*cAxs{
- {
- AxID: &attrValInt{Val: intPtr(754001152)},
- Scaling: &cScaling{
- Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])},
- Max: max,
- Min: min,
- },
- Delete: &attrValBool{Val: boolPtr(false)},
- AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])},
- NumFmt: &cNumFmt{
- FormatCode: "General",
- SourceLinked: true,
- },
- MajorTickMark: &attrValString{Val: stringPtr("none")},
- MinorTickMark: &attrValString{Val: stringPtr("none")},
- TickLblPos: &attrValString{Val: stringPtr("nextTo")},
- SpPr: f.drawPlotAreaSpPr(),
- TxPr: f.drawPlotAreaTxPr(),
- CrossAx: &attrValInt{Val: intPtr(753999904)},
- Crosses: &attrValString{Val: stringPtr("autoZero")},
- Auto: &attrValBool{Val: boolPtr(true)},
- LblAlgn: &attrValString{Val: stringPtr("ctr")},
- LblOffset: &attrValInt{Val: intPtr(100)},
- NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)},
- },
- }
- if formatSet.XAxis.MajorGridlines {
- axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
- }
- if formatSet.XAxis.MinorGridlines {
- axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
- }
- if formatSet.XAxis.TickLabelSkip != 0 {
- axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)}
- }
- return axs
+func (f *File) drawPlotAreaCatAx(pa *cPlotArea, opts *Chart) []*cAxs {
+ maxVal := &attrValFloat{Val: opts.XAxis.Maximum}
+ minVal := &attrValFloat{Val: opts.XAxis.Minimum}
+ if opts.XAxis.Maximum == nil {
+ maxVal = nil
+ }
+ if opts.XAxis.Minimum == nil {
+ minVal = nil
+ }
+ ax := &cAxs{
+ AxID: &attrValInt{Val: intPtr(100000000)},
+ Scaling: &cScaling{
+ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])},
+ Max: maxVal,
+ Min: minVal,
+ },
+ Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)},
+ AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
+ NumFmt: &cNumFmt{FormatCode: "General"},
+ MajorTickMark: &attrValString{Val: stringPtr("none")},
+ MinorTickMark: &attrValString{Val: stringPtr("none")},
+ Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""),
+ TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.XAxis.TickLabelPosition])},
+ SpPr: f.drawPlotAreaSpPr(),
+ TxPr: f.drawPlotAreaTxPr(&opts.XAxis),
+ CrossAx: &attrValInt{Val: intPtr(100000001)},
+ Crosses: &attrValString{Val: stringPtr("autoZero")},
+ Auto: &attrValBool{Val: boolPtr(true)},
+ LblAlgn: &attrValString{Val: stringPtr("ctr")},
+ LblOffset: &attrValInt{Val: intPtr(100)},
+ NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)},
+ }
+ if numFmt := f.drawChartNumFmt(opts.XAxis.NumFmt); numFmt != nil {
+ ax.NumFmt = numFmt
+ }
+ if opts.XAxis.MajorGridLines {
+ ax.MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+ }
+ if opts.XAxis.MinorGridLines {
+ ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+ }
+ if opts.XAxis.TickLabelSkip != 0 {
+ ax.TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)}
+ }
+ if opts.order > 0 && opts.YAxis.Secondary && pa.CatAx != nil {
+ ax.AxID = &attrValInt{Val: intPtr(opts.XAxis.axID)}
+ ax.Delete = &attrValBool{Val: boolPtr(true)}
+ ax.Crosses = nil
+ ax.CrossAx = &attrValInt{Val: intPtr(opts.YAxis.axID)}
+ return []*cAxs{pa.CatAx[0], ax}
+ }
+ return []*cAxs{ax}
}
// drawPlotAreaValAx provides a function to draw the c:valAx element.
-func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs {
- min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)}
- max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)}
- if formatSet.YAxis.Minimum == 0 {
- min = nil
+func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs {
+ maxVal := &attrValFloat{Val: opts.YAxis.Maximum}
+ minVal := &attrValFloat{Val: opts.YAxis.Minimum}
+ if opts.YAxis.Maximum == nil {
+ maxVal = nil
}
- if formatSet.YAxis.Maximum == 0 {
- max = nil
+ if opts.YAxis.Minimum == nil {
+ minVal = nil
}
var logBase *attrValFloat
- if formatSet.YAxis.LogBase >= 2 && formatSet.YAxis.LogBase <= 1000 {
- logBase = &attrValFloat{Val: float64Ptr(formatSet.YAxis.LogBase)}
+ if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 {
+ logBase = &attrValFloat{Val: float64Ptr(opts.YAxis.LogBase)}
+ }
+ ax := &cAxs{
+ AxID: &attrValInt{Val: intPtr(100000001)},
+ Scaling: &cScaling{
+ LogBase: logBase,
+ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
+ Max: maxVal,
+ Min: minVal,
+ },
+ Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
+ AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])},
+ Title: f.drawPlotAreaTitles(opts.YAxis.Title, "horz"),
+ NumFmt: &cNumFmt{
+ FormatCode: chartValAxNumFmtFormatCode[opts.Type],
+ },
+ MajorTickMark: &attrValString{Val: stringPtr("none")},
+ MinorTickMark: &attrValString{Val: stringPtr("none")},
+ TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])},
+ SpPr: f.drawPlotAreaSpPr(),
+ TxPr: f.drawPlotAreaTxPr(&opts.YAxis),
+ CrossAx: &attrValInt{Val: intPtr(100000000)},
+ Crosses: &attrValString{Val: stringPtr("autoZero")},
+ CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])},
+ }
+ if numFmt := f.drawChartNumFmt(opts.YAxis.NumFmt); numFmt != nil {
+ ax.NumFmt = numFmt
+ }
+ if opts.YAxis.MajorGridLines {
+ ax.MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+ }
+ if opts.YAxis.MinorGridLines {
+ ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+ }
+ if pos, ok := tickLblPosNone[opts.Type]; ok {
+ ax.TickLblPos.Val = stringPtr(pos)
+ }
+ if opts.YAxis.MajorUnit != 0 {
+ ax.MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)}
+ }
+ if opts.order > 0 && opts.YAxis.Secondary && pa.ValAx != nil {
+ ax.AxID = &attrValInt{Val: intPtr(opts.YAxis.axID)}
+ ax.AxPos = &attrValString{Val: stringPtr("r")}
+ ax.Crosses = &attrValString{Val: stringPtr("max")}
+ ax.CrossAx = &attrValInt{Val: intPtr(opts.XAxis.axID)}
+ return []*cAxs{pa.ValAx[0], ax}
+ }
+ return []*cAxs{ax}
+}
+
+// drawPlotAreaSerAx provides a function to draw the c:serAx element.
+func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs {
+ maxVal := &attrValFloat{Val: opts.YAxis.Maximum}
+ minVal := &attrValFloat{Val: opts.YAxis.Minimum}
+ if opts.YAxis.Maximum == nil {
+ maxVal = nil
+ }
+ if opts.YAxis.Minimum == nil {
+ minVal = nil
}
- axs := []*cAxs{
+ return []*cAxs{
{
- AxID: &attrValInt{Val: intPtr(753999904)},
+ AxID: &attrValInt{Val: intPtr(100000005)},
Scaling: &cScaling{
- LogBase: logBase,
- Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])},
- Max: max,
- Min: min,
+ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
+ Max: maxVal,
+ Min: minVal,
},
- Delete: &attrValBool{Val: boolPtr(false)},
- AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])},
- NumFmt: &cNumFmt{
- FormatCode: chartValAxNumFmtFormatCode[formatSet.Type],
- SourceLinked: true,
- },
- MajorTickMark: &attrValString{Val: stringPtr("none")},
- MinorTickMark: &attrValString{Val: stringPtr("none")},
- TickLblPos: &attrValString{Val: stringPtr("nextTo")},
- SpPr: f.drawPlotAreaSpPr(),
- TxPr: f.drawPlotAreaTxPr(),
- CrossAx: &attrValInt{Val: intPtr(754001152)},
- Crosses: &attrValString{Val: stringPtr("autoZero")},
- CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])},
+ Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
+ AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
+ TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])},
+ SpPr: f.drawPlotAreaSpPr(),
+ TxPr: f.drawPlotAreaTxPr(nil),
+ CrossAx: &attrValInt{Val: intPtr(100000001)},
},
}
- if formatSet.YAxis.MajorGridlines {
- axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+}
+
+// drawChartFont provides a function to draw the a:rPr element.
+func drawChartFont(fnt *Font, r *aRPr) {
+ if fnt == nil {
+ return
+ }
+ r.B = fnt.Bold
+ r.I = fnt.Italic
+ if idx := inStrSlice(supportedDrawingUnderlineTypes, fnt.Underline, true); idx != -1 {
+ r.U = supportedDrawingUnderlineTypes[idx]
+ }
+ if fnt.Color != "" {
+ if r.SolidFill == nil {
+ r.SolidFill = &aSolidFill{}
+ }
+ r.SolidFill.SchemeClr = nil
+ r.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(fnt.Color), "#", ""))}
}
- if formatSet.YAxis.MinorGridlines {
- axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
+ if fnt.Family != "" {
+ if r.Latin == nil {
+ r.Latin = &xlsxCTTextFont{}
+ }
+ r.Latin.Typeface = fnt.Family
}
- if pos, ok := valTickLblPos[formatSet.Type]; ok {
- axs[0].TickLblPos.Val = stringPtr(pos)
+ if fnt.Size > 0 {
+ r.Sz = fnt.Size * 100
}
- if formatSet.YAxis.MajorUnit != 0 {
- axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)}
+ if fnt.Strike {
+ r.Strike = "sngStrike"
}
- return axs
}
-// drawPlotAreaSerAx provides a function to draw the c:serAx element.
-func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs {
- min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)}
- max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)}
- if formatSet.YAxis.Minimum == 0 {
- min = nil
+// drawPlotAreaTitles provides a function to draw the c:title element.
+func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle {
+ if len(runs) == 0 {
+ return nil
}
- if formatSet.YAxis.Maximum == 0 {
- max = nil
+ title := &cTitle{Tx: cTx{Rich: &cRich{}}, Overlay: &attrValBool{Val: boolPtr(false)}}
+ for _, run := range runs {
+ r := &aR{T: run.Text}
+ drawChartFont(run.Font, &r.RPr)
+ title.Tx.Rich.P = append(title.Tx.Rich.P, aP{
+ PPr: &aPPr{DefRPr: aRPr{}},
+ R: r,
+ EndParaRPr: &aEndParaRPr{Lang: "en-US", AltLang: "en-US"},
+ })
}
- return []*cAxs{
- {
- AxID: &attrValInt{Val: intPtr(832256642)},
- Scaling: &cScaling{
- Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])},
- Max: max,
- Min: min,
- },
- Delete: &attrValBool{Val: boolPtr(false)},
- AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])},
- TickLblPos: &attrValString{Val: stringPtr("nextTo")},
- SpPr: f.drawPlotAreaSpPr(),
- TxPr: f.drawPlotAreaTxPr(),
- CrossAx: &attrValInt{Val: intPtr(753999904)},
- },
+ if vert == "horz" {
+ title.Tx.Rich.BodyPr = aBodyPr{Rot: -5400000, Vert: vert}
}
+ return title
}
// drawPlotAreaSpPr provides a function to draw the c:spPr element.
@@ -1093,8 +1252,8 @@ func (f *File) drawPlotAreaSpPr() *cSpPr {
}
// drawPlotAreaTxPr provides a function to draw the c:txPr element.
-func (f *File) drawPlotAreaTxPr() *cTxPr {
- return &cTxPr{
+func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr {
+ cTxPr := &cTxPr{
BodyPr: aBodyPr{
Rot: -60000000,
SpcFirstLastPara: true,
@@ -1121,7 +1280,7 @@ func (f *File) drawPlotAreaTxPr() *cTxPr {
LumOff: &attrValInt{Val: intPtr(85000)},
},
},
- Latin: &aLatin{Typeface: "+mn-lt"},
+ Latin: &xlsxCTTextFont{Typeface: "+mn-lt"},
Ea: &aEa{Typeface: "+mn-ea"},
Cs: &aCs{Typeface: "+mn-cs"},
},
@@ -1129,29 +1288,77 @@ func (f *File) drawPlotAreaTxPr() *cTxPr {
EndParaRPr: &aEndParaRPr{Lang: "en-US"},
},
}
+ if opts != nil {
+ drawChartFont(&opts.Font, &cTxPr.P.PPr.DefRPr)
+ if -90 <= opts.Alignment.TextRotation && opts.Alignment.TextRotation <= 90 {
+ cTxPr.BodyPr.Rot = opts.Alignment.TextRotation * 60000
+ }
+ if idx := inStrSlice(supportedDrawingTextVerticalType, opts.Alignment.Vertical, true); idx != -1 {
+ cTxPr.BodyPr.Vert = supportedDrawingTextVerticalType[idx]
+ }
+ }
+ return cTxPr
+}
+
+// drawChartLn provides a function to draw the a:ln element.
+func (f *File) drawChartLn(opts *ChartLine) *aLn {
+ ln := &aLn{
+ W: f.ptToEMUs(opts.Width),
+ Cap: "flat",
+ Cmpd: "sng",
+ Algn: "ctr",
+ }
+ switch opts.Type {
+ case ChartLineSolid:
+ ln.SolidFill = &aSolidFill{
+ SchemeClr: &aSchemeClr{
+ Val: "tx1",
+ LumMod: &attrValInt{
+ Val: intPtr(15000),
+ },
+ LumOff: &attrValInt{
+ Val: intPtr(85000),
+ },
+ },
+ }
+ return ln
+ case ChartLineNone:
+ ln.NoFill = &attrValString{}
+ return ln
+ default:
+ return nil
+ }
}
// drawingParser provides a function to parse drawingXML. In order to solve
// the problem that the label structure is changed after serialization and
// deserialization, two different structures: decodeWsDr and encodeWsDr are
// defined.
-func (f *File) drawingParser(path string) (*xlsxWsDr, int) {
+func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) {
var (
err error
ok bool
)
-
- if f.Drawings[path] == nil {
- content := xlsxWsDr{}
- content.A = NameSpaceDrawingML
- content.Xdr = NameSpaceDrawingMLSpreadSheet
- if _, ok = f.XLSX[path]; ok { // Append Model
+ _, ok = f.Drawings.Load(path)
+ if !ok {
+ content := xlsxWsDr{
+ NS: NameSpaceDrawingMLSpreadSheet.Value,
+ Xdr: NameSpaceDrawingMLSpreadSheet.Value,
+ A: NameSpaceDrawingML.Value,
+ }
+ if _, ok = f.Pkg.Load(path); ok { // Append Model
decodeWsDr := decodeWsDr{}
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
Decode(&decodeWsDr); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return nil, 0, err
}
content.R = decodeWsDr.R
+ for _, v := range decodeWsDr.AlternateContent {
+ content.AlternateContent = append(content.AlternateContent, &xlsxAlternateContent{
+ Content: v.Content,
+ XMLNSMC: SourceRelationshipCompatibility.Value,
+ })
+ }
for _, v := range decodeWsDr.OneCellAnchor {
content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{
EditAs: v.EditAs,
@@ -1165,34 +1372,38 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) {
})
}
}
- f.Drawings[path] = &content
+ f.Drawings.Store(path, &content)
+ }
+ var wsDr *xlsxWsDr
+ if drawing, ok := f.Drawings.Load(path); ok && drawing != nil {
+ wsDr = drawing.(*xlsxWsDr)
}
- wsDr := f.Drawings[path]
- return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2
+ wsDr.mu.Lock()
+ defer wsDr.mu.Unlock()
+ return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2, nil
}
// addDrawingChart provides a function to add chart graphic frame by given
// sheet, drawingXML, cell, width, height, relationship index and format sets.
-func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, formatSet *formatPicture) error {
+func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *GraphicOptions) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
- colIdx := col - 1
- rowIdx := row - 1
-
- width = int(float64(width) * formatSet.XScale)
- height = int(float64(height) * formatSet.YScale)
- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 :=
- f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height)
- content, cNvPrID := f.drawingParser(drawingXML)
+ width = int(float64(width) * opts.ScaleX)
+ height = int(float64(height) * opts.ScaleY)
+ colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
+ content, cNvPrID, err := f.drawingParser(drawingXML)
+ if err != nil {
+ return err
+ }
twoCellAnchor := xdrCellAnchor{}
- twoCellAnchor.EditAs = formatSet.Positioning
+ twoCellAnchor.EditAs = opts.Positioning
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = formatSet.OffsetX * EMU
+ from.ColOff = opts.OffsetX * EMU
from.Row = rowStart
- from.RowOff = formatSet.OffsetY * EMU
+ from.RowOff = opts.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
@@ -1210,9 +1421,9 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
- URI: NameSpaceDrawingMLChart,
+ URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
- C: NameSpaceDrawingMLChart,
+ C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
@@ -1222,23 +1433,26 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI
graphic, _ := xml.Marshal(graphicFrame)
twoCellAnchor.GraphicFrame = string(graphic)
twoCellAnchor.ClientData = &xdrClientData{
- FLocksWithSheet: formatSet.FLocksWithSheet,
- FPrintsWithSheet: formatSet.FPrintsWithSheet,
+ FLocksWithSheet: *opts.Locked,
+ FPrintsWithSheet: *opts.PrintObject,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
- f.Drawings[drawingXML] = content
+ f.Drawings.Store(drawingXML, content)
return err
}
// addSheetDrawingChart provides a function to add chart graphic frame for
// chartsheet by given sheet, drawingXML, width, height, relationship index
// and format sets.
-func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *formatPicture) {
- content, cNvPrID := f.drawingParser(drawingXML)
+func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOptions) error {
+ content, cNvPrID, err := f.drawingParser(drawingXML)
+ if err != nil {
+ return err
+ }
absoluteAnchor := xdrCellAnchor{
- EditAs: formatSet.Positioning,
+ EditAs: opts.Positioning,
Pos: &xlsxPoint2D{},
- Ext: &xlsxExt{},
+ Ext: &aExt{},
}
graphicFrame := xlsxGraphicFrame{
@@ -1250,9 +1464,9 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
- URI: NameSpaceDrawingMLChart,
+ URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
- C: NameSpaceDrawingMLChart,
+ C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
@@ -1262,52 +1476,126 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma
graphic, _ := xml.Marshal(graphicFrame)
absoluteAnchor.GraphicFrame = string(graphic)
absoluteAnchor.ClientData = &xdrClientData{
- FLocksWithSheet: formatSet.FLocksWithSheet,
- FPrintsWithSheet: formatSet.FPrintsWithSheet,
+ FLocksWithSheet: *opts.Locked,
+ FPrintsWithSheet: *opts.PrintObject,
}
content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor)
- f.Drawings[drawingXML] = content
- return
+ f.Drawings.Store(drawingXML, content)
+ return err
}
-// deleteDrawing provides a function to delete chart graphic frame by given by
+// deleteDrawing provides a function to delete the chart graphic frame and
+// returns deleted embed relationships ID (for unique picture cell anchor) by
// given coordinates and graphic type.
-func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err error) {
+func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) ([]string, error) {
var (
- wsDr *xlsxWsDr
- deTwoCellAnchor *decodeTwoCellAnchor
+ err error
+ rID string
+ delRID, refRID []string
+ rIDMaps = map[string]int{}
+ wsDr *xlsxWsDr
+ deCellAnchor *decodeCellAnchor
)
xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{
"Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil },
"Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil },
}
- decodeTwoCellAnchorFuncs := map[string]func(anchor *decodeTwoCellAnchor) bool{
- "Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil },
- "Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil },
- }
- wsDr, _ = f.drawingParser(drawingXML)
- for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
- if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) {
- if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row {
- wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
- idx--
+ decodeCellAnchorFuncs := map[string]func(anchor *decodeCellAnchor) bool{
+ "Chart": func(anchor *decodeCellAnchor) bool { return anchor.Pic == nil },
+ "Pic": func(anchor *decodeCellAnchor) bool { return anchor.Pic != nil },
+ }
+ onAnchorCell := func(c, r int) bool { return c == col && r == row }
+ if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
+ return delRID, err
+ }
+ deleteCellAnchor := func(ca []*xdrCellAnchor) ([]*xdrCellAnchor, error) {
+ for idx := 0; idx < len(ca); idx++ {
+ if err = nil; ca[idx].From != nil && xdrCellAnchorFuncs[drawingType](ca[idx]) {
+ rID = extractEmbedRID(ca[idx].Pic, nil)
+ rIDMaps[rID]++
+ if onAnchorCell(ca[idx].From.Col, ca[idx].From.Row) {
+ refRID = append(refRID, rID)
+ ca = append(ca[:idx], ca[idx+1:]...)
+ idx--
+ rIDMaps[rID]--
+ }
+ continue
+ }
+ deCellAnchor = new(decodeCellAnchor)
+ if err = f.xmlNewDecoder(strings.NewReader("" + ca[idx].GraphicFrame + "")).
+ Decode(deCellAnchor); err != nil && err != io.EOF {
+ return ca, err
+ }
+ if err = nil; deCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deCellAnchor) {
+ rID = extractEmbedRID(nil, deCellAnchor.Pic)
+ rIDMaps[rID]++
+ if onAnchorCell(deCellAnchor.From.Col, deCellAnchor.From.Row) {
+ refRID = append(refRID, rID)
+ ca = append(ca[:idx], ca[idx+1:]...)
+ idx--
+ rIDMaps[rID]--
+ }
}
}
+ return ca, err
+ }
+ if wsDr.OneCellAnchor, err = deleteCellAnchor(wsDr.OneCellAnchor); err != nil {
+ return delRID, err
+ }
+ if wsDr.TwoCellAnchor, err = deleteCellAnchor(wsDr.TwoCellAnchor); err != nil {
+ return delRID, err
+ }
+ f.Drawings.Store(drawingXML, wsDr)
+ return getUnusedCellAnchorRID(delRID, refRID, rIDMaps), err
+}
+
+// extractEmbedRID returns embed relationship ID by giving cell anchor.
+func extractEmbedRID(pic *xlsxPic, decodePic *decodePic) string {
+ var rID string
+ if pic != nil {
+ rID = pic.BlipFill.Blip.Embed
+ }
+ if decodePic != nil {
+ rID = decodePic.BlipFill.Blip.Embed
}
- for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
- deTwoCellAnchor = new(decodeTwoCellAnchor)
- if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")).
- Decode(deTwoCellAnchor); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
- return
+ return rID
+}
+
+// getUnusedCellAnchorRID returns relationship ID lists in the cell anchor which
+// for remove.
+func getUnusedCellAnchorRID(delRID, refRID []string, rIDMaps map[string]int) []string {
+ for _, rID := range refRID {
+ if rIDMaps[rID] == 0 && inStrSlice(delRID, rID, false) == -1 {
+ delRID = append(delRID, rID)
}
- if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) {
- if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
- wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
- idx--
- }
+ }
+ return delRID
+}
+
+// deleteDrawingRels provides a function to delete relationships in
+// xl/drawings/_rels/drawings%d.xml.rels by giving drawings relationships path
+// and relationship ID.
+func (f *File) deleteDrawingRels(rels, rID string) {
+ drawingRels, _ := f.relsReader(rels)
+ if drawingRels == nil {
+ drawingRels = &xlsxRelationships{}
+ }
+ drawingRels.mu.Lock()
+ defer drawingRels.mu.Unlock()
+ for k, v := range drawingRels.Relationships {
+ if v.ID == rID {
+ drawingRels.Relationships = append(drawingRels.Relationships[:k], drawingRels.Relationships[k+1:]...)
}
}
- f.Drawings[drawingXML] = wsDr
- return err
+ f.Relationships.Store(rels, drawingRels)
+}
+
+// genAxID provides a function to generate ID for primary and secondary
+// horizontal or vertical axis.
+func (f *File) genAxID(opts *Chart) []*attrValInt {
+ opts.XAxis.axID, opts.YAxis.axID = 100000000, 100000001
+ if opts.order > 0 && opts.YAxis.Secondary {
+ opts.XAxis.axID, opts.YAxis.axID = 100000003, 100000004
+ }
+ return []*attrValInt{{Val: intPtr(opts.XAxis.axID)}, {Val: intPtr(opts.YAxis.axID)}}
}
diff --git a/drawing_test.go b/drawing_test.go
index 0a380eda39..9160824062 100644
--- a/drawing_test.go
+++ b/drawing_test.go
@@ -1,27 +1,49 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX files. Support reads and writes XLSX file generated by
-// Microsoft Excel™ 2007 and later. Support save file without losing original
-// charts of XLSX. This library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
+ "encoding/xml"
+ "sync"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func TestDrawingParser(t *testing.T) {
f := File{
- Drawings: make(map[string]*xlsxWsDr),
- XLSX: map[string][]byte{
- "charset": MacintoshCyrillicCharset,
- "wsDr": []byte(``)},
+ Drawings: sync.Map{},
+ Pkg: sync.Map{},
}
+ f.Pkg.Store("charset", MacintoshCyrillicCharset)
+ f.Pkg.Store("wsDr", []byte(xml.Header+``))
// Test with one cell anchor
- f.drawingParser("wsDr")
- // Test with unsupport charset
- f.drawingParser("charset")
+ _, _, err := f.drawingParser("wsDr")
+ assert.NoError(t, err)
+ // Test with unsupported charset
+ _, _, err = f.drawingParser("charset")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test with alternate content
+ f.Drawings = sync.Map{}
+ f.Pkg.Store("wsDr", []byte(xml.Header+``))
+ _, _, err = f.drawingParser("wsDr")
+ assert.NoError(t, err)
+}
+
+func TestDeleteDrawingRels(t *testing.T) {
+ f := NewFile()
+ // Test delete drawing relationships with unsupported charset
+ rels := "xl/drawings/_rels/drawing1.xml.rels"
+ f.Relationships.Delete(rels)
+ f.Pkg.Store(rels, MacintoshCyrillicCharset)
+ f.deleteDrawingRels(rels, "")
}
diff --git a/errors.go b/errors.go
index a31c93ab43..409f22d563 100644
--- a/errors.go
+++ b/errors.go
@@ -1,30 +1,341 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "fmt"
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+var (
+ // ErrAddVBAProject defined the error message on add the VBA project in
+ // the workbook.
+ ErrAddVBAProject = errors.New("unsupported VBA project")
+ // ErrAttrValBool defined the error message on marshal and unmarshal
+ // boolean type XML attribute.
+ ErrAttrValBool = errors.New("unexpected child of attrValBool")
+ // ErrCellCharsLength defined the error message for receiving a cell
+ // characters length that exceeds the limit.
+ ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
+ // ErrCellStyles defined the error message on cell styles exceeds the limit.
+ ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
+ // ErrColumnNumber defined the error message on receive an invalid column
+ // number.
+ ErrColumnNumber = fmt.Errorf("the column number must be greater than or equal to %d and less than or equal to %d", MinColumns, MaxColumns)
+ // ErrColumnWidth defined the error message on receive an invalid column
+ // width.
+ ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth)
+ // ErrCoordinates defined the error message on invalid coordinates tuples
+ // length.
+ ErrCoordinates = errors.New("coordinates length must be 4")
+ // ErrCustomNumFmt defined the error message on receive the empty custom number format.
+ ErrCustomNumFmt = errors.New("custom number format can not be empty")
+ // ErrDataValidationFormulaLength defined the error message for receiving a
+ // data validation formula length that exceeds the limit.
+ ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength)
+ // ErrDataValidationRange defined the error message on set decimal range
+ // exceeds limit.
+ ErrDataValidationRange = errors.New("data validation range exceeds limit")
+ // ErrDefinedNameDuplicate defined the error message on the same name
+ // already exists on the scope.
+ ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope")
+ // ErrDefinedNameScope defined the error message on not found defined name
+ // in the given scope.
+ ErrDefinedNameScope = errors.New("no defined name on the scope")
+ // ErrExistsSheet defined the error message on given sheet already exists.
+ ErrExistsSheet = errors.New("the same name sheet already exists")
+ // ErrExistsTableName defined the error message on given table already exists.
+ ErrExistsTableName = errors.New("the same name table already exists")
+ // ErrFontLength defined the error message on the length of the font
+ // family name overflow.
+ ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength)
+ // ErrFontSize defined the error message on the size of the font is invalid.
+ ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize)
+ // ErrFormControlValue defined the error message for receiving a scroll
+ // value exceeds limit.
+ ErrFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue)
+ // ErrGroupSheets defined the error message on group sheets.
+ ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
+ // ErrImgExt defined the error message on receive an unsupported image
+ // extension.
+ ErrImgExt = errors.New("unsupported image extension")
+ // ErrInvalidFormula defined the error message on receive an invalid
+ // formula.
+ ErrInvalidFormula = errors.New("formula not valid")
+ // ErrMaxFilePathLength defined the error message on receive the file path
+ // length overflow.
+ ErrMaxFilePathLength = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength)
+ // ErrMaxRowHeight defined the error message on receive an invalid row
+ // height.
+ ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight)
+ // ErrMaxRows defined the error message on receive a row number exceeds maximum limit.
+ ErrMaxRows = errors.New("row number exceeds maximum limit")
+ // ErrNameLength defined the error message on receiving the defined name or
+ // table name length exceeds the limit.
+ ErrNameLength = fmt.Errorf("the name length exceeds the %d characters limit", MaxFieldLength)
+ // ErrOptionsUnzipSizeLimit defined the error message for receiving
+ // invalid UnzipSizeLimit and UnzipXMLSizeLimit.
+ ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit")
+ // ErrOutlineLevel defined the error message on receive an invalid outline
+ // level number.
+ ErrOutlineLevel = errors.New("invalid outline level")
+ // ErrPageSetupAdjustTo defined the error message for receiving a page setup
+ // adjust to value exceeds limit.
+ ErrPageSetupAdjustTo = errors.New("adjust to value must be between 10 and 400")
+ // ErrParameterInvalid defined the error message on receive the invalid
+ // parameter.
+ ErrParameterInvalid = errors.New("parameter is invalid")
+ // ErrParameterRequired defined the error message on receive the empty
+ // parameter.
+ ErrParameterRequired = errors.New("parameter is required")
+ // ErrPasswordLengthInvalid defined the error message on invalid password
+ // length.
+ ErrPasswordLengthInvalid = errors.New("password length invalid")
+ // ErrPivotTableClassicLayout defined the error message on enable
+ // ClassicLayout and CompactData in the same time.
+ ErrPivotTableClassicLayout = errors.New("cannot enable ClassicLayout and CompactData in the same time")
+ // ErrSave defined the error message for saving file.
+ ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write")
+ // ErrSheetIdx defined the error message on receive the invalid worksheet
+ // index.
+ ErrSheetIdx = errors.New("invalid worksheet index")
+ // ErrSheetNameBlank defined the error message on receive the blank sheet
+ // name.
+ ErrSheetNameBlank = errors.New("the sheet name can not be blank")
+ // ErrSheetNameInvalid defined the error message on receive the sheet name
+ // contains invalid characters.
+ ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]")
+ // ErrSheetNameLength defined the error message on receiving the sheet
+ // name length exceeds the limit.
+ ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
+ // ErrSheetNameSingleQuote defined the error message on the first or last
+ // character of the sheet name was a single quote.
+ ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote")
+ // ErrSparkline defined the error message on receive the invalid sparkline
+ // parameters.
+ ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters")
+ // ErrSparklineLocation defined the error message on missing Location
+ // parameters
+ ErrSparklineLocation = errors.New("parameter 'Location' is required")
+ // ErrSparklineRange defined the error message on missing sparkline Range
+ // parameters
+ ErrSparklineRange = errors.New("parameter 'Range' is required")
+ // ErrSparklineStyle defined the error message on receive the invalid
+ // sparkline Style parameters.
+ ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35")
+ // ErrSparklineType defined the error message on receive the invalid
+ // sparkline Type parameters.
+ ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
+ // ErrStreamSetColStyle defined the error message on set column style in
+ // stream writing mode.
+ ErrStreamSetColStyle = errors.New("must call the SetColStyle function before the SetRow function")
+ // ErrStreamSetColWidth defined the error message on set column width in
+ // stream writing mode.
+ ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function")
+ // ErrStreamSetPanes defined the error message on set panes in stream
+ // writing mode.
+ ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function")
+ // ErrTotalSheetHyperlinks defined the error message on hyperlinks count
+ // overflow.
+ ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet")
+ // ErrUnknownEncryptMechanism defined the error message on unsupported
+ // encryption mechanism.
+ ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
+ // ErrUnprotectSheet defined the error message on worksheet has set no
+ // protection.
+ ErrUnprotectSheet = errors.New("worksheet has set no protect")
+ // ErrUnprotectSheetPassword defined the error message on remove sheet
+ // protection with password verification failed.
+ ErrUnprotectSheetPassword = errors.New("worksheet protect password not match")
+ // ErrUnprotectWorkbook defined the error message on workbook has set no
+ // protection.
+ ErrUnprotectWorkbook = errors.New("workbook has set no protect")
+ // ErrUnprotectWorkbookPassword defined the error message on remove workbook
+ // protection with password verification failed.
+ ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
+ // ErrUnsupportedEncryptMechanism defined the error message on unsupported
+ // encryption mechanism.
+ ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism")
+ // ErrUnsupportedHashAlgorithm defined the error message on unsupported
+ // hash algorithm.
+ ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
+ // ErrUnsupportedNumberFormat defined the error message on unsupported number format
+ // expression.
+ ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
+ // ErrWorkbookFileFormat defined the error message on receive an
+ // unsupported workbook file format.
+ ErrWorkbookFileFormat = errors.New("unsupported workbook file format")
+ // ErrWorkbookPassword defined the error message on receiving the incorrect
+ // workbook password.
+ ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
+)
+
+// ErrSheetNotExist defined an error of sheet that does not exist.
+type ErrSheetNotExist struct {
+ SheetName string
+}
+
+// Error returns the error message on receiving the non existing sheet name.
+func (err ErrSheetNotExist) Error() string {
+ return fmt.Sprintf("sheet %s does not exist", err.SheetName)
+}
+
+// newCellNameToCoordinatesError defined the error message on converts
+// alphanumeric cell name to coordinates.
+func newCellNameToCoordinatesError(cell string, err error) error {
+ return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err)
+}
+
+// newCoordinatesToCellNameError defined the error message on converts [X, Y]
+// coordinates to alpha-numeric cell name.
+func newCoordinatesToCellNameError(col, row int) error {
+ return fmt.Errorf("invalid cell reference [%d, %d]", col, row)
+}
+
+// newFieldLengthError defined the error message on receiving the field length
+// overflow.
+func newFieldLengthError(name string) error {
+ return fmt.Errorf("field %s must be less than or equal to 255 characters", name)
+}
+
+// newInvalidAutoFilterColumnError defined the error message on receiving the
+// incorrect index of column.
+func newInvalidAutoFilterColumnError(col string) error {
+ return fmt.Errorf("incorrect index of column %q", col)
+}
+
+// newInvalidAutoFilterExpError defined the error message on receiving the
+// incorrect number of tokens in criteria expression.
+func newInvalidAutoFilterExpError(exp string) error {
+ return fmt.Errorf("incorrect number of tokens in criteria %q", exp)
+}
+
+// newInvalidAutoFilterOperatorError defined the error message on receiving the
+// incorrect expression operator.
+func newInvalidAutoFilterOperatorError(op, exp string) error {
+ return fmt.Errorf("the operator %q in expression %q is not valid in relation to Blanks/NonBlanks", op, exp)
+}
+
+// newInvalidCellNameError defined the error message on receiving the invalid
+// cell name.
+func newInvalidCellNameError(cell string) error {
+ return fmt.Errorf("invalid cell name %q", cell)
+}
+
+// newInvalidColumnNameError defined the error message on receiving the
+// invalid column name.
func newInvalidColumnNameError(col string) error {
return fmt.Errorf("invalid column name %q", col)
}
+// newInvalidExcelDateError defined the error message on receiving the data
+// with negative values.
+func newInvalidExcelDateError(dateValue float64) error {
+ return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue)
+}
+
+// newInvalidLinkTypeError defined the error message on receiving the invalid
+// hyper link type.
+func newInvalidLinkTypeError(linkType string) error {
+ return fmt.Errorf("invalid link type %q", linkType)
+}
+
+// newInvalidNameError defined the error message on receiving the invalid
+// defined name or table name.
+func newInvalidNameError(name string) error {
+ return fmt.Errorf("invalid name %q, the name should be starts with a letter or underscore, can not include a space or character, and can not conflict with an existing name in the workbook", name)
+}
+
+// newInvalidOptionalValue defined the error message on receiving the invalid
+// optional value.
+func newInvalidOptionalValue(name, value string, values []string) error {
+ return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, strings.Join(values, ", "))
+}
+
+// newInvalidRowNumberError defined the error message on receiving the invalid
+// row number.
func newInvalidRowNumberError(row int) error {
return fmt.Errorf("invalid row number %d", row)
}
-func newInvalidCellNameError(cell string) error {
- return fmt.Errorf("invalid cell name %q", cell)
+// newInvalidSlicerNameError defined the error message on receiving the invalid
+// slicer name.
+func newInvalidSlicerNameError(name string) error {
+ return fmt.Errorf("invalid slicer name %q", name)
}
-func newInvalidExcelDateError(dateValue float64) error {
- return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue)
+// newInvalidStyleID defined the error message on receiving the invalid style
+// ID.
+func newInvalidStyleID(styleID int) error {
+ return fmt.Errorf("invalid style ID %d", styleID)
+}
+
+// newNoExistSlicerError defined the error message on receiving the non existing
+// slicer name.
+func newNoExistSlicerError(name string) error {
+ return fmt.Errorf("slicer %s does not exist", name)
+}
+
+// newNoExistTableError defined the error message on receiving the non existing
+// table name.
+func newNoExistTableError(name string) error {
+ return fmt.Errorf("table %s does not exist", name)
+}
+
+// newNotWorksheetError defined the error message on receiving a sheet which
+// not a worksheet.
+func newNotWorksheetError(name string) error {
+ return fmt.Errorf("sheet %s is not a worksheet", name)
+}
+
+// newPivotTableDataRangeError defined the error message on receiving the
+// invalid pivot table data range.
+func newPivotTableDataRangeError(msg string) error {
+ return fmt.Errorf("parameter 'DataRange' parsing error: %s", msg)
+}
+
+// newPivotTableRangeError defined the error message on receiving the invalid
+// pivot table range.
+func newPivotTableRangeError(msg string) error {
+ return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", msg)
+}
+
+// newStreamSetRowError defined the error message on the stream writer
+// receiving the non-ascending row number.
+func newStreamSetRowError(row int) error {
+ return fmt.Errorf("row %d has already been written", row)
+}
+
+// newUnknownFilterTokenError defined the error message on receiving a unknown
+// filter operator token.
+func newUnknownFilterTokenError(token string) error {
+ return fmt.Errorf("unknown operator: %s", token)
+}
+
+// newUnsupportedChartType defined the error message on receiving the chart
+// type are unsupported.
+func newUnsupportedChartType(chartType ChartType) error {
+ return fmt.Errorf("unsupported chart type %d", chartType)
+}
+
+// newUnzipSizeLimitError defined the error message on unzip size exceeds the
+// limit.
+func newUnzipSizeLimitError(unzipSizeLimit int64) error {
+ return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit)
+}
+
+// newViewIdxError defined the error message on receiving a invalid sheet view
+// index.
+func newViewIdxError(viewIndex int) error {
+ return fmt.Errorf("view index %d out of range", viewIndex)
}
diff --git a/errors_test.go b/errors_test.go
index 207e80aabb..971802f79f 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -21,5 +21,5 @@ func TestNewInvalidCellNameError(t *testing.T) {
}
func TestNewInvalidExcelDateError(t *testing.T) {
- assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported supported")
+ assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported")
}
diff --git a/excelize.go b/excelize.go
index 5cc88e961f..8448999ab2 100644
--- a/excelize.go
+++ b/excelize.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
//
// See https://xuri.me/excelize for more information about this package.
package excelize
@@ -16,116 +16,219 @@ import (
"archive/zip"
"bytes"
"encoding/xml"
- "errors"
- "fmt"
"io"
- "io/ioutil"
"os"
- "path"
+ "path/filepath"
"strconv"
"strings"
+ "sync"
"golang.org/x/net/html/charset"
)
// File define a populated spreadsheet file struct.
type File struct {
- xmlAttr map[string][]xml.Attr
- checked map[string]bool
+ mu sync.Mutex
+ checked sync.Map
+ formulaChecked bool
+ options *Options
+ sharedStringItem [][]uint
+ sharedStringsMap map[string]int
+ sharedStringTemp *os.File
sheetMap map[string]string
+ streams map[string]*StreamWriter
+ tempFiles sync.Map
+ xmlAttr sync.Map
CalcChain *xlsxCalcChain
+ CharsetReader charsetTranscoderFn
Comments map[string]*xlsxComments
ContentTypes *xlsxTypes
- Drawings map[string]*xlsxWsDr
+ DecodeVMLDrawing map[string]*decodeVmlDrawing
+ DecodeCellImages *decodeCellImages
+ Drawings sync.Map
Path string
+ Pkg sync.Map
+ Relationships sync.Map
SharedStrings *xlsxSST
- sharedStringsMap map[string]int
- Sheet map[string]*xlsxWorksheet
+ Sheet sync.Map
SheetCount int
Styles *xlsxStyleSheet
- Theme *xlsxTheme
- DecodeVMLDrawing map[string]*decodeVmlDrawing
+ Theme *decodeTheme
VMLDrawing map[string]*vmlDrawing
+ VolatileDeps *xlsxVolTypes
WorkBook *xlsxWorkbook
- Relationships map[string]*xlsxRelationships
- XLSX map[string][]byte
- CharsetReader charsetTranscoderFn
}
+// charsetTranscoderFn set user-defined codepage transcoder function for open
+// the spreadsheet from non-UTF-8 encoding.
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
-// OpenFile take the name of an spreadsheet file and returns a populated
-// spreadsheet file struct for it.
-func OpenFile(filename string) (*File, error) {
- file, err := os.Open(filename)
+// Options define the options for opening and reading the spreadsheet.
+//
+// MaxCalcIterations specifies the maximum iterations for iterative
+// calculation, the default value is 0.
+//
+// Password specifies the password of the spreadsheet in plain text.
+//
+// RawCellValue specifies if apply the number format for the cell value or get
+// the raw value.
+//
+// UnzipSizeLimit specifies to unzip size limit in bytes on open the
+// spreadsheet, this value should be greater than or equal to
+// UnzipXMLSizeLimit, the default size limit is 16GB.
+//
+// UnzipXMLSizeLimit specifies the memory limit on unzipping worksheet and
+// shared string table in bytes, worksheet XML will be extracted to system
+// temporary directory when the file size is over this value, this value
+// should be less than or equal to UnzipSizeLimit, the default value is
+// 16MB.
+//
+// ShortDatePattern specifies the short date number format code. In the
+// spreadsheet applications, date formats display date and time serial numbers
+// as date values. Date formats that begin with an asterisk (*) respond to
+// changes in regional date and time settings that are specified for the
+// operating system. Formats without an asterisk are not affected by operating
+// system settings. The ShortDatePattern used for specifies apply date formats
+// that begin with an asterisk.
+//
+// LongDatePattern specifies the long date number format code.
+//
+// LongTimePattern specifies the long time number format code.
+//
+// CultureInfo specifies the country code for applying built-in language number
+// format code these effect by the system's local language settings.
+type Options struct {
+ MaxCalcIterations uint
+ Password string
+ RawCellValue bool
+ UnzipSizeLimit int64
+ UnzipXMLSizeLimit int64
+ ShortDatePattern string
+ LongDatePattern string
+ LongTimePattern string
+ CultureInfo CultureName
+}
+
+// OpenFile take the name of a spreadsheet file and returns a populated
+// spreadsheet file struct for it. For example, open spreadsheet with
+// password protection:
+//
+// f, err := excelize.OpenFile("Book1.xlsx", excelize.Options{Password: "password"})
+//
+// Close the file by Close function after opening the spreadsheet.
+func OpenFile(filename string, opts ...Options) (*File, error) {
+ file, err := os.Open(filepath.Clean(filename))
if err != nil {
return nil, err
}
- defer file.Close()
- f, err := OpenReader(file)
+ f, err := OpenReader(file, opts...)
if err != nil {
- return nil, err
+ if closeErr := file.Close(); closeErr != nil {
+ return f, closeErr
+ }
+ return f, err
}
f.Path = filename
- return f, nil
+ return f, file.Close()
}
// newFile is object builder
func newFile() *File {
return &File{
- xmlAttr: make(map[string][]xml.Attr),
- checked: make(map[string]bool),
+ options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
+ xmlAttr: sync.Map{},
+ checked: sync.Map{},
sheetMap: make(map[string]string),
+ tempFiles: sync.Map{},
Comments: make(map[string]*xlsxComments),
- Drawings: make(map[string]*xlsxWsDr),
+ Drawings: sync.Map{},
sharedStringsMap: make(map[string]int),
- Sheet: make(map[string]*xlsxWorksheet),
+ Sheet: sync.Map{},
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
VMLDrawing: make(map[string]*vmlDrawing),
- Relationships: make(map[string]*xlsxRelationships),
+ Relationships: sync.Map{},
CharsetReader: charset.NewReaderLabel,
}
}
+// checkOpenReaderOptions check and validate options field value for open
+// reader.
+func (f *File) checkOpenReaderOptions() error {
+ if f.options.UnzipSizeLimit == 0 {
+ f.options.UnzipSizeLimit = UnzipSizeLimit
+ if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
+ f.options.UnzipSizeLimit = f.options.UnzipXMLSizeLimit
+ }
+ }
+ if f.options.UnzipXMLSizeLimit == 0 {
+ f.options.UnzipXMLSizeLimit = StreamChunkSize
+ if f.options.UnzipSizeLimit < f.options.UnzipXMLSizeLimit {
+ f.options.UnzipXMLSizeLimit = f.options.UnzipSizeLimit
+ }
+ }
+ if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
+ return ErrOptionsUnzipSizeLimit
+ }
+ return f.checkDateTimePattern()
+}
+
// OpenReader read data stream from io.Reader and return a populated
// spreadsheet file.
-func OpenReader(r io.Reader) (*File, error) {
- b, err := ioutil.ReadAll(r)
+func OpenReader(r io.Reader, opts ...Options) (*File, error) {
+ b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
-
+ f := newFile()
+ f.options = f.getOptions(opts...)
+ if err = f.checkOpenReaderOptions(); err != nil {
+ return nil, err
+ }
+ if bytes.Contains(b, oleIdentifier) {
+ if b, err = Decrypt(b, f.options); err != nil {
+ return nil, ErrWorkbookFileFormat
+ }
+ }
zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
if err != nil {
- identifier := []byte{
- // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier
- 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00,
- 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00,
- 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00,
- 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00,
- 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
- }
- if bytes.Contains(b, identifier) {
- return nil, errors.New("not support encrypted file currently")
+ if len(f.options.Password) > 0 {
+ return nil, ErrWorkbookPassword
}
return nil, err
}
-
- file, sheetCount, err := ReadZipReader(zr)
+ file, sheetCount, err := f.ReadZipReader(zr)
if err != nil {
return nil, err
}
- f := newFile()
- f.SheetCount, f.XLSX = sheetCount, file
- f.CalcChain = f.calcChainReader()
- f.sheetMap = f.getSheetMap()
- f.Styles = f.stylesReader()
- f.Theme = f.themeReader()
- return f, nil
+ f.SheetCount = sheetCount
+ for k, v := range file {
+ f.Pkg.Store(k, v)
+ }
+ if f.CalcChain, err = f.calcChainReader(); err != nil {
+ return f, err
+ }
+ if f.sheetMap, err = f.getSheetMap(); err != nil {
+ return f, err
+ }
+ if f.Styles, err = f.stylesReader(); err != nil {
+ return f, err
+ }
+ f.Theme, err = f.themeReader()
+ return f, err
+}
+
+// getOptions provides a function to parse the optional settings for open
+// and reading spreadsheet.
+func (f *File) getOptions(opts ...Options) *Options {
+ options := f.options
+ for _, opt := range opts {
+ options = &opt
+ }
+ return options
}
// CharsetTranscoder Set user defined codepage transcoder function for open
-// XLSX from non UTF-8 encoding.
+// workbook from non UTF-8 encoding.
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }
// Creates new XML decoder with charset reader.
@@ -136,103 +239,193 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
}
// setDefaultTimeStyle provides a function to set default numbers format for
-// time.Time type cell value by given worksheet name, cell coordinates and
+// time.Time type cell value by given worksheet name, cell reference and
// number format code.
-func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error {
- s, err := f.GetCellStyle(sheet, axis)
+func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error {
+ styleIdx, err := f.GetCellStyle(sheet, cell)
if err != nil {
return err
}
- if s == 0 {
- style, _ := f.NewStyle(&Style{NumFmt: format})
- _ = f.SetCellStyle(sheet, axis, axis, style)
+ if styleIdx == 0 {
+ styleIdx, _ = f.NewStyle(&Style{NumFmt: format})
+ } else {
+ style, _ := f.GetStyle(styleIdx)
+ style.NumFmt = format
+ styleIdx, _ = f.NewStyle(style)
}
- return err
+ return f.SetCellStyle(sheet, cell, cell, styleIdx)
}
// workSheetReader provides a function to get the pointer to the structure
// after deserialization by given worksheet name.
-func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) {
+func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
var (
name string
ok bool
)
-
- if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok {
- err = fmt.Errorf("sheet %s is not exist", sheet)
+ if err = checkSheetName(sheet); err != nil {
return
}
- if xlsx = f.Sheet[name]; f.Sheet[name] == nil {
- if strings.HasPrefix(name, "xl/chartsheets") {
- err = fmt.Errorf("sheet %s is chart sheet", sheet)
+ if name, ok = f.getSheetXMLPath(sheet); !ok {
+ err = ErrSheetNotExist{sheet}
+ return
+ }
+ if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
+ ws = worksheet.(*xlsxWorksheet)
+ return
+ }
+ for _, sheetType := range []string{"xl/chartsheets", "xl/dialogsheet", "xl/macrosheet"} {
+ if strings.HasPrefix(name, sheetType) {
+ err = newNotWorksheetError(sheet)
return
}
- xlsx = new(xlsxWorksheet)
- if _, ok := f.xmlAttr[name]; !ok {
- d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name))))
- f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...)
+ }
+ ws = new(xlsxWorksheet)
+ if attrs, ok := f.xmlAttr.Load(name); !ok {
+ d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name))))
+ if attrs == nil {
+ attrs = []xml.Attr{}
}
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
- Decode(xlsx); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
+ attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
+ f.xmlAttr.Store(name, attrs)
+ }
+ if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))).
+ Decode(ws); err != nil && err != io.EOF {
+ return
+ }
+ err = nil
+ if _, ok = f.checked.Load(name); !ok {
+ ws.checkSheet()
+ if err = ws.checkRow(); err != nil {
return
}
- err = nil
- if f.checked == nil {
- f.checked = make(map[string]bool)
- }
- if ok = f.checked[name]; !ok {
- checkSheet(xlsx)
- if err = checkRow(xlsx); err != nil {
- return
- }
- f.checked[name] = true
- }
- f.Sheet[name] = xlsx
+ f.checked.Store(name, true)
}
-
+ f.Sheet.Store(name, ws)
return
}
// checkSheet provides a function to fill each row element and make that is
// continuous in a worksheet of XML.
-func checkSheet(xlsx *xlsxWorksheet) {
- var row int
- for _, r := range xlsx.SheetData.Row {
+func (ws *xlsxWorksheet) checkSheet() {
+ var (
+ row int
+ r0Rows []xlsxRow
+ lastRowNum = func(r xlsxRow) int {
+ var num int
+ for _, cell := range r.C {
+ if _, row, err := CellNameToCoordinates(cell.R); err == nil {
+ if row > num {
+ num = row
+ }
+ }
+ }
+ return num
+ }
+ )
+ for i := 0; i < len(ws.SheetData.Row); i++ {
+ r := ws.SheetData.Row[i]
+ if r.R == 0 || r.R == row {
+ num := lastRowNum(r)
+ if num > row {
+ row = num
+ }
+ if num == 0 {
+ row++
+ }
+ r.R = row
+ r0Rows = append(r0Rows, r)
+ ws.SheetData.Row = append(ws.SheetData.Row[:i], ws.SheetData.Row[i+1:]...)
+ i--
+ continue
+ }
if r.R != 0 && r.R > row {
row = r.R
- continue
}
- row++
}
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
row = 0
- for _, r := range xlsx.SheetData.Row {
+ for _, r := range ws.SheetData.Row {
if r.R != 0 {
sheetData.Row[r.R-1] = r
row = r.R
- continue
}
- row++
- r.R = row
- sheetData.Row[row-1] = r
+ }
+ for _, r0Row := range r0Rows {
+ sheetData.Row[r0Row.R-1].R = r0Row.R
+ ws.checkSheetR0(&sheetData, &r0Row, true)
}
for i := 1; i <= row; i++ {
sheetData.Row[i-1].R = i
+ ws.checkSheetR0(&sheetData, &sheetData.Row[i-1], false)
}
- xlsx.SheetData = sheetData
+}
+
+// checkSheetR0 handle the row element with r="0" attribute, cells in this row
+// could be disorderly, the cell in this row can be used as the value of
+// which cell is empty in the normal rows.
+func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, rowData *xlsxRow, r0 bool) {
+ checkRow := func(col, row int, r0 bool, cell xlsxC) {
+ rowIdx := row - 1
+ columns, colIdx := len(sheetData.Row[rowIdx].C), col-1
+ for c := columns; c < col; c++ {
+ sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{})
+ }
+ if !sheetData.Row[rowIdx].C[colIdx].hasValue() {
+ sheetData.Row[rowIdx].C[colIdx] = cell
+ }
+ if r0 {
+ sheetData.Row[rowIdx].C[colIdx] = cell
+ }
+ }
+ var err error
+ for i, cell := range rowData.C {
+ col, row := i+1, rowData.R
+ if cell.R == "" {
+ checkRow(col, row, r0, cell)
+ continue
+ }
+ if col, row, err = CellNameToCoordinates(cell.R); err == nil && r0 {
+ checkRow(col, row, r0, cell)
+ }
+ }
+ ws.SheetData = *sheetData
+}
+
+// setRels provides a function to set relationships by given relationship ID,
+// XML path, relationship type, target and target mode.
+func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
+ rels, _ := f.relsReader(relPath)
+ if rels == nil || rID == "" {
+ return f.addRels(relPath, relType, target, targetMode)
+ }
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
+ var ID int
+ for i, rel := range rels.Relationships {
+ if rel.ID == rID {
+ rels.Relationships[i].Type = relType
+ rels.Relationships[i].Target = target
+ rels.Relationships[i].TargetMode = targetMode
+ ID, _ = strconv.Atoi(strings.TrimPrefix(rID, "rId"))
+ break
+ }
+ }
+ return ID
}
// addRels provides a function to add relationships by given XML path,
// relationship type, target and target mode.
func (f *File) addRels(relPath, relType, target, targetMode string) int {
- var uniqPart = map[string]string{
+ uniqPart := map[string]string{
SourceRelationshipSharedStrings: "/xl/sharedStrings.xml",
}
- rels := f.relsReader(relPath)
+ rels, _ := f.relsReader(relPath)
if rels == nil {
rels = &xlsxRelationships{}
}
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
var rID int
for idx, rel := range rels.Relationships {
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
@@ -256,49 +449,54 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
Target: target,
TargetMode: targetMode,
})
- f.Relationships[relPath] = rels
+ f.Relationships.Store(relPath, rels)
return rID
}
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
-// Office Excel 2007 and 2010. This function will be remove value tag when met a
+// Office Excel application. This function will be remove value tag when met a
// cell have a linked value. Reference
-// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating
+// https://learn.microsoft.com/en-us/archive/msdn-technet-forums/e16bae1f-6a2c-4325-8013-e989a3479066
//
-// Notice: after open XLSX file Excel will be update linked value and generate
-// new value and will prompt save file or not.
+// Notice: after opening generated workbook, Excel will update the linked value
+// and generate a new value and will prompt to save the file or not.
//
// For example:
//
-//
-//
-// SUM(Sheet2!D2,Sheet2!D11)
-// 100
-//
-//
+//
+//
+// SUM(Sheet2!D2,Sheet2!D11)
+// 100
+//
+//
//
// to
//
-//
-//
-// SUM(Sheet2!D2,Sheet2!D11)
-//
-//
-//
+//
+//
+// SUM(Sheet2!D2,Sheet2!D11)
+//
+//
func (f *File) UpdateLinkedValue() error {
- wb := f.workbookReader()
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
// recalculate formulas
wb.CalcPr = nil
for _, name := range f.GetSheetList() {
- xlsx, err := f.workSheetReader(name)
+ ws, err := f.workSheetReader(name)
if err != nil {
+ if err.Error() == newNotWorksheetError(name).Error() {
+ continue
+ }
return err
}
- for indexR := range xlsx.SheetData.Row {
- for indexC, col := range xlsx.SheetData.Row[indexR].C {
+ for indexR := range ws.SheetData.Row {
+ for indexC, col := range ws.SheetData.Row[indexR].C {
if col.F != nil && col.V != "" {
- xlsx.SheetData.Row[indexR].C[indexC].V = ""
- xlsx.SheetData.Row[indexR].C[indexC].T = ""
+ ws.SheetData.Row[indexR].C[indexC].V = ""
+ ws.SheetData.Row[indexR].C[indexC].T = ""
}
}
}
@@ -307,32 +505,44 @@ func (f *File) UpdateLinkedValue() error {
}
// AddVBAProject provides the method to add vbaProject.bin file which contains
-// functions and/or macros. The file extension should be .xlsm. For example:
-//
-// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil {
-// fmt.Println(err)
-// }
-// if err := f.AddVBAProject("vbaProject.bin"); err != nil {
-// fmt.Println(err)
-// }
-// if err := f.SaveAs("macros.xlsm"); err != nil {
-// fmt.Println(err)
-// }
+// functions and/or macros. The file extension should be XLSM or XLTM. For
+// example:
//
-func (f *File) AddVBAProject(bin string) error {
+// codeName := "Sheet1"
+// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
+// CodeName: &codeName,
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// file, err := os.ReadFile("vbaProject.bin")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.AddVBAProject(file); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("macros.xlsm"); err != nil {
+// fmt.Println(err)
+// return
+// }
+func (f *File) AddVBAProject(file []byte) error {
var err error
// Check vbaProject.bin exists first.
- if _, err = os.Stat(bin); os.IsNotExist(err) {
- return err
+ if !bytes.Contains(file, oleIdentifier) {
+ return ErrAddVBAProject
}
- if path.Ext(bin) != ".bin" {
- return errors.New("unsupported VBA project extension")
+ rels, err := f.relsReader(f.getWorkbookRelsPath())
+ if err != nil {
+ return err
}
- f.setContentTypePartVBAProjectExtensions()
- wb := f.relsReader("xl/_rels/workbook.xml.rels")
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
var rID int
var ok bool
- for _, rel := range wb.Relationships {
+ for _, rel := range rels.Relationships {
if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject {
ok = true
continue
@@ -344,22 +554,26 @@ func (f *File) AddVBAProject(bin string) error {
}
rID++
if !ok {
- wb.Relationships = append(wb.Relationships, xlsxRelationship{
+ rels.Relationships = append(rels.Relationships, xlsxRelationship{
ID: "rId" + strconv.Itoa(rID),
Target: "vbaProject.bin",
Type: SourceRelationshipVBAProject,
})
}
- file, _ := ioutil.ReadFile(bin)
- f.XLSX["xl/vbaProject.bin"] = file
+ f.Pkg.Store("xl/vbaProject.bin", file)
return err
}
-// setContentTypePartVBAProjectExtensions provides a function to set the
-// content type for relationship parts and the main document part.
-func (f *File) setContentTypePartVBAProjectExtensions() {
+// setContentTypePartProjectExtensions provides a function to set the content
+// type for relationship parts and the main document part.
+func (f *File) setContentTypePartProjectExtensions(contentType string) error {
var ok bool
- content := f.contentTypesReader()
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
for _, v := range content.Defaults {
if v.Extension == "bin" {
ok = true
@@ -367,7 +581,7 @@ func (f *File) setContentTypePartVBAProjectExtensions() {
}
for idx, o := range content.Overrides {
if o.PartName == "/xl/workbook.xml" {
- content.Overrides[idx].ContentType = ContentTypeMacro
+ content.Overrides[idx].ContentType = contentType
}
}
if !ok {
@@ -376,4 +590,79 @@ func (f *File) setContentTypePartVBAProjectExtensions() {
ContentType: ContentTypeVBA,
})
}
+ return err
+}
+
+// metadataReader provides a function to get the pointer to the structure
+// after deserialization of xl/metadata.xml.
+func (f *File) metadataReader() (*xlsxMetadata, error) {
+ var mataData xlsxMetadata
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLMetadata)))).
+ Decode(&mataData); err != nil && err != io.EOF {
+ return &mataData, err
+ }
+ return &mataData, nil
+}
+
+// richValueReader provides a function to get the pointer to the structure after
+// deserialization of xl/richData/richvalue.xml.
+func (f *File) richValueReader() (*xlsxRichValueData, error) {
+ var richValue xlsxRichValueData
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))).
+ Decode(&richValue); err != nil && err != io.EOF {
+ return &richValue, err
+ }
+ return &richValue, nil
+}
+
+// richValueRelReader provides a function to get the pointer to the structure
+// after deserialization of xl/richData/richValueRel.xml.
+func (f *File) richValueRelReader() (*xlsxRichValueRels, error) {
+ var richValueRels xlsxRichValueRels
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))).
+ Decode(&richValueRels); err != nil && err != io.EOF {
+ return &richValueRels, err
+ }
+ return &richValueRels, nil
+}
+
+// richValueWebImageReader provides a function to get the pointer to the
+// structure after deserialization of xl/richData/rdRichValueWebImage.xml.
+func (f *File) richValueWebImageReader() (*xlsxWebImagesSupportingRichData, error) {
+ var richValueWebImages xlsxWebImagesSupportingRichData
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueWebImagePart)))).
+ Decode(&richValueWebImages); err != nil && err != io.EOF {
+ return &richValueWebImages, err
+ }
+ return &richValueWebImages, nil
+}
+
+// getRichDataRichValueRelRelationships provides a function to get relationships
+// from xl/richData/_rels/richValueRel.xml.rels by given relationship ID.
+func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship {
+ if rels, _ := f.relsReader(defaultXMLRdRichValueRelRels); rels != nil {
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
+ for _, v := range rels.Relationships {
+ if v.ID == rID {
+ return &v
+ }
+ }
+ }
+ return nil
+}
+
+// getRichValueWebImageRelationships provides a function to get relationships
+// from xl/richData/_rels/rdRichValueWebImage.xml.rels by given relationship ID.
+func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship {
+ if rels, _ := f.relsReader(defaultXMLRdRichValueWebImagePartRels); rels != nil {
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
+ for _, v := range rels.Relationships {
+ if v.ID == rID {
+ return &v
+ }
+ }
+ }
+ return nil
}
diff --git a/excelize_test.go b/excelize_test.go
index c2bd3ad828..9684db2cd6 100644
--- a/excelize_test.go
+++ b/excelize_test.go
@@ -1,6 +1,7 @@
package excelize
import (
+ "archive/zip"
"bytes"
"compress/gzip"
"encoding/xml"
@@ -9,12 +10,13 @@ import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
- "io/ioutil"
+ "io"
"math"
"os"
"path/filepath"
"strconv"
"strings"
+ "sync"
"testing"
"time"
@@ -22,76 +24,93 @@ import (
)
func TestOpenFile(t *testing.T) {
- // Test update the spreadsheet file.
+ // Test update the spreadsheet file
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
- // Test get all the rows in a not exists worksheet.
+ // Test get all the rows in a not exists worksheet
_, err = f.GetRows("Sheet4")
- assert.EqualError(t, err, "sheet Sheet4 is not exist")
- // Test get all the rows in a worksheet.
+ assert.EqualError(t, err, "sheet Sheet4 does not exist")
+ // Test get all the rows with invalid sheet name
+ _, err = f.GetRows("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test get all the rows in a worksheet
rows, err := f.GetRows("Sheet2")
- assert.NoError(t, err)
- for _, row := range rows {
- for _, cell := range row {
- t.Log(cell, "\t")
- }
- t.Log("\r\n")
+ expected := [][]string{
+ {"Monitor", "", "Brand", "", "inlineStr"},
+ {"> 23 Inch", "19", "HP", "200"},
+ {"20-23 Inch", "24", "DELL", "450"},
+ {"17-20 Inch", "56", "Lenove", "200"},
+ {"< 17 Inch", "21", "SONY", "510"},
+ {"", "", "Acer", "315"},
+ {"", "", "IBM", "127"},
+ {"", "", "ASUS", "89"},
+ {"", "", "Apple", "348"},
+ {"", "", "SAMSUNG", "53"},
+ {"", "", "Other", "37", "", "", "", "", ""},
}
- assert.NoError(t, f.UpdateLinkedValue())
+ assert.NoError(t, err)
+ assert.Equal(t, expected, rows)
- assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)))
- assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)))
+ assert.NoError(t, f.UpdateLinkedValue())
- // Test set cell value with illegal row number.
- assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)),
- `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(100.1588, 'f', -1, 32)))
+ assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(-100.1588, 'f', -1, 64)))
+ // Test set cell value with invalid sheet name
+ assert.EqualError(t, f.SetCellDefault("Sheet:1", "A1", ""), ErrSheetNameInvalid.Error())
+ // Test set cell value with illegal row number
+ assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(-100.1588, 'f', -1, 64)),
+ newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100))
- // Test set cell integer value with illegal row number.
- assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ // Test set cell integer value with illegal row number
+ assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ // Test set cell integer value with invalid sheet name
+ assert.EqualError(t, f.SetCellInt("Sheet:1", "A1", 100), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns"))
- // Test max characters in a cell.
- assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)))
- f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.")
- // Test set worksheet name with illegal name.
- f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.")
- assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 is not exist")
- assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 is not exist")
- assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist")
-
- // Test set cell string value with illegal row number.
- assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ // Test max characters in a cell
+ assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", TotalCellChars+2)))
+ _, err = f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.")
+ assert.EqualError(t, err, ErrSheetNameLength.Error())
+ // Test set worksheet name with illegal name
+ assert.EqualError(t, f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title."), ErrSheetNameLength.Error())
+ assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 does not exist")
+ assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 does not exist")
+ assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 does not exist")
+ // Test set cell string data type value with invalid sheet name
+ assert.EqualError(t, f.SetCellStr("Sheet:1", "A1", "1"), ErrSheetNameInvalid.Error())
+ // Test set cell string value with illegal row number
+ assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
f.SetActiveSheet(2)
- // Test get cell formula with given rows number.
+ // Test get cell formula with given rows number
_, err = f.GetCellFormula("Sheet1", "B19")
assert.NoError(t, err)
- // Test get cell formula with illegal worksheet name.
+ // Test get cell formula with illegal worksheet name
_, err = f.GetCellFormula("Sheet2", "B20")
assert.NoError(t, err)
_, err = f.GetCellFormula("Sheet1", "B20")
assert.NoError(t, err)
- // Test get cell formula with illegal rows number.
+ // Test get cell formula with illegal rows number
_, err = f.GetCellFormula("Sheet1", "B")
- assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
// Test get shared cell formula
_, err = f.GetCellFormula("Sheet2", "H11")
assert.NoError(t, err)
_, err = f.GetCellFormula("Sheet2", "I11")
assert.NoError(t, err)
- getSharedForumula(&xlsxWorksheet{}, "")
+ getSharedFormula(&xlsxWorksheet{}, 0, "")
- // Test read cell value with given illegal rows number.
+ // Test read cell value with given illegal rows number
_, err = f.GetCellValue("Sheet2", "a-1")
- assert.EqualError(t, err, `cannot convert cell "A-1" to coordinates: invalid cell name "A-1"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A-1", newInvalidCellNameError("A-1")).Error())
_, err = f.GetCellValue("Sheet2", "A")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
- // Test read cell value with given lowercase column number.
+ // Test read cell value with given lowercase column number
_, err = f.GetCellValue("Sheet2", "a5")
assert.NoError(t, err)
_, err = f.GetCellValue("Sheet2", "C11")
@@ -100,7 +119,7 @@ func TestOpenFile(t *testing.T) {
assert.NoError(t, err)
_, err = f.GetCellValue("Sheet2", "D12")
assert.NoError(t, err)
- // Test SetCellValue function.
+ // Test SetCellValue function
assert.NoError(t, f.SetCellValue("Sheet2", "F1", " Hello"))
assert.NoError(t, f.SetCellValue("Sheet2", "G1", []byte("World")))
assert.NoError(t, f.SetCellValue("Sheet2", "F2", 42))
@@ -109,7 +128,7 @@ func TestOpenFile(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet2", "F5", int32(1<<32/2-1)))
assert.NoError(t, f.SetCellValue("Sheet2", "F6", int64(1<<32/2-1)))
assert.NoError(t, f.SetCellValue("Sheet2", "F7", float32(42.65418)))
- assert.NoError(t, f.SetCellValue("Sheet2", "F8", float64(-42.65418)))
+ assert.NoError(t, f.SetCellValue("Sheet2", "F8", -42.65418))
assert.NoError(t, f.SetCellValue("Sheet2", "F9", float32(42)))
assert.NoError(t, f.SetCellValue("Sheet2", "F10", float64(42)))
assert.NoError(t, f.SetCellValue("Sheet2", "F11", uint(1<<32-1)))
@@ -120,77 +139,107 @@ func TestOpenFile(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet2", "F16", true))
assert.NoError(t, f.SetCellValue("Sheet2", "F17", complex64(5+10i)))
- // Test on not exists worksheet.
- assert.EqualError(t, f.SetCellDefault("SheetN", "A1", ""), "sheet SheetN is not exist")
- assert.EqualError(t, f.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN is not exist")
- assert.EqualError(t, f.SetCellBool("SheetN", "A1", true), "sheet SheetN is not exist")
- assert.EqualError(t, f.SetCellFormula("SheetN", "A1", ""), "sheet SheetN is not exist")
- assert.EqualError(t, f.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN is not exist")
+ // Test on not exists worksheet
+ assert.EqualError(t, f.SetCellDefault("SheetN", "A1", ""), "sheet SheetN does not exist")
+ assert.EqualError(t, f.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN does not exist")
+ assert.EqualError(t, f.SetCellBool("SheetN", "A1", true), "sheet SheetN does not exist")
+ assert.EqualError(t, f.SetCellFormula("SheetN", "A1", ""), "sheet SheetN does not exist")
+ assert.EqualError(t, f.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN does not exist")
// Test boolean write
- booltest := []struct {
+ boolTest := []struct {
value bool
+ raw bool
expected string
}{
- {false, "0"},
- {true, "1"},
+ {false, true, "0"},
+ {true, true, "1"},
+ {false, false, "FALSE"},
+ {true, false, "TRUE"},
}
- for _, test := range booltest {
+ for _, test := range boolTest {
assert.NoError(t, f.SetCellValue("Sheet2", "F16", test.value))
- val, err := f.GetCellValue("Sheet2", "F16")
+ val, err := f.GetCellValue("Sheet2", "F16", Options{RawCellValue: test.raw})
assert.NoError(t, err)
assert.Equal(t, test.expected, val)
}
assert.NoError(t, f.SetCellValue("Sheet2", "G2", nil))
- assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected")
+ assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now()))
assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC()))
+ assert.EqualError(t, f.SetCellValue("SheetN", "A1", time.Now()), "sheet SheetN does not exist")
// 02:46:40
assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13)))
- // Test completion column.
+ // Test completion column
assert.NoError(t, f.SetCellValue("Sheet2", "M2", nil))
- // Test read cell value with given axis large than exists row.
+ // Test read cell value with given cell reference large than exists row
_, err = f.GetCellValue("Sheet2", "E231")
assert.NoError(t, err)
- // Test get active worksheet of spreadsheet and get worksheet name of spreadsheet by given worksheet index.
+ // Test get active worksheet of spreadsheet and get worksheet name of
+ // spreadsheet by given worksheet index
f.GetSheetName(f.GetActiveSheetIndex())
- // Test get worksheet index of spreadsheet by given worksheet name.
- f.GetSheetIndex("Sheet1")
- // Test get worksheet name of spreadsheet by given invalid worksheet index.
+ // Test get worksheet index of spreadsheet by given worksheet name
+ _, err = f.GetSheetIndex("Sheet1")
+ assert.NoError(t, err)
+ // Test get worksheet name of spreadsheet by given invalid worksheet index
f.GetSheetName(4)
- // Test get worksheet map of workbook.
+ // Test get worksheet map of workbook
f.GetSheetMap()
for i := 1; i <= 300; i++ {
assert.NoError(t, f.SetCellStr("Sheet2", "c"+strconv.Itoa(i), strconv.Itoa(i)))
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx")))
- assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), "file name length exceeds maximum limit")
+ assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), ErrMaxFilePathLength.Error())
+ assert.NoError(t, f.Close())
}
func TestSaveFile(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
+ assert.NoError(t, err)
+ assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookFileFormat.Error())
+ for _, ext := range []string{".xlam", ".xlsm", ".xlsx", ".xltm", ".xltx"} {
+ assert.NoError(t, f.SaveAs(filepath.Join("test", fmt.Sprintf("TestSaveFile%s", ext))))
}
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsx")))
+ assert.NoError(t, f.Close())
+
f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.Save())
+ assert.NoError(t, f.Close())
+
+ t.Run("for_save_multiple_times", func(t *testing.T) {
+ {
+ f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", "A20", 20))
+ assert.NoError(t, f.Save())
+
+ assert.NoError(t, f.SetCellValue("Sheet1", "A21", 21))
+ assert.NoError(t, f.Save())
+ assert.NoError(t, f.Close())
+ }
+ {
+ f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
+ assert.NoError(t, err)
+ val, err := f.GetCellValue("Sheet1", "A20")
+ assert.NoError(t, err)
+ assert.Equal(t, "20", val)
+ val, err = f.GetCellValue("Sheet1", "A21")
+ assert.NoError(t, err)
+ assert.Equal(t, "21", val)
+ assert.NoError(t, f.Close())
+ }
+ })
}
func TestSaveAsWrongPath(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if assert.NoError(t, err) {
- // Test write file to not exist directory.
- err = f.SaveAs("")
- if assert.Error(t, err) {
- assert.True(t, os.IsNotExist(err), "Error: %v: Expected os.IsNotExists(err) == true", err)
- }
- }
+ assert.NoError(t, err)
+ // Test write file to not exist directory
+ assert.Error(t, f.SaveAs(filepath.Join("x", "Book1.xlsx")))
+ assert.NoError(t, f.Close())
}
func TestCharsetTranscoder(t *testing.T) {
@@ -200,17 +249,82 @@ func TestCharsetTranscoder(t *testing.T) {
func TestOpenReader(t *testing.T) {
_, err := OpenReader(strings.NewReader(""))
- assert.EqualError(t, err, "zip: not a valid zip file")
- _, err = OpenReader(bytes.NewReader([]byte{
- 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00,
- 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00,
- 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00,
- 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00,
- 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
- }))
- assert.EqualError(t, err, "not support encrypted file currently")
+ assert.EqualError(t, err, zip.ErrFormat.Error())
+ _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1})
+ assert.EqualError(t, err, ErrWorkbookFileFormat.Error())
+
+ // Prepare unusual workbook, made the specified internal XML parts missing
+ // or contain unsupported charset
+ preset := func(filePath string, notExist bool) *bytes.Buffer {
+ source, err := zip.OpenReader(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+ buf := new(bytes.Buffer)
+ zw := zip.NewWriter(buf)
+ for _, item := range source.File {
+ // The following statements can be simplified as zw.Copy(item) in go1.17
+ if notExist && item.Name == filePath {
+ continue
+ }
+ writer, err := zw.Create(item.Name)
+ assert.NoError(t, err)
+ readerCloser, err := item.Open()
+ assert.NoError(t, err)
+ _, err = io.Copy(writer, readerCloser)
+ assert.NoError(t, err)
+ }
+ if !notExist {
+ fi, err := zw.Create(filePath)
+ assert.NoError(t, err)
+ _, err = fi.Write(MacintoshCyrillicCharset)
+ assert.NoError(t, err)
+ }
+ assert.NoError(t, zw.Close())
+ return buf
+ }
+ // Test open workbook with unsupported charset internal XML parts
+ for _, defaultXMLPath := range []string{
+ defaultXMLPathCalcChain,
+ defaultXMLPathStyles,
+ defaultXMLPathWorkbookRels,
+ } {
+ _, err = OpenReader(preset(defaultXMLPath, false))
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ }
+ // Test open workbook without internal XML parts
+ for _, defaultXMLPath := range []string{
+ defaultXMLPathCalcChain,
+ defaultXMLPathStyles,
+ defaultXMLPathWorkbookRels,
+ } {
+ _, err = OpenReader(preset(defaultXMLPath, true))
+ assert.NoError(t, err)
+ }
+
+ // Test open spreadsheet with unzip size limit
+ _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipSizeLimit: 100})
+ assert.EqualError(t, err, newUnzipSizeLimitError(100).Error())
+
+ // Test open password protected spreadsheet created by Microsoft Office Excel 2010
+ f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"})
+ assert.NoError(t, err)
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SECRET", val)
+ assert.NoError(t, f.Close())
+
+ // Test open password protected spreadsheet created by LibreOffice 7.0.0.3
+ f, err = OpenFile(filepath.Join("test", "encryptAES.xlsx"), Options{Password: "password"})
+ assert.NoError(t, err)
+ val, err = f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "SECRET", val)
+ assert.NoError(t, f.Close())
+
+ // Test open spreadsheet with invalid options
+ _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, UnzipXMLSizeLimit: 2})
+ assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error())
- // Test unexpected EOF.
+ // Test unexpected EOF
var b bytes.Buffer
w := gzip.NewWriter(&b)
defer w.Close()
@@ -236,11 +350,11 @@ func TestOpenReader(t *testing.T) {
0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x41, 0x00, 0x00, 0x00, 0x5d, 0x00,
0x00, 0x00, 0x00, 0x00,
}))
- assert.EqualError(t, err, "zip: unsupported compression algorithm")
+ assert.EqualError(t, err, zip.ErrAlgorithm.Error())
}
func TestBrokenFile(t *testing.T) {
- // Test write file with broken file struct.
+ // Test write file with broken file struct
f := File{}
t.Run("SaveWithoutName", func(t *testing.T) {
@@ -248,20 +362,21 @@ func TestBrokenFile(t *testing.T) {
})
t.Run("SaveAsEmptyStruct", func(t *testing.T) {
- // Test write file with broken file struct with given path.
- assert.NoError(t, f.SaveAs(filepath.Join("test", "BrokenFile.SaveAsEmptyStruct.xlsx")))
+ // Test write file with broken file struct with given path
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "BadWorkbook.SaveAsEmptyStruct.xlsx")))
})
t.Run("OpenBadWorkbook", func(t *testing.T) {
- // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml.
+ // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml
f3, err := OpenFile(filepath.Join("test", "BadWorkbook.xlsx"))
f3.GetActiveSheetIndex()
f3.SetActiveSheet(1)
assert.NoError(t, err)
+ assert.NoError(t, f3.Close())
})
t.Run("OpenNotExistsFile", func(t *testing.T) {
- // Test open a spreadsheet file with given illegal path.
+ // Test open a spreadsheet file with given illegal path
_, err := OpenFile(filepath.Join("test", "NotExistsFile.xlsx"))
if assert.Error(t, err) {
assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true")
@@ -270,98 +385,120 @@ func TestBrokenFile(t *testing.T) {
}
func TestNewFile(t *testing.T) {
- // Test create a spreadsheet file.
+ // Test create a spreadsheet file
f := NewFile()
- f.NewSheet("Sheet1")
- f.NewSheet("XLSXSheet2")
- f.NewSheet("XLSXSheet3")
- assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56))
+ _, err := f.NewSheet("Sheet1")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellInt("Sheet2", "A23", 56))
assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42"))
f.SetActiveSheet(0)
- // Test add picture to sheet with scaling and positioning.
- err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"),
- `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- // Test add picture to worksheet without formatset.
- err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "")
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Test add picture to sheet with scaling and positioning
+ assert.NoError(t, f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"),
+ &GraphicOptions{ScaleX: 0.5, ScaleY: 0.5, Positioning: "absolute"}))
- // Test add picture to worksheet with invalid formatset.
- err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`)
- if !assert.Error(t, err) {
- t.FailNow()
- }
+ // Test add picture to worksheet without options
+ assert.NoError(t, f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx")))
-}
-
-func TestAddDrawingVML(t *testing.T) {
- // Test addDrawingVML with illegal cell coordinates.
- f := NewFile()
- assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), `cannot convert cell "*" to coordinates: invalid cell name "*"`)
+ assert.NoError(t, f.Save())
}
func TestSetCellHyperLink(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if err != nil {
- t.Log(err)
- }
- // Test set cell hyperlink in a work sheet already have hyperlinks.
- assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
- // Test add first hyperlink in a work sheet.
- assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
- // Test add Location hyperlink in a work sheet.
+ assert.NoError(t, err)
+ // Test set cell hyperlink in a work sheet already have hyperlinks
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/xuri/excelize", "External"))
+ // Test add first hyperlink in a work sheet
+ assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/xuri/excelize", "External"))
+ // Test add Location hyperlink in a work sheet
assert.NoError(t, f.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location"))
-
- assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`)
-
+ // Test add Location hyperlink with display & tooltip in a work sheet
+ display, tooltip := "Display value", "Hover text"
+ assert.NoError(t, f.SetCellHyperLink("Sheet2", "D7", "Sheet1!D9", "Location", HyperlinkOpts{
+ Display: &display,
+ Tooltip: &tooltip,
+ }))
+ // Test set cell hyperlink with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"))
+ assert.Equal(t, newInvalidLinkTypeError(""), f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""))
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`)
-
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx")))
+ assert.NoError(t, f.Close())
f = NewFile()
_, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
- f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)}
- assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet")
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)}
+ assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/xuri/excelize", "External"), ErrTotalSheetHyperlinks.Error())
f = NewFile()
_, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
- err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
+ err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External")
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+
+ // Test update cell hyperlink
+ f = NewFile()
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "https://github.com", "External"))
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External"))
+ link, target, err := f.GetCellHyperLink("Sheet1", "A1")
+ assert.Equal(t, link, true)
+ assert.Equal(t, "https://github.com/xuri/excelize", target)
+ assert.NoError(t, err)
+
+ // Test remove hyperlink for a cell
+ f = NewFile()
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A1:D4"
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"))
+ // Test remove hyperlink for a cell with invalid cell reference
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
+ ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A:A"
+ assert.Error(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
}
func TestGetCellHyperLink(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
_, _, err = f.GetCellHyperLink("Sheet1", "")
assert.EqualError(t, err, `invalid cell name ""`)
link, target, err := f.GetCellHyperLink("Sheet1", "A22")
assert.NoError(t, err)
- t.Log(link, target)
+ assert.Equal(t, link, true)
+ assert.Equal(t, target, "https://github.com/xuri/excelize")
+
link, target, err = f.GetCellHyperLink("Sheet2", "D6")
assert.NoError(t, err)
- t.Log(link, target)
+ assert.Equal(t, link, false)
+ assert.Equal(t, target, "")
+
link, target, err = f.GetCellHyperLink("Sheet3", "H3")
- assert.EqualError(t, err, "sheet Sheet3 is not exist")
- t.Log(link, target)
+ assert.EqualError(t, err, "sheet Sheet3 does not exist")
+ assert.Equal(t, link, false)
+ assert.Equal(t, target, "")
+
+ assert.NoError(t, f.Close())
f = NewFile()
_, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
- f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{
Hyperlink: []xlsxHyperlink{{Ref: "A1"}},
}
link, target, err = f.GetCellHyperLink("Sheet1", "A1")
@@ -369,64 +506,31 @@ func TestGetCellHyperLink(t *testing.T) {
assert.Equal(t, link, true)
assert.Equal(t, target, "")
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: []xlsxHyperlink{{Ref: "A:A"}}}
link, target, err = f.GetCellHyperLink("Sheet1", "A1")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.Equal(t, link, false)
assert.Equal(t, target, "")
-}
-
-func TestSetCellFormula(t *testing.T) {
- f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- assert.NoError(t, f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)"))
- assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)"))
-
- // Test set cell formula with illegal rows number.
- assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`)
-
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx")))
-
- f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- // Test remove cell formula.
- assert.NoError(t, f.SetCellFormula("Sheet1", "A1", ""))
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx")))
- // Test remove all cell formula.
- assert.NoError(t, f.SetCellFormula("Sheet1", "B1", ""))
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx")))
+ // Test get cell hyperlink with invalid sheet name
+ _, _, err = f.GetCellHyperLink("Sheet:1", "A1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
}
func TestSetSheetBackground(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")))
+ assert.NoError(t, f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx")))
+ assert.NoError(t, f.Close())
}
func TestSetSheetBackgroundErrors(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
err = f.SetSheetBackground("Sheet2", filepath.Join("test", "not_exists", "not_exists.png"))
if assert.Error(t, err) {
@@ -434,19 +538,30 @@ func TestSetSheetBackgroundErrors(t *testing.T) {
}
err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx"))
- assert.EqualError(t, err, "unsupported image extension")
+ assert.EqualError(t, err, ErrImgExt.Error())
+ // Test set sheet background on not exist worksheet
+ err = f.SetSheetBackground("SheetN", filepath.Join("test", "images", "background.jpg"))
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test set sheet background with invalid sheet name
+ assert.EqualError(t, f.SetSheetBackground("Sheet:1", filepath.Join("test", "images", "background.jpg")), ErrSheetNameInvalid.Error())
+ assert.NoError(t, f.Close())
+
+ // Test set sheet background with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetSheetBackground("Sheet1", filepath.Join("test", "images", "background.jpg")), "XML syntax error on line 1: invalid UTF-8")
}
-// TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function
-// to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching
-// contents.
+// TestWriteArrayFormula tests the extended options of SetCellFormula by writing
+// an array function to a workbook. In the resulting file, the lines 2 and 3 as
+// well as 4 and 5 should have matching contents
func TestWriteArrayFormula(t *testing.T) {
cell := func(col, row int) string {
c, err := CoordinatesToCellName(col, row)
if err != nil {
t.Fatal(err)
}
-
return c
}
@@ -513,7 +628,7 @@ func TestWriteArrayFormula(t *testing.T) {
valCell := cell(1, i+firstResLine)
assocCell := cell(2, i+firstResLine)
- assert.NoError(t, f.SetCellInt("Sheet1", valCell, values[i]))
+ assert.NoError(t, f.SetCellInt("Sheet1", valCell, int64(values[i])))
assert.NoError(t, f.SetCellStr("Sheet1", assocCell, sample[assoc[i]]))
}
@@ -527,17 +642,17 @@ func TestWriteArrayFormula(t *testing.T) {
stdevCell := cell(i+2, 4)
calcStdevCell := cell(i+2, 5)
- assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, average(i)))
- assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, stdev(i)))
+ assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, int64(average(i))))
+ assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, int64(stdev(i))))
// Average can be done with AVERAGEIF
assert.NoError(t, f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange)))
ref := stdevCell + ":" + stdevCell
- t := STCellFormulaTypeArray
+ arr := STCellFormulaTypeArray
// Use an array formula for standard deviation
- f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange),
- FormulaOpts{}, FormulaOpts{Type: &t}, FormulaOpts{Ref: &ref})
+ assert.NoError(t, f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange),
+ FormulaOpts{}, FormulaOpts{Type: &arr}, FormulaOpts{Ref: &ref}))
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestWriteArrayFormula.xlsx")))
@@ -545,58 +660,60 @@ func TestWriteArrayFormula(t *testing.T) {
func TestSetCellStyleAlignment(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
var style int
- style, err = f.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"top","wrap_text":true}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Alignment: &Alignment{Horizontal: "center", Indent: 1, JustifyLastLine: true, ReadingOrder: 0, RelativeIndent: 1, ShrinkToFit: true, TextRotation: 45, Vertical: "top", WrapText: true}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A22", "A22", style))
- // Test set cell style with given illegal rows number.
- assert.EqualError(t, f.SetCellStyle("Sheet1", "A", "A22", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.SetCellStyle("Sheet1", "A22", "A", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
-
- // Test get cell style with given illegal rows number.
+ // Test set cell style with given illegal rows number
+ assert.EqualError(t, f.SetCellStyle("Sheet1", "A", "A22", style), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ assert.EqualError(t, f.SetCellStyle("Sheet1", "A22", "A", style), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ // Test set cell style with invalid sheet name
+ assert.EqualError(t, f.SetCellStyle("Sheet:1", "A1", "A2", style), ErrSheetNameInvalid.Error())
+ // Test get cell style with given illegal rows number
index, err := f.GetCellStyle("Sheet1", "A")
assert.Equal(t, 0, index)
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+
+ // Test get cell style with invalid sheet name
+ _, err = f.GetCellStyle("Sheet:1", "A1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx")))
}
func TestSetCellStyleBorder(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
var style int
- // Test set border on overlapping area with vertical variants shading styles gradient fill.
- style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Test set border on overlapping range with vertical variants shading styles gradient fill
+ style, err = f.NewStyle(&Style{
+ Border: []Border{
+ {Type: "left", Color: "0000FF", Style: 3},
+ {Type: "top", Color: "00FF00", Style: 4},
+ {Type: "bottom", Color: "FFFF00", Style: 5},
+ {Type: "right", Color: "FF0000", Style: 6},
+ {Type: "diagonalDown", Color: "A020F0", Style: 7},
+ {Type: "diagonalUp", Color: "A020F0", Style: 8},
+ },
+ })
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "J21", "L25", style))
- style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":3},{"type":"bottom","color":"FFFF00","style":4},{"type":"right","color":"FF0000","style":5},{"type":"diagonalDown","color":"A020F0","style":6},{"type":"diagonalUp","color":"A020F0","style":7}],"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 1}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style))
- style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":3},{"type":"bottom","color":"FFFF00","style":4},{"type":"right","color":"FF0000","style":5},{"type":"diagonalDown","color":"A020F0","style":6},{"type":"diagonalUp","color":"A020F0","style":7}],"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":4}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: 2}, {Type: "top", Color: "00FF00", Style: 3}, {Type: "bottom", Color: "FFFF00", Style: 4}, {Type: "right", Color: "FF0000", Style: 5}, {Type: "diagonalDown", Color: "A020F0", Style: 6}, {Type: "diagonalUp", Color: "A020F0", Style: 7}}, Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 4}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style))
- // Test set border and solid style pattern fill for a single cell.
+ // Test set border and solid style pattern fill for a single cell
style, err = f.NewStyle(&Style{
Border: []Border{
{
@@ -632,13 +749,11 @@ func TestSetCellStyleBorder(t *testing.T) {
},
Fill: Fill{
Type: "pattern",
- Color: []string{"#E0EBF5"},
+ Color: []string{"E0EBF5"},
Pattern: 1,
},
})
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "O22", "O22", style))
@@ -647,81 +762,81 @@ func TestSetCellStyleBorder(t *testing.T) {
func TestSetCellStyleBorderErrors(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- // Set border with invalid style parameter.
- _, err = f.NewStyle("")
- if !assert.EqualError(t, err, "unexpected end of JSON input") {
- t.FailNow()
- }
+ assert.NoError(t, err)
- // Set border with invalid style index number.
- _, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":-1},{"type":"top","color":"00FF00","style":14},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Set border with invalid style index number
+ _, err = f.NewStyle(&Style{Border: []Border{{Type: "left", Color: "0000FF", Style: -1}, {Type: "top", Color: "00FF00", Style: 14}, {Type: "bottom", Color: "FFFF00", Style: 5}, {Type: "right", Color: "FF0000", Style: 6}, {Type: "diagonalDown", Color: "A020F0", Style: 9}, {Type: "diagonalUp", Color: "A020F0", Style: 8}}})
+ assert.NoError(t, err)
}
func TestSetCellStyleNumberFormat(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- // Test only set fill and number format for a cell.
+ // Test only set fill and number format for a cell
col := []string{"L", "M", "N", "O", "P"}
- data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}
+ idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}
value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"}
- for i, v := range value {
- for k, d := range data {
- c := col[i] + strconv.Itoa(k+1)
+ expected := [][]string{
+ {"37947.75", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947 3/4", "37947 3/4", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", " 37,948 ", " $37,948 ", " 37,947.75 ", " $37,947.75 ", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"},
+ {"-37947.75", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947 3/4", "-37947 3/4", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", " (37,948)", " $(37,948)", " (37,947.75)", " $(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"},
+ {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0 ", "0 ", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", " 0 ", " $0 ", " 0.01 ", " $0.01 ", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"},
+ {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2 1/9", "2 1/10", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", " 2 ", " $2 ", " 2.10 ", " $2.10 ", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"},
+ {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", " String ", " String ", " String ", " String ", "String", "String", "String", "String", "String"},
+ }
+
+ for c, v := range value {
+ for r, idx := range idxTbl {
+ cell := col[c] + strconv.Itoa(r+1)
var val float64
val, err = strconv.ParseFloat(v, 64)
if err != nil {
- assert.NoError(t, f.SetCellValue("Sheet2", c, v))
+ assert.NoError(t, f.SetCellValue("Sheet2", cell, v))
} else {
- assert.NoError(t, f.SetCellValue("Sheet2", c, val))
+ assert.NoError(t, f.SetCellValue("Sheet2", cell, val))
}
- style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`)
+ style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: idx})
if !assert.NoError(t, err) {
t.FailNow()
}
- assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style))
- t.Log(f.GetCellValue("Sheet2", c))
+ assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style))
+ cellValue, err := f.GetCellValue("Sheet2", cell)
+ assert.Equal(t, expected[c][r], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %s c: %d r: %d", cell, value[c], builtInNumFmt[idx], c, r))
+ assert.NoError(t, err)
}
}
var style int
- style, err = f.NewStyle(`{"number_format":-1}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{NumFmt: -1})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx")))
+
+ // Test get cell value with built-in number format code 22 with custom short date pattern
+ f = NewFile(Options{ShortDatePattern: "yyyy-m-dd"})
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", 45074.625694444447))
+ style, err = f.NewStyle(&Style{NumFmt: 22})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
+ cellValue, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "2023-5-28 15:01", cellValue)
}
func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
t.Run("TestBook3", func(t *testing.T) {
f, err := prepareTestBook3()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3))
var style int
- style, err = f.NewStyle(`{"number_format": 188, "decimal_places": -1}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(-1)})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
- style, err = f.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(31), NegRed: true})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
@@ -730,80 +845,112 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
t.Run("TestBook4", func(t *testing.T) {
f, err := prepareTestBook4()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5))
- _, err = f.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ _, err = f.NewStyle(&Style{NumFmt: 26})
+ assert.NoError(t, err)
- style, err := f.NewStyle(`{"number_format": 27}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err := f.NewStyle(&Style{NumFmt: 27})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
- style, err = f.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{NumFmt: 31})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
- style, err = f.NewStyle(`{"number_format": 71, "lang": "th-th"}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{NumFmt: 71})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx")))
})
}
+func TestSetCellStyleLangNumberFormat(t *testing.T) {
+ rawCellValues := make([][]string, 42)
+ for i := 0; i < 42; i++ {
+ rawCellValues[i] = []string{"45162"}
+ }
+ for lang, expected := range map[CultureName][][]string{
+ CultureNameUnknown: rawCellValues,
+ CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"08-24-56"}, {"4356년 08월 24일"}, {"0시 00분"}, {"0시 00분 00초"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ } {
+ f, err := prepareTestBook5(Options{CultureInfo: lang})
+ assert.NoError(t, err)
+ rows, err := f.GetRows("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, rows)
+ assert.NoError(t, f.Close())
+ }
+ // Test apply language number format code with date and time pattern
+ for lang, expected := range map[CultureName][][]string{
+ CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"4356-8-24"}, {"4356년 08월 24일"}, {"00:00:00"}, {"00:00:00"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
+ } {
+ f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
+ assert.NoError(t, err)
+ rows, err := f.GetRows("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, rows)
+ assert.NoError(t, f.Close())
+ }
+ // Test open workbook with invalid date and time pattern options
+ _, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongDatePattern: "0.00"})
+ assert.Equal(t, ErrUnsupportedNumberFormat, err)
+ _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongTimePattern: "0.00"})
+ assert.Equal(t, ErrUnsupportedNumberFormat, err)
+ _, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{ShortDatePattern: "0.00"})
+ assert.Equal(t, ErrUnsupportedNumberFormat, err)
+}
+
func TestSetCellStyleCustomNumberFormat(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5))
- style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`)
+ customNumFmt := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"
+ style, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
- style, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@","font":{"color":"#9A0511"}}`)
+ style, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt, Font: &Font{Color: "9A0511"}})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
- _, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yy;@"}`)
+ customNumFmt = "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yy;@"
+ _, err = f.NewStyle(&Style{CustomNumFmt: &customNumFmt})
assert.NoError(t, err)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx")))
}
func TestSetCellStyleFill(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
var style int
- // Test set fill for cell with invalid parameter.
- style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`)
+ // Test set fill for cell with invalid parameter
+ style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 6}})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style))
- style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`)
+ style, err = f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF"}, Shading: 1}})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style))
- style, err = f.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`)
+ style, err = f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{}, Shading: 1}})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style))
- style, err = f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 19}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx")))
@@ -811,43 +958,31 @@ func TestSetCellStyleFill(t *testing.T) {
func TestSetCellStyleFont(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
var style int
- style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "777777", Underline: "single"}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A1", "A1", style))
- style, err = f.NewStyle(`{"font":{"italic":true,"underline":"double"}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Font: &Font{Italic: true, Underline: "double"}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A2", "A2", style))
- style, err = f.NewStyle(`{"font":{"bold":true}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Font: &Font{Bold: true}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A3", "A3", style))
- style, err = f.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Font: &Font{Bold: true, Family: "", Size: 0, Color: "", Underline: ""}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style))
- style, err = f.NewStyle(`{"font":{"color":"#777777","strike":true}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Font: &Font{Color: "777777", Strike: true}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A5", "A5", style))
@@ -856,67 +991,55 @@ func TestSetCellStyleFont(t *testing.T) {
func TestSetCellStyleProtection(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
var style int
- style, err = f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ style, err = f.NewStyle(&Style{Protection: &Protection{Hidden: true, Locked: true}})
+ assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet2", "A6", "A6", style))
err = f.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
}
func TestSetDeleteSheet(t *testing.T) {
t.Run("TestBook3", func(t *testing.T) {
f, err := prepareTestBook3()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- f.DeleteSheet("XLSXSheet3")
+ assert.NoError(t, f.DeleteSheet("Sheet3"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx")))
})
t.Run("TestBook4", func(t *testing.T) {
f, err := prepareTestBook4()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- f.DeleteSheet("Sheet1")
- assert.EqualError(t, f.AddComment("Sheet1", "A1", ""), "unexpected end of JSON input")
- assert.NoError(t, f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`))
+ assert.NoError(t, err)
+ assert.NoError(t, f.DeleteSheet("Sheet1"))
+ assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx")))
})
}
func TestSheetVisibility(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.SetSheetVisible("Sheet2", false))
+ assert.NoError(t, f.SetSheetVisible("Sheet2", false, true))
assert.NoError(t, f.SetSheetVisible("Sheet1", false))
assert.NoError(t, f.SetSheetVisible("Sheet1", true))
- assert.Equal(t, true, f.GetSheetVisible("Sheet1"))
-
+ visible, err := f.GetSheetVisible("Sheet1")
+ assert.Equal(t, true, visible)
+ assert.NoError(t, err)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx")))
}
func TestCopySheet(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- idx := f.NewSheet("CopySheet")
+ idx, err := f.NewSheet("CopySheet")
+ assert.NoError(t, err)
assert.NoError(t, f.CopySheet(0, idx))
assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello"))
@@ -929,15 +1052,9 @@ func TestCopySheet(t *testing.T) {
func TestCopySheetError(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- assert.EqualError(t, f.copySheet(-1, -2), "sheet is not exist")
- if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") {
- t.FailNow()
- }
-
+ assert.NoError(t, err)
+ assert.EqualError(t, f.copySheet(-1, -2), ErrSheetNameBlank.Error())
+ assert.EqualError(t, f.CopySheet(-1, -2), ErrSheetIdx.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx")))
}
@@ -946,23 +1063,6 @@ func TestGetSheetComments(t *testing.T) {
assert.Equal(t, "", f.getSheetComments("sheet0"))
}
-func TestSetActiveSheet(t *testing.T) {
- f := NewFile()
- f.WorkBook.BookViews = nil
- f.SetActiveSheet(1)
- f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}}
- f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}}
- f.SetActiveSheet(1)
- f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil
- f.SetActiveSheet(1)
-}
-
-func TestSetSheetVisible(t *testing.T) {
- f := NewFile()
- f.WorkBook.Sheets.Sheet[0].Name = "SheetN"
- assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN is not exist")
-}
-
func TestGetActiveSheetIndex(t *testing.T) {
f := NewFile()
f.WorkBook.BookViews = nil
@@ -971,267 +1071,548 @@ func TestGetActiveSheetIndex(t *testing.T) {
func TestRelsWriter(t *testing.T) {
f := NewFile()
- f.Relationships["xl/worksheets/sheet/rels/sheet1.xml.rel"] = &xlsxRelationships{}
+ f.Relationships.Store("xl/worksheets/sheet/rels/sheet1.xml.rel", &xlsxRelationships{})
f.relsWriter()
}
-func TestGetSheetView(t *testing.T) {
- f := NewFile()
- _, err := f.getSheetView("SheetN", 0)
- assert.EqualError(t, err, "sheet SheetN is not exist")
-}
-
func TestConditionalFormat(t *testing.T) {
f := NewFile()
sheet1 := f.GetSheetName(0)
- fillCells(f, sheet1, 10, 15)
+ assert.NoError(t, fillCells(f, sheet1, 10, 15))
var format1, format2, format3, format4 int
var err error
- // Rose format for bad conditional.
- format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Rose format for bad conditional
+ format1, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
+ assert.NoError(t, err)
- // Light yellow format for neutral conditional.
- format2, err = f.NewConditionalStyle(`{"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Light yellow format for neutral conditional
+ format2, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEEAA0"}, Pattern: 1}})
+ assert.NoError(t, err)
- // Light green format for good conditional.
- format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Light green format for good conditional
+ format3, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}})
+ assert.NoError(t, err)
- // conditional style with align and left border.
- format4, err = f.NewConditionalStyle(`{"alignment":{"wrap_text":true},"border":[{"type":"left","color":"#000000","style":1}]}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // conditional style with align and left border
+ format4, err = f.NewConditionalStyle(&Style{Alignment: &Alignment{WrapText: true}, Border: []Border{{Type: "left", Color: "000000", Style: 1}}})
+ assert.NoError(t, err)
- // Color scales: 2 color.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`))
- // Color scales: 3 color.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`))
- // Hightlight cells rules: between...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)))
- // Hightlight cells rules: Greater Than...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)))
- // Hightlight cells rules: Equal To...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)))
- // Hightlight cells rules: Not Equal To...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)))
- // Hightlight cells rules: Duplicate Values...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)))
+ // Color scales: 2 color
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "A1:A10",
+ []ConditionalFormatOptions{
+ {
+ Type: "2_color_scale",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ MinColor: "#F8696B",
+ MaxColor: "#63BE7B",
+ },
+ },
+ ))
+ // Color scales: 3 color
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "B1:B10",
+ []ConditionalFormatOptions{
+ {
+ Type: "3_color_scale",
+ Criteria: "=",
+ MinType: "min",
+ MidType: "percentile",
+ MaxType: "max",
+ MinColor: "#F8696B",
+ MidColor: "#FFEB84",
+ MaxColor: "#63BE7B",
+ },
+ },
+ ))
+ // Highlight cells rules: between...
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10",
+ []ConditionalFormatOptions{
+ {
+ Type: "cell",
+ Criteria: "between",
+ Format: &format1,
+ MinValue: "6",
+ MaxValue: "8",
+ },
+ },
+ ))
+ // Highlight cells rules: Greater Than...
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10",
+ []ConditionalFormatOptions{
+ {
+ Type: "cell",
+ Criteria: ">",
+ Format: &format3,
+ Value: "6",
+ },
+ },
+ ))
+ // Highlight cells rules: Equal To...
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10",
+ []ConditionalFormatOptions{
+ {
+ Type: "top",
+ Criteria: "=",
+ Format: &format3,
+ },
+ },
+ ))
+ // Highlight cells rules: Not Equal To...
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10",
+ []ConditionalFormatOptions{
+ {
+ Type: "unique",
+ Criteria: "=",
+ Format: &format2,
+ },
+ },
+ ))
+ // Highlight cells rules: Duplicate Values...
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10",
+ []ConditionalFormatOptions{
+ {
+ Type: "duplicate",
+ Criteria: "=",
+ Format: &format2,
+ },
+ },
+ ))
// Top/Bottom rules: Top 10%.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)))
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10",
+ []ConditionalFormatOptions{
+ {
+ Type: "top",
+ Criteria: "=",
+ Format: &format1,
+ Value: "6",
+ Percent: true,
+ },
+ },
+ ))
// Top/Bottom rules: Above Average...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)))
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10",
+ []ConditionalFormatOptions{
+ {
+ Type: "average",
+ Criteria: "=",
+ Format: &format3,
+ AboveAverage: true,
+ },
+ },
+ ))
// Top/Bottom rules: Below Average...
- assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)))
- // Data Bars: Gradient Fill.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`))
- // Use a formula to determine which cells to format.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)))
- // Alignment/Border cells rules.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"0"}]`, format4)))
-
- // Test set invalid format set in conditional format.
- assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input")
- // Set conditional format on not exists worksheet.
- assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN is not exist")
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10",
+ []ConditionalFormatOptions{
+ {
+ Type: "average",
+ Criteria: "=",
+ Format: &format1,
+ AboveAverage: false,
+ },
+ },
+ ))
+ // Data Bars: Gradient Fill
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10",
+ []ConditionalFormatOptions{
+ {
+ Type: "data_bar",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ },
+ },
+ ))
+ // Use a formula to determine which cells to format
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10",
+ []ConditionalFormatOptions{
+ {
+ Type: "formula",
+ Criteria: "L2<3",
+ Format: &format1,
+ },
+ },
+ ))
+ // Alignment/Border cells rules
+ assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10",
+ []ConditionalFormatOptions{
+ {
+ Type: "cell",
+ Criteria: ">",
+ Format: &format4,
+ Value: "0",
+ },
+ },
+ ))
+ // Test set conditional format with invalid cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.SetConditionalFormat("Sheet1", "A1:-", nil))
+ // Test set conditional format on not exists worksheet
+ assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist")
+ // Test set conditional format with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetConditionalFormat("Sheet:1", "L1:L10", nil))
err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- // Set conditional format with illegal valid type.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`))
- // Set conditional format with illegal criteria type.
- assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`))
+ // Set conditional format with illegal valid type
+ assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
+ []ConditionalFormatOptions{
+ {
+ Type: "",
+ Criteria: "=",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ },
+ },
+ ))
+ // Set conditional format with illegal criteria type
+ assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
+ []ConditionalFormatOptions{
+ {
+ Type: "data_bar",
+ Criteria: "",
+ MinType: "min",
+ MaxType: "max",
+ BarColor: "#638EC6",
+ },
+ },
+ ))
+ // Test create conditional format with invalid custom number format
+ var exp string
+ _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp})
+ assert.Equal(t, ErrCustomNumFmt, err)
- // Set conditional format with file without dxfs element shold not return error.
+ // Set conditional format with file without dxfs element should not return error
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- _, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-}
-
-func TestConditionalFormatError(t *testing.T) {
- f := NewFile()
- sheet1 := f.GetSheetName(0)
-
- fillCells(f, sheet1, 10, 15)
+ assert.NoError(t, err)
- // Set conditional format with illegal JSON string should return error.
- _, err := f.NewConditionalStyle("")
- if !assert.EqualError(t, err, "unexpected end of JSON input") {
- t.FailNow()
- }
+ _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "", Color: []string{"FEC7CE"}, Pattern: 1}})
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
}
func TestSharedStrings(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
rows, err := f.GetRows("Sheet1")
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.Equal(t, "A", rows[0][0])
rows, err = f.GetRows("Sheet2")
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.Equal(t, "Test Weight (Kgs)", rows[0][0])
+ assert.NoError(t, f.Close())
+}
+
+func TestSetSheetCol(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+
+ assert.NoError(t, f.SetSheetCol("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}))
+
+ assert.EqualError(t, f.SetSheetCol("Sheet1", "", &[]interface{}{"cell", nil, 2}),
+ newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
+ // Test set worksheet column values with invalid sheet name
+ assert.EqualError(t, f.SetSheetCol("Sheet:1", "A1", &[]interface{}{nil}), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error())
+ assert.EqualError(t, f.SetSheetCol("Sheet1", "B27", &f), ErrParameterInvalid.Error())
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetCol.xlsx")))
+ assert.NoError(t, f.Close())
}
func TestSetSheetRow(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
assert.NoError(t, f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}))
assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}),
- `cannot convert cell "" to coordinates: invalid cell name ""`)
-
- assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`)
- assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), `pointer to slice expected`)
+ newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
+ // Test set worksheet row with invalid sheet name
+ assert.EqualError(t, f.SetSheetRow("Sheet:1", "A1", &[]interface{}{1}), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error())
+ assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), ErrParameterInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx")))
-}
-
-func TestThemeColor(t *testing.T) {
- t.Log(ThemeColor("000000", -0.1))
- t.Log(ThemeColor("000000", 0))
- t.Log(ThemeColor("000000", 1))
+ assert.NoError(t, f.Close())
}
func TestHSL(t *testing.T) {
var hsl HSL
- t.Log(hsl.RGBA())
- t.Log(hslModel(hsl))
- t.Log(hslModel(color.Gray16{Y: uint16(1)}))
- t.Log(HSLToRGB(0, 1, 0.4))
- t.Log(HSLToRGB(0, 1, 0.6))
- t.Log(hueToRGB(0, 0, -1))
- t.Log(hueToRGB(0, 0, 2))
- t.Log(hueToRGB(0, 0, 1.0/7))
- t.Log(hueToRGB(0, 0, 0.4))
- t.Log(hueToRGB(0, 0, 2.0/4))
- t.Log(RGBToHSL(255, 255, 0))
- t.Log(RGBToHSL(0, 255, 255))
- t.Log(RGBToHSL(250, 100, 50))
- t.Log(RGBToHSL(50, 100, 250))
- t.Log(RGBToHSL(250, 50, 100))
+ r, g, b, a := hsl.RGBA()
+ assert.Equal(t, uint32(0), r)
+ assert.Equal(t, uint32(0), g)
+ assert.Equal(t, uint32(0), b)
+ assert.Equal(t, uint32(0xffff), a)
+ assert.Equal(t, HSL{0, 0, 0}, hslModel(hsl))
+ assert.Equal(t, HSL{0, 0, 0}, hslModel(color.Gray16{Y: uint16(1)}))
+ R, G, B := HSLToRGB(0, 1, 0.4)
+ assert.Equal(t, uint8(204), R)
+ assert.Equal(t, uint8(0), G)
+ assert.Equal(t, uint8(0), B)
+ R, G, B = HSLToRGB(0, 1, 0.6)
+ assert.Equal(t, uint8(255), R)
+ assert.Equal(t, uint8(51), G)
+ assert.Equal(t, uint8(51), B)
+ assert.Equal(t, 0.0, hueToRGB(0, 0, -1))
+ assert.Equal(t, 0.0, hueToRGB(0, 0, 2))
+ assert.Equal(t, 0.0, hueToRGB(0, 0, 1.0/7))
+ assert.Equal(t, 0.0, hueToRGB(0, 0, 0.4))
+ assert.Equal(t, 0.0, hueToRGB(0, 0, 2.0/4))
+ h, s, l := RGBToHSL(255, 255, 0)
+ assert.Equal(t, 0.16666666666666666, h)
+ assert.Equal(t, 1.0, s)
+ assert.Equal(t, 0.5, l)
+ h, s, l = RGBToHSL(0, 255, 255)
+ assert.Equal(t, 0.5, h)
+ assert.Equal(t, 1.0, s)
+ assert.Equal(t, 0.5, l)
+ h, s, l = RGBToHSL(250, 100, 50)
+ assert.Equal(t, 0.041666666666666664, h)
+ assert.Equal(t, 0.9523809523809524, s)
+ assert.Equal(t, 0.5882352941176471, l)
+ h, s, l = RGBToHSL(50, 100, 250)
+ assert.Equal(t, 0.625, h)
+ assert.Equal(t, 0.9523809523809524, s)
+ assert.Equal(t, 0.5882352941176471, l)
+ h, s, l = RGBToHSL(250, 50, 100)
+ assert.Equal(t, 0.9583333333333334, h)
+ assert.Equal(t, 0.9523809523809524, s)
+ assert.Equal(t, 0.5882352941176471, l)
}
func TestProtectSheet(t *testing.T) {
f := NewFile()
- assert.NoError(t, f.ProtectSheet("Sheet1", nil))
- assert.NoError(t, f.ProtectSheet("Sheet1", &FormatSheetProtection{
+ sheetName := f.GetSheetName(0)
+ assert.EqualError(t, f.ProtectSheet(sheetName, nil), ErrParameterInvalid.Error())
+ // Test protect worksheet with XOR hash algorithm
+ assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
Password: "password",
EditScenarios: false,
}))
-
+ ws, err := f.workSheetReader(sheetName)
+ assert.NoError(t, err)
+ assert.Equal(t, "83AF", ws.SheetProtection.Password)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx")))
- // Test protect not exists worksheet.
- assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN is not exist")
+ // Test protect worksheet with SHA-512 hash algorithm
+ assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
+ AlgorithmName: "SHA-512",
+ Password: "password",
+ }))
+ ws, err = f.workSheetReader(sheetName)
+ assert.NoError(t, err)
+ assert.Len(t, ws.SheetProtection.SaltValue, 24)
+ assert.Len(t, ws.SheetProtection.HashValue, 88)
+ assert.Equal(t, int(sheetProtectionSpinCount), ws.SheetProtection.SpinCount)
+ // Test remove sheet protection with an incorrect password
+ assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), ErrUnprotectSheetPassword.Error())
+ // Test remove sheet protection with invalid sheet name
+ assert.EqualError(t, f.UnprotectSheet("Sheet:1", "wrongPassword"), ErrSheetNameInvalid.Error())
+ // Test remove sheet protection with password verification
+ assert.NoError(t, f.UnprotectSheet(sheetName, "password"))
+ // Test protect worksheet with empty password
+ assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{}))
+ assert.Equal(t, "", ws.SheetProtection.Password)
+ // Test protect worksheet with password exceeds the limit length
+ assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
+ AlgorithmName: "MD4",
+ Password: strings.Repeat("s", MaxFieldLength+1),
+ }), ErrPasswordLengthInvalid.Error())
+ // Test protect worksheet with unsupported hash algorithm
+ assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
+ AlgorithmName: "RIPEMD-160",
+ Password: "password",
+ }), ErrUnsupportedHashAlgorithm.Error())
+ // Test protect not exists worksheet
+ assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN does not exist")
+ // Test protect sheet with invalid sheet name
+ assert.EqualError(t, f.ProtectSheet("Sheet:1", nil), ErrSheetNameInvalid.Error())
}
func TestUnprotectSheet(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- // Test unprotect not exists worksheet.
- assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist")
+ assert.NoError(t, err)
+ // Test remove protection on not exists worksheet
+ assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN does not exist")
assert.NoError(t, f.UnprotectSheet("Sheet1"))
+ assert.EqualError(t, f.UnprotectSheet("Sheet1", "password"), ErrUnprotectSheet.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx")))
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ sheetName := f.GetSheetName(0)
+ assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{Password: "password"}))
+ // Test remove sheet protection with an incorrect password
+ assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), ErrUnprotectSheetPassword.Error())
+ // Test remove sheet protection with password verification
+ assert.NoError(t, f.UnprotectSheet(sheetName, "password"))
+ // Test with invalid salt value
+ assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
+ AlgorithmName: "SHA-512",
+ Password: "password",
+ }))
+ ws, err := f.workSheetReader(sheetName)
+ assert.NoError(t, err)
+ ws.SheetProtection.SaltValue = "YWJjZA====="
+ assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), "illegal base64 data at input byte 8")
+}
+
+func TestProtectWorkbook(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.ProtectWorkbook(nil))
+ // Test protect workbook with default hash algorithm
+ assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
+ Password: "password",
+ LockStructure: true,
+ }))
+ wb, err := f.workbookReader()
+ assert.NoError(t, err)
+ assert.Equal(t, "SHA-512", wb.WorkbookProtection.WorkbookAlgorithmName)
+ assert.Len(t, wb.WorkbookProtection.WorkbookSaltValue, 24)
+ assert.Len(t, wb.WorkbookProtection.WorkbookHashValue, 88)
+ assert.Equal(t, int(workbookProtectionSpinCount), wb.WorkbookProtection.WorkbookSpinCount)
+
+ // Test protect workbook with password exceeds the limit length
+ assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
+ AlgorithmName: "MD4",
+ Password: strings.Repeat("s", MaxFieldLength+1),
+ }), ErrPasswordLengthInvalid.Error())
+ // Test protect workbook with unsupported hash algorithm
+ assert.EqualError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
+ AlgorithmName: "RIPEMD-160",
+ Password: "password",
+ }), ErrUnsupportedHashAlgorithm.Error())
+ // Test protect workbook with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.ProtectWorkbook(nil), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestUnprotectWorkbook(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+
+ assert.NoError(t, f.UnprotectWorkbook())
+ assert.EqualError(t, f.UnprotectWorkbook("password"), ErrUnprotectWorkbook.Error())
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectWorkbook.xlsx")))
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{Password: "password"}))
+ // Test remove workbook protection with an incorrect password
+ assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), ErrUnprotectWorkbookPassword.Error())
+ // Test remove workbook protection with password verification
+ assert.NoError(t, f.UnprotectWorkbook("password"))
+ // Test with invalid salt value
+ assert.NoError(t, f.ProtectWorkbook(&WorkbookProtectionOptions{
+ AlgorithmName: "SHA-512",
+ Password: "password",
+ }))
+ wb, err := f.workbookReader()
+ assert.NoError(t, err)
+ wb.WorkbookProtection.WorkbookSaltValue = "YWJjZA====="
+ assert.EqualError(t, f.UnprotectWorkbook("wrongPassword"), "illegal base64 data at input byte 8")
+ // Test remove workbook protection with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.UnprotectWorkbook(), "XML syntax error on line 1: invalid UTF-8")
}
func TestSetDefaultTimeStyle(t *testing.T) {
f := NewFile()
// Test set default time style on not exists worksheet.
- assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN is not exist")
+ assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN does not exist")
+
+ // Test set default time style on invalid cell
+ assert.EqualError(t, f.setDefaultTimeStyle("Sheet1", "", 42), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
}
func TestAddVBAProject(t *testing.T) {
f := NewFile()
- assert.NoError(t, f.SetSheetPrOptions("Sheet1", CodeName("Sheet1")))
- assert.EqualError(t, f.AddVBAProject("macros.bin"), "stat macros.bin: no such file or directory")
- assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "Book1.xlsx")), "unsupported VBA project extension")
- assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin")))
- // Test add VBA project twice.
- assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin")))
+ file, err := os.ReadFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
+ assert.EqualError(t, f.AddVBAProject(file), ErrAddVBAProject.Error())
+ file, err = os.ReadFile(filepath.Join("test", "vbaProject.bin"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddVBAProject(file))
+ // Test add VBA project twice
+ assert.NoError(t, f.AddVBAProject(file))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm")))
+ // Test add VBA with unsupported charset workbook relationships
+ f.Relationships.Delete(defaultXMLPathWorkbookRels)
+ f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddVBAProject(file), "XML syntax error on line 1: invalid UTF-8")
}
func TestContentTypesReader(t *testing.T) {
- // Test unsupport charset.
+ // Test unsupported charset
f := NewFile()
f.ContentTypes = nil
- f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset
- f.contentTypesReader()
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ _, err := f.contentTypesReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestWorkbookReader(t *testing.T) {
- // Test unsupport charset.
+ // Test unsupported charset
f := NewFile()
f.WorkBook = nil
- f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset
- f.workbookReader()
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ _, err := f.workbookReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestWorkSheetReader(t *testing.T) {
- // Test unsupport charset.
+ // Test unsupported charset
f := NewFile()
- delete(f.Sheet, "xl/worksheets/sheet1.xml")
- f.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
_, err := f.workSheetReader("Sheet1")
- assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8")
- // Test on no checked worksheet.
+ // Test on no checked worksheet
f = NewFile()
- delete(f.Sheet, "xl/worksheets/sheet1.xml")
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(``)
- f.checked = nil
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``))
+ f.checked = sync.Map{}
_, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
}
func TestRelsReader(t *testing.T) {
- // Test unsupport charset.
+ // Test unsupported charset
f := NewFile()
- rels := "xl/_rels/workbook.xml.rels"
- f.Relationships[rels] = nil
- f.XLSX[rels] = MacintoshCyrillicCharset
- f.relsReader(rels)
+ rels := defaultXMLPathWorkbookRels
+ f.Relationships.Store(rels, nil)
+ f.Pkg.Store(rels, MacintoshCyrillicCharset)
+ _, err := f.relsReader(rels)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestDeleteSheetFromWorkbookRels(t *testing.T) {
f := NewFile()
- rels := "xl/_rels/workbook.xml.rels"
- f.Relationships[rels] = nil
+ rels := defaultXMLPathWorkbookRels
+ f.Relationships.Store(rels, nil)
assert.Equal(t, f.deleteSheetFromWorkbookRels("rID"), "")
}
+func TestUpdateLinkedValue(t *testing.T) {
+ f := NewFile()
+ // Test update lined value with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.UpdateLinkedValue(), "XML syntax error on line 1: invalid UTF-8")
+}
+
func TestAttrValToInt(t *testing.T) {
_, err := attrValToInt("r", []xml.Attr{
- {Name: xml.Name{Local: "r"}, Value: "s"}})
+ {Name: xml.Name{Local: "r"}, Value: "s"},
+ })
assert.EqualError(t, err, `strconv.Atoi: parsing "s": invalid syntax`)
}
@@ -1241,25 +1622,30 @@ func prepareTestBook1() (*File, error) {
return nil, err
}
- err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"),
- `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)
- if err != nil {
+ if err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"),
+ &GraphicOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"}); err != nil {
return nil, err
}
- // Test add picture to worksheet with offset, external hyperlink and positioning.
- err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"),
- `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)
- if err != nil {
+ // Test add picture to worksheet with offset, external hyperlink and positioning
+ if err := f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"),
+ &GraphicOptions{
+ OffsetX: 10,
+ OffsetY: 10,
+ Hyperlink: "https://github.com/xuri/excelize",
+ HyperlinkType: "External",
+ Positioning: "oneCell",
+ },
+ ); err != nil {
return nil, err
}
- file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg"))
+ file, err := os.ReadFile(filepath.Join("test", "images", "excel.jpg"))
if err != nil {
return nil, err
}
- err = f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file)
+ err = f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".jpg", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}})
if err != nil {
return nil, err
}
@@ -1269,28 +1655,26 @@ func prepareTestBook1() (*File, error) {
func prepareTestBook3() (*File, error) {
f := NewFile()
- f.NewSheet("Sheet1")
- f.NewSheet("XLSXSheet2")
- f.NewSheet("XLSXSheet3")
- if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil {
+ if _, err := f.NewSheet("Sheet2"); err != nil {
+ return nil, err
+ }
+ if _, err := f.NewSheet("Sheet3"); err != nil {
+ return nil, err
+ }
+ if err := f.SetCellInt("Sheet2", "A23", 56); err != nil {
return nil, err
}
if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil {
return nil, err
}
f.SetActiveSheet(0)
-
- err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"),
- `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`)
- if err != nil {
+ if err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"),
+ &GraphicOptions{ScaleX: 0.5, ScaleY: 0.5, Positioning: "absolute"}); err != nil {
return nil, err
}
-
- err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "")
- if err != nil {
+ if err := f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), nil); err != nil {
return nil, err
}
-
return f, nil
}
@@ -1312,20 +1696,50 @@ func prepareTestBook4() (*File, error) {
return f, nil
}
-func fillCells(f *File, sheet string, colCount, rowCount int) {
+func prepareTestBook5(opts Options) (*File, error) {
+ f := NewFile(opts)
+ var rowNum int
+ for _, idxRange := range [][]int{{27, 36}, {50, 81}} {
+ for numFmtIdx := idxRange[0]; numFmtIdx <= idxRange[1]; numFmtIdx++ {
+ rowNum++
+ styleID, err := f.NewStyle(&Style{NumFmt: numFmtIdx})
+ if err != nil {
+ return f, err
+ }
+ cell, err := CoordinatesToCellName(1, rowNum)
+ if err != nil {
+ return f, err
+ }
+ if err := f.SetCellValue("Sheet1", cell, 45162); err != nil {
+ return f, err
+ }
+ if err := f.SetCellStyle("Sheet1", cell, cell, styleID); err != nil {
+ return f, err
+ }
+ }
+ }
+ return f, nil
+}
+
+func fillCells(f *File, sheet string, colCount, rowCount int) error {
for col := 1; col <= colCount; col++ {
for row := 1; row <= rowCount; row++ {
cell, _ := CoordinatesToCellName(col, row)
if err := f.SetCellStr(sheet, cell, cell); err != nil {
- fmt.Println(err)
+ return err
}
}
}
+ return nil
}
func BenchmarkOpenFile(b *testing.B) {
for i := 0; i < b.N; i++ {
- if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ if err != nil {
+ b.Error(err)
+ }
+ if err := f.Close(); err != nil {
b.Error(err)
}
}
diff --git a/file.go b/file.go
index 34ec359114..aa0816c9c2 100644
--- a/file.go
+++ b/file.go
@@ -1,122 +1,240 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"archive/zip"
"bytes"
- "errors"
- "fmt"
+ "encoding/xml"
"io"
"os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
)
-// NewFile provides a function to create new file by default template. For
-// example:
+// NewFile provides a function to create new file by default template.
+// For example:
//
-// xlsx := NewFile()
-//
-func NewFile() *File {
- file := make(map[string][]byte)
- file["_rels/.rels"] = []byte(XMLHeader + templateRels)
- file["docProps/app.xml"] = []byte(XMLHeader + templateDocpropsApp)
- file["docProps/core.xml"] = []byte(XMLHeader + templateDocpropsCore)
- file["xl/_rels/workbook.xml.rels"] = []byte(XMLHeader + templateWorkbookRels)
- file["xl/theme/theme1.xml"] = []byte(XMLHeader + templateTheme)
- file["xl/worksheets/sheet1.xml"] = []byte(XMLHeader + templateSheet)
- file["xl/styles.xml"] = []byte(XMLHeader + templateStyles)
- file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook)
- file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes)
+// f := NewFile()
+func NewFile(opts ...Options) *File {
f := newFile()
- f.SheetCount, f.XLSX = 1, file
- f.CalcChain = f.calcChainReader()
- f.Comments = make(map[string]*xlsxComments)
- f.ContentTypes = f.contentTypesReader()
- f.Drawings = make(map[string]*xlsxWsDr)
- f.Styles = f.stylesReader()
- f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing)
- f.VMLDrawing = make(map[string]*vmlDrawing)
- f.WorkBook = f.workbookReader()
- f.Relationships = make(map[string]*xlsxRelationships)
- f.Relationships["xl/_rels/workbook.xml.rels"] = f.relsReader("xl/_rels/workbook.xml.rels")
- f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1")
+ f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
+ f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
+ f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
+ f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels))
+ f.Pkg.Store("xl/theme/theme1.xml", []byte(xml.Header+templateTheme))
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+templateSheet))
+ f.Pkg.Store(defaultXMLPathStyles, []byte(xml.Header+templateStyles))
+ f.Pkg.Store(defaultXMLPathWorkbook, []byte(xml.Header+templateWorkbook))
+ f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes))
+ f.SheetCount = 1
+ f.CalcChain, _ = f.calcChainReader()
+ f.ContentTypes, _ = f.contentTypesReader()
+ f.Styles, _ = f.stylesReader()
+ f.WorkBook, _ = f.workbookReader()
+ f.Relationships = sync.Map{}
+ rels, _ := f.relsReader(defaultXMLPathWorkbookRels)
+ f.Relationships.Store(defaultXMLPathWorkbookRels, rels)
f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml"
- f.Theme = f.themeReader()
+ ws, _ := f.workSheetReader("Sheet1")
+ f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
+ f.Theme, _ = f.themeReader()
+ f.options = f.getOptions(opts...)
return f
}
-// Save provides a function to override the xlsx file with origin path.
-func (f *File) Save() error {
+// Save provides a function to override the spreadsheet with origin path.
+func (f *File) Save(opts ...Options) error {
if f.Path == "" {
- return fmt.Errorf("no path defined for file, consider File.WriteTo or File.Write")
+ return ErrSave
+ }
+ for i := range opts {
+ f.options = &opts[i]
}
- return f.SaveAs(f.Path)
+ return f.SaveAs(f.Path, *f.options)
}
-// SaveAs provides a function to create or update to an xlsx file at the
+// SaveAs provides a function to create or update to a spreadsheet at the
// provided path.
-func (f *File) SaveAs(name string) error {
- if len(name) > FileNameLength {
- return errors.New("file name length exceeds maximum limit")
+func (f *File) SaveAs(name string, opts ...Options) error {
+ if len(name) > MaxFilePathLength {
+ return ErrMaxFilePathLength
+ }
+ f.Path = name
+ if _, ok := supportedContentTypes[strings.ToLower(filepath.Ext(f.Path))]; !ok {
+ return ErrWorkbookFileFormat
}
- file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
+ file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
if err != nil {
return err
}
defer file.Close()
- return f.Write(file)
+ return f.Write(file, opts...)
+}
+
+// Close closes and cleanup the open temporary file for the spreadsheet.
+func (f *File) Close() error {
+ var err error
+ if f.sharedStringTemp != nil {
+ if err := f.sharedStringTemp.Close(); err != nil {
+ return err
+ }
+ }
+ f.tempFiles.Range(func(k, v interface{}) bool {
+ if err = os.Remove(v.(string)); err != nil {
+ return false
+ }
+ return true
+ })
+ for _, stream := range f.streams {
+ _ = stream.rawData.Close()
+ }
+ return err
}
// Write provides a function to write to an io.Writer.
-func (f *File) Write(w io.Writer) error {
- _, err := f.WriteTo(w)
+func (f *File) Write(w io.Writer, opts ...Options) error {
+ _, err := f.WriteTo(w, opts...)
return err
}
// WriteTo implements io.WriterTo to write the file.
-func (f *File) WriteTo(w io.Writer) (int64, error) {
- buf, err := f.WriteToBuffer()
- if err != nil {
+func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) {
+ for i := range opts {
+ f.options = &opts[i]
+ }
+ if len(f.Path) != 0 {
+ contentType, ok := supportedContentTypes[strings.ToLower(filepath.Ext(f.Path))]
+ if !ok {
+ return 0, ErrWorkbookFileFormat
+ }
+ if err := f.setContentTypePartProjectExtensions(contentType); err != nil {
+ return 0, err
+ }
+ }
+ if f.options != nil && f.options.Password != "" {
+ buf, err := f.WriteToBuffer()
+ if err != nil {
+ return 0, err
+ }
+ return buf.WriteTo(w)
+ }
+ if err := f.writeDirectToWriter(w); err != nil {
return 0, err
}
- return buf.WriteTo(w)
+ return 0, nil
}
-// WriteToBuffer provides a function to get bytes.Buffer from the saved file.
+// WriteToBuffer provides a function to get bytes.Buffer from the saved file,
+// and it allocates space in memory. Be careful when the file size is large.
func (f *File) WriteToBuffer() (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
zw := zip.NewWriter(buf)
+
+ if err := f.writeToZip(zw); err != nil {
+ return buf, zw.Close()
+ }
+
+ if f.options != nil && f.options.Password != "" {
+ if err := zw.Close(); err != nil {
+ return buf, err
+ }
+ b, err := Encrypt(buf.Bytes(), f.options)
+ if err != nil {
+ return buf, err
+ }
+ buf.Reset()
+ buf.Write(b)
+ return buf, nil
+ }
+ return buf, zw.Close()
+}
+
+// writeDirectToWriter provides a function to write to io.Writer.
+func (f *File) writeDirectToWriter(w io.Writer) error {
+ zw := zip.NewWriter(w)
+ if err := f.writeToZip(zw); err != nil {
+ _ = zw.Close()
+ return err
+ }
+ return zw.Close()
+}
+
+// writeToZip provides a function to write to zip.Writer
+func (f *File) writeToZip(zw *zip.Writer) error {
f.calcChainWriter()
f.commentsWriter()
f.contentTypesWriter()
f.drawingsWriter()
+ f.volatileDepsWriter()
f.vmlDrawingWriter()
f.workBookWriter()
f.workSheetWriter()
f.relsWriter()
+ _ = f.sharedStringsLoader()
f.sharedStringsWriter()
f.styleSheetWriter()
+ f.themeWriter()
- for path, content := range f.XLSX {
+ for path, stream := range f.streams {
fi, err := zw.Create(path)
if err != nil {
- zw.Close()
- return buf, err
+ return err
}
- _, err = fi.Write(content)
- if err != nil {
- zw.Close()
- return buf, err
+ var from io.Reader
+ if from, err = stream.rawData.Reader(); err != nil {
+ _ = stream.rawData.Close()
+ return err
+ }
+ if _, err = io.Copy(fi, from); err != nil {
+ return err
}
}
- return buf, zw.Close()
+ var (
+ err error
+ files, tempFiles []string
+ )
+ f.Pkg.Range(func(path, content interface{}) bool {
+ if _, ok := f.streams[path.(string)]; ok {
+ return true
+ }
+ files = append(files, path.(string))
+ return true
+ })
+ sort.Sort(sort.Reverse(sort.StringSlice(files)))
+ for _, path := range files {
+ var fi io.Writer
+ if fi, err = zw.Create(path); err != nil {
+ break
+ }
+ content, _ := f.Pkg.Load(path)
+ _, err = fi.Write(content.([]byte))
+ }
+ f.tempFiles.Range(func(path, content interface{}) bool {
+ if _, ok := f.Pkg.Load(path); ok {
+ return true
+ }
+ tempFiles = append(tempFiles, path.(string))
+ return true
+ })
+ sort.Sort(sort.Reverse(sort.StringSlice(tempFiles)))
+ for _, path := range tempFiles {
+ var fi io.Writer
+ if fi, err = zw.Create(path); err != nil {
+ break
+ }
+ _, err = fi.Write(f.readBytes(path))
+ }
+ return err
}
diff --git a/file_test.go b/file_test.go
index e27b754af1..4272a7b4f1 100644
--- a/file_test.go
+++ b/file_test.go
@@ -3,10 +3,14 @@ package excelize
import (
"bufio"
"bytes"
+ "os"
+ "path/filepath"
"strings"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func BenchmarkWrite(b *testing.B) {
@@ -19,12 +23,12 @@ func BenchmarkWrite(b *testing.B) {
if err != nil {
b.Error(err)
}
- if err := f.SetCellDefault("Sheet1", val, s); err != nil {
+ if err := f.SetCellValue("Sheet1", val, s); err != nil {
b.Error(err)
}
}
}
- // Save xlsx file by the given path.
+ // Save spreadsheet by the given path.
err := f.SaveAs("./test.xlsx")
if err != nil {
b.Error(err)
@@ -33,16 +37,61 @@ func BenchmarkWrite(b *testing.B) {
}
func TestWriteTo(t *testing.T) {
- f := File{}
- buf := bytes.Buffer{}
- f.XLSX = make(map[string][]byte, 0)
- f.XLSX["/d/"] = []byte("s")
- _, err := f.WriteTo(bufio.NewWriter(&buf))
- assert.EqualError(t, err, "zip: write to directory")
- delete(f.XLSX, "/d/")
+ // Test WriteToBuffer err
+ {
+ f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
+ f.Pkg.Store("/d/", []byte("s"))
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.EqualError(t, err, "zip: write to directory")
+ f.Pkg.Delete("/d/")
+ }
// Test file path overflow
- const maxUint16 = 1<<16 - 1
- f.XLSX[strings.Repeat("s", maxUint16+1)] = nil
- _, err = f.WriteTo(bufio.NewWriter(&buf))
- assert.EqualError(t, err, "zip: FileHeader.Name too long")
+ {
+ f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
+ const maxUint16 = 1<<16 - 1
+ f.Pkg.Store(strings.Repeat("s", maxUint16+1), nil)
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.EqualError(t, err, "zip: FileHeader.Name too long")
+ }
+ // Test StreamsWriter err
+ {
+ f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
+ f.Pkg.Store("s", nil)
+ f.streams = make(map[string]*StreamWriter)
+ file, _ := os.Open("123")
+ f.streams["s"] = &StreamWriter{rawData: bufferedWriter{tmp: file}}
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.Nil(t, err)
+ }
+ // Test write with temporary file
+ {
+ f, buf := File{tempFiles: sync.Map{}}, bytes.Buffer{}
+ const maxUint16 = 1<<16 - 1
+ f.tempFiles.Store("s", "")
+ f.tempFiles.Store(strings.Repeat("s", maxUint16+1), "")
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.EqualError(t, err, "zip: FileHeader.Name too long")
+ }
+ // Test write with unsupported workbook file format
+ {
+ f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
+ f.Pkg.Store("/d", []byte("s"))
+ f.Path = "Book1.xls"
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.EqualError(t, err, ErrWorkbookFileFormat.Error())
+ }
+ // Test write with unsupported charset content types.
+ {
+ f, buf := NewFile(), bytes.Buffer{}
+ f.ContentTypes, f.Path = nil, filepath.Join("test", "TestWriteTo.xlsx")
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ _, err := f.WriteTo(bufio.NewWriter(&buf))
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ }
+}
+
+func TestClose(t *testing.T) {
+ f := NewFile()
+ f.tempFiles.Store("/d/", "/d/")
+ require.Error(t, f.Close())
}
diff --git a/go.mod b/go.mod
index f94f33beb2..53b05c8e78 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,22 @@
-module github.com/360EntSecGroup-Skylar/excelize/v2
+module github.com/xuri/excelize/v2
-go 1.12
+go 1.23.0
+
+require (
+ github.com/richardlehane/mscfb v1.0.4
+ github.com/stretchr/testify v1.10.0
+ github.com/tiendc/go-deepcopy v1.5.1
+ github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79
+ github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba
+ golang.org/x/crypto v0.36.0
+ golang.org/x/image v0.25.0
+ golang.org/x/net v0.38.0
+ golang.org/x/text v0.23.0
+)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/kr/text v0.2.0 // indirect
- github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
- github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
- github.com/stretchr/testify v1.5.1
- github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91
- golang.org/x/image v0.0.0-20200430140353-33d19683fad8
- golang.org/x/net v0.0.0-20200506145744-7e3656a0809f
- golang.org/x/text v0.3.2 // indirect
- gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
- gopkg.in/yaml.v2 v2.2.8 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/richardlehane/msoleps v1.0.4 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 7fa49fe501..e66a0ba3e4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,39 +1,29 @@
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8=
-github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
-golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tiendc/go-deepcopy v1.5.1 h1:5ymXIB8ReIywehne6oy3HgywC8LicXYucPBNnj5QQxE=
+github.com/tiendc/go-deepcopy v1.5.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
+github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
+github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=
+github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
+golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hsl.go b/hsl.go
index c30c165a5b..b8fb77b21a 100644
--- a/hsl.go
+++ b/hsl.go
@@ -60,26 +60,26 @@ func hslModel(c color.Color) color.Color {
return HSL{h, s, l}
}
-// RGBToHSL converts an RGB triple to a HSL triple.
+// RGBToHSL converts an RGB triple to an HSL triple.
func RGBToHSL(r, g, b uint8) (h, s, l float64) {
fR := float64(r) / 255
fG := float64(g) / 255
fB := float64(b) / 255
- max := math.Max(math.Max(fR, fG), fB)
- min := math.Min(math.Min(fR, fG), fB)
- l = (max + min) / 2
- if max == min {
+ maxVal := math.Max(math.Max(fR, fG), fB)
+ minVal := math.Min(math.Min(fR, fG), fB)
+ l = (maxVal + minVal) / 2
+ if maxVal == minVal {
// Achromatic.
h, s = 0, 0
} else {
// Chromatic.
- d := max - min
+ d := maxVal - minVal
if l > 0.5 {
- s = d / (2.0 - max - min)
+ s = d / (2.0 - maxVal - minVal)
} else {
- s = d / (max + min)
+ s = d / (maxVal + minVal)
}
- switch max {
+ switch maxVal {
case fR:
h = (fG - fB) / d
if fG < fB {
@@ -95,7 +95,7 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) {
return
}
-// HSLToRGB converts an HSL triple to a RGB triple.
+// HSLToRGB converts an HSL triple to an RGB triple.
func HSLToRGB(h, s, l float64) (r, g, b uint8) {
var fR, fG, fB float64
if s == 0 {
diff --git a/lib.go b/lib.go
index acb4590142..e06e7f5105 100644
--- a/lib.go
+++ b/lib.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -18,53 +18,129 @@ import (
"encoding/xml"
"fmt"
"io"
+ "math"
+ "math/big"
+ "os"
+ "reflect"
+ "regexp"
"strconv"
"strings"
)
-// ReadZipReader can be used to read the spreadsheet in memory without touching the
-// filesystem.
-func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
- var err error
- var docPart = map[string]string{
- "[content_types].xml": "[Content_Types].xml",
- "xl/sharedstrings.xml": "xl/sharedStrings.xml",
- }
- fileList := make(map[string][]byte, len(r.File))
- worksheets := 0
+// ReadZipReader extract spreadsheet with given options.
+func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
+ var (
+ err error
+ docPart = map[string]string{
+ "[content_types].xml": defaultXMLPathContentTypes,
+ "xl/sharedstrings.xml": defaultXMLPathSharedStrings,
+ }
+ fileList = make(map[string][]byte, len(r.File))
+ worksheets int
+ unzipSize int64
+ )
for _, v := range r.File {
- fileName := v.Name
- if partName, ok := docPart[strings.ToLower(v.Name)]; ok {
+ fileSize := v.FileInfo().Size()
+ unzipSize += fileSize
+ if unzipSize > f.options.UnzipSizeLimit {
+ return fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit)
+ }
+ fileName := strings.ReplaceAll(v.Name, "\\", "/")
+ if partName, ok := docPart[strings.ToLower(fileName)]; ok {
fileName = partName
}
- if fileList[fileName], err = readFile(v); err != nil {
- return nil, 0, err
+ if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit {
+ tempFile, err := f.unzipToTemp(v)
+ if tempFile != "" {
+ f.tempFiles.Store(fileName, tempFile)
+ }
+ if err == nil {
+ continue
+ }
}
- if strings.HasPrefix(v.Name, "xl/worksheets/sheet") {
+ if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") {
worksheets++
+ if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() {
+ tempFile, err := f.unzipToTemp(v)
+ if tempFile != "" {
+ f.tempFiles.Store(fileName, tempFile)
+ }
+ if err == nil {
+ continue
+ }
+ }
+ }
+ if fileList[fileName], err = readFile(v); err != nil {
+ return nil, 0, err
}
}
return fileList, worksheets, nil
}
-// readXML provides a function to read XML content as string.
+// unzipToTemp unzip the zip entity to the system temporary directory and
+// returned the unzipped file path.
+func (f *File) unzipToTemp(zipFile *zip.File) (string, error) {
+ tmp, err := os.CreateTemp(os.TempDir(), "excelize-")
+ if err != nil {
+ return "", err
+ }
+ rc, err := zipFile.Open()
+ if err != nil {
+ return tmp.Name(), err
+ }
+ if _, err = io.Copy(tmp, rc); err != nil {
+ return tmp.Name(), err
+ }
+ if err = rc.Close(); err != nil {
+ return tmp.Name(), err
+ }
+ return tmp.Name(), tmp.Close()
+}
+
+// readXML provides a function to read XML content as bytes.
func (f *File) readXML(name string) []byte {
- if content, ok := f.XLSX[name]; ok {
- return content
+ if content, _ := f.Pkg.Load(name); content != nil {
+ return content.([]byte)
+ }
+ if content, ok := f.streams[name]; ok {
+ return content.rawData.buf.Bytes()
}
return []byte{}
}
+// readBytes read file as bytes by given path.
+func (f *File) readBytes(name string) []byte {
+ content := f.readXML(name)
+ if len(content) != 0 {
+ return content
+ }
+ file, err := f.readTemp(name)
+ if err != nil {
+ return content
+ }
+ content, _ = io.ReadAll(file)
+ f.Pkg.Store(name, content)
+ _ = file.Close()
+ return content
+}
+
+// readTemp read file from system temporary directory by given path.
+func (f *File) readTemp(name string) (file *os.File, err error) {
+ path, ok := f.tempFiles.Load(name)
+ if !ok {
+ return
+ }
+ file, err = os.Open(path.(string))
+ return
+}
+
// saveFileList provides a function to update given file content in file list
-// of XLSX.
+// of spreadsheet.
func (f *File) saveFileList(name string, content []byte) {
- newContent := make([]byte, 0, len(XMLHeader)+len(content))
- newContent = append(newContent, []byte(XMLHeader)...)
- newContent = append(newContent, content...)
- f.XLSX[name] = newContent
+ f.Pkg.Store(name, append([]byte(xml.Header), content...))
}
-// Read file content as string in a archive file.
+// Read file content as string in an archive file.
func readFile(file *zip.File) ([]byte, error) {
rc, err := file.Open()
if err != nil {
@@ -73,26 +149,23 @@ func readFile(file *zip.File) ([]byte, error) {
dat := make([]byte, 0, file.FileInfo().Size())
buff := bytes.NewBuffer(dat)
_, _ = io.Copy(buff, rc)
- rc.Close()
- return buff.Bytes(), nil
+ return buff.Bytes(), rc.Close()
}
// SplitCellName splits cell name to column name and row number.
//
// Example:
//
-// excelize.SplitCellName("AK74") // return "AK", 74, nil
-//
+// excelize.SplitCellName("AK74") // return "AK", 74, nil
func SplitCellName(cell string) (string, int, error) {
alpha := func(r rune) bool {
- return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
+ return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || (r == 36)
}
-
if strings.IndexFunc(cell, alpha) == 0 {
i := strings.LastIndexFunc(cell, alpha)
if i >= 0 && i < len(cell)-1 {
- col, rowstr := cell[:i+1], cell[i+1:]
- if row, err := strconv.Atoi(rowstr); err == nil && row > 0 {
+ col, rowStr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:]
+ if row, err := strconv.Atoi(rowStr); err == nil && row > 0 {
return col, row, nil
}
}
@@ -121,13 +194,12 @@ func JoinCellName(col string, row int) (string, error) {
}
// ColumnNameToNumber provides a function to convert Excel sheet column name
-// to int. Column name case insensitive. The function returns an error if
-// column name incorrect.
+// (case-insensitive) to int. The function returns an error if column name
+// incorrect.
//
// Example:
//
-// excelize.ColumnNameToNumber("AK") // returns 37, nil
-//
+// excelize.ColumnNameToNumber("AK") // returns 37, nil
func ColumnNameToNumber(name string) (int, error) {
if len(name) == 0 {
return -1, newInvalidColumnNameError(name)
@@ -145,8 +217,8 @@ func ColumnNameToNumber(name string) (int, error) {
}
multi *= 26
}
- if col > TotalColumns {
- return -1, fmt.Errorf("column number exceeds maximum limit")
+ if col > MaxColumns {
+ return -1, ErrColumnNumber
}
return col, nil
}
@@ -156,21 +228,23 @@ func ColumnNameToNumber(name string) (int, error) {
//
// Example:
//
-// excelize.ColumnNumberToName(37) // returns "AK", nil
-//
+// excelize.ColumnNumberToName(37) // returns "AK", nil
func ColumnNumberToName(num int) (string, error) {
- if num < 1 {
- return "", fmt.Errorf("incorrect column number %d", num)
+ if num < MinColumns || num > MaxColumns {
+ return "", ErrColumnNumber
}
- if num > TotalColumns {
- return "", fmt.Errorf("column number exceeds maximum limit")
+ estimatedLength := 0
+ for n := num; n > 0; n = (n - 1) / 26 {
+ estimatedLength++
}
- var col string
+
+ result := make([]byte, estimatedLength)
for num > 0 {
- col = string((num-1)%26+65) + col
+ estimatedLength--
+ result[estimatedLength] = byte((num-1)%26 + 'A')
num = (num - 1) / 26
}
- return col, nil
+ return string(result), nil
}
// CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates
@@ -178,20 +252,17 @@ func ColumnNumberToName(num int) (string, error) {
//
// Example:
//
-// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil
-// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil
-//
+// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil
+// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil
func CellNameToCoordinates(cell string) (int, int, error) {
- const msg = "cannot convert cell %q to coordinates: %v"
-
- colname, row, err := SplitCellName(cell)
+ colName, row, err := SplitCellName(cell)
if err != nil {
- return -1, -1, fmt.Errorf(msg, cell, err)
+ return -1, -1, newCellNameToCoordinatesError(cell, err)
}
if row > TotalRows {
- return -1, -1, fmt.Errorf("row number exceeds maximum limit")
+ return -1, -1, ErrMaxRows
}
- col, err := ColumnNameToNumber(colname)
+ col, err := ColumnNameToNumber(colName)
return col, row, err
}
@@ -200,54 +271,311 @@ func CellNameToCoordinates(cell string) (int, int, error) {
//
// Example:
//
-// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil
-//
-func CoordinatesToCellName(col, row int) (string, error) {
+// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil
+// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil
+func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
if col < 1 || row < 1 {
- return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row)
+ return "", newCoordinatesToCellNameError(col, row)
+ }
+ if row > TotalRows {
+ return "", ErrMaxRows
+ }
+ sign := ""
+ for _, a := range abs {
+ if a {
+ sign = "$"
+ }
+ }
+ colName, err := ColumnNumberToName(col)
+ return sign + colName + sign + strconv.Itoa(row), err
+}
+
+// rangeRefToCoordinates provides a function to convert range reference to a
+// pair of coordinates.
+func rangeRefToCoordinates(ref string) ([]int, error) {
+ rng := strings.Split(strings.ReplaceAll(ref, "$", ""), ":")
+ if len(rng) < 2 {
+ return nil, ErrParameterInvalid
+ }
+ return cellRefsToCoordinates(rng[0], rng[1])
+}
+
+// cellRefsToCoordinates provides a function to convert cell range to a
+// pair of coordinates.
+func cellRefsToCoordinates(firstCell, lastCell string) ([]int, error) {
+ coordinates := make([]int, 4)
+ var err error
+ coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell)
+ if err != nil {
+ return coordinates, err
+ }
+ coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell)
+ return coordinates, err
+}
+
+// sortCoordinates provides a function to correct the cell range, such
+// correct C1:B3 to B1:C3.
+func sortCoordinates(coordinates []int) error {
+ if len(coordinates) != 4 {
+ return ErrCoordinates
+ }
+ if coordinates[2] < coordinates[0] {
+ coordinates[2], coordinates[0] = coordinates[0], coordinates[2]
+ }
+ if coordinates[3] < coordinates[1] {
+ coordinates[3], coordinates[1] = coordinates[1], coordinates[3]
+ }
+ return nil
+}
+
+// coordinatesToRangeRef provides a function to convert a pair of coordinates
+// to range reference.
+func coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) {
+ if len(coordinates) != 4 {
+ return "", ErrCoordinates
+ }
+ firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1], abs...)
+ if err != nil {
+ return "", err
+ }
+ lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3], abs...)
+ if err != nil {
+ return "", err
+ }
+ return firstCell + ":" + lastCell, err
+}
+
+// getDefinedNameRefTo convert defined name to reference range.
+func (f *File) getDefinedNameRefTo(definedNameName, currentSheet string) (refTo string) {
+ var workbookRefTo, worksheetRefTo string
+ for _, definedName := range f.GetDefinedName() {
+ if definedName.Name == definedNameName {
+ // worksheet scope takes precedence over scope workbook when both definedNames exist
+ if definedName.Scope == "Workbook" {
+ workbookRefTo = definedName.RefersTo
+ }
+ if definedName.Scope == currentSheet {
+ worksheetRefTo = definedName.RefersTo
+ }
+ }
+ }
+ refTo = workbookRefTo
+ if worksheetRefTo != "" {
+ refTo = worksheetRefTo
+ }
+ return
+}
+
+// flatSqref convert reference sequence to cell reference list.
+func flatSqref(sqref string) (cells map[int][][]int, err error) {
+ var coordinates []int
+ cells = make(map[int][][]int)
+ for _, ref := range strings.Fields(sqref) {
+ rng := strings.Split(ref, ":")
+ switch len(rng) {
+ case 1:
+ var col, row int
+ col, row, err = CellNameToCoordinates(rng[0])
+ if err != nil {
+ return
+ }
+ cells[col] = append(cells[col], []int{col, row})
+ case 2:
+ if coordinates, err = rangeRefToCoordinates(ref); err != nil {
+ return
+ }
+ _ = sortCoordinates(coordinates)
+ for c := coordinates[0]; c <= coordinates[2]; c++ {
+ for r := coordinates[1]; r <= coordinates[3]; r++ {
+ cells[c] = append(cells[c], []int{c, r})
+ }
+ }
+ }
+ }
+ return
+}
+
+// inCoordinates provides a method to check if a coordinate is present in
+// coordinates array, and return the index of its location, otherwise
+// return -1.
+func inCoordinates(a [][]int, x []int) int {
+ for idx, n := range a {
+ if x[0] == n[0] && x[1] == n[1] {
+ return idx
+ }
}
- colname, err := ColumnNumberToName(col)
- return fmt.Sprintf("%s%d", colname, row), err
+ return -1
+}
+
+// inStrSlice provides a method to check if an element is present in an array,
+// and return the index of its location, otherwise return -1.
+func inStrSlice(a []string, x string, caseSensitive bool) int {
+ for idx, n := range a {
+ if !caseSensitive && strings.EqualFold(x, n) {
+ return idx
+ }
+ if x == n {
+ return idx
+ }
+ }
+ return -1
+}
+
+// inFloat64Slice provides a method to check if an element is present in a
+// float64 array, and return the index of its location, otherwise return -1.
+func inFloat64Slice(a []float64, x float64) int {
+ for idx, n := range a {
+ if x == n {
+ return idx
+ }
+ }
+ return -1
}
// boolPtr returns a pointer to a bool with the given value.
func boolPtr(b bool) *bool { return &b }
-// intPtr returns a pointer to a int with the given value.
+// intPtr returns a pointer to an int with the given value.
func intPtr(i int) *int { return &i }
-// float64Ptr returns a pofloat64er to a float64 with the given value.
+// uintPtr returns a pointer to an unsigned integer with the given value.
+func uintPtr(u uint) *uint { return &u }
+
+// float64Ptr returns a pointer to a float64 with the given value.
func float64Ptr(f float64) *float64 { return &f }
// stringPtr returns a pointer to a string with the given value.
func stringPtr(s string) *string { return &s }
-// defaultTrue returns true if b is nil, or the pointed value.
-func defaultTrue(b *bool) bool {
- if b == nil {
- return true
+// Value extracts string data type text from a attribute value.
+func (avb *attrValString) Value() string {
+ if avb != nil && avb.Val != nil {
+ return *avb.Val
+ }
+ return ""
+}
+
+// Value extracts boolean data type value from a attribute value.
+func (avb *attrValBool) Value() bool {
+ if avb != nil && avb.Val != nil {
+ return *avb.Val
}
- return *b
+ return false
}
-// parseFormatSet provides a method to convert format string to []byte and
-// handle empty string.
-func parseFormatSet(formatSet string) []byte {
- if formatSet != "" {
- return []byte(formatSet)
+// Value extracts float64 data type numeric from a attribute value.
+func (attr *attrValFloat) Value() float64 {
+ if attr != nil && attr.Val != nil {
+ return *attr.Val
}
- return []byte("{}")
+ return 0
+}
+
+// MarshalXML convert the boolean data type to literal values 0 or 1 on
+// serialization.
+func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ attr := xml.Attr{
+ Name: xml.Name{
+ Space: start.Name.Space,
+ Local: "val",
+ },
+ Value: "0",
+ }
+ if avb.Val != nil {
+ if *avb.Val {
+ attr.Value = "1"
+ } else {
+ attr.Value = "0"
+ }
+ }
+ start.Attr = []xml.Attr{attr}
+ if err := e.EncodeToken(start); err != nil {
+ return err
+ }
+ return e.EncodeToken(start.End())
+}
+
+// UnmarshalXML convert the literal values true, false, 1, 0 of the XML
+// attribute to boolean data type on deserialization.
+func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ for {
+ t, err := d.Token()
+ if err != nil {
+ return err
+ }
+ found := false
+ switch t.(type) {
+ case xml.StartElement:
+ return ErrAttrValBool
+ case xml.EndElement:
+ found = true
+ }
+ if found {
+ break
+ }
+ }
+ for _, attr := range start.Attr {
+ if attr.Name.Local == "val" {
+ if attr.Value == "" {
+ val := true
+ avb.Val = &val
+ } else {
+ val, err := strconv.ParseBool(attr.Value)
+ if err != nil {
+ return err
+ }
+ avb.Val = &val
+ }
+ return nil
+ }
+ }
+ defaultVal := true
+ avb.Val = &defaultVal
+ return nil
+}
+
+// MarshalXML encodes ext element with specified namespace attributes on
+// serialization.
+func (ext xlsxExt) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ start.Attr = ext.xmlns
+ return e.EncodeElement(decodeExt{URI: ext.URI, Content: ext.Content}, start)
+}
+
+// UnmarshalXML extracts ext element attributes namespace by giving XML decoder
+// on deserialization.
+func (ext *xlsxExt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ for _, attr := range start.Attr {
+ if attr.Name.Local == "uri" {
+ continue
+ }
+ if attr.Name.Space == "xmlns" {
+ attr.Name.Space = ""
+ attr.Name.Local = "xmlns:" + attr.Name.Local
+ }
+ ext.xmlns = append(ext.xmlns, attr)
+ }
+ e := &decodeExt{}
+ if err := d.DecodeElement(&e, &start); err != nil {
+ return err
+ }
+ ext.URI, ext.Content = e.URI, e.Content
+ return nil
}
// namespaceStrictToTransitional provides a method to convert Strict and
// Transitional namespaces.
func namespaceStrictToTransitional(content []byte) []byte {
- var namespaceTranslationDic = map[string]string{
- StrictSourceRelationship: SourceRelationship.Value,
- StrictSourceRelationshipChart: SourceRelationshipChart,
- StrictSourceRelationshipComments: SourceRelationshipComments,
- StrictSourceRelationshipImage: SourceRelationshipImage,
- StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value,
+ namespaceTranslationDic := map[string]string{
+ StrictNameSpaceDocumentPropertiesVariantTypes: NameSpaceDocumentPropertiesVariantTypes.Value,
+ StrictNameSpaceDrawingMLMain: NameSpaceDrawingMLMain,
+ StrictNameSpaceExtendedProperties: NameSpaceExtendedProperties,
+ StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value,
+ StrictSourceRelationship: SourceRelationship.Value,
+ StrictSourceRelationshipChart: SourceRelationshipChart,
+ StrictSourceRelationshipComments: SourceRelationshipComments,
+ StrictSourceRelationshipExtendProperties: SourceRelationshipExtendProperties,
+ StrictSourceRelationshipImage: SourceRelationshipImage,
+ StrictSourceRelationshipOfficeDocument: SourceRelationshipOfficeDocument,
}
for s, n := range namespaceTranslationDic {
content = bytesReplace(content, []byte(s), []byte(n), -1)
@@ -255,14 +583,14 @@ func namespaceStrictToTransitional(content []byte) []byte {
return content
}
-// bytesReplace replace old bytes with given new.
-func bytesReplace(s, old, new []byte, n int) []byte {
+// bytesReplace replace source bytes with given target.
+func bytesReplace(s, source, target []byte, n int) []byte {
if n == 0 {
return s
}
- if len(old) < len(new) {
- return bytes.Replace(s, old, new, n)
+ if len(source) < len(target) {
+ return bytes.Replace(s, source, target, n)
}
if n < 0 {
@@ -271,18 +599,18 @@ func bytesReplace(s, old, new []byte, n int) []byte {
var wid, i, j, w int
for i, j = 0, 0; i < len(s) && j < n; j++ {
- wid = bytes.Index(s[i:], old)
+ wid = bytes.Index(s[i:], source)
if wid < 0 {
break
}
w += copy(s[w:], s[i:i+wid])
- w += copy(s[w:], new)
- i += wid + len(old)
+ w += copy(s[w:], target)
+ i += wid + len(source)
}
w += copy(s[w:], s[i:])
- return s[0:w]
+ return s[:w]
}
// genSheetPasswd provides a method to generate password for worksheet
@@ -306,7 +634,7 @@ func genSheetPasswd(plaintext string) string {
charPos++
rotatedBits := value >> 15 // rotated bits beyond bit 15
value &= 0x7fff // first 15 bits
- password ^= (value | rotatedBits)
+ password ^= value | rotatedBits
}
password ^= int64(len(plaintext))
password ^= 0xCE4B
@@ -325,6 +653,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr {
case xml.StartElement:
tokenIdx++
if tokenIdx == 1 {
+ var ns bool
+ for i := 0; i < len(startElement.Attr); i++ {
+ if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value &&
+ startElement.Attr[i].Name == NameSpaceSpreadSheet.Name {
+ ns = true
+ }
+ }
+ if !ns {
+ startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet)
+ }
return startElement.Attr
}
}
@@ -338,6 +676,9 @@ func genXMLNamespace(attr []xml.Attr) string {
var rootElement string
for _, v := range attr {
if lastSpace := getXMLNamespace(v.Name.Space, attr); lastSpace != "" {
+ if lastSpace == NameSpaceXML {
+ lastSpace = "xml"
+ }
rootElement += fmt.Sprintf("%s:%s=\"%s\" ", lastSpace, v.Name.Local, v.Value)
continue
}
@@ -359,64 +700,233 @@ func getXMLNamespace(space string, attr []xml.Attr) string {
// replaceNameSpaceBytes provides a function to replace the XML root element
// attribute by the given component part path and XML content.
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
- var oldXmlns = []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
- var newXmlns = []byte(templateNamespaceIDMap)
- if attr, ok := f.xmlAttr[path]; ok {
- newXmlns = []byte(genXMLNamespace(attr))
+ sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
+ targetXmlns := []byte(templateNamespaceIDMap)
+ if attrs, ok := f.xmlAttr.Load(path); ok {
+ targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr)))
}
- return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1)
+ return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1)
}
-// addNameSpaces provides a function to add a XML attribute by the given
+// addNameSpaces provides a function to add an XML attribute by the given
// component part path.
func (f *File) addNameSpaces(path string, ns xml.Attr) {
exist := false
mc := false
ignore := -1
- if attr, ok := f.xmlAttr[path]; ok {
- for i, attribute := range attr {
- if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space {
+ if attrs, ok := f.xmlAttr.Load(path); ok {
+ for i, attr := range attrs.([]xml.Attr) {
+ if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space {
exist = true
}
- if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" {
+ if attr.Name.Local == "Ignorable" && getXMLNamespace(attr.Name.Space, attrs.([]xml.Attr)) == "mc" {
ignore = i
}
- if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" {
+ if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" {
mc = true
}
}
}
if !exist {
- f.xmlAttr[path] = append(f.xmlAttr[path], ns)
+ attrs, _ := f.xmlAttr.Load(path)
+ if attrs == nil {
+ attrs = []xml.Attr{}
+ }
+ attrs = append(attrs.([]xml.Attr), ns)
+ f.xmlAttr.Store(path, attrs)
if !mc {
- f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility)
+ attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility)
+ f.xmlAttr.Store(path, attrs)
}
if ignore == -1 {
- f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{
+ attrs = append(attrs.([]xml.Attr), xml.Attr{
Name: xml.Name{Local: "Ignorable", Space: "mc"},
Value: ns.Name.Local,
})
+ f.xmlAttr.Store(path, attrs)
return
}
f.setIgnorableNameSpace(path, ignore, ns)
}
}
-// setIgnorableNameSpace provides a function to set XML namespace as ignorable by the given
-// attribute.
+// setIgnorableNameSpace provides a function to set XML namespace as ignorable
+// by the given attribute.
func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) {
ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"}
- if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 {
- f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local))
+ xmlAttrs, _ := f.xmlAttr.Load(path)
+ if inStrSlice(strings.Fields(xmlAttrs.([]xml.Attr)[index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
+ xmlAttrs.([]xml.Attr)[index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", xmlAttrs.([]xml.Attr)[index].Value, ns.Name.Local))
+ f.xmlAttr.Store(path, xmlAttrs)
}
}
// addSheetNameSpace add XML attribute for worksheet.
func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) {
- name, _ := f.sheetMap[trimSheetName(sheet)]
+ name, _ := f.getSheetXMLPath(sheet)
f.addNameSpaces(name, ns)
}
+// isNumeric determines whether an expression is a valid numeric type and get
+// the precision for the numeric.
+func isNumeric(s string) (bool, int, float64) {
+ if strings.Contains(s, "_") {
+ return false, 0, 0
+ }
+ var decimal big.Float
+ _, ok := decimal.SetString(s)
+ if !ok {
+ return false, 0, 0
+ }
+ var noScientificNotation string
+ flt, _ := decimal.Float64()
+ noScientificNotation = strconv.FormatFloat(flt, 'f', -1, 64)
+ return true, len(strings.ReplaceAll(noScientificNotation, ".", "")), flt
+}
+
+var (
+ bstrExp = regexp.MustCompile(`_x[a-fA-F\d]{4}_`)
+ bstrEscapeExp = regexp.MustCompile(`x[a-fA-F\d]{4}_`)
+)
+
+// bstrUnmarshal parses the binary basic string, this will trim escaped string
+// literal which not permitted in an XML 1.0 document. The basic string
+// variant type can store any valid Unicode character. Unicode's characters
+// that cannot be directly represented in XML as defined by the XML 1.0
+// specification, shall be escaped using the Unicode numerical character
+// representation escape character format _xHHHH_, where H represents a
+// hexadecimal character in the character's value. For example: The Unicode
+// character 8 is not permitted in an XML 1.0 document, so it shall be
+// escaped as _x0008_. To store the literal form of an escape sequence, the
+// initial underscore shall itself be escaped (i.e. stored as _x005F_). For
+// example: The string literal _x0008_ would be stored as _x005F_x0008_.
+func bstrUnmarshal(s string) (result string) {
+ matches, l, cursor := bstrExp.FindAllStringSubmatchIndex(s, -1), len(s), 0
+ for _, match := range matches {
+ result += s[cursor:match[0]]
+ subStr := s[match[0]:match[1]]
+ if subStr == "_x005F_" {
+ cursor = match[1]
+ result += "_"
+ continue
+ }
+ if bstrExp.MatchString(subStr) {
+ cursor = match[1]
+ v, _ := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`)
+ result += v
+ }
+ }
+ if cursor < l {
+ result += s[cursor:]
+ }
+ return result
+}
+
+// bstrMarshal encode the escaped string literal which not permitted in an XML
+// 1.0 document.
+func bstrMarshal(s string) (result string) {
+ matches, l, cursor := bstrExp.FindAllStringSubmatchIndex(s, -1), len(s), 0
+ for _, match := range matches {
+ result += s[cursor:match[0]]
+ subStr := s[match[0]:match[1]]
+ if subStr == "_x005F_" {
+ cursor = match[1]
+ if match[1]+6 <= l && bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) {
+ _, err := strconv.Unquote(`"\u` + s[match[1]+1:match[1]+5] + `"`)
+ if err == nil {
+ result += subStr + "x005F" + subStr
+ continue
+ }
+ }
+ result += subStr + "x005F_"
+ continue
+ }
+ if bstrExp.MatchString(subStr) {
+ cursor = match[1]
+ if _, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`); err == nil {
+ result += "_x005F" + subStr
+ continue
+ }
+ }
+ }
+ if cursor < l {
+ result += s[cursor:]
+ }
+ return result
+}
+
+// newRat converts decimals to rational fractions with the required precision.
+func newRat(n float64, iterations int64, prec float64) *big.Rat {
+ x := int64(math.Floor(n))
+ y := n - float64(x)
+ rat := continuedFraction(y, 1, iterations, prec)
+ return rat.Add(rat, new(big.Rat).SetInt64(x))
+}
+
+// continuedFraction returns rational from decimal with the continued fraction
+// algorithm.
+func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat {
+ if i >= limit || n <= prec {
+ return big.NewRat(0, 1)
+ }
+ inverted := 1 / n
+ y := int64(math.Floor(inverted))
+ x := inverted - float64(y)
+ ratY := new(big.Rat).SetInt64(y)
+ ratNext := continuedFraction(x, i+1, limit, prec)
+ res := ratY.Add(ratY, ratNext)
+ res = res.Inv(res)
+ return res
+}
+
+// assignFieldValue assigns the value from an immutable reflect.Value to a
+// mutable reflect.Value based on the type of the immutable value.
+func assignFieldValue(field string, immutable, mutable reflect.Value) {
+ switch immutable.Kind() {
+ case reflect.Bool:
+ mutable.FieldByName(field).SetBool(immutable.Bool())
+ case reflect.Int:
+ mutable.FieldByName(field).SetInt(immutable.Int())
+ default:
+ mutable.FieldByName(field).SetString(immutable.String())
+ }
+}
+
+// setNoPtrFieldsVal assigns values from the pointer or no-pointer structs
+// fields (immutable) value to no-pointer struct field.
+func setNoPtrFieldsVal(fields []string, immutable, mutable reflect.Value) {
+ for _, field := range fields {
+ immutableField := immutable.FieldByName(field)
+ if immutableField.Kind() == reflect.Ptr {
+ if immutableField.IsValid() && !immutableField.IsNil() {
+ assignFieldValue(field, immutableField.Elem(), mutable)
+ }
+ continue
+ }
+ assignFieldValue(field, immutableField, mutable)
+ }
+}
+
+// setPtrFieldsVal assigns values from the pointer or no-pointer structs
+// fields (immutable) value to pointer struct field.
+func setPtrFieldsVal(fields []string, immutable, mutable reflect.Value) {
+ for _, field := range fields {
+ immutableField := immutable.FieldByName(field)
+ if immutableField.Kind() == reflect.Ptr {
+ if immutableField.IsValid() && !immutableField.IsNil() {
+ mutable.FieldByName(field).Set(immutableField.Elem())
+ }
+ continue
+ }
+ if immutableField.IsZero() {
+ continue
+ }
+ ptr := reflect.New(immutableField.Type())
+ ptr.Elem().Set(immutableField)
+ mutable.FieldByName(field).Set(ptr)
+ }
+}
+
// Stack defined an abstract data type that serves as a collection of elements.
type Stack struct {
list *list.List
@@ -424,8 +934,8 @@ type Stack struct {
// NewStack create a new stack.
func NewStack() *Stack {
- list := list.New()
- return &Stack{list}
+ l := list.New()
+ return &Stack{l}
}
// Push a value onto the top of the stack.
diff --git a/lib_test.go b/lib_test.go
index f3e9b3e529..f6bd94fe64 100644
--- a/lib_test.go
+++ b/lib_test.go
@@ -1,13 +1,19 @@
package excelize
import (
+ "archive/zip"
+ "bytes"
"encoding/xml"
"fmt"
+ "io"
+ "os"
"strconv"
"strings"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
var validColumns = []struct {
@@ -73,7 +79,7 @@ func TestColumnNameToNumber_Error(t *testing.T) {
}
}
_, err := ColumnNameToNumber("XFE")
- assert.EqualError(t, err, "column number exceeds maximum limit")
+ assert.ErrorIs(t, err, ErrColumnNumber)
}
func TestColumnNumberToName_OK(t *testing.T) {
@@ -97,8 +103,8 @@ func TestColumnNumberToName_Error(t *testing.T) {
assert.Equal(t, "", out)
}
- _, err = ColumnNumberToName(TotalColumns + 1)
- assert.EqualError(t, err, "column number exceeds maximum limit")
+ _, err = ColumnNumberToName(MaxColumns + 1)
+ assert.ErrorIs(t, err, ErrColumnNumber)
}
func TestSplitCellName_OK(t *testing.T) {
@@ -153,7 +159,6 @@ func TestJoinCellName_Error(t *testing.T) {
test(col.Name, row)
}
}
-
}
func TestCellNameToCoordinates_OK(t *testing.T) {
@@ -178,7 +183,7 @@ func TestCellNameToCoordinates_Error(t *testing.T) {
}
}
_, _, err := CellNameToCoordinates("A1048577")
- assert.EqualError(t, err, "row number exceeds maximum limit")
+ assert.EqualError(t, err, ErrMaxRows.Error())
}
func TestCoordinatesToCellName_OK(t *testing.T) {
@@ -211,16 +216,91 @@ func TestCoordinatesToCellName_Error(t *testing.T) {
}
}
+func TestCoordinatesToRangeRef(t *testing.T) {
+ _, err := coordinatesToRangeRef([]int{})
+ assert.EqualError(t, err, ErrCoordinates.Error())
+ _, err = coordinatesToRangeRef([]int{1, -1, 1, 1})
+ assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
+ _, err = coordinatesToRangeRef([]int{1, 1, 1, -1})
+ assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
+ ref, err := coordinatesToRangeRef([]int{1, 1, 1, 1})
+ assert.NoError(t, err)
+ assert.EqualValues(t, ref, "A1:A1")
+}
+
+func TestSortCoordinates(t *testing.T) {
+ assert.EqualError(t, sortCoordinates(make([]int, 3)), ErrCoordinates.Error())
+}
+
+func TestInStrSlice(t *testing.T) {
+ assert.EqualValues(t, -1, inStrSlice([]string{}, "", true))
+}
+
+func TestAttrValue(t *testing.T) {
+ assert.Empty(t, (&attrValString{}).Value())
+ assert.False(t, (&attrValBool{}).Value())
+ assert.Zero(t, (&attrValFloat{}).Value())
+}
+
+func TestBoolValMarshal(t *testing.T) {
+ bold := true
+ node := &xlsxFont{B: &attrValBool{Val: &bold}}
+ data, err := xml.Marshal(node)
+ assert.NoError(t, err)
+ assert.Equal(t, ``, string(data))
+
+ node = &xlsxFont{}
+ err = xml.Unmarshal(data, node)
+ assert.NoError(t, err)
+ assert.NotEqual(t, nil, node)
+ assert.NotEqual(t, nil, node.B)
+ assert.NotEqual(t, nil, node.B.Val)
+ assert.Equal(t, true, *node.B.Val)
+}
+
+func TestBoolValUnmarshalXML(t *testing.T) {
+ node := xlsxFont{}
+ assert.NoError(t, xml.Unmarshal([]byte(""), &node))
+ assert.Equal(t, true, *node.B.Val)
+ for content, err := range map[string]string{
+ "": "unexpected child of attrValBool",
+ "": "strconv.ParseBool: parsing \"x\": invalid syntax",
+ } {
+ assert.EqualError(t, xml.Unmarshal([]byte(content), &node), err)
+ }
+ attr := attrValBool{}
+ assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error())
+}
+
+func TestExtUnmarshalXML(t *testing.T) {
+ f, extLst := NewFile(), decodeExtLst{}
+ expected := fmt.Sprintf(``,
+ ExtURISlicerCachesX14, NameSpaceSpreadSheetX14.Value)
+ assert.NoError(t, f.xmlNewDecoder(strings.NewReader(expected)).Decode(&extLst))
+ assert.Len(t, extLst.Ext, 1)
+ assert.Equal(t, extLst.Ext[0].URI, ExtURISlicerCachesX14)
+}
+
func TestBytesReplace(t *testing.T) {
s := []byte{0x01}
assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0))
}
+func TestGetRootElement(t *testing.T) {
+ assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0)
+ // Test get workbook root element which all workbook XML namespace has prefix
+ f := NewFile()
+ d := f.xmlNewDecoder(bytes.NewReader([]byte(``)))
+ assert.Len(t, getRootElement(d), 3)
+}
+
func TestSetIgnorableNameSpace(t *testing.T) {
f := NewFile()
- f.xmlAttr["xml_path"] = []xml.Attr{{}}
+ f.xmlAttr.Store("xml_path", []xml.Attr{{}})
f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}})
- assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value)
+ attrs, ok := f.xmlAttr.Load("xml_path")
+ assert.EqualValues(t, "c14", attrs.([]xml.Attr)[0].Value)
+ assert.True(t, ok)
}
func TestStack(t *testing.T) {
@@ -228,3 +308,85 @@ func TestStack(t *testing.T) {
assert.Equal(t, s.Peek(), nil)
assert.Equal(t, s.Pop(), nil)
}
+
+func TestGenXMLNamespace(t *testing.T) {
+ assert.Equal(t, genXMLNamespace([]xml.Attr{
+ {Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"},
+ }), `xml:space="preserve">`)
+}
+
+func TestBstrUnmarshal(t *testing.T) {
+ bstrs := map[string]string{
+ "*": "*",
+ "*_x0000_": "*\x00",
+ "*_x0008_": "*\b",
+ "_x0008_*": "\b*",
+ "*_x0008_*": "*\b*",
+ "*_x4F60__x597D_": "*你好",
+ "*_xG000_": "*_xG000_",
+ "*_xG05F_x0001_*": "*_xG05F\x01*",
+ "*_x005F__x0008_*": "*_\b*",
+ "*_x005F_x0001_*": "*_x0001_*",
+ "*_x005f_x005F__x0008_*": "*_x005F_\b*",
+ "*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F\x06*",
+ "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*",
+ "_x005F__x0008_******": "_\b******",
+ "******_x005F__x0008_": "******_\b",
+ "******_x005F__x0008_******": "******_\b******",
+ "_x000x_x005F_x000x_": "_x000x_x000x_",
+ }
+ for bstr, expected := range bstrs {
+ assert.Equal(t, expected, bstrUnmarshal(bstr), bstr)
+ }
+}
+
+func TestBstrMarshal(t *testing.T) {
+ bstrs := map[string]string{
+ "*_xG05F_*": "*_xG05F_*",
+ "*_x0008_*": "*_x005F_x0008_*",
+ "*_x005F_*": "*_x005F_x005F_*",
+ "*_x005F_xG006_*": "*_x005F_x005F_xG006_*",
+ "*_x005F_x0006_*": "*_x005F_x005F_x005F_x0006_*",
+ }
+ for bstr, expected := range bstrs {
+ assert.Equal(t, expected, bstrMarshal(bstr))
+ }
+}
+
+func TestReadBytes(t *testing.T) {
+ f := &File{tempFiles: sync.Map{}}
+ sheet := "xl/worksheets/sheet1.xml"
+ f.tempFiles.Store(sheet, "/d/")
+ assert.Equal(t, []byte{}, f.readBytes(sheet))
+}
+
+func TestUnzipToTemp(t *testing.T) {
+ os.Setenv("TMPDIR", "test")
+ defer os.Unsetenv("TMPDIR")
+ assert.NoError(t, os.Chmod(os.TempDir(), 0o444))
+ f := NewFile()
+ data := []byte("PK\x03\x040000000PK\x01\x0200000" +
+ "0000000000000000000\x00" +
+ "\x00\x00\x00\x00\x00000000000000PK\x01" +
+ "\x020000000000000000000" +
+ "00000\v\x00\x00\x00\x00\x00000000000" +
+ "00000000000000PK\x01\x0200" +
+ "00000000000000000000" +
+ "00\v\x00\x00\x00\x00\x00000000000000" +
+ "00000000000PK\x01\x020000<" +
+ "0\x00\x0000000000000000\v\x00\v" +
+ "\x00\x00\x00\x00\x0000000000\x00\x00\x00\x00000" +
+ "00000000PK\x01\x0200000000" +
+ "0000000000000000\v\x00\x00\x00" +
+ "\x00\x0000PK\x05\x06000000\x05\x00\xfd\x00\x00\x00" +
+ "\v\x00\x00\x00\x00\x00")
+ z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
+ assert.NoError(t, err)
+
+ _, err = f.unzipToTemp(z.File[0])
+ require.Error(t, err)
+ assert.NoError(t, os.Chmod(os.TempDir(), 0o755))
+
+ _, err = f.unzipToTemp(z.File[0])
+ assert.EqualError(t, err, "EOF")
+}
diff --git a/merge.go b/merge.go
index b233335090..9f2d25b04d 100644
--- a/merge.go
+++ b/merge.go
@@ -1,176 +1,286 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import (
- "fmt"
- "strings"
-)
+import "strings"
-// MergeCell provides a function to merge cells by given coordinate area and
-// sheet name. For example create a merged cell of D3:E9 on Sheet1:
+// Rect gets merged cell rectangle coordinates sequence.
+func (mc *xlsxMergeCell) Rect() ([]int, error) {
+ var err error
+ if mc.rect == nil {
+ mergedCellsRef := mc.Ref
+ if !strings.Contains(mergedCellsRef, ":") {
+ mergedCellsRef += ":" + mergedCellsRef
+ }
+ mc.rect, err = rangeRefToCoordinates(mergedCellsRef)
+ }
+ return mc.rect, err
+}
+
+// MergeCell provides a function to merge cells by given range reference and
+// sheet name. Merging cells only keeps the upper-left cell value, and
+// discards the other values. For example create a merged cell of D3:E9 on
+// Sheet1:
//
-// err := f.MergeCell("Sheet1", "D3", "E9")
+// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
-// those merged cells that already exist will be removed.
-//
-// B1(x1,y1) D1(x2,y1)
-// +------------------------+
-// | |
-// A4(x3,y3) | C4(x4,y3) |
-// +------------------------+ |
-// | | | |
-// | |B5(x1,y2) | D5(x2,y2)|
-// | +------------------------+
-// | |
-// |A8(x3,y4) C8(x4,y4)|
-// +------------------------+
+// those merged cells that already exist will be removed. The cell references
+// tuple after merging in the following range will be: A1(x3,y1) D1(x2,y1)
+// A8(x3,y4) D8(x2,y4)
//
-func (f *File) MergeCell(sheet, hcell, vcell string) error {
- rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
+// B1(x1,y1) D1(x2,y1)
+// +------------------------+
+// | |
+// A4(x3,y3) | C4(x4,y3) |
+// +------------------------+ |
+// | | | |
+// | |B5(x1,y2) | D5(x2,y2)|
+// | +------------------------+
+// | |
+// |A8(x3,y4) C8(x4,y4)|
+// +------------------------+
+func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error {
+ rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil {
return err
}
- // Correct the coordinate area, such correct C1:B3 to B1:C3.
- _ = sortCoordinates(rect1)
+ // Correct the range reference, such correct C1:B3 to B1:C3.
+ _ = sortCoordinates(rect)
- hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
- vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])
+ topLeftCell, _ = CoordinatesToCellName(rect[0], rect[1])
+ bottomRightCell, _ = CoordinatesToCellName(rect[2], rect[3])
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- ref := hcell + ":" + vcell
- if xlsx.MergeCells != nil {
- for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
- cellData := xlsx.MergeCells.Cells[i]
- if cellData == nil {
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ for col := rect[0]; col <= rect[2]; col++ {
+ for row := rect[1]; row <= rect[3]; row++ {
+ if col == rect[0] && row == rect[1] {
continue
}
- cc := strings.Split(cellData.Ref, ":")
- if len(cc) != 2 {
- return fmt.Errorf("invalid area %q", cellData.Ref)
- }
-
- rect2, err := f.areaRefToCoordinates(cellData.Ref)
- if err != nil {
- return err
- }
-
- // Delete the merged cells of the overlapping area.
- if isOverlap(rect1, rect2) {
- xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
- i--
-
- if rect1[0] > rect2[0] {
- rect1[0], rect2[0] = rect2[0], rect1[0]
- }
-
- if rect1[2] < rect2[2] {
- rect1[2], rect2[2] = rect2[2], rect1[2]
- }
-
- if rect1[1] > rect2[1] {
- rect1[1], rect2[1] = rect2[1], rect1[1]
- }
-
- if rect1[3] < rect2[3] {
- rect1[3], rect2[3] = rect2[3], rect1[3]
- }
- hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
- vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])
- ref = hcell + ":" + vcell
- }
+ ws.prepareSheetXML(col, row)
+ c := &ws.SheetData.Row[row-1].C[col-1]
+ c.setCellDefault("")
+ _ = f.removeFormula(c, ws, sheet)
}
- xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
+ }
+ ref := topLeftCell + ":" + bottomRightCell
+ if ws.MergeCells != nil {
+ ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect})
} else {
- xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}}
+ ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref, rect: rect}}}
}
+ ws.MergeCells.Count = len(ws.MergeCells.Cells)
return err
}
-// UnmergeCell provides a function to unmerge a given coordinate area.
-// For example unmerge area D3:E9 on Sheet1:
+// UnmergeCell provides a function to unmerge a given range reference.
+// For example unmerge range reference D3:E9 on Sheet1:
//
-// err := f.UnmergeCell("Sheet1", "D3", "E9")
+// err := f.UnmergeCell("Sheet1", "D3", "E9")
//
-// Attention: overlapped areas will also be unmerged.
-func (f *File) UnmergeCell(sheet string, hcell, vcell string) error {
- xlsx, err := f.workSheetReader(sheet)
+// Attention: overlapped range will also be unmerged.
+func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ rect1, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil {
return err
}
- // Correct the coordinate area, such correct C1:B3 to B1:C3.
+ // Correct the range reference, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect1)
// return nil since no MergeCells in the sheet
- if xlsx.MergeCells == nil {
+ if ws.MergeCells == nil {
return nil
}
-
+ if err = f.mergeOverlapCells(ws); err != nil {
+ return err
+ }
i := 0
- for _, cellData := range xlsx.MergeCells.Cells {
- if cellData == nil {
+ for _, mergeCell := range ws.MergeCells.Cells {
+ if mergeCell == nil {
continue
}
- cc := strings.Split(cellData.Ref, ":")
- if len(cc) != 2 {
- return fmt.Errorf("invalid area %q", cellData.Ref)
- }
-
- rect2, err := f.areaRefToCoordinates(cellData.Ref)
- if err != nil {
- return err
+ mergedCellsRef := mergeCell.Ref
+ if !strings.Contains(mergedCellsRef, ":") {
+ mergedCellsRef += ":" + mergedCellsRef
}
-
+ rect2, _ := rangeRefToCoordinates(mergedCellsRef)
if isOverlap(rect1, rect2) {
continue
}
- xlsx.MergeCells.Cells[i] = cellData
+ ws.MergeCells.Cells[i] = mergeCell
i++
}
- xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i]
+ ws.MergeCells.Cells = ws.MergeCells.Cells[:i]
+ ws.MergeCells.Count = len(ws.MergeCells.Cells)
+ if ws.MergeCells.Count == 0 {
+ ws.MergeCells = nil
+ }
return nil
}
-// GetMergeCells provides a function to get all merged cells from a worksheet
-// currently.
+// GetMergeCells provides a function to get all merged cells from a specific
+// worksheet.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return mergeCells, err
}
- if xlsx.MergeCells != nil {
- mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells))
-
- for i := range xlsx.MergeCells.Cells {
- ref := xlsx.MergeCells.Cells[i].Ref
- axis := strings.Split(ref, ":")[0]
- val, _ := f.GetCellValue(sheet, axis)
+ if ws.MergeCells != nil {
+ if err = f.mergeOverlapCells(ws); err != nil {
+ return mergeCells, err
+ }
+ mergeCells = make([]MergeCell, 0, len(ws.MergeCells.Cells))
+ for i := range ws.MergeCells.Cells {
+ ref := ws.MergeCells.Cells[i].Ref
+ cell := strings.Split(ref, ":")[0]
+ val, _ := f.GetCellValue(sheet, cell)
mergeCells = append(mergeCells, []string{ref, val})
}
}
-
return mergeCells, err
}
+// overlapRange calculate overlap range of merged cells, and returns max
+// column and rows of the range.
+func overlapRange(ws *xlsxWorksheet) (row, col int, err error) {
+ var rect []int
+ for _, mergeCell := range ws.MergeCells.Cells {
+ if mergeCell == nil {
+ continue
+ }
+ if rect, err = mergeCell.Rect(); err != nil {
+ return
+ }
+ x1, y1, x2, y2 := rect[0], rect[1], rect[2], rect[3]
+ if x1 > col {
+ col = x1
+ }
+ if x2 > col {
+ col = x2
+ }
+ if y1 > row {
+ row = y1
+ }
+ if y2 > row {
+ row = y2
+ }
+ }
+ return
+}
+
+// flatMergedCells convert merged cells range reference to cell-matrix.
+func flatMergedCells(ws *xlsxWorksheet, matrix [][]*xlsxMergeCell) error {
+ for i, cell := range ws.MergeCells.Cells {
+ rect, err := cell.Rect()
+ if err != nil {
+ return err
+ }
+ x1, y1, x2, y2 := rect[0]-1, rect[1]-1, rect[2]-1, rect[3]-1
+ var overlapCells []*xlsxMergeCell
+ for x := x1; x <= x2; x++ {
+ for y := y1; y <= y2; y++ {
+ if matrix[x][y] != nil {
+ overlapCells = append(overlapCells, matrix[x][y])
+ }
+ matrix[x][y] = cell
+ }
+ }
+ if len(overlapCells) != 0 {
+ newCell := cell
+ for _, overlapCell := range overlapCells {
+ newCell = mergeCell(cell, overlapCell)
+ }
+ newRect, _ := newCell.Rect()
+ x1, y1, x2, y2 := newRect[0]-1, newRect[1]-1, newRect[2]-1, newRect[3]-1
+ for x := x1; x <= x2; x++ {
+ for y := y1; y <= y2; y++ {
+ matrix[x][y] = newCell
+ }
+ }
+ ws.MergeCells.Cells[i] = newCell
+ }
+ }
+ return nil
+}
+
+// mergeOverlapCells merge overlap cells.
+func (f *File) mergeOverlapCells(ws *xlsxWorksheet) error {
+ rows, cols, err := overlapRange(ws)
+ if err != nil {
+ return err
+ }
+ if rows == 0 || cols == 0 {
+ return nil
+ }
+ matrix := make([][]*xlsxMergeCell, cols)
+ for i := range matrix {
+ matrix[i] = make([]*xlsxMergeCell, rows)
+ }
+ _ = flatMergedCells(ws, matrix)
+ mergeCells := ws.MergeCells.Cells[:0]
+ for _, cell := range ws.MergeCells.Cells {
+ rect, _ := cell.Rect()
+ x1, y1, x2, y2 := rect[0]-1, rect[1]-1, rect[2]-1, rect[3]-1
+ if matrix[x1][y1] == cell {
+ mergeCells = append(mergeCells, cell)
+ for x := x1; x <= x2; x++ {
+ for y := y1; y <= y2; y++ {
+ matrix[x][y] = nil
+ }
+ }
+ }
+ }
+ ws.MergeCells.Count, ws.MergeCells.Cells = len(mergeCells), mergeCells
+ return nil
+}
+
+// mergeCell merge two cells.
+func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell {
+ rect1, _ := cell1.Rect()
+ rect2, _ := cell2.Rect()
+
+ if rect1[0] > rect2[0] {
+ rect1[0], rect2[0] = rect2[0], rect1[0]
+ }
+
+ if rect1[2] < rect2[2] {
+ rect1[2], rect2[2] = rect2[2], rect1[2]
+ }
+
+ if rect1[1] > rect2[1] {
+ rect1[1], rect2[1] = rect2[1], rect1[1]
+ }
+
+ if rect1[3] < rect2[3] {
+ rect1[3], rect2[3] = rect2[3], rect1[3]
+ }
+ topLeftCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
+ bottomRightCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
+ return &xlsxMergeCell{rect: rect1, Ref: topLeftCell + ":" + bottomRightCell}
+}
+
// MergeCell define a merged cell data.
// It consists of the following structure.
// example: []string{"D4:E10", "cell value"}
@@ -181,16 +291,18 @@ func (m *MergeCell) GetCellValue() string {
return (*m)[1]
}
-// GetStartAxis returns the merge start axis.
-// example: "C2"
+// GetStartAxis returns the top left cell reference of merged range, for
+// example: "C2".
func (m *MergeCell) GetStartAxis() string {
- axis := strings.Split((*m)[0], ":")
- return axis[0]
+ return strings.Split((*m)[0], ":")[0]
}
-// GetEndAxis returns the merge end axis.
-// example: "D4"
+// GetEndAxis returns the bottom right cell reference of merged range, for
+// example: "D4".
func (m *MergeCell) GetEndAxis() string {
- axis := strings.Split((*m)[0], ":")
- return axis[1]
+ coordinates := strings.Split((*m)[0], ":")
+ if len(coordinates) == 2 {
+ return coordinates[1]
+ }
+ return coordinates[0]
}
diff --git a/merge_test.go b/merge_test.go
index afe75aac01..fcdbcfd647 100644
--- a/merge_test.go
+++ b/merge_test.go
@@ -12,73 +12,100 @@ func TestMergeCell(t *testing.T) {
if !assert.NoError(t, err) {
t.FailNow()
}
- assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9"))
- assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9"))
- assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13"))
- assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8"))
- assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13"))
- assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15"))
- assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13"))
- assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12"))
+ assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ for _, cells := range [][]string{
+ {"D9", "D9"},
+ {"D9", "E9"},
+ {"H14", "G13"},
+ {"C9", "D8"},
+ {"F11", "G13"},
+ {"H7", "B15"},
+ {"D11", "F13"},
+ {"G10", "K12"},
+ } {
+ assert.NoError(t, f.MergeCell("Sheet1", cells[0], cells[1]))
+ }
assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell"))
assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100))
- assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5)))
- assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
+ assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5))
+ assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)"))
value, err := f.GetCellValue("Sheet1", "H11")
- assert.Equal(t, "0.5", value)
+ assert.Equal(t, "100", value)
assert.NoError(t, err)
- value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate.
+ // Merged cell ref is single coordinate
+ value, err = f.GetCellValue("Sheet2", "A6")
assert.Equal(t, "", value)
assert.NoError(t, err)
value, err = f.GetCellFormula("Sheet1", "G12")
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
assert.NoError(t, err)
- f.NewSheet("Sheet3")
- assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13"))
- assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12"))
-
- assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5
- assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5
-
- assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5"))
- assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6
-
- assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5"))
- assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6
-
- assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7"))
- assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7
-
- assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12"))
- assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12
-
- assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10"))
- assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12"))
-
- assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12"))
- assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10"))
-
- assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13"))
- assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11"))
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
- // Test get merged cells on not exists worksheet.
- assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist")
+ for _, cells := range [][]string{
+ {"D11", "F13"},
+ {"G10", "K12"},
+ {"B1", "D5"}, // B1:D5
+ {"E1", "F5"}, // E1:F5
+ {"H2", "I5"},
+ {"I4", "J6"}, // H2:J6
+ {"M2", "N5"},
+ {"L4", "M6"}, // L2:N6
+ {"P4", "Q7"},
+ {"O2", "P5"}, // O2:Q7
+ {"A9", "B12"},
+ {"B7", "C9"}, // A7:C12
+ {"E9", "F10"},
+ {"D8", "G12"},
+ {"I8", "I12"},
+ {"I10", "K10"},
+ {"M8", "Q13"},
+ {"N10", "O11"},
+ } {
+ assert.NoError(t, f.MergeCell("Sheet3", cells[0], cells[1]))
+ }
+ // Test merge cells on not exists worksheet
+ assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist")
+ // Test merged cells with invalid sheet name
+ assert.EqualError(t, f.MergeCell("Sheet:1", "N10", "O11"), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))
+ assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
+ // Test getting merged cells with the same start and end axis
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
+ mergedCells, err := f.GetMergeCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
+ assert.Equal(t, "A1", mergedCells[0].GetEndAxis())
+ assert.Empty(t, mergedCells[0].GetCellValue())
+}
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
- assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)
+func TestMergeCellOverlap(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.MergeCell("Sheet1", "A1", "C2"))
+ assert.NoError(t, f.MergeCell("Sheet1", "B2", "D3"))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCellOverlap.xlsx")))
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
- assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ f, err := OpenFile(filepath.Join("test", "TestMergeCellOverlap.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ mc, err := f.GetMergeCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, mc, 1)
+ assert.Equal(t, "A1", mc[0].GetStartAxis())
+ assert.Equal(t, "D3", mc[0].GetEndAxis())
+ assert.Equal(t, "", mc[0].GetCellValue())
+ assert.NoError(t, f.Close())
}
func TestGetMergeCells(t *testing.T) {
@@ -121,10 +148,13 @@ func TestGetMergeCells(t *testing.T) {
assert.Equal(t, wants[i].start, m.GetStartAxis())
assert.Equal(t, wants[i].end, m.GetEndAxis())
}
-
- // Test get merged cells on not exists worksheet.
+ // Test get merged cells with invalid sheet name
+ _, err = f.GetMergeCells("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ // Test get merged cells on not exists worksheet
_, err = f.GetMergeCells("SheetN")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
}
func TestUnmergeCell(t *testing.T) {
@@ -134,36 +164,58 @@ func TestUnmergeCell(t *testing.T) {
}
sheet1 := f.GetSheetName(0)
- xlsx, err := f.workSheetReader(sheet1)
+ sheet, err := f.workSheetReader(sheet1)
assert.NoError(t, err)
- mergeCellNum := len(xlsx.MergeCells.Cells)
+ mergeCellNum := len(sheet.MergeCells.Cells)
- assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
- // unmerge the mergecell that contains A1
+ // Test unmerge the merged cells that contains A1
assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1"))
- if len(xlsx.MergeCells.Cells) != mergeCellNum-1 {
+ if len(sheet.MergeCells.Cells) != mergeCellNum-1 {
t.FailNow()
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx")))
+ assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
- // Test unmerged area on not exists worksheet.
- assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist")
+ // Test unmerged range reference on not exists worksheet
+ assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN does not exist")
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil
+ // Test unmerge the merged cells with invalid sheet name
+ assert.EqualError(t, f.UnmergeCell("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error())
+
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = nil
assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15"))
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7"))
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
- assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
+ assert.NoError(t, f.UnmergeCell("Sheet1", "A2", "B3"))
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
- assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
+ assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+}
+func TestFlatMergedCells(t *testing.T) {
+ ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ""}}}}
+ assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "cannot convert cell \"\" to coordinates: invalid cell name \"\"")
+}
+
+func TestMergeCellsParser(t *testing.T) {
+ ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}
+ _, err := ws.mergeCellsParser("A1")
+ assert.NoError(t, err)
}
diff --git a/numfmt.go b/numfmt.go
new file mode 100644
index 0000000000..c5f6094c84
--- /dev/null
+++ b/numfmt.go
@@ -0,0 +1,7313 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "fmt"
+ "math"
+ "math/big"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xuri/nfp"
+)
+
+// languageInfo defined the required fields of localization support for number
+// format.
+type languageInfo struct {
+ apFmt string
+ tags, weekdayNames, weekdayNamesAbbr []string
+ useGannen bool
+ localMonth func(t time.Time, abbr int) string
+}
+
+// numberFormat directly maps the number format parser runtime required
+// fields.
+type numberFormat struct {
+ opts *Options
+ cellType CellType
+ section []nfp.Section
+ t time.Time
+ sectionIdx int
+ date1904, isNumeric, hours, seconds, useMillisecond, useGannen bool
+ number float64
+ ap, localCode, result, value, valueSectionType string
+ switchArgument, currencyString string
+ fracHolder, fracPadding, intHolder, intPadding, expBaseLen int
+ percent int
+ useCommaSep, useFraction, usePointer, usePositive, useScientificNotation bool
+}
+
+// CultureName is the type of supported language country codes types for apply
+// number format.
+type CultureName byte
+
+// This section defines the currently supported country code types enumeration
+// for apply number format.
+const (
+ CultureNameUnknown CultureName = iota
+ CultureNameEnUS
+ CultureNameJaJP
+ CultureNameKoKR
+ CultureNameZhCN
+ CultureNameZhTW
+)
+
+var (
+ // Excel styles can reference number formats that are built-in, all of which
+ // have an id less than 164. Note that this number format code list is under
+ // English localization.
+ builtInNumFmt = map[int]string{
+ 0: "general",
+ 1: "0",
+ 2: "0.00",
+ 3: "#,##0",
+ 4: "#,##0.00",
+ 9: "0%",
+ 10: "0.00%",
+ 11: "0.00E+00",
+ 12: "# ?/?",
+ 13: "# ??/??",
+ 14: "mm-dd-yy",
+ 15: "d-mmm-yy",
+ 16: "d-mmm",
+ 17: "mmm-yy",
+ 18: "h:mm AM/PM",
+ 19: "h:mm:ss AM/PM",
+ 20: "hh:mm",
+ 21: "hh:mm:ss",
+ 22: "m/d/yy hh:mm",
+ 37: "#,##0 ;(#,##0)",
+ 38: "#,##0 ;[red](#,##0)",
+ 39: "#,##0.00 ;(#,##0.00)",
+ 40: "#,##0.00 ;[red](#,##0.00)",
+ 41: "_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)",
+ 42: "_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)",
+ 43: "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)",
+ 44: "_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)",
+ 45: "mm:ss",
+ 46: "[h]:mm:ss",
+ 47: "mm:ss.0",
+ 48: "##0.0E+0",
+ 49: "@",
+ }
+ // langNumFmt defined number format code provided for language glyphs where
+ // they occur in different language.
+ langNumFmt = map[string]map[int]string{
+ "zh-tw": {
+ 27: "[$-404]e/m/d",
+ 28: "[$-404]e\"年\"m\"月\"d\"日\"",
+ 29: "[$-404]e\"年\"m\"月\"d\"日\"",
+ 30: "m/d/yy",
+ 31: "yyyy\"年\"m\"月\"d\"日\"",
+ 32: "hh\"時\"mm\"分\"",
+ 33: "hh\"時\"mm\"分\"ss\"秒\"",
+ 34: "上午/下午hh\"時\"mm\"分\"",
+ 35: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
+ 36: "[$-404]e/m/d",
+ 50: "[$-404]e/m/d",
+ 51: "[$-404]e\"年\"m\"月\"d\"日\"",
+ 52: "上午/下午hh\"時\"mm\"分\"",
+ 53: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
+ 54: "[$-404]e\"年\"m\"月\"d\"日\"",
+ 55: "上午/下午hh\"時\"mm\"分\"",
+ 56: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
+ 57: "[$-404]e/m/d",
+ 58: "[$-404]e\"年\"m\"月\"d\"日\"",
+ },
+ "zh-cn": {
+ 27: "yyyy\"年\"m\"月\"",
+ 28: "m\"月\"d\"日\"",
+ 29: "m\"月\"d\"日\"",
+ 30: "m/d/yy",
+ 31: "yyyy\"年\"m\"月\"d\"日\"",
+ 32: "h\"时\"mm\"分\"",
+ 33: "h\"时\"mm\"分\"ss\"秒\"",
+ 34: "上午/下午h\"时\"mm\"分\"",
+ 35: "上午/下午h\"时\"mm\"分\"ss\"秒\"",
+ 36: "yyyy\"年\"m\"月\"",
+ 50: "yyyy\"年\"m\"月\"",
+ 51: "m\"月\"d\"日\"",
+ 52: "yyyy\"年\"m\"月\"",
+ 53: "m\"月\"d\"日\"",
+ 54: "m\"月\"d\"日\"",
+ 55: "上午/下午h\"时\"mm\"分\"",
+ 56: "上午/下午h\"时\"mm\"分\"ss\"秒\"",
+ 57: "yyyy\"年\"m\"月\"",
+ 58: "m\"月\"d\"日\"",
+ },
+ "ja-jp": {
+ 27: "[$-411]ge.m.d",
+ 28: "[$-411]ggge\"年\"m\"月\"d\"日\"",
+ 29: "[$-411]ggge\"年\"m\"月\"d\"日\"",
+ 30: "m/d/yy",
+ 31: "yyyy\"年\"m\"月\"d\"日\"",
+ 32: "h\"時\"mm\"分\"",
+ 33: "h\"時\"mm\"分\"ss\"秒\"",
+ 34: "yyyy\"年\"m\"月\"",
+ 35: "m\"月\"d\"日\"",
+ 36: "[$-411]ge.m.d",
+ 50: "[$-411]ge.m.d",
+ 51: "[$-411]ggge\"年\"m\"月\"d\"日\"",
+ 52: "yyyy\"年\"m\"月\"",
+ 53: "m\"月\"d\"日\"",
+ 54: "[$-411]ggge\"年\"m\"月\"d\"日\"",
+ 55: "yyyy\"年\"m\"月\"",
+ 56: "m\"月\"d\"日\"",
+ 57: "[$-411]ge.m.d",
+ 58: "[$-411]ggge\"年\"m\"月\"d\"日\"",
+ },
+ "ko-kr": {
+ 27: "yyyy\"年\" mm\"月\" dd\"日\"",
+ 28: "mm-dd",
+ 29: "mm-dd",
+ 30: "mm-dd-yy",
+ 31: "yyyy\"년\" mm\"월\" dd\"일\"",
+ 32: "h\"시\" mm\"분\"",
+ 33: "h\"시\" mm\"분\" ss\"초\"",
+ 34: "yyyy-mm-dd",
+ 35: "yyyy-mm-dd",
+ 36: "yyyy\"年\" mm\"月\" dd\"日\"",
+ 50: "yyyy\"年\" mm\"月\" dd\"日\"",
+ 51: "mm-dd",
+ 52: "yyyy-mm-dd",
+ 53: "yyyy-mm-dd",
+ 54: "mm-dd",
+ 55: "yyyy-mm-dd",
+ 56: "yyyy-mm-dd",
+ 57: "yyyy\"年\" mm\"月\" dd\"日\"",
+ 58: "mm-dd",
+ },
+ "th-th": {
+ 59: "t0",
+ 60: "t0.00",
+ 61: "t#,##0",
+ 62: "t#,##0.00",
+ 67: "t0%",
+ 68: "t0.00%",
+ 69: "t# ?/?",
+ 70: "t# ??/??",
+ 71: "\u0E27/\u0E14/\u0E1B\u0E1B\u0E1B\u0E1B",
+ 72: "\u0E27-\u0E14\u0E14\u0E14-\u0E1B\u0E1B",
+ 73: "\u0E27-\u0E14\u0E14\u0E14",
+ 74: "\u0E14\u0E14\u0E14-\u0E1B\u0E1B",
+ 75: "\u0E0A:\u0E19\u0E19",
+ 76: "\u0E0A:\u0E19\u0E19:\u0E17\u0E17",
+ 77: "\u0E27/\u0E14/\u0E1B\u0E1B\u0E1B\u0E1B \u0E0A:\u0E19\u0E19",
+ 78: "\u0E19\u0E19:\u0E17\u0E17",
+ 79: "[\u0E0A%5D]\u0E19\u0E19:\u0E17\u0E17",
+ 80: "\u0E19\u0E19:\u0E17\u0E17.0",
+ 81: "d/m/bb",
+ },
+ }
+ // currencyNumFmt defined the currency number format map.
+ currencyNumFmt = map[int]string{
+ 164: "\"¥\"#,##0.00",
+ 165: "[$$-409]#,##0.00",
+ 166: "[$$-45C]#,##0.00",
+ 167: "[$$-1004]#,##0.00",
+ 168: "[$$-404]#,##0.00",
+ 169: "[$$-C09]#,##0.00",
+ 170: "[$$-2809]#,##0.00",
+ 171: "[$$-1009]#,##0.00",
+ 172: "[$$-2009]#,##0.00",
+ 173: "[$$-1409]#,##0.00",
+ 174: "[$$-4809]#,##0.00",
+ 175: "[$$-2C09]#,##0.00",
+ 176: "[$$-2409]#,##0.00",
+ 177: "[$$-1000]#,##0.00",
+ 178: "#,##0.00\\ [$$-C0C]",
+ 179: "[$$-475]#,##0.00",
+ 180: "[$$-83E]#,##0.00",
+ 181: "[$$-86B]\\ #,##0.00",
+ 182: "[$$-340A]\\ #,##0.00",
+ 183: "[$$-240A]#,##0.00",
+ 184: "[$$-300A]\\ #,##0.00",
+ 185: "[$$-440A]#,##0.00",
+ 186: "[$$-80A]#,##0.00",
+ 187: "[$$-500A]#,##0.00",
+ 188: "[$$-540A]#,##0.00",
+ 189: "[$$-380A]\\ #,##0.00",
+ 190: "[$£-809]#,##0.00",
+ 191: "[$£-491]#,##0.00",
+ 192: "[$£-452]#,##0.00",
+ 193: "[$¥-804]#,##0.00",
+ 194: "[$¥-411]#,##0.00",
+ 195: "[$¥-478]#,##0.00",
+ 196: "[$¥-451]#,##0.00",
+ 197: "[$¥-480]#,##0.00",
+ 198: "#,##0.00\\ [$\u058F-42B]",
+ 199: "[$\u060B-463]#,##0.00",
+ 200: "[$\u060B-48C]#,##0.00",
+ 201: "[$\u09F3-845]\\ #,##0.00",
+ 202: "#,##0.00[$\u17DB-453]",
+ 203: "[$\u20A1-140A]#,##0.00",
+ 204: "[$\u20A6-468]\\ #,##0.00",
+ 205: "[$\u20A6-470]\\ #,##0.00",
+ 206: "[$\u20A9-412]#,##0.00",
+ 207: "[$\u20AA-40D]\\ #,##0.00",
+ 208: "#,##0.00\\ [$\u20AB-42A]",
+ 209: "#,##0.00\\ [$\u20AC-42D]",
+ 210: "#,##0.00\\ [$\u20AC-47E]",
+ 211: "#,##0.00\\ [$\u20AC-403]",
+ 212: "#,##0.00\\ [$\u20AC-483]",
+ 213: "[$\u20AC-813]\\ #,##0.00",
+ 214: "[$\u20AC-413]\\ #,##0.00",
+ 215: "[$\u20AC-1809]#,##0.00",
+ 216: "#,##0.00\\ [$\u20AC-425]",
+ 217: "[$\u20AC-2]\\ #,##0.00",
+ 218: "#,##0.00\\ [$\u20AC-1]",
+ 219: "#,##0.00\\ [$\u20AC-40B]",
+ 220: "#,##0.00\\ [$\u20AC-80C]",
+ 221: "#,##0.00\\ [$\u20AC-40C]",
+ 222: "#,##0.00\\ [$\u20AC-140C]",
+ 223: "#,##0.00\\ [$\u20AC-180C]",
+ 224: "[$\u20AC-200C]#,##0.00",
+ 225: "#,##0.00\\ [$\u20AC-456]",
+ 226: "#,##0.00\\ [$\u20AC-C07]",
+ 227: "#,##0.00\\ [$\u20AC-407]",
+ 228: "#,##0.00\\ [$\u20AC-1007]",
+ 229: "#,##0.00\\ [$\u20AC-408]",
+ 230: "#,##0.00\\ [$\u20AC-243B]",
+ 231: "[$\u20AC-83C]#,##0.00",
+ 232: "[$\u20AC-410]\\ #,##0.00",
+ 233: "[$\u20AC-476]#,##0.00",
+ 234: "#,##0.00\\ [$\u20AC-2C1A]",
+ 235: "[$\u20AC-426]\\ #,##0.00",
+ 236: "#,##0.00\\ [$\u20AC-427]",
+ 237: "#,##0.00\\ [$\u20AC-82E]",
+ 238: "#,##0.00\\ [$\u20AC-46E]",
+ 239: "[$\u20AC-43A]#,##0.00",
+ 240: "#,##0.00\\ [$\u20AC-C3B]",
+ 241: "#,##0.00\\ [$\u20AC-482]",
+ 242: "#,##0.00\\ [$\u20AC-816]",
+ 243: "#,##0.00\\ [$\u20AC-301A]",
+ 244: "#,##0.00\\ [$\u20AC-203B]",
+ 245: "#,##0.00\\ [$\u20AC-41B]",
+ 246: "#,##0.00\\ [$\u20AC-424]",
+ 247: "#,##0.00\\ [$\u20AC-C0A]",
+ 248: "#,##0.00\\ [$\u20AC-81D]",
+ 249: "#,##0.00\\ [$\u20AC-484]",
+ 250: "#,##0.00\\ [$\u20AC-42E]",
+ 251: "[$\u20AC-462]\\ #,##0.00",
+ 252: "#,##0.00\\ [$₭-454]",
+ 253: "#,##0.00\\ [$₮-450]",
+ 254: "[$\u20AE-C50]#,##0.00",
+ 255: "[$\u20B1-3409]#,##0.00",
+ 256: "[$\u20B1-464]#,##0.00",
+ 257: "#,##0.00[$\u20B4-422]",
+ 258: "[$\u20B8-43F]#,##0.00",
+ 259: "[$\u20B9-460]#,##0.00",
+ 260: "[$\u20B9-4009]\\ #,##0.00",
+ 261: "[$\u20B9-447]\\ #,##0.00",
+ 262: "[$\u20B9-439]\\ #,##0.00",
+ 263: "[$\u20B9-44B]\\ #,##0.00",
+ 264: "[$\u20B9-860]#,##0.00",
+ 265: "[$\u20B9-457]\\ #,##0.00",
+ 266: "[$\u20B9-458]#,##0.00",
+ 267: "[$\u20B9-44E]\\ #,##0.00",
+ 268: "[$\u20B9-861]#,##0.00",
+ 269: "[$\u20B9-448]\\ #,##0.00",
+ 270: "[$\u20B9-446]\\ #,##0.00",
+ 271: "[$\u20B9-44F]\\ #,##0.00",
+ 272: "[$\u20B9-459]#,##0.00",
+ 273: "[$\u20B9-449]\\ #,##0.00",
+ 274: "[$\u20B9-820]#,##0.00",
+ 275: "#,##0.00\\ [$\u20BA-41F]",
+ 276: "#,##0.00\\ [$\u20BC-42C]",
+ 277: "#,##0.00\\ [$\u20BC-82C]",
+ 278: "#,##0.00\\ [$\u20BD-419]",
+ 279: "#,##0.00[$\u20BD-485]",
+ 280: "#,##0.00\\ [$\u20BE-437]",
+ 281: "[$B/.-180A]\\ #,##0.00",
+ 282: "[$Br-472]#,##0.00",
+ 283: "[$Br-477]#,##0.00",
+ 284: "#,##0.00[$Br-473]",
+ 285: "[$Bs-46B]\\ #,##0.00",
+ 286: "[$Bs-400A]\\ #,##0.00",
+ 287: "[$Bs.-200A]\\ #,##0.00",
+ 288: "[$BWP-832]\\ #,##0.00",
+ 289: "[$C$-4C0A]#,##0.00",
+ 290: "[$CA$-85D]#,##0.00",
+ 291: "[$CA$-47C]#,##0.00",
+ 292: "[$CA$-45D]#,##0.00",
+ 293: "[$CFA-340C]#,##0.00",
+ 294: "[$CFA-280C]#,##0.00",
+ 295: "#,##0.00\\ [$CFA-867]",
+ 296: "#,##0.00\\ [$CFA-488]",
+ 297: "#,##0.00\\ [$CHF-100C]",
+ 298: "[$CHF-1407]\\ #,##0.00",
+ 299: "[$CHF-807]\\ #,##0.00",
+ 300: "[$CHF-810]\\ #,##0.00",
+ 301: "[$CHF-417]\\ #,##0.00",
+ 302: "[$CLP-47A]\\ #,##0.00",
+ 303: "[$CN¥-850]#,##0.00",
+ 304: "#,##0.00\\ [$DZD-85F]",
+ 305: "[$FCFA-2C0C]#,##0.00",
+ 306: "#,##0.00\\ [$Ft-40E]",
+ 307: "[$G-3C0C]#,##0.00",
+ 308: "[$Gs.-3C0A]\\ #,##0.00",
+ 309: "[$GTQ-486]#,##0.00",
+ 310: "[$HK$-C04]#,##0.00",
+ 311: "[$HK$-3C09]#,##0.00",
+ 312: "#,##0.00\\ [$HRK-41A]",
+ 313: "[$IDR-3809]#,##0.00",
+ 314: "[$IQD-492]#,##0.00",
+ 315: "#,##0.00\\ [$ISK-40F]",
+ 316: "[$K-455]#,##0.00",
+ 317: "#,##0.00\\ [$K\u010D-405]",
+ 318: "#,##0.00\\ [$KM-141A]",
+ 319: "#,##0.00\\ [$KM-101A]",
+ 320: "#,##0.00\\ [$KM-181A]",
+ 321: "[$kr-438]\\ #,##0.00",
+ 322: "[$kr-43B]\\ #,##0.00",
+ 323: "#,##0.00\\ [$kr-83B]",
+ 324: "[$kr-414]\\ #,##0.00",
+ 325: "[$kr-814]\\ #,##0.00",
+ 326: "#,##0.00\\ [$kr-41D]",
+ 327: "[$kr.-406]\\ #,##0.00",
+ 328: "[$kr.-46F]\\ #,##0.00",
+ 329: "[$Ksh-441]#,##0.00",
+ 330: "[$L-818]#,##0.00",
+ 331: "[$L-819]#,##0.00",
+ 332: "[$L-480A]\\ #,##0.00",
+ 333: "#,##0.00\\ [$Lek\u00EB-41C]",
+ 334: "[$MAD-45F]#,##0.00",
+ 335: "[$MAD-380C]#,##0.00",
+ 336: "#,##0.00\\ [$MAD-105F]",
+ 337: "[$MOP$-1404]#,##0.00",
+ 338: "#,##0.00\\ [$MVR-465]_-",
+ 339: "#,##0.00[$Nfk-873]",
+ 340: "[$NGN-466]#,##0.00",
+ 341: "[$NGN-467]#,##0.00",
+ 342: "[$NGN-469]#,##0.00",
+ 343: "[$NGN-471]#,##0.00",
+ 344: "[$NOK-103B]\\ #,##0.00",
+ 345: "[$NOK-183B]\\ #,##0.00",
+ 346: "[$NZ$-481]#,##0.00",
+ 347: "[$PKR-859]\\ #,##0.00",
+ 348: "[$PYG-474]#,##0.00",
+ 349: "[$Q-100A]#,##0.00",
+ 350: "[$R-436]\\ #,##0.00",
+ 351: "[$R-1C09]\\ #,##0.00",
+ 352: "[$R-435]\\ #,##0.00",
+ 353: "[$R$-416]\\ #,##0.00",
+ 354: "[$RD$-1C0A]#,##0.00",
+ 355: "#,##0.00\\ [$RF-487]",
+ 356: "[$RM-4409]#,##0.00",
+ 357: "[$RM-43E]#,##0.00",
+ 358: "#,##0.00\\ [$RON-418]",
+ 359: "[$Rp-421]#,##0.00",
+ 360: "[$Rs-420]#,##0.00_-",
+ 361: "[$Rs.-849]\\ #,##0.00",
+ 362: "#,##0.00\\ [$RSD-81A]",
+ 363: "#,##0.00\\ [$RSD-C1A]",
+ 364: "#,##0.00\\ [$RUB-46D]",
+ 365: "#,##0.00\\ [$RUB-444]",
+ 366: "[$S/.-C6B]\\ #,##0.00",
+ 367: "[$S/.-280A]\\ #,##0.00",
+ 368: "#,##0.00\\ [$SEK-143B]",
+ 369: "#,##0.00\\ [$SEK-1C3B]",
+ 370: "#,##0.00\\ [$so\u02BBm-443]",
+ 371: "#,##0.00\\ [$so\u02BBm-843]",
+ 372: "#,##0.00\\ [$SYP-45A]",
+ 373: "[$THB-41E]#,##0.00",
+ 374: "#,##0.00[$TMT-442]",
+ 375: "[$US$-3009]#,##0.00",
+ 376: "[$ZAR-46C]\\ #,##0.00",
+ 377: "[$ZAR-430]#,##0.00",
+ 378: "[$ZAR-431]#,##0.00",
+ 379: "[$ZAR-432]\\ #,##0.00",
+ 380: "[$ZAR-433]#,##0.00",
+ 381: "[$ZAR-434]\\ #,##0.00",
+ 382: "#,##0.00\\ [$z\u0142-415]",
+ 383: "#,##0.00\\ [$\u0434\u0435\u043D-42F]",
+ 384: "#,##0.00\\ [$КМ-201A]",
+ 385: "#,##0.00\\ [$КМ-1C1A]",
+ 386: "#,##0.00\\ [$\u043B\u0432.-402]",
+ 387: "#,##0.00\\ [$р.-423]",
+ 388: "#,##0.00\\ [$\u0441\u043E\u043C-440]",
+ 389: "#,##0.00\\ [$\u0441\u043E\u043C-428]",
+ 390: "[$\u062C.\u0645.-C01]\\ #,##0.00_-",
+ 391: "[$\u062F.\u0623.-2C01]\\ #,##0.00_-",
+ 392: "[$\u062F.\u0625.-3801]\\ #,##0.00_-",
+ 393: "[$\u062F.\u0628.-3C01]\\ #,##0.00_-",
+ 394: "[$\u062F.\u062A.-1C01]\\ #,##0.00_-",
+ 395: "[$\u062F.\u062C.-1401]\\ #,##0.00_-",
+ 396: "[$\u062F.\u0639.-801]\\ #,##0.00_-",
+ 397: "[$\u062F.\u0643.-3401]\\ #,##0.00_-",
+ 398: "[$\u062F.\u0644.-1001]#,##0.00_-",
+ 399: "[$\u062F.\u0645.-1801]\\ #,##0.00_-",
+ 400: "[$\u0631-846]\\ #,##0.00",
+ 401: "[$\u0631.\u0633.-401]\\ #,##0.00_-",
+ 402: "[$\u0631.\u0639.-2001]\\ #,##0.00_-",
+ 403: "[$\u0631.\u0642.-4001]\\ #,##0.00_-",
+ 404: "[$\u0631.\u064A.-2401]\\ #,##0.00_-",
+ 405: "[$\u0631\u06CC\u0627\u0644-429]#,##0.00_-",
+ 406: "[$\u0644.\u0633.-2801]\\ #,##0.00_-",
+ 407: "[$\u0644.\u0644.-3001]\\ #,##0.00_-",
+ 408: "[$\u1265\u122D-45E]#,##0.00",
+ 409: "[$\u0930\u0942-461]#,##0.00",
+ 410: "[$\u0DBB\u0DD4.-45B]\\ #,##0.00",
+ 411: "[$ADP]\\ #,##0.00",
+ 412: "[$AED]\\ #,##0.00",
+ 413: "[$AFA]\\ #,##0.00",
+ 414: "[$AFN]\\ #,##0.00",
+ 415: "[$ALL]\\ #,##0.00",
+ 416: "[$AMD]\\ #,##0.00",
+ 417: "[$ANG]\\ #,##0.00",
+ 418: "[$AOA]\\ #,##0.00",
+ 419: "[$ARS]\\ #,##0.00",
+ 420: "[$ATS]\\ #,##0.00",
+ 421: "[$AUD]\\ #,##0.00",
+ 422: "[$AWG]\\ #,##0.00",
+ 423: "[$AZM]\\ #,##0.00",
+ 424: "[$AZN]\\ #,##0.00",
+ 425: "[$BAM]\\ #,##0.00",
+ 426: "[$BBD]\\ #,##0.00",
+ 427: "[$BDT]\\ #,##0.00",
+ 428: "[$BEF]\\ #,##0.00",
+ 429: "[$BGL]\\ #,##0.00",
+ 430: "[$BGN]\\ #,##0.00",
+ 431: "[$BHD]\\ #,##0.00",
+ 432: "[$BIF]\\ #,##0.00",
+ 433: "[$BMD]\\ #,##0.00",
+ 434: "[$BND]\\ #,##0.00",
+ 435: "[$BOB]\\ #,##0.00",
+ 436: "[$BOV]\\ #,##0.00",
+ 437: "[$BRL]\\ #,##0.00",
+ 438: "[$BSD]\\ #,##0.00",
+ 439: "[$BTN]\\ #,##0.00",
+ 440: "[$BWP]\\ #,##0.00",
+ 441: "[$BYR]\\ #,##0.00",
+ 442: "[$BZD]\\ #,##0.00",
+ 443: "[$CAD]\\ #,##0.00",
+ 444: "[$CDF]\\ #,##0.00",
+ 445: "[$CHE]\\ #,##0.00",
+ 446: "[$CHF]\\ #,##0.00",
+ 447: "[$CHW]\\ #,##0.00",
+ 448: "[$CLF]\\ #,##0.00",
+ 449: "[$CLP]\\ #,##0.00",
+ 450: "[$CNY]\\ #,##0.00",
+ 451: "[$COP]\\ #,##0.00",
+ 452: "[$COU]\\ #,##0.00",
+ 453: "[$CRC]\\ #,##0.00",
+ 454: "[$CSD]\\ #,##0.00",
+ 455: "[$CUC]\\ #,##0.00",
+ 456: "[$CVE]\\ #,##0.00",
+ 457: "[$CYP]\\ #,##0.00",
+ 458: "[$CZK]\\ #,##0.00",
+ 459: "[$DEM]\\ #,##0.00",
+ 460: "[$DJF]\\ #,##0.00",
+ 461: "[$DKK]\\ #,##0.00",
+ 462: "[$DOP]\\ #,##0.00",
+ 463: "[$DZD]\\ #,##0.00",
+ 464: "[$ECS]\\ #,##0.00",
+ 465: "[$ECV]\\ #,##0.00",
+ 466: "[$EEK]\\ #,##0.00",
+ 467: "[$EGP]\\ #,##0.00",
+ 468: "[$ERN]\\ #,##0.00",
+ 469: "[$ESP]\\ #,##0.00",
+ 470: "[$ETB]\\ #,##0.00",
+ 471: "[$EUR]\\ #,##0.00",
+ 472: "[$FIM]\\ #,##0.00",
+ 473: "[$FJD]\\ #,##0.00",
+ 474: "[$FKP]\\ #,##0.00",
+ 475: "[$FRF]\\ #,##0.00",
+ 476: "[$GBP]\\ #,##0.00",
+ 477: "[$GEL]\\ #,##0.00",
+ 478: "[$GHC]\\ #,##0.00",
+ 479: "[$GHS]\\ #,##0.00",
+ 480: "[$GIP]\\ #,##0.00",
+ 481: "[$GMD]\\ #,##0.00",
+ 482: "[$GNF]\\ #,##0.00",
+ 483: "[$GRD]\\ #,##0.00",
+ 484: "[$GTQ]\\ #,##0.00",
+ 485: "[$GYD]\\ #,##0.00",
+ 486: "[$HKD]\\ #,##0.00",
+ 487: "[$HNL]\\ #,##0.00",
+ 488: "[$HRK]\\ #,##0.00",
+ 489: "[$HTG]\\ #,##0.00",
+ 490: "[$HUF]\\ #,##0.00",
+ 491: "[$IDR]\\ #,##0.00",
+ 492: "[$IEP]\\ #,##0.00",
+ 493: "[$ILS]\\ #,##0.00",
+ 494: "[$INR]\\ #,##0.00",
+ 495: "[$IQD]\\ #,##0.00",
+ 496: "[$IRR]\\ #,##0.00",
+ 497: "[$ISK]\\ #,##0.00",
+ 498: "[$ITL]\\ #,##0.00",
+ 499: "[$JMD]\\ #,##0.00",
+ 500: "[$JOD]\\ #,##0.00",
+ 501: "[$JPY]\\ #,##0.00",
+ 502: "[$KAF]\\ #,##0.00",
+ 503: "[$KES]\\ #,##0.00",
+ 504: "[$KGS]\\ #,##0.00",
+ 505: "[$KHR]\\ #,##0.00",
+ 506: "[$KMF]\\ #,##0.00",
+ 507: "[$KPW]\\ #,##0.00",
+ 508: "[$KRW]\\ #,##0.00",
+ 509: "[$KWD]\\ #,##0.00",
+ 510: "[$KYD]\\ #,##0.00",
+ 511: "[$KZT]\\ #,##0.00",
+ 512: "[$LAK]\\ #,##0.00",
+ 513: "[$LBP]\\ #,##0.00",
+ 514: "[$LKR]\\ #,##0.00",
+ 515: "[$LRD]\\ #,##0.00",
+ 516: "[$LSL]\\ #,##0.00",
+ 517: "[$LTL]\\ #,##0.00",
+ 518: "[$LUF]\\ #,##0.00",
+ 519: "[$LVL]\\ #,##0.00",
+ 520: "[$LYD]\\ #,##0.00",
+ 521: "[$MAD]\\ #,##0.00",
+ 522: "[$MDL]\\ #,##0.00",
+ 523: "[$MGA]\\ #,##0.00",
+ 524: "[$MGF]\\ #,##0.00",
+ 525: "[$MKD]\\ #,##0.00",
+ 526: "[$MMK]\\ #,##0.00",
+ 527: "[$MNT]\\ #,##0.00",
+ 528: "[$MOP]\\ #,##0.00",
+ 529: "[$MRO]\\ #,##0.00",
+ 530: "[$MTL]\\ #,##0.00",
+ 531: "[$MUR]\\ #,##0.00",
+ 532: "[$MVR]\\ #,##0.00",
+ 533: "[$MWK]\\ #,##0.00",
+ 534: "[$MXN]\\ #,##0.00",
+ 535: "[$MXV]\\ #,##0.00",
+ 536: "[$MYR]\\ #,##0.00",
+ 537: "[$MZM]\\ #,##0.00",
+ 538: "[$MZN]\\ #,##0.00",
+ 539: "[$NAD]\\ #,##0.00",
+ 540: "[$NGN]\\ #,##0.00",
+ 541: "[$NIO]\\ #,##0.00",
+ 542: "[$NLG]\\ #,##0.00",
+ 543: "[$NOK]\\ #,##0.00",
+ 544: "[$NPR]\\ #,##0.00",
+ 545: "[$NTD]\\ #,##0.00",
+ 546: "[$NZD]\\ #,##0.00",
+ 547: "[$OMR]\\ #,##0.00",
+ 548: "[$PAB]\\ #,##0.00",
+ 549: "[$PEN]\\ #,##0.00",
+ 550: "[$PGK]\\ #,##0.00",
+ 551: "[$PHP]\\ #,##0.00",
+ 552: "[$PKR]\\ #,##0.00",
+ 553: "[$PLN]\\ #,##0.00",
+ 554: "[$PTE]\\ #,##0.00",
+ 555: "[$PYG]\\ #,##0.00",
+ 556: "[$QAR]\\ #,##0.00",
+ 557: "[$ROL]\\ #,##0.00",
+ 558: "[$RON]\\ #,##0.00",
+ 559: "[$RSD]\\ #,##0.00",
+ 560: "[$RUB]\\ #,##0.00",
+ 561: "[$RUR]\\ #,##0.00",
+ 562: "[$RWF]\\ #,##0.00",
+ 563: "[$SAR]\\ #,##0.00",
+ 564: "[$SBD]\\ #,##0.00",
+ 565: "[$SCR]\\ #,##0.00",
+ 566: "[$SDD]\\ #,##0.00",
+ 567: "[$SDG]\\ #,##0.00",
+ 568: "[$SDP]\\ #,##0.00",
+ 569: "[$SEK]\\ #,##0.00",
+ 570: "[$SGD]\\ #,##0.00",
+ 571: "[$SHP]\\ #,##0.00",
+ 572: "[$SIT]\\ #,##0.00",
+ 573: "[$SKK]\\ #,##0.00",
+ 574: "[$SLL]\\ #,##0.00",
+ 575: "[$SOS]\\ #,##0.00",
+ 576: "[$SPL]\\ #,##0.00",
+ 577: "[$SRD]\\ #,##0.00",
+ 578: "[$SRG]\\ #,##0.00",
+ 579: "[$STD]\\ #,##0.00",
+ 580: "[$SVC]\\ #,##0.00",
+ 581: "[$SYP]\\ #,##0.00",
+ 582: "[$SZL]\\ #,##0.00",
+ 583: "[$THB]\\ #,##0.00",
+ 584: "[$TJR]\\ #,##0.00",
+ 585: "[$TJS]\\ #,##0.00",
+ 586: "[$TMM]\\ #,##0.00",
+ 587: "[$TMT]\\ #,##0.00",
+ 588: "[$TND]\\ #,##0.00",
+ 589: "[$TOP]\\ #,##0.00",
+ 590: "[$TRL]\\ #,##0.00",
+ 591: "[$TRY]\\ #,##0.00",
+ 592: "[$TTD]\\ #,##0.00",
+ 593: "[$TWD]\\ #,##0.00",
+ 594: "[$TZS]\\ #,##0.00",
+ 595: "[$UAH]\\ #,##0.00",
+ 596: "[$UGX]\\ #,##0.00",
+ 597: "[$USD]\\ #,##0.00",
+ 598: "[$USN]\\ #,##0.00",
+ 599: "[$USS]\\ #,##0.00",
+ 600: "[$UYI]\\ #,##0.00",
+ 601: "[$UYU]\\ #,##0.00",
+ 602: "[$UZS]\\ #,##0.00",
+ 603: "[$VEB]\\ #,##0.00",
+ 604: "[$VEF]\\ #,##0.00",
+ 605: "[$VND]\\ #,##0.00",
+ 606: "[$VUV]\\ #,##0.00",
+ 607: "[$WST]\\ #,##0.00",
+ 608: "[$XAF]\\ #,##0.00",
+ 609: "[$XAG]\\ #,##0.00",
+ 610: "[$XAU]\\ #,##0.00",
+ 611: "[$XB5]\\ #,##0.00",
+ 612: "[$XBA]\\ #,##0.00",
+ 613: "[$XBB]\\ #,##0.00",
+ 614: "[$XBC]\\ #,##0.00",
+ 615: "[$XBD]\\ #,##0.00",
+ 616: "[$XCD]\\ #,##0.00",
+ 617: "[$XDR]\\ #,##0.00",
+ 618: "[$XFO]\\ #,##0.00",
+ 619: "[$XFU]\\ #,##0.00",
+ 620: "[$XOF]\\ #,##0.00",
+ 621: "[$XPD]\\ #,##0.00",
+ 622: "[$XPF]\\ #,##0.00",
+ 623: "[$XPT]\\ #,##0.00",
+ 624: "[$XTS]\\ #,##0.00",
+ 625: "[$XXX]\\ #,##0.00",
+ 626: "[$YER]\\ #,##0.00",
+ 627: "[$YUM]\\ #,##0.00",
+ 628: "[$ZAR]\\ #,##0.00",
+ 629: "[$ZMK]\\ #,##0.00",
+ 630: "[$ZMW]\\ #,##0.00",
+ 631: "[$ZWD]\\ #,##0.00",
+ 632: "[$ZWL]\\ #,##0.00",
+ 633: "[$ZWN]\\ #,##0.00",
+ 634: "[$ZWR]\\ #,##0.00",
+ }
+ // supportedTokenTypes list the supported number format token types currently.
+ supportedTokenTypes = []string{
+ nfp.TokenTypeAlignment,
+ nfp.TokenSubTypeCurrencyString,
+ nfp.TokenSubTypeLanguageInfo,
+ nfp.TokenTypeColor,
+ nfp.TokenTypeCurrencyLanguage,
+ nfp.TokenTypeDateTimes,
+ nfp.TokenTypeDecimalPoint,
+ nfp.TokenTypeDenominator,
+ nfp.TokenTypeDigitalPlaceHolder,
+ nfp.TokenTypeElapsedDateTimes,
+ nfp.TokenTypeExponential,
+ nfp.TokenTypeFraction,
+ nfp.TokenTypeGeneral,
+ nfp.TokenTypeHashPlaceHolder,
+ nfp.TokenTypeLiteral,
+ nfp.TokenTypePercent,
+ nfp.TokenTypeRepeatsChar,
+ nfp.TokenTypeSwitchArgument,
+ nfp.TokenTypeTextPlaceHolder,
+ nfp.TokenTypeThousandsSeparator,
+ nfp.TokenTypeZeroPlaceHolder,
+ }
+ // supportedNumberTokenTypes list the supported number token types.
+ supportedNumberTokenTypes = []string{
+ nfp.TokenTypeDenominator,
+ nfp.TokenTypeDigitalPlaceHolder,
+ nfp.TokenTypeExponential,
+ nfp.TokenTypeFraction,
+ nfp.TokenTypeHashPlaceHolder,
+ nfp.TokenTypePercent,
+ nfp.TokenTypeZeroPlaceHolder,
+ }
+ // supportedDateTimeTokenTypes list the supported date and time token types.
+ supportedDateTimeTokenTypes = []string{
+ nfp.TokenTypeDateTimes,
+ nfp.TokenTypeElapsedDateTimes,
+ }
+ // supportedLanguageInfo directly maps the supported language decimal ID and tags.
+ supportedLanguageInfo = map[int]languageInfo{
+ 54: {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr},
+ 1078: {tags: []string{"af-ZA"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr},
+ 28: {tags: []string{"sq"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr},
+ 1052: {tags: []string{"sq-AL"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr},
+ 132: {tags: []string{"gsw"}, localMonth: localMonthsNameAlsatian, apFmt: apFmtAlsatian, weekdayNames: weekdayNamesAlsatian, weekdayNamesAbbr: weekdayNamesAlsatianAbbr},
+ 1156: {tags: []string{"gsw-FR"}, localMonth: localMonthsNameAlsatianFrance, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAlsatianFrance, weekdayNamesAbbr: weekdayNamesAlsatianFranceAbbr},
+ 94: {tags: []string{"am"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr},
+ 1118: {tags: []string{"am-ET"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr},
+ 1: {tags: []string{"ar"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 5121: {tags: []string{"ar-DZ"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 15361: {tags: []string{"ar-BH"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 3073: {tags: []string{"ar-EG"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 2049: {tags: []string{"ar-IQ"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 11265: {tags: []string{"ar-JO"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 13313: {tags: []string{"ar-KW"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 12289: {tags: []string{"ar-LB"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 6145: {tags: []string{"ar-MA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 8193: {tags: []string{"ar-OM"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 16385: {tags: []string{"ar-QA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 1025: {tags: []string{"ar-SA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 10241: {tags: []string{"ar-SY"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 7169: {tags: []string{"ar-TN"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 14337: {tags: []string{"ar-AE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 9217: {tags: []string{"ar-YE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 43: {tags: []string{"hy"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr},
+ 1067: {tags: []string{"hy-AM"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr},
+ 77: {tags: []string{"as"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr},
+ 1101: {tags: []string{"as-IN"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr},
+ 29740: {tags: []string{"az-Cyrl"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr},
+ 2092: {tags: []string{"az-Cyrl-AZ"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr},
+ 44: {tags: []string{"az"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr},
+ 30764: {tags: []string{"az-Latn"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr},
+ 1068: {tags: []string{"az-Latn-AZ"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr},
+ 69: {tags: []string{"bn"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr},
+ 2117: {tags: []string{"bn-BD"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr},
+ 1093: {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr},
+ 109: {tags: []string{"ba"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr},
+ 1133: {tags: []string{"ba-RU"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr},
+ 45: {tags: []string{"eu"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr},
+ 1069: {tags: []string{"eu-ES"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr},
+ 35: {tags: []string{"be"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr},
+ 1059: {tags: []string{"be-BY"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr},
+ 25626: {tags: []string{"bs-Cyrl"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr},
+ 8218: {tags: []string{"bs-Cyrl-BA"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr},
+ 26650: {tags: []string{"bs-Latn"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr},
+ 30746: {tags: []string{"bs"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr},
+ 5146: {tags: []string{"bs-Latn-BA"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr},
+ 126: {tags: []string{"br"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr},
+ 1150: {tags: []string{"br-FR"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr},
+ 2: {tags: []string{"bg"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr},
+ 1026: {tags: []string{"bg-BG"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr},
+ 85: {tags: []string{"my"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese},
+ 1109: {tags: []string{"my-MM"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese},
+ 3: {tags: []string{"ca"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr},
+ 1027: {tags: []string{"ca-ES"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr},
+ 1119: {tags: []string{"tzm-Arab-MA"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr},
+ 146: {tags: []string{"ku"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish},
+ 31890: {tags: []string{"ku-Arab"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish},
+ 1170: {tags: []string{"ku-Arab-IQ"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish},
+ 92: {tags: []string{"chr"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr},
+ 31836: {tags: []string{"chr-Cher"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr},
+ 1116: {tags: []string{"chr-Cher-US"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr},
+ 4: {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
+ 30724: {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
+ 2052: {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr},
+ 4100: {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr},
+ 31748: {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
+ 3076: {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
+ 5124: {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2},
+ 1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2, useGannen: true},
+ 9: {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 4096: {tags: []string{
+ "aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML",
+ "gsw-LI", "gsw-CH", "ar-TD", "ar-KM", "ar-DJ", "ar-ER", "ar-IL", "ar-MR", "ar-PS",
+ "ar-SO", "ar-SS", "ar-SD", "ar-001", "ast", "ast-ES", "asa", "asa-TZ", "ksf", "ksf-CM",
+ "bm", "bm-Latn-ML", "bas", "bas-CM", "bem", "bem-ZM", "bez", "bez-TZ", "byn", "byn-ER",
+ "brx", "brx-IN", "ca-AD", "ca-FR", "ca-IT", "ceb", "ceb-Latn", "ceb-Latn-PH", "tzm-Latn-MA",
+ "ccp", "ccp-Cakm", "ccp-Cakm-BD", "ccp-Cakm-IN", "ce-RU", "cgg", "cgg-UG", "cu-RU", "swc",
+ "swc-CD", "kw", "ke-GB", "da-GL", "dua", "dua-CM", "nl-AW", "nl-BQ", "nl-CW", "nl-SX",
+ "nl-SR", "dz", "ebu", "ebu-KE", "en-AS", "en-AI", "en-AG", "en-AT", "en-BS", "en-BB",
+ "en-BE", "en-BM", "en-BW", "en-IO", "en-VG", "en-BI", "en-CM", "en-KY", "en-CX", "en-CC",
+ "en-CK", "en-CY", "en-DK", "en-DM", "en-ER", "en-150", "en-FK", "en-FI", "en-FJ", "en-GM",
+ "en-DE", "en-GH", "en-GI", "en-GD", "en-GU", "en-GG", "en-GY", "en-IM", "en-IL", "en-JE",
+ "en-KE", "en-KI", "en-LS", "en-LR", "en-MO", "en-MG", "en-MW", "en-MT", "en-MH", "en-MU",
+ "en-FM", "en-MS", "en-NA", "en-NR", "en-NL", "en-NG", "en-NU", "en-NF", "en-MP", "en-PK",
+ "en-PW", "en-PG", "en-PN", "en-PR", "en-RW", "en-KN", "en-LC", "en-VC", "en-WS", "en-SC",
+ "en-SL", "en-SX", "en-SI", "en-SB", "en-SS", "en-SH", "en-SD", "en-SZ", "en-SE", "en-CH",
+ "en-TZ", "en-TK", "en-TO", "en-TC", "en-TV", "en-UG", "en-UM", "en-VI", "en-VU", "en-001",
+ "en-ZM", "eo", "eo-001", "ee", "ee-GH", "ee-TG", "ewo", "ewo-CM", "fo-DK", "fr-DZ",
+ "fr-BJ", "fr-BF", "fr-BI", "fr-CF", "fr-TD", "fr-KM", "fr-CG", "fr-DJ", "fr-GQ", "fr-GF",
+ "fr-PF", "fr-GA", "fr-GP", "fr-GN", "fr-MG", "fr-MQ", "fr-MR", "fr-MU", "fr-YT", "fr-NC",
+ "fr-NE", "fr-RW", "fr-BL", "fr-MF", "fr-PM", "fr-SC", "fr-SY", "fr-TG", "fr-TN", "fr-VU",
+ "fr-WF", "fur", "fur-IT", "ff-Latn-BF", "ff-CM", "ff-Latn-CM", "ff-Latn-GM", "ff-Latn-GH",
+ "ff-GN", "ff-Latn-GN", "ff-Latn-GW", "ff-Latn-LR", "ff-MR", "ff-Latn-MR", "ff-Latn-NE",
+ "ff-Latn-SL", "lg", "lg-UG", "de-BE", "de-IT", "el-CY", "guz", "guz-KE", "ha-Latn-GH",
+ "ha-Latn-NG", "ia-FR", "ia-001", "it-SM", "it-VA", "jv", "jv-Latn", "jv-Latn-ID", "dyo",
+ "dyo-SN", "kea", "kea-CV", "kab", "kab-DZ", "kkj", "kkj-CM", "kln", "kln-KE", "kam",
+ "kam-KE", "ks-Arab-IN", "ki", "ki-KE", "sw-TZ", "sw-UG", "ko-KP", "khq", "khq-ML", "ses",
+ "ses-ML", "nmg", "nmq-CM", "ku-Arab-IR", "lkt", "lkt-US", "lag", "lag-TZ", "ln", "ln-AO",
+ "ln-CF", "ln-CD", "nds", "nds-DE", "nds-NL", "lu", "lu-CD", "luo", "luo", "luo-KE", "luy",
+ "luy-KE", "jmc", "jmc-TZ", "mgh", "mgh-MZ", "kde", "kde-TZ", "mg", "mg-MG", "gv", "gv-IM",
+ "mas", "mas-KE", "mas-TZ", "mas-IR", "mer", "mer-KE", "mgo", "mgo-CM", "mfe", "mfe-MU",
+ "mua", "mua-CM", "nqo", "nqo-GN", "nqa", "naq-NA", "nnh", "nnh-CM", "jgo", "jgo-CM",
+ "lrc-IQ", "lrc-IR", "nd", "nd-ZW", "nb-SJ", "nus", "nus-SD", "nus-SS", "nyn", "nyn-UG",
+ "om-KE", "os", "os-GE", "os-RU", "ps-PK", "fa-AF", "pt-AO", "pt-CV", "pt-GQ", "pt-GW",
+ "pt-LU", "pt-MO", "pt-MZ", "pt-ST", "pt-CH", "pt-TL", "prg-001", "ksh", "ksh-DE", "rof",
+ "rof-TZ", "rn", "rn-BI", "ru-BY", "ru-KZ", "ru-KG", "ru-UA", "rwk", "rwk-TZ", "ssy",
+ "ssy-ER", "saq", "saq-KE", "sg", "sq-CF", "sbp", "sbp-TZ", "seh", "seh-MZ", "ksb", "ksb-TZ",
+ "sn", "sn-Latn", "sn-Latn-ZW", "xog", "xog-UG", "so-DJ", "so-ET", "so-KE", "nr", "nr-ZA",
+ "st-LS", "es-BZ", "es-BR", "es-PH", "zgh", "zgh-Tfng-MA", "zgh-Tfng", "ss", "ss-ZA",
+ "ss-SZ", "sv-AX", "shi", "shi-Tfng", "shi-Tfng-MA", "shi-Latn", "shi-Latn-MA", "dav",
+ "dav-KE", "ta-MY", "ta-SG", "twq", "twq-NE", "teo", "teo-KE", "teo-UG", "bo-IN", "tig",
+ "tig-ER", "to", "to-TO", "tr-CY", "uz-Arab", "us-Arab-AF", "vai", "vai-Vaii",
+ "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae",
+ "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE",
+ }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 3081: {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 10249: {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 4105: {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 9225: {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 15369: {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 16393: {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 6153: {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 8201: {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 17417: {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 5129: {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 13321: {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 18441: {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 7177: {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 11273: {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 19465: {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 2057: {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 1033: {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 12297: {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 37: {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr},
+ 1061: {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr},
+ 56: {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr},
+ 1080: {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr},
+ 100: {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr},
+ 1124: {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr},
+ 11: {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr},
+ 1035: {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr},
+ 12: {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 2060: {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 11276: {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 3084: {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 7180: {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 9228: {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 12300: {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 1036: {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 15372: {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 5132: {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 13324: {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 14348: {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 6156: {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 8204: {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 10252: {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr},
+ 98: {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr},
+ 1122: {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr},
+ 103: {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr},
+ 31847: {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr},
+ 1127: {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr},
+ 2151: {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr},
+ 86: {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr},
+ 1110: {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr},
+ 55: {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr},
+ 1079: {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr},
+ 7: {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr},
+ 3079: {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr},
+ 1031: {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr},
+ 5127: {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr},
+ 2055: {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr},
+ 8: {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr},
+ 1032: {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr},
+ 111: {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr},
+ 1135: {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr},
+ 116: {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr},
+ 1140: {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr},
+ 71: {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr},
+ 1095: {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr},
+ 104: {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr},
+ 31848: {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr},
+ 1128: {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr},
+ 117: {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr},
+ 1141: {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr},
+ 13: {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr},
+ 1037: {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr},
+ 57: {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr},
+ 1081: {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr},
+ 14: {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr},
+ 1038: {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr},
+ 15: {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr},
+ 1039: {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr},
+ 112: {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr},
+ 1136: {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr},
+ 33: {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr},
+ 1057: {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr},
+ 93: {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr},
+ 31837: {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr},
+ 2141: {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr},
+ 30813: {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr},
+ 1117: {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr},
+ 60: {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr},
+ 2108: {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr},
+ 16: {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr},
+ 1040: {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr},
+ 2064: {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr},
+ 17: {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr},
+ 1041: {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr},
+ 75: {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr},
+ 1099: {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr},
+ 1137: {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 96: {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr},
+ 1120: {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr},
+ 2144: {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 63: {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr},
+ 1087: {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr},
+ 83: {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr},
+ 1107: {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr},
+ 134: {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr},
+ 1158: {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr},
+ 135: {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr},
+ 1159: {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr},
+ 65: {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr},
+ 1089: {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr},
+ 87: {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr},
+ 1111: {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr},
+ 18: {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr},
+ 1042: {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr},
+ 64: {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr},
+ 1088: {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr},
+ 84: {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr},
+ 1108: {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr},
+ 1142: {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr},
+ 38: {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr},
+ 1062: {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr},
+ 39: {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr},
+ 1063: {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr},
+ 31790: {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr},
+ 2094: {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr},
+ 110: {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr},
+ 1134: {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr},
+ 47: {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr},
+ 1071: {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr},
+ 62: {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr},
+ 2110: {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr},
+ 1086: {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr},
+ 76: {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr},
+ 1100: {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr},
+ 58: {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr},
+ 1082: {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr},
+ 129: {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr},
+ 1153: {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr},
+ 122: {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr},
+ 1146: {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr},
+ 78: {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr},
+ 1102: {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr},
+ 124: {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 1148: {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr},
+ 80: {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr},
+ 30800: {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr},
+ 1104: {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr},
+ 31824: {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian},
+ 2128: {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian},
+ 3152: {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN},
+ 97: {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr},
+ 2145: {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr},
+ 1121: {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr},
+ 20: {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr},
+ 31764: {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr},
+ 1044: {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr},
+ 30740: {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr},
+ 2068: {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr},
+ 130: {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr},
+ 1154: {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr},
+ 72: {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr},
+ 1096: {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr},
+ 114: {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr},
+ 1138: {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr},
+ 99: {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto},
+ 1123: {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto},
+ 41: {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian},
+ 1065: {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian},
+ 21: {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr},
+ 1045: {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr},
+ 22: {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr},
+ 1046: {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr},
+ 2070: {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr},
+ 70: {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr},
+ 31814: {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab},
+ 1094: {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr},
+ 2118: {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab},
+ 107: {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr},
+ 1131: {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr},
+ 2155: {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr},
+ 3179: {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr},
+ 24: {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr},
+ 2072: {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr},
+ 1048: {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr},
+ 23: {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr},
+ 1047: {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr},
+ 25: {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr},
+ 2073: {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr},
+ 1049: {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr},
+ 133: {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr},
+ 1157: {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr},
+ 28731: {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr},
+ 9275: {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr},
+ 31803: {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr},
+ 4155: {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr},
+ 5179: {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr},
+ 59: {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr},
+ 3131: {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr},
+ 1083: {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr},
+ 2107: {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr},
+ 29755: {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr},
+ 8251: {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr},
+ 30779: {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr},
+ 6203: {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr},
+ 7227: {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr},
+ 79: {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr},
+ 1103: {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr},
+ 145: {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr},
+ 1169: {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr},
+ 27674: {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr},
+ 7194: {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr},
+ 12314: {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr},
+ 10266: {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr},
+ 3098: {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr},
+ 28698: {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr},
+ 31770: {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr},
+ 6170: {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr},
+ 11290: {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr},
+ 9242: {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr},
+ 2074: {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr},
+ 108: {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr},
+ 1132: {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr},
+ 50: {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr},
+ 2098: {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr},
+ 1074: {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr},
+ 89: {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr},
+ 31833: {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr},
+ 2137: {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr},
+ 91: {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr},
+ 1115: {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr},
+ 27: {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr},
+ 1051: {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr},
+ 36: {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr},
+ 1060: {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr},
+ 119: {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr},
+ 1143: {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr},
+ 48: {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr},
+ 1072: {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr},
+ 10: {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr},
+ 11274: {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 8202: {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 16394: {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 13322: {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 9226: {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 5130: {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 23562: {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 7178: {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 12298: {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 17418: {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 4106: {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 18442: {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 22538: {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 2058: {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 19466: {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 6154: {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 15370: {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 10250: {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 20490: {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 1034: {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr},
+ 3082: {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr},
+ 21514: {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr},
+ 14346: {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr},
+ 29: {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr},
+ 2077: {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr},
+ 1053: {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr},
+ 90: {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr},
+ 1114: {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr},
+ 40: {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr},
+ 31784: {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr},
+ 1064: {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr},
+ 95: {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr},
+ 31839: {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr},
+ 2143: {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr},
+ 73: {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr},
+ 1097: {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr},
+ 2121: {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr},
+ 68: {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr},
+ 1092: {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr},
+ 74: {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr},
+ 1098: {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr},
+ 30: {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr},
+ 1054: {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr},
+ 81: {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr},
+ 1105: {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr},
+ 115: {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr},
+ 2163: {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr},
+ 1139: {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr},
+ 49: {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr},
+ 1073: {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr},
+ 31: {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr},
+ 1055: {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr},
+ 66: {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr},
+ 1090: {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr},
+ 34: {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr},
+ 1058: {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr},
+ 46: {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr},
+ 1070: {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr},
+ 32: {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu},
+ 2080: {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN},
+ 1056: {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu},
+ 128: {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr},
+ 1152: {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr},
+ 30787: {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr},
+ 2115: {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr},
+ 67: {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr},
+ 31811: {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr},
+ 1091: {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr},
+ 2051: {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr},
+ 51: {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr},
+ 1075: {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr},
+ 42: {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr},
+ 1066: {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr},
+ 82: {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr},
+ 1106: {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr},
+ 136: {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr},
+ 1160: {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr},
+ 52: {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr},
+ 1076: {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr},
+ 120: {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr},
+ 1144: {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr},
+ 1085: {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr},
+ 106: {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr},
+ 1130: {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr},
+ 53: {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr},
+ 1077: {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr},
+ }
+ // supportedLanguageCodeInfo directly maps the supported language code and tags.
+ supportedLanguageCodeInfo = map[string]languageInfo{
+ "JA-JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr},
+ "JA-JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true},
+ }
+ // republicOfChinaYear defined start time of the Republic of China
+ republicOfChinaYear = time.Date(1912, time.January, 1, 0, 0, 0, 0, time.UTC)
+ // republicOfChinaEraName defined the Republic of China era name for the Republic of China calendar.
+ republicOfChinaEraName = []string{"\u4e2d\u83ef\u6c11\u570b", "\u6c11\u570b", "\u524d"}
+ // japaneseEraYears list the Japanese era name periods.
+ japaneseEraYears = []time.Time{
+ time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC),
+ time.Date(1912, time.June, 30, 0, 0, 0, 0, time.UTC),
+ time.Date(1926, time.November, 25, 0, 0, 0, 0, time.UTC),
+ time.Date(1989, time.January, 8, 0, 0, 0, 0, time.UTC),
+ time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC),
+ }
+ // japaneseEraNames list the Japanese era name for the Japanese emperor reign calendar.
+ japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"}
+ // japaneseEraYear list the Japanese era name symbols.
+ japaneseEraSymbols = []string{"M", "T", "S", "H", "R"}
+ // monthNamesAfrikaans list the month names in the Afrikaans.
+ monthNamesAfrikaans = []string{"Januarie", "Februarie", "Maart", "April", "Mei", "Junie", "Julie", "Augustus", "September", "Oktober", "November", "Desember"}
+ // monthNamesAfrikaansAbbr lists the month name abbreviations in the Afrikaans.
+ monthNamesAfrikaansAbbr = []string{"Jan.", "Feb.", "Maa.", "Apr.", "Mei", "Jun.", "Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Des."}
+ // monthNamesAlbanian list the month names in the Albanian.
+ monthNamesAlbanian = []string{"janar", "shkurt", "mars", "prill", "maj", "qershor", "korrik", "gusht", "shtator", "tetor", "nëntor", "dhjetor"}
+ // monthNamesAlbanianAbbr lists the month name abbreviations in the Albanian.
+ monthNamesAlbanianAbbr = []string{"jan", "shk", "mar", "pri", "maj", "qer", "krr", "gush", "sht", "tet", "nën", "dhj"}
+ // monthNamesAlsatian list the month names in the Alsatian.
+ monthNamesAlsatian = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "Auguscht", "Septämber", "Oktoober", "Novämber", "Dezämber"}
+ // monthNamesAlsatianAbbr lists the month name abbreviations in the Alsatian France.
+ monthNamesAlsatianAbbr = []string{"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"}
+ // monthNamesAlsatianFrance list the month names in the Alsatian.
+ monthNamesAlsatianFrance = []string{"Jänner", "Feverje", "März", "Àpril", "Mai", "Jüni", "Jüli", "Augscht", "September", "Oktower", "Nowember", "Dezember"}
+ // monthNamesAlsatianFranceAbbr lists the month name abbreviations in the Alsatian France.
+ monthNamesAlsatianFranceAbbr = []string{"Jän.", "Fev.", "März", "Apr.", "Mai", "Jüni", "Jüli", "Aug.", "Sept.", "Okt.", "Now.", "Dez."}
+ // monthNamesAmharic list the month names in the Amharic.
+ monthNamesAmharic = []string{
+ "\u1303\u1295\u12E9\u12C8\u122A",
+ "\u134C\u1265\u1229\u12C8\u122A",
+ "\u121B\u122D\u127D",
+ "\u12A4\u1355\u122A\u120D",
+ "\u121C\u12ED",
+ "\u1301\u1295",
+ "\u1301\u120B\u12ED",
+ "\u12A6\u1308\u1235\u1275",
+ "\u1234\u1355\u1274\u121D\u1260\u122D",
+ "\u12A6\u12AD\u1276\u1260\u122D",
+ "\u1296\u126C\u121D\u1260\u122D",
+ "\u12F2\u1234\u121D\u1260\u122D",
+ }
+ // monthNamesAmharicAbbr lists the month name abbreviations in the Amharic.
+ monthNamesAmharicAbbr = []string{
+ "\u1303\u1295\u12E9",
+ "\u134C\u1265\u1229",
+ "\u121B\u122D\u127D",
+ "\u12A4\u1355\u122A",
+ "\u121C\u12ED",
+ "\u1301\u1295",
+ "\u1301\u120B\u12ED",
+ "\u12A6\u1308\u1235",
+ "\u1234\u1355\u1274",
+ "\u12A6\u12AD\u1276",
+ "\u1296\u126C\u121D",
+ "\u12F2\u1234\u121D",
+ }
+ // monthNamesArabic list the month names in the Arabic.
+ monthNamesArabic = []string{
+ "\u064A\u0646\u0627\u064A\u0631",
+ "\u0641\u0628\u0631\u0627\u064A\u0631",
+ "\u0645\u0627\u0631\u0633",
+ "\u0623\u0628\u0631\u064A\u0644",
+ "\u0645\u0627\u064A\u0648",
+ "\u064A\u0648\u0646\u064A\u0648",
+ "\u064A\u0648\u0644\u064A\u0648",
+ "\u0623\u063A\u0633\u0637\u0633",
+ "\u0633\u0628\u062A\u0645\u0628\u0631",
+ "\u0623\u0643\u062A\u0648\u0628\u0631",
+ "\u0646\u0648\u0641\u0645\u0628\u0631",
+ "\u062F\u064A\u0633\u0645\u0628\u0631",
+ }
+ // monthNamesArabicIraq list the month names in the Arabic Iraq.
+ monthNamesArabicIraq = []string{
+ "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A",
+ "\u0634\u0628\u0627\u0637",
+ "\u0622\u0630\u0627\u0631",
+ "\u0646\u064A\u0633\u0627\u0646",
+ "\u0623\u064A\u0627\u0631",
+ "\u062D\u0632\u064A\u0631\u0627\u0646",
+ "\u062A\u0645\u0648\u0632",
+ "\u0622\u0628",
+ "\u0623\u064A\u0644\u0648\u0644",
+ "\u062A\u0634\u0631\u064A\u0646%A0\u0627\u0644\u0623\u0648\u0644",
+ "\u062A\u0634\u0631\u064A\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A",
+ "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u0623\u0648\u0644",
+ }
+ // monthNamesArmenian list the month names in the Armenian.
+ monthNamesArmenian = []string{
+ "\u0540\u0578\u0582\u0576\u057E\u0561\u0580",
+ "\u0553\u0565\u057F\u0580\u057E\u0561\u0580",
+ "\u0544\u0561\u0580\u057F",
+ "\u0531\u057A\u0580\u056B\u056C",
+ "\u0544\u0561\u0575\u056B\u057D",
+ "\u0540\u0578\u0582\u0576\u056B\u057D",
+ "\u0540\u0578\u0582\u056C\u056B\u057D",
+ "\u0555\u0563\u0578\u057D\u057F\u0578\u057D",
+ "\u054D\u0565\u057A\u057F\u0565\u0574\u0562\u0565\u0580",
+ "\u0540\u0578\u056F\u057F\u0565\u0574\u0562\u0565\u0580",
+ "\u0546\u0578\u0575\u0565\u0574\u0562\u0565\u0580",
+ "\u0534\u0565\u056F\u057F\u0565\u0574\u0562\u0565\u0580",
+ }
+ // monthNamesArmenianAbbr lists the month name abbreviations in the Armenian.
+ monthNamesArmenianAbbr = []string{
+ "\u0540\u0576\u057E",
+ "\u0553\u057F\u057E",
+ "\u0544\u0580\u057F",
+ "\u0531\u057A\u0580",
+ "\u0544\u0575\u057D",
+ "\u0540\u0576\u057D",
+ "\u0540\u056C\u057D",
+ "\u0555\u0563\u057D",
+ "\u054D\u057A\u057F",
+ "\u0540\u056F\u057F",
+ "\u0546\u0575\u0574",
+ "\u0534\u056F\u057F",
+ }
+ // monthNamesAssamese list the month names in the Assamese.
+ monthNamesAssamese = []string{
+ "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0",
+ "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1\u09F1\u09BE\u09F0\u09C0",
+ "\u09AE\u09BE\u09B0\u09CD\u099A",
+ "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2",
+ "\u09AE\u09C7",
+ "\u099C\u09C1\u09A8",
+ "\u099C\u09C1\u09B2\u09BE\u0987",
+ "\u0986\u0997\u09B7\u09CD\u099F",
+ "\u099A\u09C7\u09AA\u09CD\u099F\u09C7\u09AE\u09CD\u09AC\u09F0",
+ "\u0985\u0995\u09CD\u099F\u09CB\u09AC\u09F0",
+ "\u09A8\u09AC\u09C7\u09AE\u09CD\u09AC\u09F0",
+ "\u09A1\u09BF\u099A\u09C7\u09AE\u09CD\u09AC\u09F0",
+ }
+ // monthNamesAssameseAbbr lists the month name abbreviations in the Assamese.
+ monthNamesAssameseAbbr = []string{
+ "\u099C\u09BE\u09A8\u09C1",
+ "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1",
+ "\u09AE\u09BE\u09B0\u09CD\u099A",
+ "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2",
+ "\u09AE\u09C7",
+ "\u099C\u09C1\u09A8",
+ "\u099C\u09C1\u09B2\u09BE\u0987",
+ "\u0986\u0997\u09B7\u09CD\u099F",
+ "\u099A\u09C7\u09AA\u09CD\u099F\u09C7",
+ "\u0985\u0995\u09CD\u099F\u09CB",
+ "\u09A8\u09AC\u09C7",
+ "\u09A1\u09BF\u099A\u09C7",
+ }
+ // monthNamesAzerbaijaniCyrillic list the month names in the Azerbaijani (Cyrillic).
+ monthNamesAzerbaijaniCyrillic = []string{
+ "j\u0430\u043D\u0432\u0430\u0440",
+ "\u0444\u0435\u0432\u0440\u0430\u043B",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0435\u043B",
+ "\u043C\u0430\u0458",
+ "\u0438\u0458\u0443\u043D",
+ "\u0438\u0458\u0443\u043B",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043D\u0442\u0458\u0430\u0431\u0440",
+ "\u043E\u043A\u0442\u0458\u0430\u0431\u0440",
+ "\u043D\u043E\u0458\u0430\u0431\u0440",
+ "\u0434\u0435\u043A\u0430\u0431\u0440",
+ }
+ // monthNamesAzerbaijaniCyrillicAbbr lists the month name abbreviations in the Azerbaijani (Cyrillic).
+ monthNamesAzerbaijaniCyrillicAbbr = []string{
+ "\u0408\u0430\u043D",
+ "\u0424\u0435\u0432",
+ "\u041C\u0430\u0440",
+ "\u0410\u043F\u0440",
+ "\u041C\u0430\u0458",
+ "\u0418\u0458\u0443\u043D",
+ "\u0418\u0458\u0443\u043B",
+ "\u0410\u0432\u0433",
+ "\u0421\u0435\u043D",
+ "\u041E\u043A\u0442",
+ "\u041D\u043E\u044F",
+ "\u0414\u0435\u043A",
+ }
+ // monthNamesAzerbaijani list the month names in the Azerbaijani.
+ monthNamesAzerbaijani = []string{"yanvar", "fevral", "mart", "aprel", "may", "iyun", "iyul", "avgust", "sentyabr", "oktyabr", "noyabr", "dekabr"}
+ // monthNamesAzerbaijaniAbbr lists the month name abbreviations in the Azerbaijani.
+ monthNamesAzerbaijaniAbbr = []string{"yan", "fev", "mar", "apr", "may", "iyn", "iyl", "avq", "sen", "okt", "noy", "dek"}
+ // monthNamesAustria list the month names in the Austrian.
+ monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}
+ // monthNamesAustriaAbbr list the month name abbreviations in the Austrian.
+ monthNamesAustriaAbbr = []string{"Jän", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"}
+ // monthNamesBangla list the month names in the Bangla.
+ monthNamesBangla = []string{
+ "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0",
+ "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0",
+ "\u09AE\u09BE\u09B0\u09CD\u099A",
+ "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2",
+ "\u09AE\u09C7",
+ "\u099C\u09C1\u09A8",
+ "\u099C\u09C1\u09B2\u09BE\u0987",
+ "\u0986\u0997\u09B8\u09CD\u099F",
+ "\u09B8\u09C7\u09AA\u09CD\u099F\u09C7\u09AE\u09CD\u09AC\u09B0",
+ "\u0985\u0995\u09CD\u099F\u09CB\u09AC\u09B0",
+ "\u09A8\u09AD\u09C7\u09AE\u09CD\u09AC\u09B0",
+ "\u09A1\u09BF\u09B8\u09C7\u09AE\u09CD\u09AC\u09B0",
+ }
+ // monthNamesBashkir list the month names in the Bashkir.
+ monthNamesBashkir = []string{
+ "\u0493\u0438\u043D\u0443\u0430\u0440",
+ "\u0444\u0435\u0432\u0440\u0430\u043B\u044C",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0435\u043B\u044C",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D\u044C",
+ "\u0438\u044E\u043B\u044C",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C",
+ "\u043E\u043A\u0442\u044F\u0431\u0440\u044C",
+ "\u043D\u043E\u044F\u0431\u0440\u044C",
+ "\u0434\u0435\u043A\u0430\u0431\u0440\u044C",
+ }
+ // monthNamesBashkirAbbr lists the month name abbreviations in the Bashkir.
+ monthNamesBashkirAbbr = []string{
+ "\u0493\u0438\u043D",
+ "\u0444\u0435\u0432",
+ "\u043C\u0430\u0440",
+ "\u0430\u043F\u0440",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D",
+ "\u0438\u044E\u043B",
+ "\u0430\u0432\u0433",
+ "\u0441\u0435\u043D",
+ "\u043E\u043A\u0442",
+ "\u043D\u043E\u044F",
+ "\u0434\u0435\u043A",
+ }
+ // monthNamesBasque list the month names in the Basque.
+ monthNamesBasque = []string{"urtarrila", "otsaila", "martxoa", "apirila", "maiatza", "ekaina", "uztaila", "abuztua", "iraila", "urria", "azaroa", "abendua"}
+ // monthNamesBasqueAbbr lists the month name abbreviations in the Basque.
+ monthNamesBasqueAbbr = []string{"urt.", "ots.", "mar.", "api.", "mai.", "eka.", "uzt.", "abu.", "ira.", "urr.", "aza.", "abe."}
+ // monthNamesBelarusian list the month names in the Belarusian.
+ monthNamesBelarusian = []string{
+ "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C",
+ "\u043B\u044E\u0442\u044B",
+ "\u0441\u0430\u043A\u0430\u0432\u0456\u043A",
+ "\u043A\u0440\u0430\u0441\u0430\u0432\u0456\u043A",
+ "\u043C\u0430\u0439",
+ "\u0447\u044D\u0440\u0432\u0435\u043D\u044C",
+ "\u043B\u0456\u043F\u0435\u043D\u044C",
+ "\u0436\u043D\u0456\u0432\u0435\u043D\u044C",
+ "\u0432\u0435\u0440\u0430\u0441\u0435\u043D\u044C",
+ "\u043A\u0430\u0441\u0442\u0440\u044B\u0447\u043D\u0456\u043A",
+ "\u043B\u0456\u0441\u0442\u0430\u043F\u0430\u0434",
+ "\u0441\u043D\u0435\u0436\u0430\u043D\u044C",
+ }
+ // monthNamesBelarusianAbbr lists the month name abbreviations in the Belarusian.
+ monthNamesBelarusianAbbr = []string{
+ "\u0441\u0442\u0443\u0434\u0437",
+ "\u043B\u044E\u0442",
+ "\u0441\u0430\u043A",
+ "\u043A\u0440\u0430\u0441",
+ "\u043C\u0430\u0439",
+ "\u0447\u044D\u0440\u0432",
+ "\u043B\u0456\u043F",
+ "\u0436\u043D",
+ "\u0432\u0435\u0440",
+ "\u043A\u0430\u0441\u0442\u0440",
+ "\u043B\u0456\u0441\u0442",
+ "\u0441\u043D\u0435\u0436",
+ }
+ // monthNamesBosnianCyrillic list the month names in the Bosnian (Cyrillic).
+ monthNamesBosnianCyrillic = []string{
+ "\u0458\u0430\u043D\u0443\u0430\u0440",
+ "\u0444\u0435\u0431\u0440\u0443\u0430\u0440",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0438\u043B",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D",
+ "\u0458\u0443\u043B",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440",
+ "\u043E\u043A\u0442\u043E\u0431\u0430\u0440",
+ "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440",
+ "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440",
+ }
+ // monthNamesBosnianCyrillicAbbr lists the month name abbreviations in the Bosnian (Cyrillic).
+ monthNamesBosnianCyrillicAbbr = []string{
+ "\u0458\u0430\u043D",
+ "\u0444\u0435\u0431",
+ "\u043C\u0430\u0440",
+ "\u0430\u043F\u0440",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D",
+ "\u0458\u0443\u043B",
+ "\u0430\u0432\u0433",
+ "\u0441\u0435\u043F",
+ "\u043E\u043A\u0442",
+ "\u043D\u043E\u0432",
+ "\u0434\u0435\u0446",
+ }
+ // monthNamesBosnian list the month names in the Bosnian.
+ monthNamesBosnian = []string{"januar", "februar", "mart", "april", "maj", "juni", "juli", "august", "septembar", "oktobar", "novembar", "decembar"}
+ // monthNamesBosnianAbbr lists the month name abbreviations in the Bosnian.
+ monthNamesBosnianAbbr = []string{"jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}
+ // monthNamesBreton list the month names in the Breton.
+ monthNamesBreton = []string{"Genver", "Cʼhwevrer", "Meurzh", "Ebrel", "Mae", "Mezheven", "Gouere", "Eost", "Gwengolo", "Here", "Du", "Kerzu"}
+ // monthNamesBretonAbbr lists the month name abbreviations in the Breton.
+ monthNamesBretonAbbr = []string{"Gen.", "Cʼhwe.", "Meur.", "Ebr.", "Mae", "Mezh.", "Goue.", "Eost", "Gwen.", "Here", "Du", "Kzu."}
+ // monthNamesBulgarian list the month names in the Bulgarian.
+ monthNamesBulgarian = []string{
+ "\u044F\u043D\u0443\u0430\u0440\u0438",
+ "\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0438\u043B",
+ "\u043C\u0430\u0439",
+ "\u044E\u043D\u0438",
+ "\u044E\u043B\u0438",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043F\u0442\u0435\u043C\u0432\u0440\u0438",
+ "\u043E\u043A\u0442\u043E\u043C\u0432\u0440\u0438",
+ "\u043D\u043E\u0435\u043C\u0432\u0440\u0438",
+ "\u0434\u0435\u043A\u0435\u043C\u0432\u0440\u0438",
+ }
+ // monthNamesBurmese list the month names in the Burmese.
+ monthNamesBurmese = []string{
+ "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E",
+ "\u1016\u1031\u1016\u1031\u102C\u103A\u101D\u102B\u101B\u102E",
+ "\u1019\u1010\u103A",
+ "\u1027\u1015\u103C\u102E",
+ "\u1019\u1031",
+ "\u1007\u103D\u1014\u103A",
+ "\u1007\u1030\u101C\u102D\u102F\u1004\u103A",
+ "\u1029\u1002\u102F\u1010\u103A",
+ "\u1005\u1000\u103A\u1010\u1004\u103A\u1018\u102C",
+ "\u1021\u1031\u102C\u1000\u103A\u1010\u102D\u102F\u1018\u102C",
+ "\u1014\u102D\u102F\u101D\u1004\u103A\u1018\u102C",
+ "\u1012\u102E\u1007\u1004\u103A\u1018\u102C",
+ }
+ // monthNamesBurmeseAbbr lists the month name abbreviations in the Burmese.
+ monthNamesBurmeseAbbr = []string{
+ "\u1007\u1014\u103A",
+ "\u1016\u1031",
+ "\u1019\u1010\u103A",
+ "\u1027",
+ "\u1019\u1031",
+ "\u1007\u103D\u1014\u103A",
+ "\u1007\u1030",
+ "\u1029",
+ "\u1005\u1000\u103A",
+ "\u1021\u1031\u102C\u1000\u103A",
+ "\u1014\u102D\u102F",
+ "\u1012\u102E",
+ }
+ // monthNamesCaribbean list the month names in the Caribbean.
+ monthNamesCaribbean = []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"}
+ // monthNamesCaribbeanAbbr lists the month name abbreviations in the Caribbean.
+ monthNamesCaribbeanAbbr = []string{"Janv.", "Févr.", "Mars", "Avr.", "Mai", "Juin", "Juil.", "Août", "Sept.", "Oct.", "Nov.", "Déc."}
+ // monthNamesCentralKurdish list the month names in the Central Kurdish.
+ monthNamesCentralKurdish = []string{
+ "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645",
+ "\u0634\u0648\u0628\u0627\u062A",
+ "\u0626\u0627\u0632\u0627\u0631",
+ "\u0646\u06CC\u0633\u0627\u0646",
+ "\u0626\u0627\u06CC\u0627\u0631",
+ "\u062D\u0648\u0632\u06D5\u06CC\u0631\u0627\u0646",
+ "\u062A\u06D5\u0645\u0648\u0648\u0632",
+ "\u0626\u0627\u0628",
+ "\u0626\u06D5\u06CC\u0644\u0648\u0648\u0644",
+ "\u062A\u0634\u0631\u06CC\u0646\u06CC%20\u06CC\u06D5\u06A9\u06D5\u0645",
+ "\u062A\u0634\u0631\u06CC\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645",
+ "\u06A9\u0627\u0646\u0648\u0646\u06CC%20\u06CC\u06D5\u06A9\u06D5\u0645",
+ }
+ // monthNamesCherokee list the month names in the Cherokee.
+ monthNamesCherokee = []string{
+ "\u13A4\u13C3\u13B8\u13D4\u13C5",
+ "\u13A7\u13A6\u13B5",
+ "\u13A0\u13C5\u13F1",
+ "\u13DD\u13EC\u13C2",
+ "\u13A0\u13C2\u13CD\u13AC\u13D8",
+ "\u13D5\u13AD\u13B7\u13F1",
+ "\u13AB\u13F0\u13C9\u13C2",
+ "\u13A6\u13B6\u13C2",
+ "\u13DA\u13B5\u13CD\u13D7",
+ "\u13DA\u13C2\u13C5\u13D7",
+ "\u13C5\u13D3\u13D5\u13C6",
+ "\u13A4\u13CD\u13A9\u13F1",
+ }
+ // monthNamesCherokeeAbbr lists the month name abbreviations in the Cherokee.
+ monthNamesCherokeeAbbr = []string{
+ "\u13A4\u13C3\u13B8",
+ "\u13A7\u13A6\u13B5",
+ "\u13A0\u13C5\u13F1",
+ "\u13DD\u13EC\u13C2",
+ "\u13A0\u13C2\u13CD",
+ "\u13D5\u13AD\u13B7",
+ "\u13AB\u13F0\u13C9",
+ "\u13A6\u13B6\u13C2",
+ "\u13DA\u13B5\u13CD",
+ "\u13DA\u13C2\u13C5",
+ "\u13C5\u13D3\u13D5",
+ "\u13A4\u13CD\u13A9",
+ }
+ // monthNamesChinese list the month names in the Chinese.
+ monthNamesChinese = []string{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"}
+ // monthNamesChineseAbbr lists the month name abbreviations in the Chinese.
+ monthNamesChineseAbbr = []string{"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"}
+ // monthNamesChineseNum list the month number and character abbreviation in the Chinese.
+ monthNamesChineseNum = []string{"0月", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月"}
+ // monthNamesEstonian list the month names in the Estonian.
+ monthNamesEstonian = []string{"jaanuar", "veebruar", "märts", "aprill", "mai", "juuni", "juuli", "august", "september", "oktoober", "november", "detsember"}
+ // monthNamesEstonianAbbr lists the month name abbreviations in the Estonian.
+ monthNamesEstonianAbbr = []string{"jaan", "veebr", "märts", "apr", "mai", "juuni", "juuli", "aug", "sept", "okt", "nov", "dets"}
+ // monthNamesFaroese list the month names in the Faroese.
+ monthNamesFaroese = []string{"januar", "februar", "mars", "apríl", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"}
+ // monthsNameFaroeseAbbr lists the month name abbreviations in the Faroese.
+ monthsNameFaroeseAbbr = []string{"jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "des"}
+ // monthNamesFilipino list the month names in the Filipino.
+ monthNamesFilipino = []string{"Enero", "Pebrero", "Marso", "Abril", "Mayo", "Hunyo", "Hulyo", "Agosto", "Setyembre", "Oktubre", "Nobyembre", "Disyembre"}
+ // monthNamesFilipinoAbbr lists the month name abbreviations in the Filipino.
+ monthNamesFilipinoAbbr = []string{"Ene", "Peb", "Mar", "Abr", "May", "Hun", "Hul", "Ago", "Set", "Okt", "Nob", "Dis"}
+ // monthsNamesFinnish list the month names in the Finnish.
+ monthNamesFinnish = []string{"Etammikuu", "helmikuu", "maaliskuu", "huhtikuu", "toukokuu", "kesäkuu", "heinäkuu", "elokuu", "syyskuu", "lokakuu", "marraskuu", "joulukuu"}
+ // monthsNamesFinnishAbbr lists the month name abbreviations in the Finnish.
+ monthNamesFinnishAbbr = []string{"tammi", "helmi", "maalis", "huhti", "touko", "kesä", "heinä", "elo", "syys", "loka", "marras", "joulu"}
+ // monthNamesFrench list the month names in the French.
+ monthNamesFrench = []string{"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"}
+ // monthNamesFrenchAbbr lists the month name abbreviations in the French.
+ monthNamesFrenchAbbr = []string{"janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."}
+ // monthNamesFrisian list the month names in the Frisian.
+ monthNamesFrisian = []string{"Jannewaris", "Febrewaris", "Maart", "April", "Maaie", "Juny", "July", "Augustus", "Septimber", "Oktober", "Novimber", "Desimber"}
+ // monthNamesFrisianAbbr lists the month name abbreviations in the Frisian.
+ monthNamesFrisianAbbr = []string{"jan", "feb", "Mrt", "Apr", "maa", "Jun", "Jul", "Aug", "sep", "Okt", "Nov", "Des"}
+ // monthNamesFulah list the month names in the Fulah.
+ monthNamesFulah = []string{"siilo", "colte", "mbooy", "seeɗto", "duujal", "korse", "morso", "juko", "siilto", "yarkomaa", "jolal", "bowte"}
+ // monthNamesFulahAbbr lists the month name abbreviations in the Fulah.
+ monthNamesFulahAbbr = []string{"sii", "col", "mbo", "see", "duu", "kor", "mor", "juk", "slt", "yar", "jol", "bow"}
+ // monthNamesGalician list the month names in the Galician.
+ monthNamesGalician = []string{"Xaneiro", "Febreiro", "Marzo", "Abril", "Maio", "Xuño", "Xullo", "Agosto", "Setembro", "Outubro", "Novembro", "Decembro"}
+ // monthNamesGalicianAbbr lists the month name abbreviations in the Galician.
+ monthNamesGalicianAbbr = []string{"Xan.", "Feb.", "Mar.", "Abr.", "Maio", "Xuño", "Xul.", "Ago.", "Set.", "Out.", "Nov.", "Dec."}
+ // monthNamesGeorgian list the month names in the Georgian.
+ monthNamesGeorgian = []string{
+ "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8",
+ "\u10D7\u10D4\u10D1\u10D4\u10E0\u10D5\u10D0\u10DA\u10D8",
+ "\u10DB\u10D0\u10E0\u10E2\u10D8",
+ "\u10D0\u10DE\u10E0\u10D8\u10DA\u10D8",
+ "\u10DB\u10D0\u10D8\u10E1\u10D8",
+ "\u10D8\u10D5\u10DC\u10D8\u10E1\u10D8",
+ "\u10D8\u10D5\u10DA\u10D8\u10E1\u10D8",
+ "\u10D0\u10D2\u10D5\u10D8\u10E1\u10E2\u10DD",
+ "\u10E1\u10D4\u10E5\u10E2\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8",
+ "\u10DD\u10E5\u10E2\u10DD\u10DB\u10D1\u10D4\u10E0\u10D8",
+ "\u10DC\u10DD\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8",
+ "\u10D3\u10D4\u10D9\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8",
+ }
+ // monthNamesGeorgianAbbr lists the month name abbreviations in the Georgian.
+ monthNamesGeorgianAbbr = []string{
+ "\u10D8\u10D0\u10DC",
+ "\u10D7\u10D4\u10D1",
+ "\u10DB\u10D0\u10E0",
+ "\u10D0\u10DE\u10E0",
+ "\u10DB\u10D0\u10D8",
+ "\u10D8\u10D5\u10DC",
+ "\u10D8\u10D5\u10DA",
+ "\u10D0\u10D2\u10D5",
+ "\u10E1\u10D4\u10E5",
+ "\u10DD\u10E5\u10E2",
+ "\u10DC\u10DD\u10D4",
+ "\u10D3\u10D4\u10D9",
+ }
+ // monthNamesGerman list the month names in the German.
+ monthNamesGerman = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}
+ // monthNamesGermanAbbr list the month abbreviations in the German.
+ monthNamesGermanAbbr = []string{"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"}
+ // monthNamesGreek list the month names in the Greek.
+ monthNamesGreek = []string{
+ "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2",
+ "\u03A6\u03B5\u03B2\u03C1\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2",
+ "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2",
+ "\u0391\u03C0\u03C1\u03AF\u03BB\u03B9\u03BF\u03C2",
+ "\u039C\u03AC\u03B9\u03BF\u03C2",
+ "\u0399\u03BF\u03CD\u03BD\u03B9\u03BF\u03C2",
+ "\u0399\u03BF\u03CD\u03BB\u03B9\u03BF\u03C2",
+ "\u0391\u03CD\u03B3\u03BF\u03C5\u03C3\u03C4\u03BF\u03C2",
+ "\u03A3\u03B5\u03C0\u03C4\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2",
+ "\u039F\u03BA\u03C4\u03CE\u03B2\u03C1\u03B9\u03BF\u03C2",
+ "\u039D\u03BF\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2",
+ "\u0394\u03B5\u03BA\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2",
+ }
+ // monthNamesGreekAbbr list the month abbreviations in the Greek.
+ monthNamesGreekAbbr = []string{
+ "\u0399\u03B1\u03BD",
+ "\u03A6\u03B5\u03B2",
+ "\u039C\u03B1\u03C1",
+ "\u0391\u03C0\u03C1",
+ "\u039C\u03B1\u03CA",
+ "\u0399\u03BF\u03C5\u03BD",
+ "\u0399\u03BF\u03C5\u03BB",
+ "\u0391\u03C5\u03B3",
+ "\u03A3\u03B5\u03C0",
+ "\u039F\u03BA\u03C4",
+ "\u039D\u03BF\u03B5",
+ "\u0394\u03B5\u03BA",
+ }
+ // monthNamesGreenlandic list the month names in the Greenlandic.
+ monthNamesGreenlandic = []string{"januaari", "februaari", "marsi", "apriili", "maaji", "juuni", "juuli", "aggusti", "septembari", "oktobari", "novembari", "decembari"}
+ // monthNamesGreenlandicAbbr list the month abbreviations in the Greenlandic.
+ monthNamesGreenlandicAbbr = []string{"jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}
+ // monthNamesGuarani list the month names in the Guarani.
+ monthNamesGuarani = []string{"jasyte\u0129", "jasyk%F5i", "jasyapy", "jasyrundy", "jasypo", "jasypote\u0129", "jasypok%F5i", "jasypoapy", "jasyporundy", "jasypa", "jasypate\u0129", "jasypak%F5i"}
+ // monthNamesGuaraniAbbr list the month abbreviations in the Guarani.
+ monthNamesGuaraniAbbr = []string{"jteĩ", "jkõi", "japy", "jrun", "jpo", "jpot", "jpok", "jpoa", "jpor", "jpa", "jpat", "jpak"}
+ // monthNamesGujarati list the month names in the Gujarati.
+ monthNamesGujarati = []string{
+ "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0",
+ "\u0AAB\u0AC7\u0AAC\u0ACD\u0AB0\u0AC1\u0A86\u0AB0\u0AC0",
+ "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A",
+ "\u0A8F\u0AAA\u0ACD\u0AB0\u0ABF\u0AB2",
+ "\u0AAE\u0AC7",
+ "\u0A9C\u0AC2\u0AA8",
+ "\u0A9C\u0AC1\u0AB2\u0ABE\u0A88",
+ "\u0A91\u0A97\u0AB8\u0ACD\u0A9F",
+ "\u0AB8\u0AAA\u0ACD\u0A9F\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0",
+ "\u0A91\u0A95\u0ACD\u0A9F\u0ACB\u0AAC\u0AB0",
+ "\u0AA8\u0AB5\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0",
+ "\u0AA1\u0ABF\u0AB8\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0",
+ }
+ // monthNamesGujaratiAbbr list the month abbreviations in the Gujarati.
+ monthNamesGujaratiAbbr = []string{
+ "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1",
+ "\u0AAB\u0AC7\u0AAC\u0ACD\u0AB0\u0AC1",
+ "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A",
+ "\u0A8F\u0AAA\u0ACD\u0AB0\u0ABF\u0AB2",
+ "\u0AAE\u0AC7",
+ "\u0A9C\u0AC2\u0AA8",
+ "\u0A9C\u0AC1\u0AB2\u0ABE\u0A88",
+ "\u0A91\u0A97",
+ "\u0AB8\u0AAA\u0ACD\u0A9F\u0AC7",
+ "\u0A91\u0A95\u0ACD\u0A9F\u0ACB",
+ "\u0AA8\u0AB5\u0AC7",
+ "\u0AA1\u0ABF\u0AB8\u0AC7",
+ }
+ // monthNamesHausa list the month names in the Hausa.
+ monthNamesHausa = []string{"Janairu", "Fabrairu", "Maris", "Afirilu", "Mayu", "Yuni", "Yuli", "Agusta", "Satumba", "Oktoba", "Nuwamba", "Disamba"}
+ // monthNamesHawaiian list the month names in the Hawaiian.
+ monthNamesHawaiian = []string{"Ianuali", "Pepeluali", "Malaki", "ʻApelila", "Mei", "Iune", "Iulai", "ʻAukake", "Kepakemapa", "ʻOkakopa", "Nowemapa", "Kekemapa"}
+ // monthNamesHawaiianAbbr list the month name abbreviations in the Hawaiiann.
+ monthNamesHawaiianAbbr = []string{"Ian.", "Pep.", "Mal.", "ʻAp.", "Mei", "Iun.", "Iul.", "ʻAu.", "Kep.", "ʻOk.", "Now.", "Kek."}
+ // monthNamesHebrew list the month names in the Hebrew.
+ monthNamesHebrew = []string{
+ "\u05D9\u05E0\u05D5\u05D0\u05E8",
+ "\u05E4\u05D1\u05E8\u05D5\u05D0\u05E8",
+ "\u05DE\u05E8\u05E5",
+ "\u05D0\u05E4\u05E8\u05D9\u05DC",
+ "\u05DE\u05D0\u05D9",
+ "\u05D9\u05D5\u05E0\u05D9",
+ "\u05D9\u05D5\u05DC\u05D9",
+ "\u05D0\u05D5\u05D2\u05D5\u05E1\u05D8",
+ "\u05E1\u05E4\u05D8\u05DE\u05D1\u05E8",
+ "\u05D0\u05D5\u05E7\u05D8\u05D5\u05D1\u05E8",
+ "\u05E0\u05D5\u05D1\u05DE\u05D1\u05E8",
+ "\u05D3\u05E6\u05DE\u05D1\u05E8",
+ }
+ // monthNamesHindi list the month names in the Hindi.
+ monthNamesHindi = []string{
+ "\u091C\u0928\u0935\u0930\u0940",
+ "\u092B\u0930\u0935\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u0905\u092A\u094D\u0930\u0948\u0932",
+ "\u092E\u0908",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u093E\u0908",
+ "\u0905\u0917\u0938\u094D\u0924",
+ "\u0938\u093F\u0924\u092E\u094D\u092C\u0930",
+ "\u0905\u0915\u094D\u0924\u0942\u092C\u0930",
+ "\u0928\u0935\u092E\u094D\u092C\u0930",
+ "\u0926\u093F\u0938\u092E\u094D\u092C\u0930",
+ }
+ // monthNamesHungarian list the month names in the Hungarian.
+ monthNamesHungarian = []string{"január", "február", "március", "április", "május", "június", "július", "augusztus", "szeptember", "október", "november", "december"}
+ // monthNamesHungarianAbbr list the month name abbreviations in the Hungarian.
+ monthNamesHungarianAbbr = []string{"jan.", "febr.", "márc.", "ápr.", "máj.", "jún.", "júl.", "aug.", "szept.", "okt.", "nov.", "dec."}
+ // monthNamesIcelandic list the month names in the Icelandic.
+ monthNamesIcelandic = []string{"janúar", "febrúar", "mars", "apríl", "maí", "júní", "júlí", "ágúst", "september", "október", "nóvember", "desember"}
+ // monthNamesIcelandicAbbr list the month name abbreviations in the Icelandic.
+ monthNamesIcelandicAbbr = []string{"jan.", "feb.", "mar.", "apr.", "maí", "jún.", "júl.", "ágú.", "sep.", "okt.", "nóv.", "des."}
+ // monthNamesIgbo list the month names in the Igbo.
+ monthNamesIgbo = []string{"Jenụwarị", "Febụwarị", "Machị", "Eprelu", "Mey", "Juun", "Julaị", "Ọgọst", "Septemba", "Ọcktọba", "Nọvemba", "Disemba"}
+ // monthNamesIgboAbbr list the month name abbreviations in the Igbo.
+ monthNamesIgboAbbr = []string{"Jen", "Feb", "Mac", "Epr", "Mey", "Jun", "Jul", "Ọgọ", "Sep", "Ọkt", "Nọv", "Dis"}
+ // monthNamesIndonesian list the month names in the Indonesian.
+ monthNamesIndonesian = []string{"Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"}
+ // monthNamesIndonesianAbbr list the month name abbreviations in the Indonesian.
+ monthNamesIndonesianAbbr = []string{"Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"}
+ // monthNamesInuktitut list the month names in the Inuktitut.
+ monthNamesInuktitut = []string{"Jaannuari", "Viivvuari", "Maatsi", "Iipuri", "Mai", "Juuni", "Julai", "Aaggiisi", "Sitipiri", "Utupiri", "Nuvipiri", "Tisipiri"}
+ // monthNamesInuktitutAbbr list the month name abbreviations in the Inuktitut.
+ monthNamesInuktitutAbbr = []string{"Jan", "Viv", "Mas", "Ipu", "Mai", "Jun", "Jul", "Agi", "Sii", "Uut", "Nuv", "Tis"}
+ // monthNamesIrish list the month names in the Irish.
+ monthNamesIrish = []string{"Eanáir", "Feabhra", "Márta", "Aibreán", "Bealtaine", "Meitheamh", "Iúil", "Lúnasa", "Meán Fómhair", "Deireadh Fómhair", "Samhain", "Nollaig"}
+ // monthNamesIrishAbbr lists the month abbreviations in the Irish.
+ monthNamesIrishAbbr = []string{"Ean", "Feabh", "Márta", "Aib", "Beal", "Meith", "Iúil", "Lún", "MFómh", "DFómh", "Samh", "Noll"}
+ // monthNamesItalian list the month names in the Italian.
+ monthNamesItalian = []string{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"}
+ // monthNamesItalianAbbr list the month name abbreviations in the Italian.
+ monthNamesItalianAbbr = []string{"gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"}
+ // monthNamesKannada list the month names in the Kannada.
+ monthNamesKannada = []string{
+ "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF",
+ "\u0CAB\u0CC6\u0CAC\u0CCD\u0CB0\u0CB5\u0CB0\u0CBF",
+ "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD",
+ "\u0C8F\u0C8F\u0CAA\u0CCD\u0CB0\u0CBF\u0CB2\u0CCD",
+ "\u0CAE\u0CC7",
+ "\u0C9C\u0CC2\u0CA8\u0CCD",
+ "\u0C9C\u0CC1\u0CB2\u0CC8",
+ "\u0C86\u0C97\u0CB8\u0CCD\u0C9F\u0CCD",
+ "\u0CB8\u0CC6\u0CAA\u0CCD\u0C9F\u0C82\u0CAC\u0CB0\u0CCD",
+ "\u0C85\u0C95\u0CCD\u0C9F\u0CCB\u0CAC\u0CB0\u0CCD",
+ "\u0CA8\u0CB5\u0CC6\u0C82\u0CAC\u0CB0\u0CCD",
+ "\u0CA1\u0CBF\u0CB8\u0CC6\u0C82\u0CAC\u0CB0\u0CCD",
+ }
+ // monthNamesKannadaAbbr lists the month abbreviations in the Kannada.
+ monthNamesKannadaAbbr = []string{
+ "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF",
+ "\u0CAB\u0CC6\u0CAC\u0CCD\u0CB0\u0CB5\u0CB0\u0CBF",
+ "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD",
+ "\u0C8E\u0CAA\u0CCD\u0CB0\u0CBF\u0CB2\u0CCD",
+ "\u0CAE\u0CC7",
+ "\u0C9C\u0CC2\u0CA8\u0CCD",
+ "\u0C9C\u0CC1\u0CB2\u0CC8",
+ "\u0C86\u0C97\u0CB8\u0CCD\u0C9F\u0CCD",
+ "\u0CB8\u0CC6\u0CAA\u0CCD\u0C9F\u0C82\u0CAC\u0CB0\u0CCD",
+ "\u0C85\u0C95\u0CCD\u0C9F\u0CCB\u0CAC\u0CB0\u0CCD",
+ "\u0CA8\u0CB5\u0CC6\u0C82\u0CAC\u0CB0\u0CCD",
+ "\u0CA1\u0CBF\u0CB8\u0CC6\u0C82\u0CAC\u0CB0\u0CCD",
+ }
+ // monthNamesKashmiri list the month names in the Kashmiri.
+ monthNamesKashmiri = []string{
+ "\u062C\u0646\u0624\u0631\u06CC",
+ "\u0641\u0631\u0624\u0631\u06CC",
+ "\u0645\u0627\u0631\u0655\u0686",
+ "\u0627\u067E\u0631\u06CC\u0644",
+ "\u0645\u06CC\u0654",
+ "\u062C\u0648\u0657\u0646",
+ "\u062C\u0648\u0657\u0644\u0627\u06CC\u06CC",
+ "\u0627\u06AF\u0633\u062A",
+ "\u0633\u062A\u0645\u0628\u0631",
+ "\u0627\u06A9\u062A\u0648\u0657\u0628\u0631",
+ "\u0646\u0648\u0645\u0628\u0631",
+ "\u062F\u0633\u0645\u0628\u0631",
+ }
+ // monthNamesKazakh list the month names in the Kazakh.
+ monthNamesKazakh = []string{
+ "\u049A\u0430\u04A3\u0442\u0430\u0440",
+ "\u0410\u049B\u043F\u0430\u043D",
+ "\u041D\u0430\u0443\u0440\u044B\u0437",
+ "\u0421\u04D9\u0443\u0456\u0440",
+ "\u041C\u0430\u043C\u044B\u0440",
+ "\u041C\u0430\u0443\u0441\u044B\u043C",
+ "\u0428\u0456\u043B\u0434\u0435",
+ "\u0422\u0430\u043C\u044B\u0437",
+ "\u049A\u044B\u0440\u043A\u04AF\u0439\u0435\u043A",
+ "\u049A\u0430\u0437\u0430\u043D",
+ "\u049A\u0430\u0440\u0430\u0448\u0430",
+ "\u0416\u0435\u043B\u0442\u043E\u049B\u0441\u0430\u043D",
+ }
+ // monthNamesKazakhAbbr list the month name abbreviations in the Kazakh.
+ monthNamesKazakhAbbr = []string{
+ "\u049B\u0430\u04A3",
+ "\u0430\u049B\u043F",
+ "\u043D\u0430\u0443",
+ "\u0441\u04D9\u0443",
+ "\u043C\u0430\u043C",
+ "\u043C\u0430\u0443",
+ "\u0448\u0456\u043B",
+ "\u0442\u0430\u043C",
+ "\u049B\u044B\u0440",
+ "\u049B\u0430\u0437",
+ "\u049B\u0430\u0440",
+ "\u0436\u0435\u043B",
+ }
+ // monthNamesKhmer list the month names in the Khmer.
+ monthNamesKhmer = []string{
+ "\u1798\u1780\u179A\u17B6",
+ "\u1780\u17BB\u1798\u17D2\u1797\u17C8",
+ "\u1798\u17B7\u1793\u17B6",
+ "\u1798\u17C1\u179F\u17B6",
+ "\u17A7\u179F\u1797\u17B6",
+ "\u1798\u17B7\u1790\u17BB\u1793\u17B6",
+ "\u1780\u1780\u17D2\u1780\u178A\u17B6",
+ "\u179F\u17B8\u17A0\u17B6",
+ "\u1780\u1789\u17D2\u1789\u17B6",
+ "\u178F\u17BB\u179B\u17B6",
+ "\u179C\u17B7\u1785\u17D2\u1786\u17B7\u1780\u17B6",
+ "\u1792\u17D2\u1793\u17BC",
+ }
+ // monthNamesKhmerAbbr list the month name abbreviations in the Khmer.
+ monthNamesKhmerAbbr = []string{
+ "\u17E1", "\u17E2", "\u17E3", "\u17E4", "\u17E5", "\u17E6", "\u17E7", "\u17E8", "\u17E9", "\u17E1\u17E0", "\u17E1\u17E1", "\u17E1\u17E2",
+ "\u1798", "\u1780", "\u1798", "\u1798", "\u17A7", "\u1798", "\u1780", "\u179F", "\u1780", "\u178F", "\u179C", "\u1792",
+ }
+ // monthNamesKiche list the month names in the Kiche.
+ monthNamesKiche = []string{"nab'e ik'", "ukab' ik'", "urox ik'", "ukaj ik'", "uro ik'", "uwaq ik'", "uwuq ik'", "uwajxaq ik'", "ub'elej ik'", "ulaj ik'", "ujulaj ik'", "ukab'laj ik'"}
+ // monthNamesKicheAbbr list the month name abbreviations in the Kiche.
+ monthNamesKicheAbbr = []string{"nab'e", "ukab'", "urox", "ukaj", "uro", "uwaq", "uwuq", "uwajxaq", "ub'elej", "ulaj", "ujulaj", "ukab'laj"}
+ // monthNamesKinyarwanda list the month names in the Kinyarwanda.
+ monthNamesKinyarwanda = []string{"Mutarama", "Gashyantare", "Werurwe", "Mata", "Gicuransi", "Kamena", "Nyakanga", "Kanama", "Nzeli", "Ukwakira", "Ugushyingo", "Ukuboza"}
+ // monthNamesKinyarwandaAbbr list the month name abbreviations in the Kinyarwanda.
+ monthNamesKinyarwandaAbbr = []string{"mut.", "gas.", "wer.", "mat.", "gic.", "kam.", "Nyak", "kan.", "nze.", "Ukwak", "Ugus", "Ukub"}
+ // monthNamesKiswahili list the month names in the Kiswahili.
+ monthNamesKiswahili = []string{"Januari", "Februari", "Machi", "Aprili", "Mei", "Juni", "Julai", "Agosti", "Septemba", "Oktoba", "Novemba", "Desemba"}
+ // monthNamesKiswahiliAbbr list the month name abbreviations in the Kiswahili.
+ monthNamesKiswahiliAbbr = []string{"Jan", "Feb", "Mac", "Apr", "Mei", "Jun", "Jul", "Ago", "Sep", "Okt", "Nov", "Des"}
+ // monthNamesKonkani list the month names in the Konkani.
+ monthNamesKonkani = []string{
+ "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940",
+ "\u092B\u0947\u092C\u094D\u0930\u0941\u0935\u093E\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u090F\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u0948",
+ "\u0911\u0917\u0938\u094D\u091F",
+ "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930",
+ "\u0911\u0915\u094D\u091F\u094B\u092C\u0930",
+ "\u0928\u094B\u0935\u0947\u092E\u094D\u092C\u0930",
+ "\u0921\u093F\u0938\u0947\u0902\u092C\u0930",
+ }
+ // monthNamesKonkaniAbbr list the month name abbreviations in the Konkani.
+ monthNamesKonkaniAbbr = []string{
+ "\u091C\u093E\u0928\u0947",
+ "\u092B\u0947\u092C\u094D\u0930\u0941",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u090F\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u0948",
+ "\u0911\u0917.",
+ "\u0938\u092A\u094D\u091F\u0947\u0902.",
+ "\u0911\u0915\u094D\u091F\u094B.",
+ "\u0928\u094B\u0935\u0947.",
+ "\u0921\u093F\u0938\u0947\u0902",
+ }
+ // monthNamesKoreanAbbr lists out the month number plus 월 for the Korean language.
+ monthNamesKoreanAbbr = []string{"1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"}
+ // monthNamesKyrgyz list the month names in the Kyrgyz.
+ monthNamesKyrgyz = []string{
+ "\u042F\u043D\u0432\u0430\u0440\u044C",
+ "\u0424\u0435\u0432\u0440\u0430\u043B\u044C",
+ "\u041C\u0430\u0440\u0442",
+ "\u0410\u043F\u0440\u0435\u043B\u044C",
+ "\u041C\u0430\u0439",
+ "\u0418\u044E\u043D\u044C",
+ "\u0418\u044E\u043B\u044C",
+ "\u0410\u0432\u0433\u0443\u0441\u0442",
+ "\u0421\u0435\u043D\u0442\u044F\u0431\u0440\u044C",
+ "\u041E\u043A\u0442\u044F\u0431\u0440\u044C",
+ "\u041D\u043E\u044F\u0431\u0440\u044C",
+ "\u0414\u0435\u043A\u0430\u0431\u0440\u044C",
+ }
+ // monthNamesKyrgyzAbbr lists the month name abbreviations in the Kyrgyz.
+ monthNamesKyrgyzAbbr = []string{
+ "\u042F\u043D\u0432",
+ "\u0424\u0435\u0432",
+ "\u041C\u0430\u0440",
+ "\u0410\u043F\u0440",
+ "\u041C\u0430\u0439",
+ "\u0418\u044E\u043D",
+ "\u0418\u044E\u043B",
+ "\u0410\u0432\u0433",
+ "\u0421\u0435\u043D",
+ "\u041E\u043A\u0442",
+ "\u041D\u043E\u044F",
+ "\u0414\u0435\u043A",
+ }
+ // monthNamesLao list the month names in the Lao.
+ monthNamesLao = []string{
+ "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99",
+ "\u0E81\u0EB8\u0EA1\u0E9E\u0EB2",
+ "\u0EA1\u0EB5\u0E99\u0EB2",
+ "\u0EC0\u0EA1\u0EAA\u0EB2",
+ "\u0E9E\u0EB6\u0E94\u0EAA\u0EB0\u0E9E\u0EB2",
+ "\u0EA1\u0EB4\u0E96\u0EB8\u0E99\u0EB2",
+ "\u0E81\u0ECD\u0EA5\u0EB0\u0E81\u0EBB\u0E94",
+ "\u0EAA\u0EB4\u0E87\u0EAB\u0EB2",
+ "\u0E81\u0EB1\u0E99\u0E8D\u0EB2",
+ "\u0E95\u0EB8\u0EA5\u0EB2",
+ "\u0E9E\u0EB0\u0E88\u0EB4\u0E81",
+ "\u0E97\u0EB1\u0E99\u0EA7\u0EB2",
+ }
+ // monthNamesLaoAbbr lists the month name abbreviations in the Lao.
+ monthNamesLaoAbbr = []string{
+ "\u0EA1.\u0E81.",
+ "\u0E81.\u0E9E.",
+ "\u0EA1.\u0E99.",
+ "\u0EA1.\u0EAA.",
+ "\u0E9E.\u0E9E.",
+ "\u0EA1\u0EB4.\u0E96.",
+ "\u0E81.\u0EA5.",
+ "\u0EAA.\u0EAB.",
+ "\u0E81.\u0E8D.",
+ "\u0E95.\u0EA5.",
+ "\u0E9E.\u0E88.",
+ "\u0E97.\u0EA7.",
+ }
+ // monthNamesLatin list the month names in the Latin.
+ monthNamesLatin = []string{"Ianuarius", "Februarius", "Martius", "Aprilis", "Maius", "Iunius", "Quintilis", "Sextilis", "September", "October", "November", "December"}
+ // monthNamesLatinAbbr list the month name abbreviations in the Latin.
+ monthNamesLatinAbbr = []string{"Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Quint", "Sext", "Sept", "Oct", "Nov", "Dec"}
+ // monthNamesLatvian list the month names in the Latvian.
+ monthNamesLatvian = []string{"janvāris", "februāris", "marts", "aprīlis", "maijs", "jūnijs", "jūlijs", "augusts", "septembris", "oktobris", "novembris", "decembris"}
+ // monthNamesLatvianAbbr list the month name abbreviations in the Latvian.
+ monthNamesLatvianAbbr = []string{"janv.", "febr.", "marts", "apr.", "maijs", "jūn.", "jūl.", "aug.", "sept.", "okt.", "nov.", "dec."}
+ // monthNamesLithuanian list the month names in the Lithuanian.
+ monthNamesLithuanian = []string{"sausis", "vasaris", "kovas", "balandis", "gegužė", "birželis", "liepa", "rugpjūtis", "rugsėjis", "spalis", "lapkritis", "gruodis"}
+ // monthNamesLithuanianAbbr list the month name abbreviations in the Lithuanian.
+ monthNamesLithuanianAbbr = []string{"saus.", "vas.", "kov.", "bal.", "geg.", "birž.", "liep.", "rugp.", "rugs.", "spal.", "lapkr.", "gruod."}
+ // monthNamesLowerSorbian list the month names in the Lower Sorbian.
+ monthNamesLowerSorbian = []string{"januar", "februar", "měrc", "apryl", "maj", "junij", "julij", "awgust", "september", "oktober", "nowember", "december"}
+ // monthNamesLowerSorbianAbbr list the month name abbreviations in the LowerSorbian.
+ monthNamesLowerSorbianAbbr = []string{"jan", "feb", "měr", "apr", "maj", "jun", "jul", "awg", "sep", "okt", "now", "dec"}
+ // monthNamesLuxembourgish list the month names in the Lower Sorbian.
+ monthNamesLuxembourgish = []string{"Januar", "Februar", "Mäerz", "Abrëll", "Mee", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}
+ // monthNamesLuxembourgishAbbr list the month name abbreviations in the Luxembourgish.
+ monthNamesLuxembourgishAbbr = []string{"Jan", "Feb", "Mäe", "Abr", "Mee", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"}
+ // monthNamesMacedonian list the month names in the Lower Sorbian.
+ monthNamesMacedonian = []string{
+ "\u0458\u0430\u043D\u0443\u0430\u0440\u0438",
+ "\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0438\u043B",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D\u0438",
+ "\u0458\u0443\u043B\u0438",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043F\u0442\u0435\u043C\u0432\u0440\u0438",
+ "\u043E\u043A\u0442\u043E\u043C\u0432\u0440\u0438",
+ "\u043D\u043E\u0435\u043C\u0432\u0440\u0438",
+ "\u0434\u0435\u043A\u0435\u043C\u0432\u0440\u0438",
+ }
+ // monthNamesMacedonianAbbr list the month name abbreviations in the Macedonian.
+ monthNamesMacedonianAbbr = []string{
+ "\u0458\u0430\u043D.",
+ "\u0444\u0435\u0432.",
+ "\u043C\u0430\u0440.",
+ "\u0430\u043F\u0440.",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D.",
+ "\u0458\u0443\u043B.",
+ "\u0430\u0432\u0433.",
+ "\u0441\u0435\u043F\u0442.",
+ "\u043E\u043A\u0442.",
+ "\u043D\u043E\u0435\u043C.",
+ "\u0434\u0435\u043A.",
+ }
+ // monthNamesMalay list the month names in the Malay.
+ monthNamesMalay = []string{"Januari", "Februari", "Mac", "April", "Mei", "Jun", "Julai", "Ogos", "September", "Oktober", "November", "Disember"}
+ // monthNamesMalayAbbr list the month name abbreviations in the Malay.
+ monthNamesMalayAbbr = []string{"Jan", "Feb", "Mac", "Apr", "Mei", "Jun", "Jul", "Ogo", "Sep", "Okt", "Nov", "Dis"}
+ // monthNamesMalayalam list the month names in the Malayalam.
+ monthNamesMalayalam = []string{
+ "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F",
+ "\u0D2B\u0D46\u0D2C\u0D4D\u0D30\u0D41\u0D35\u0D30\u0D3F",
+ "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D",
+ "\u0D0F\u0D2A\u0D4D\u0D30\u0D3F\u0D32\u0D4D\u200D",
+ "\u0D2E\u0D47\u0D2F\u0D4D",
+ "\u0D1C\u0D42\u0D7A",
+ "\u0D1C\u0D42\u0D32\u0D48",
+ "\u0D06\u0D17\u0D38\u0D4D\u0D31\u0D4D\u0D31\u0D4D",
+ "\u0D38\u0D46\u0D2A\u0D4D\u200C\u0D31\u0D4D\u0D31\u0D02\u0D2C\u0D30\u0D4D\u200D",
+ "\u0D12\u0D15\u0D4D\u200C\u0D1F\u0D4B\u0D2C\u0D30\u0D4D\u200D",
+ "\u0D28\u0D35\u0D02\u0D2C\u0D30\u0D4D\u200D",
+ "\u0D21\u0D3F\u0D38\u0D02\u0D2C\u0D30\u0D4D\u200D",
+ }
+ // monthNamesMalayalamAbbr list the month name abbreviations in the Malayalam.
+ monthNamesMalayalamAbbr = []string{
+ "\u0D1C\u0D28\u0D41",
+ "\u0D2B\u0D46\u0D2C\u0D4D\u0D30\u0D41",
+ "\u0D2E\u0D3E\u0D7C",
+ "\u0D0F\u0D2A\u0D4D\u0D30\u0D3F",
+ "\u0D2E\u0D47\u0D2F\u0D4D",
+ "\u0D1C\u0D42\u0D7A",
+ "\u0D1C\u0D42\u0D32\u0D48",
+ "\u0D13\u0D17",
+ "\u0D38\u0D46\u0D2A\u0D4D\u0D31\u0D4D\u0D31\u0D02",
+ "\u0D12\u0D15\u0D4D\u0D1F\u0D4B",
+ "\u0D28\u0D35\u0D02",
+ "\u0D21\u0D3F\u0D38\u0D02",
+ }
+ // monthNamesMaltese list the month names in the Maltese.
+ monthNamesMaltese = []string{"Jannar", "Frar", "Marzu", "April", "Mejju", "Ġunju", "Lulju", "Awwissu", "Settembru", "Ottubru", "Novembru", "Diċembru"}
+ // monthNamesMalteseAbbr list the month name abbreviations in the Maltese.
+ monthNamesMalteseAbbr = []string{"Jan", "Fra", "Mar", "Apr", "Mej", "Ġun", "Lul", "Aww", "Set", "Ott", "Nov", "Diċ"}
+ // monthNamesMaori list the month names in the Maori.
+ monthNamesMaori = []string{"Kohitātea", "Huitanguru", "Poutūterangi", "Paengawhāwhā", "Haratua", "Pipiri", "Hōngongoi", "Hereturikōkā", "Mahuru", "Whiringa ā-nuku", "Whiringa ā-rangi", "Hakihea"}
+ // monthNamesMaoriAbbr list the month name abbreviations in the Maori.
+ monthNamesMaoriAbbr = []string{"Kohi", "Hui", "Pou", "Pae", "Hara", "Pipi", "Hōngo", "Here", "Mahu", "Nuku", "Rangi", "Haki"}
+ // monthNamesMapudungun list the month name abbreviations in the Mapudungun.
+ monthNamesMapudungun = []string{"Kiñe Tripantu", "Epu", "Kila", "Meli", "Kechu", "Cayu", "Regle", "Purha", "Aiya", "Marhi", "Marhi Kiñe", "Marhi Epu"}
+ // monthNamesMarathi list the month names in the Marathi.
+ monthNamesMarathi = []string{
+ "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940",
+ "\u092B\u0947\u092C\u094D\u0930\u0941\u0935\u093E\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u090F\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u0948",
+ "\u0911\u0917\u0938\u094D\u091F",
+ "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930",
+ "\u0911\u0915\u094D\u091F\u094B\u092C\u0930",
+ "\u0928\u094B\u0935\u094D\u0939\u0947\u0902\u092C\u0930",
+ "\u0921\u093F\u0938\u0947\u0902\u092C\u0930",
+ }
+ // monthNamesMarathiAbbr lists the month name abbreviations in Marathi.
+ monthNamesMarathiAbbr = []string{
+ "\u091C\u093E\u0928\u0947.",
+ "\u092B\u0947\u092C\u094D\u0930\u0941.",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u090F\u092A\u094D\u0930\u093F",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u0948",
+ "\u0911\u0917.",
+ "\u0938\u092A\u094D\u091F\u0947\u0902.",
+ "\u0911\u0915\u094D\u091F\u094B.",
+ "\u0928\u094B\u0935\u094D\u0939\u0947\u0902.",
+ "\u0921\u093F\u0938\u0947\u0902.",
+ }
+ // monthNamesMohawk list the month names in the Mohawk.
+ monthNamesMohawk = []string{"Tsothohrkó:Wa", "Enniska", "Enniskó:Wa", "Onerahtókha", "Onerahtohkó:Wa", "Ohiari:Ha", "Ohiarihkó:Wa", "Seskéha", "Seskehkó:Wa", "Kenténha", "Kentenhkó:Wa", "Tsothóhrha"}
+ // monthNamesMongolian list the month names in the Mongolian.
+ monthNamesMongolian = []string{
+ "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440",
+ "\u0425\u043E\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0414\u04E9\u0440\u04E9\u0432\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440",
+ "\u0422\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0417\u0443\u0440\u0433\u0430\u0430\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0414\u043E\u043B\u043E\u043E\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u041D\u0430\u0439\u043C\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0415\u0441\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440",
+ "\u0410\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ "\u0410\u0440\u0432\u0430\u043D \u043D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440",
+ "\u0410\u0440\u0432\u0430\u043D \u0445\u043E\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440",
+ }
+ // monthNamesMongolianAbbr lists the month name abbreviations in Mongolian.
+ monthNamesMongolianAbbr = []string{"1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар"}
+ // monthNamesMoroccoAbbr lists the month name abbreviations in the Morocco.
+ monthNamesMoroccoAbbr = []string{"jan.", "fév.", "mar.", "avr.", "mai", "jui.", "juil.", "août", "sept.", "oct.", "nov.", "déc."}
+ // monthNamesNepali list the month names in the Nepali.
+ monthNamesNepali = []string{
+ "\u091C\u0928\u0935\u0930\u0940",
+ "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u0905\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u093E\u0908",
+ "\u0905\u0917\u0938\u094D\u0924",
+ "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930",
+ "\u0905\u0915\u094D\u091F\u094B\u092C\u0930",
+ "\u0928\u094B\u092D\u0947\u092E\u094D\u092C\u0930",
+ "\u0921\u093F\u0938\u0947\u092E\u094D\u092C\u0930",
+ }
+ // monthNamesNepaliAbbr lists the month name abbreviations in the Nepali.
+ monthNamesNepaliAbbr = []string{
+ "\u091C\u0928",
+ "\u092B\u0947\u092C",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u0905\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u093E\u0908",
+ "\u0905\u0917",
+ "\u0938\u0947\u092A\u094D\u091F",
+ "\u0905\u0915\u094D\u091F",
+ "\u0928\u094B\u092D",
+ "\u0921\u093F\u0938",
+ }
+ // monthNamesNepaliIN list the month names in the India Nepali.
+ monthNamesNepaliIN = []string{
+ "\u091C\u0928\u0935\u0930\u0940",
+ "\u092B\u0930\u0935\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u0905\u092A\u094D\u0930\u0947\u0932",
+ "\u092E\u0908",
+ "\u091C\u0941\u0928",
+ "\u091C\u0941\u0932\u093E\u0908",
+ "\u0905\u0917\u0938\u094D\u091F",
+ "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930",
+ "\u0905\u0915\u094D\u091F\u094B\u092C\u0930",
+ "\u0928\u094B\u092D\u0947\u092E\u094D\u092C\u0930",
+ "\u0926\u093F\u0938\u092E\u094D\u092C\u0930",
+ }
+ // monthNamesNepaliINAbbr lists the month name abbreviations in the India Nepali.
+ monthNamesNepaliINAbbr = []string{
+ "\u091C\u0928\u0935\u0930\u0940",
+ "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u0905\u092A\u094D\u0930\u093F",
+ "\u092E\u0947",
+ "\u091C\u0941\u0928",
+ "\u091C\u0941\u0932\u093E",
+ "\u0905\u0917\u0938\u094D\u091F",
+ "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930",
+ "\u0905\u0915\u094D\u091F\u094B",
+ "\u0928\u094B\u092D\u0947",
+ "\u0921\u093F\u0938\u0947",
+ }
+ // monthNamesNigeria list the month names in the Nigeria.
+ monthNamesNigeria = []string{"samwiee", "feeburyee", "marsa", "awril", "me", "suyeŋ", "sulyee", "ut", "satambara", "oktoobar", "nowamburu", "deesamburu"}
+ // monthNamesNigeriaAbbr lists the month name abbreviations in the Nigeria.
+ monthNamesNigeriaAbbr = []string{"samw", "feeb", "mar", "awr", "me", "suy", "sul", "ut", "sat", "okt", "now", "dees"}
+ // monthNamesNorwegian list the month names in the Norwegian.
+ monthNamesNorwegian = []string{"januar", "februar", "mars", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"}
+ // monthNamesOccitan list the month names in the Occitan.
+ monthNamesOccitan = []string{"genièr", "febrièr", "març", "abril", "mai", "junh", "julhet", "agost", "setembre", "octobre", "novembre", "decembre"}
+ // monthNamesOccitanAbbr lists the month name abbreviations in the Occitan.
+ monthNamesOccitanAbbr = []string{"gen.", "feb.", "març", "abr.", "mai", "junh", "julh", "ag.", "set.", "oct.", "nov.", "dec."}
+ // monthNamesOdia list the month names in the Odia.
+ monthNamesOdia = []string{
+ "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40",
+ "\u0B2B\u0B47\u0B2C\u0B43\u0B06\u0B30\u0B40",
+ "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A",
+ "\u0B0F\u0B2A\u0B4D\u0B30\u0B3F\u0B32\u0B4D\u200C",
+ "\u0B2E\u0B47",
+ "\u0B1C\u0B41\u0B28\u0B4D\u200C",
+ "\u0B1C\u0B41\u0B32\u0B3E\u0B07",
+ "\u0B05\u0B17\u0B37\u0B4D\u0B1F",
+ "\u0B38\u0B47\u0B2A\u0B4D\u0B1F\u0B47\u0B2E\u0B4D\u0B2C\u0B30",
+ "\u0B05\u0B15\u0B4D\u0B1F\u0B4B\u0B2C\u0B30",
+ "\u0B28\u0B2D\u0B47\u0B2E\u0B4D\u0B2C\u0B30",
+ "\u0B21\u0B3F\u0B38\u0B47\u0B2E\u0B4D\u0B2C\u0B30",
+ }
+ // monthNamesOromo list the month names in the Oromo.
+ monthNamesOromo = []string{"Amajjii", "Guraandhala", "Bitooteessa", "Elba", "Caamsa", "Waxabajjii", "Adooleessa", "Hagayya", "Fuulbana", "Onkololeessa", "Sadaasa", "Muddee"}
+ // monthNamesOromoAbbr list the month abbreviations in the Oromo.
+ monthNamesOromoAbbr = []string{"Ama", "Gur", "Bit", "Elb", "Cam", "Wax", "Ado", "Hag", "Ful", "Onk", "Sad", "Mud"}
+ // monthNamesPashto list the month names in the Pashto.
+ monthNamesPashto = []string{
+ "\u0633\u0644\u0648\u0627\u063A\u0647",
+ "\u0643\u0628",
+ "\u0648\u0631\u0649",
+ "\u063A\u0648\u064A\u0649",
+ "\u063A\u0628\u0631\u06AB\u0648\u0644\u0649",
+ "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649",
+ "\u0632\u0645\u0631\u0649",
+ "\u0648\u0696\u0649",
+ "\u062A\u0644\u0647",
+ "\u0644\u0693\u0645",
+ "\u0644\u0646\u0688 \u06CD",
+ "\u0645\u0631\u063A\u0648\u0645\u0649",
+ }
+ // monthNamesPersian list the month names in the Persian.
+ monthNamesPersian = []string{
+ "\u0698\u0627\u0646\u0648\u064A\u0647",
+ "\u0641\u0648\u0631\u064A\u0647",
+ "\u0645\u0627\u0631\u0633",
+ "\u0622\u0648\u0631\u064A\u0644",
+ "\u0645\u0647",
+ "\u0698\u0648\u0626\u0646",
+ "\u0698\u0648\u0626\u064A\u0647",
+ "\u0627\u0648\u062A",
+ "\u0633\u067E\u062A\u0627\u0645\u0628\u0631",
+ "\u0627\u064F\u0643\u062A\u0628\u0631",
+ "\u0646\u0648\u0627\u0645\u0628\u0631",
+ "\u062F\u0633\u0627\u0645\u0628\u0631",
+ }
+ // monthNamesPolish list the month names in the Polish.
+ monthNamesPolish = []string{"styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"}
+ // monthNamesPortuguese list the month names in the Portuguese.
+ monthNamesPortuguese = []string{"janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"}
+ // monthNamesPunjabi list the month names in the Punjabi.
+ monthNamesPunjabi = []string{
+ "\u0A1C\u0A28\u0A35\u0A30\u0A40",
+ "\u0A2B\u0A3C\u0A30\u0A35\u0A30\u0A40",
+ "\u0A2E\u0A3E\u0A30\u0A1A",
+ "\u0A05\u0A2A\u0A4D\u0A30\u0A48\u0A32",
+ "\u0A2E\u0A08",
+ "\u0A1C\u0A42\u0A28",
+ "\u0A1C\u0A41\u0A32\u0A3E\u0A08",
+ "\u0A05\u0A17\u0A38\u0A24",
+ "\u0A38\u0A24\u0A70\u0A2C\u0A30",
+ "\u0A05\u0A15\u0A24\u0A42\u0A2C\u0A30",
+ "\u0A28\u0A35\u0A70\u0A2C\u0A30",
+ "\u0A26\u0A38\u0A70\u0A2C\u0A30",
+ }
+ // monthNamesPunjabiArab list the month names in the Punjabi Arab.
+ monthNamesPunjabiArab = []string{
+ "\u062C\u0646\u0648\u0631\u06CC",
+ "\u0641\u0631\u0648\u0631\u06CC",
+ "\u0645\u0627\u0631\u0686",
+ "\u0627\u067E\u0631\u06CC\u0644",
+ "\u0645\u0626\u06CC",
+ "\u062C\u0648\u0646",
+ "\u062C\u0648\u0644\u0627\u0626\u06CC",
+ "\u0627\u06AF\u0633\u062A",
+ "\u0633\u062A\u0645\u0628\u0631",
+ "\u0627\u06A9\u062A\u0648\u0628\u0631",
+ "\u0646\u0648\u0645\u0628\u0631",
+ "\u062F\u0633\u0645\u0628\u0631",
+ }
+ // monthNamesQuechua list the month names in the Quechua.
+ monthNamesQuechua = []string{"Qulla puquy", "Hatun puquy", "Pauqar waray", "ayriwa", "Aymuray", "Inti raymi", "Anta Sitwa", "Qhapaq Sitwa", "Uma raymi", "Kantaray", "Ayamarq'a", "Kapaq Raymi"}
+ // monthNamesQuechuaEcuador list the month names in the Quechua Ecuador.
+ monthNamesQuechuaEcuador = []string{"kulla", "panchi", "pawkar", "ayriwa", "aymuray", "raymi", "sitwa", "karwa", "kuski", "wayru", "sasi", "kapak"}
+ // monthNamesRomanian list the month names in the Romanian.
+ monthNamesRomanian = []string{"ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie"}
+ // monthNamesRomanianAbbr list the month abbreviations in the Romanian.
+ monthNamesRomanianAbbr = []string{"ian.", "feb.", "mar.", "apr.", "mai", "iun.", "iul.", "aug.", "sept.", "oct.", "nov.", "dec."}
+ // monthNamesRomansh list the month names in the Romansh.
+ monthNamesRomansh = []string{"schaner", "favrer", "mars", "avrigl", "matg", "zercladur", "fanadur", "avust", "settember", "october", "november", "december"}
+ // monthNamesRomanshAbbr list the month abbreviations in the Romansh.
+ monthNamesRomanshAbbr = []string{"schan.", "favr.", "mars", "avr.", "matg", "zercl.", "fan.", "avust", "sett.", "oct.", "nov.", "dec."}
+ // monthNamesRussian list the month names in the Russian.
+ monthNamesRussian = []string{
+ "\u044F\u043D\u0432\u0430\u0440\u044C",
+ "\u0444\u0435\u0432\u0440\u0430\u043B\u044C",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0435\u043B\u044C",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D\u044C",
+ "\u0438\u044E\u043B\u044C",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C",
+ "\u043E\u043A\u0442\u044F\u0431\u0440\u044C",
+ "\u043D\u043E\u044F\u0431\u0440\u044C",
+ "\u0434\u0435\u043A\u0430\u0431\u0440\u044C",
+ }
+ // monthNamesRussianAbbr list the month abbreviations in the Russian.
+ monthNamesRussianAbbr = []string{
+ "\u044F\u043D\u0432.",
+ "\u0444\u0435\u0432.",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440.",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D\u044C",
+ "\u0438\u044E\u043B\u044C",
+ "\u0430\u0432\u0433.",
+ "\u0441\u0435\u043D.",
+ "\u043E\u043A\u0442.",
+ "\u043D\u043E\u044F.",
+ "\u0434\u0435\u043A.",
+ }
+ // monthNamesSakha list the month names in the Sakha.
+ monthNamesSakha = []string{
+ "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443",
+ "\u041E\u043B\u0443\u043D\u043D\u044C\u0443",
+ "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440",
+ "\u041C\u0443\u0443\u0441 \u0443\u0441\u0442\u0430\u0440",
+ "\u042B\u0430\u043C \u044B\u0439\u0430",
+ "\u0411\u044D\u0441 \u044B\u0439\u0430",
+ "\u041E\u0442 \u044B\u0439\u0430",
+ "\u0410\u0442\u044B\u0440\u0434\u044C\u0430\u0445 \u044B\u0439\u0430",
+ "\u0411\u0430\u043B\u0430\u0495\u0430\u043D \u044B\u0439\u0430",
+ "\u0410\u043B\u0442\u044B\u043D\u043D\u044C\u044B",
+ "\u0421\u044D\u0442\u0438\u043D\u043D\u044C\u0438",
+ "\u0410\u0445\u0441\u044B\u043D\u043D\u044C\u044B",
+ }
+ // monthNamesSakhaAbbr list the month abbreviations in the Sakha.
+ monthNamesSakhaAbbr = []string{
+ "\u0422\u0445\u0441",
+ "\u041E\u043B\u043D",
+ "\u041A\u043B\u043D",
+ "\u041C\u0441\u0443",
+ "\u042B\u0430\u043C",
+ "\u0411\u044D\u0441",
+ "\u041E\u0442\u044B",
+ "\u0410\u0442\u0440",
+ "\u0411\u043B\u0495",
+ "\u0410\u043B\u0442",
+ "\u0421\u044D\u0442",
+ "\u0410\u0445\u0441",
+ }
+ // monthNamesSami list the month names in the Sami.
+ monthNamesSami = []string{"uđđâivemáánu", "kuovâmáánu", "njuhčâmáánu", "cuáŋuimáánu", "vyesimáánu", "kesimáánu", "syeinimáánu", "porgemáánu", "čohčâmáánu", "roovvâdmáánu", "skammâmáánu", "juovlâmáánu"}
+ // monthNamesSamiAbbr list the month abbreviations in the Sami.
+ monthNamesSamiAbbr = []string{"uđiv", "kuov", "njuh", "cuáŋ", "vyes", "kesi", "syei", "porg", "čohč", "roov", "skam", "juov"}
+ // monthNamesSamiLule list the month names in the Sami (Lule).
+ monthNamesSamiLule = []string{"ådåjakmánno", "guovvamánno", "sjnjuktjamánno", "vuoratjismánno", "moarmesmánno", "biehtsemánno", "sjnjilltjamánno", "bårggemánno", "ragátmánno", "gålgådismánno", "basádismánno", "javllamánno"}
+ // monthNamesSamiLuleAbbr list the month abbreviations in the Sami (Lule).
+ monthNamesSamiLuleAbbr = []string{"ådåj", "guov", "snju", "vuor", "moar", "bieh", "snji", "bårg", "ragá", "gålg", "basá", "javl"}
+ // monthNamesSamiNorthern list the month names in the Sami (Northern).
+ monthNamesSamiNorthern = []string{"ođđajagemánnu", "guovvamánnu", "njukčamánnu", "cuoŋománnu", "miessemánnu", "geassemánnu", "suoidnemánnu", "borgemánnu", "čakčamánnu", "golggotmánnu", "skábmamánnu", "juovlamánnu"}
+ // monthNamesSamiNorthernAbbr list the month abbreviations in the Sami (Northern).
+ monthNamesSamiNorthernAbbr = []string{"ođđj", "guov", "njuk", "cuoŋ", "mies", "geas", "suoi", "borg", "čakč", "golg", "skáb", "juov"}
+ // monthNamesSamiSkolt list the month names in the Sami (Skolt).
+ monthNamesSamiSkolt = []string{"ođđee´jjmään", "tä´lvvmään", "pâ´zzlâšttam-mään", "njuhččmään", "vue´ssmään", "ǩie´ssmään", "suei´nnmään", "på´rǧǧmään", "čõhččmään", "kålggmään", "skamm-mään", "rosttovmään"}
+ // monthNamesSamiSouthern list the month names in the Sami (Southern).
+ monthNamesSamiSouthern = []string{"tsïengele", "goevte", "njoktje", "voerhtje", "suehpede", "ruffie", "snjaltje", "mïetske", "skïerede", "golke", "rahka", "goeve"}
+ // monthNamesSamiSouthernAbbr list the month abbreviations in the Sami (Southern).
+ monthNamesSamiSouthernAbbr = []string{"tsïen", "goevt", "njok", "voer", "sueh", "ruff", "snja", "mïet", "skïer", "golk", "rahk", "goev"}
+ // monthNamesSanskrit list the month names in the Sanskrit.
+ monthNamesSanskrit = []string{
+ "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940",
+ "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940",
+ "\u092E\u093E\u0930\u094D\u091A",
+ "\u090F\u092A\u094D\u0930\u093F\u0932",
+ "\u092E\u0947",
+ "\u091C\u0942\u0928",
+ "\u091C\u0941\u0932\u0948",
+ "\u0911\u0917\u0938\u094D\u091F",
+ "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930",
+ "\u0911\u0915\u094D\u091F\u094B\u092C\u0930",
+ "\u0928\u094B\u0935\u094D\u0939\u0947\u0902\u092C\u0930",
+ "\u0921\u093F\u0938\u0947\u0902\u092C\u0930",
+ }
+ // monthNamesScottishGaelic list the month names in the Scottish Gaelic.
+ monthNamesScottishGaelic = []string{"Am Faoilleach", "An Gearran", "Am Màrt", "An Giblean", "An Cèitean", "An t-Ògmhios", "An t-Iuchar", "An Lùnastal", "An t-Sultain", "An Dàmhair", "An t-Samhain", "An Dùbhlachd"}
+ // monthNamesScottishGaelicAbbr list the month abbreviations in the ScottishGaelic.
+ monthNamesScottishGaelicAbbr = []string{"Faoi", "Gear", "Màrt", "Gibl", "Cèit", "Ògmh", "Iuch", "Lùna", "Sult", "Dàmh", "Samh", "Dùbh"}
+ // monthNamesSerbian list the month names in the Serbian (Cyrillic).
+ monthNamesSerbian = []string{
+ "\u0458\u0430\u043D\u0443\u0430\u0440",
+ "\u0444\u0435\u0431\u0440\u0443\u0430\u0440",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0438\u043B",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D",
+ "\u0458\u0443\u043B",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440",
+ "\u043E\u043A\u0442\u043E\u0431\u0430\u0440",
+ "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440",
+ "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440",
+ }
+ // monthNamesSerbianAbbr lists the month name abbreviations in the Serbian
+ // (Cyrillic).
+ monthNamesSerbianAbbr = []string{
+ "\u0458\u0430\u043D.",
+ "\u0444\u0435\u0431.",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440.",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D",
+ "\u0458\u0443\u043B",
+ "\u0430\u0432\u0433.",
+ "\u0441\u0435\u043F\u0442.",
+ "\u043E\u043A\u0442.",
+ "\u043D\u043E\u0432.",
+ "\u0434\u0435\u0446.",
+ }
+ // monthNamesSerbianBA list the month names in the Serbian (Cyrillic) Bosnia
+ // and Herzegovina.
+ monthNamesSerbianBA = []string{
+ "\u0458\u0430\u043D\u0443\u0430\u0440",
+ "\u0444\u0435\u0431\u0440\u0443\u0430\u0440",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0438\u043B",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D\u0438",
+ "\u0458\u0443\u043B\u0438",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440",
+ "\u043E\u043A\u0442\u043E\u0431\u0430\u0440",
+ "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440",
+ "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440",
+ }
+ // monthNamesSerbianBAAbbr lists the month name abbreviations in the Serbian
+ // (Cyrillic) Bosnia and Herzegovina.
+ monthNamesSerbianBAAbbr = []string{
+ "\u0458\u0430\u043D",
+ "\u0444\u0435\u0431",
+ "\u043C\u0430\u0440",
+ "\u0430\u043F\u0440",
+ "\u043C\u0430\u0458",
+ "\u0458\u0443\u043D",
+ "\u0458\u0443\u043B",
+ "\u0430\u0432\u0433",
+ "\u0441\u0435\u043F",
+ "\u043E\u043A\u0442",
+ "\u043D\u043E\u0432",
+ "\u0434\u0435\u0446",
+ }
+ // monthNamesSerbianLatin list the month names in the Serbian (Latin).
+ monthNamesSerbianLatin = []string{"januar", "februar", "mart", "april", "maj", "jun", "jul", "avgust", "septembar", "oktobar", "novembar", "decembar"}
+ // monthNamesSerbianLatinAbbr lists the month name abbreviations in the
+ // Serbian(Latin) and Montenegro (Former).
+ monthNamesSerbianLatinAbbr = []string{"jan.", "feb.", "mart", "apr.", "maj", "jun", "jul", "avg.", "sept.", "okt.", "nov.", "dec."}
+ // monthNamesSesothoSaLeboa list the month names in the Sesotho sa Leboa.
+ monthNamesSesothoSaLeboa = []string{"Janaware", "Feberware", "Matšhe", "Aprele", "Mei", "June", "Julae", "Agostose", "Setemere", "Oktoboro", "Nofemere", "Disemere"}
+ // monthNamesSesothoSaLeboaAbbr lists the month name abbreviations in the
+ // Sesotho sa Leboa.
+ monthNamesSesothoSaLeboaAbbr = []string{"Jan", "Feb", "Matš", "Apr", "Mei", "June", "Julae", "Agost", "Set", "Oky", "Nof", "Dis"}
+ // monthNamesSetswana list the month names in the Setswana.
+ monthNamesSetswana = []string{"Ferikgong", "Tlhakole", "Mopitlwe", "Moranang", "Motsheganang", "Seetebosigo", "Phukwi", "Phatwe", "Lwetse", "Diphalane", "Ngwanatsele", "Sedimonthole"}
+ // monthNamesSetswanaAbbr lists the month name abbreviations in the Setswana.
+ monthNamesSetswanaAbbr = []string{"Fer.", "Tlh.", "Mop.", "Mor.", "Motsh.", "Seet.", "Phk.", "Pht.", "Lwetse.", "Diph.", "Ngwn.", "Sed."}
+ // monthNamesSindhi list the month names in the Sindhi.
+ monthNamesSindhi = []string{
+ "\u062C\u0646\u0648\u0631\u064A",
+ "\u0641\u0631\u0648\u0631\u064A",
+ "\u0645\u0627\u0631\u0686",
+ "\u0627\u067E\u0631\u064A\u0644",
+ "\u0645\u0654\u064A",
+ "\u062C\u0648\u0646",
+ "\u062C\u0648\u0644\u0627\u0621\u0650",
+ "\u0622\u06AF\u0633\u062A",
+ "\u0633\u062A\u0645\u0628\u0631",
+ "\u0622\u06A9\u062A\u0648\u0628\u0631",
+ "\u0646\u0648\u0645\u0628\u0631",
+ "\u068A\u0633\u0645\u0628\u0631",
+ }
+ // monthNamesSinhala list the month names in the Sinhala.
+ monthNamesSinhala = []string{
+ "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2",
+ "\u0DB4\u0DD9\u0DB6\u0DBB\u0DC0\u0DCF\u0DBB\u0DD2",
+ "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4",
+ "\u0D85\u0DB4\u0DCA\u200D\u0DBB\u0DDA\u0DBD\u0DCA",
+ "\u0DB8\u0DD0\u0DBA\u0DD2",
+ "\u0DA2\u0DD6\u0DB1\u0DD2",
+ "\u0DA2\u0DD6\u0DBD\u0DD2",
+ "\u0D85\u0D9C\u0DDD\u0DC3\u0DCA\u0DAD\u0DD4",
+ "\u0DC3\u0DD0\u0DB4\u0DCA\u0DAD\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA",
+ "\u0D94\u0D9A\u0DCA\u0DAD\u0DDD\u0DB6\u0DBB\u0DCA",
+ "\u0DB1\u0DDC\u0DC0\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA",
+ "\u0DAF\u0DD9\u0DC3\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA",
+ }
+ // monthNamesSinhalaAbbr lists the month name abbreviations in Sinhala.
+ monthNamesSinhalaAbbr = []string{
+ "\u0DA2\u0DB1.",
+ "\u0DB4\u0DD9\u0DB6.",
+ "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4.",
+ "\u0D85\u0DB4\u0DCA\u200D\u0DBB\u0DDA\u0DBD\u0DCA.",
+ "\u0DB8\u0DD0\u0DBA\u0DD2",
+ "\u0DA2\u0DD6\u0DB1\u0DD2",
+ "\u0DA2\u0DD6\u0DBD\u0DD2",
+ "\u0D85\u0D9C\u0DDD.",
+ "\u0DC3\u0DD0\u0DB4\u0DCA.",
+ "\u0D94\u0D9A\u0DCA.",
+ "\u0DB1\u0DDC\u0DC0\u0DD0.",
+ "\u0DAF\u0DD9\u0DC3\u0DD0.",
+ }
+ // monthNamesSlovak list the month names in the Slovak.
+ monthNamesSlovak = []string{"január", "február", "marec", "apríl", "máj", "jún", "júl", "august", "september", "október", "november", "december"}
+ // monthNamesSlovenian list the month names in the Slovenian.
+ monthNamesSlovenian = []string{"januar", "februar", "marec", "april", "maj", "junij", "julij", "avgust", "september", "oktober", "november", "december"}
+ // monthNamesSlovenianAbbr list the month abbreviations in the Slovenian.
+ monthNamesSlovenianAbbr = []string{"jan.", "feb.", "mar.", "apr.", "maj", "jun.", "jul.", "avg.", "sep.", "okt.", "nov.", "dec."}
+ // monthNamesSomali list the month names in the Somali.
+ monthNamesSomali = []string{"Jannaayo", "Febraayo", "Maarso", "Abriil", "May", "Juun", "Luuliyo", "Ogost", "Sebtembar", "Oktoobar", "Nofembar", "Desembar"}
+ // monthNamesSomaliAbbr list the month abbreviations in the Somali.
+ monthNamesSomaliAbbr = []string{"Jan", "Feb", "Mar", "Abr", "May", "Jun", "Lul", "Ogs", "Seb", "Okt", "Nof", "Dis"}
+ // monthNamesSotho list the month names in the Sotho.
+ monthNamesSotho = []string{"Phesekgong", "Hlakola", "Hlakubele", "Mmese", "Motsheanong", "Phupjane", "Phupu", "Phata", "Leotshe", "Mphalane", "Pundungwane", "Tshitwe"}
+ // monthNamesSothoAbbr list the month abbreviations in the Sotho.
+ monthNamesSothoAbbr = []string{"Phe", "Kol", "Ube", "Mme", "Mot", "Jan", "Upu", "Pha", "Leo", "Mph", "Pun", "Tsh"}
+ // monthNamesSpanish list the month names in the Spanish.
+ monthNamesSpanish = []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"}
+ // monthNamesSpanishAbbr list the month abbreviations in the Spanish.
+ monthNamesSpanishAbbr = []string{"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"}
+ // monthNamesSpanishPE list the month names in the Spanish Peru.
+ monthNamesSpanishPE = []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Setiembre", "Octubre", "Noviembre", "Diciembre"}
+ // monthNamesSpanishPEAbbr list the month abbreviations in the Spanish Peru.
+ monthNamesSpanishPEAbbr = []string{"Ene.", "Feb.", "Mar.", "Abr.", "May.", "Jun.", "Jul.", "Ago.", "Set.", "Oct.", "Nov.", "Dic."}
+ // monthNamesSwedish list the month names in the Swedish.
+ monthNamesSwedish = []string{"januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", "oktober", "november", "december"}
+ // monthNamesSwedishAbbr list the month abbreviations in the Swedish.
+ monthNamesSwedishAbbr = []string{"jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}
+ // monthNamesSwedishFIAbbr list the month abbreviations in the Swedish Finland.
+ monthNamesSwedishFIAbbr = []string{"jan.", "feb.", "mars", "apr.", "maj", "juni", "juli", "aug.", "sep.", "okt.", "nov.", "dec."}
+ // monthNamesSyriac list the month names in the Syriac.
+ monthNamesSyriac = []string{
+ "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D",
+ "\u072B\u0712\u071B",
+ "\u0710\u0715\u072A",
+ "\u0722\u071D\u0723\u0722",
+ "\u0710\u071D\u072A",
+ "\u071A\u0719\u071D\u072A\u0722",
+ "\u072C\u0721\u0718\u0719",
+ "\u0710\u0712",
+ "\u0710\u071D\u0720\u0718\u0720",
+ "\u072C\u072B\u072A\u071D%A0\u0729\u0715\u071D\u0721",
+ "\u072C\u072B\u072A\u071D%A0\u0710\u071A\u072A\u071D",
+ "\u071F\u0722\u0718\u0722%A0\u0729\u0715\u071D\u0721",
+ }
+ // monthNamesSyriacAbbr lists the month name abbreviations in the Syriac.
+ monthNamesSyriacAbbr = []string{
+ "\u071F\u0722%A0\u070F\u0712",
+ "\u072B\u0712\u071B",
+ "\u0710\u0715\u072A",
+ "\u0722\u071D\u0723\u0722",
+ "\u0710\u071D\u072A",
+ "\u071A\u0719\u071D\u072A\u0722",
+ "\u072C\u0721\u0718\u0719",
+ "\u0710\u0712",
+ "\u0710\u071D\u0720\u0718\u0720",
+ "\u070F\u072C\u072B%A0\u070F\u0710",
+ "\u070F\u072C\u072B%A0\u070F\u0712",
+ "\u070F\u071F\u0722%A0\u070F\u0710",
+ }
+ // monthNamesSyllabics list the month names in the Syllabics.
+ monthNamesSyllabics = []string{
+ "\u152E\u14D0\u14C4\u140A\u1546",
+ "\u1556\u155D\u1557\u140A\u1546",
+ "\u14AB\u1466\u14EF",
+ "\u1404\u1433\u1546",
+ "\u14AA\u1403",
+ "\u152B\u14C2",
+ "\u152A\u14DA\u1403",
+ "\u140B\u14A1\u148C\u14EF",
+ "\u14EF\u144E\u1431\u1546",
+ "\u1405\u1450\u1431\u1546",
+ "\u14C4\u1555\u1431\u1546",
+ "\u144E\u14EF\u1431\u1546",
+ }
+ // monthNamesSyllabicsAbbr lists the month name abbreviations in the Syllabics.
+ monthNamesSyllabicsAbbr = []string{
+ "\u152E\u14D0\u14C4",
+ "\u1556\u155D\u1557",
+ "\u14AB\u1466\u14EF",
+ "\u1404\u1433\u1546",
+ "\u14AA\u1403",
+ "\u152B\u14C2",
+ "\u152A\u14DA\u1403",
+ "\u140B\u14A1\u148C",
+ "\u14EF\u144E\u1431",
+ "\u1405\u1450\u1431",
+ "\u14C4\u1555\u1431",
+ "\u144E\u14EF\u1431",
+ }
+ // monthNamesTajik list the month names in the Tajik.
+ monthNamesTajik = []string{
+ "\u044F\u043D\u0432\u0430\u0440",
+ "\u0444\u0435\u0432\u0440\u0430\u043B",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0435\u043B",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D",
+ "\u0438\u044E\u043B",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043D\u0442\u044F\u0431\u0440",
+ "\u043E\u043A\u0442\u044F\u0431\u0440",
+ "\u043D\u043E\u044F\u0431\u0440",
+ "\u0434\u0435\u043A\u0430\u0431\u0440",
+ }
+ // monthNamesTajikAbbr lists the month name abbreviations in Tajik.
+ monthNamesTajikAbbr = []string{
+ "\u044F\u043D\u0432",
+ "\u0444\u0435\u0432",
+ "\u043C\u0430\u0440",
+ "\u0430\u043F\u0440",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D",
+ "\u0438\u044E\u043B",
+ "\u0430\u0432\u0433",
+ "\u0441\u0435\u043D",
+ "\u043E\u043A\u0442",
+ "\u043D\u043E\u044F",
+ "\u0434\u0435\u043A",
+ }
+ // monthNamesTamazight list the month names in the Tamazight.
+ monthNamesTamazight = []string{"Yennayer", "Furar", "Meghres", "Yebrir", "Magu", "Yunyu", "Yulyu", "Ghuct", "Cutenber", "Tuber", "Nunember", "Dujanbir"}
+ // monthNamesTamazightAbbr list the month abbreviations in the Tamazight.
+ monthNamesTamazightAbbr = []string{"Yen", "Fur", "Megh", "Yeb", "May", "Yun", "Yul", "Ghu", "Cut", "Tub", "Nun", "Duj"}
+ // monthNamesTamil list the month names in the Tamil.
+ monthNamesTamil = []string{
+ "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF",
+ "\u0BAA\u0BBF\u0BAA\u0BCD\u0BB0\u0BB5\u0BB0\u0BBF",
+ "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD",
+ "\u0B8F\u0BAA\u0BCD\u0BB0\u0BB2\u0BCD",
+ "\u0BAE\u0BC7",
+ "\u0B9C\u0BC2\u0BA9\u0BCD",
+ "\u0B9C\u0BC2\u0BB2\u0BC8",
+ "\u0B86\u0B95\u0BB8\u0BCD\u0B9F\u0BCD",
+ "\u0B9A\u0BC6\u0BAA\u0BCD\u0B9F\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD",
+ "\u0B85\u0B95\u0BCD\u0B9F\u0BCB\u0BAA\u0BB0\u0BCD",
+ "\u0BA8\u0BB5\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD",
+ "\u0B9F\u0BBF\u0B9A\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD",
+ }
+ // monthNamesTamilAbbr lists the month name abbreviations in Tamil.
+ monthNamesTamilAbbr = []string{
+ "\u0B9C\u0BA9.",
+ "\u0BAA\u0BBF\u0BAA\u0BCD.",
+ "\u0BAE\u0BBE\u0BB0\u0BCD.",
+ "\u0B8F\u0BAA\u0BCD.",
+ "\u0BAE\u0BC7",
+ "\u0B9C\u0BC2\u0BA9\u0BCD",
+ "\u0B9C\u0BC2\u0BB2\u0BC8",
+ "\u0B86\u0B95.",
+ "\u0B9A\u0BC6\u0BAA\u0BCD.",
+ "\u0B85\u0B95\u0BCD.",
+ "\u0BA8\u0BB5.",
+ "\u0B9F\u0BBF\u0B9A.",
+ }
+ // monthNamesTatar list the month names in the Tatar.
+ monthNamesTatar = []string{
+ "\u0433\u044B\u0439\u043D\u0432\u0430\u0440",
+ "\u0444\u0435\u0432\u0440\u0430\u043B\u044C",
+ "\u043C\u0430\u0440\u0442",
+ "\u0430\u043F\u0440\u0435\u043B\u044C",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D\u044C",
+ "\u0438\u044E\u043B\u044C",
+ "\u0430\u0432\u0433\u0443\u0441\u0442",
+ "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C",
+ "\u043E\u043A\u0442\u044F\u0431\u0440\u044C",
+ "\u043D\u043E\u044F\u0431\u0440\u044C",
+ "\u0434\u0435\u043A\u0430\u0431\u0440\u044C",
+ }
+ // monthNamesTatarAbbr lists the month name abbreviations in the Tatar.
+ monthNamesTatarAbbr = []string{
+ "\u0433\u044B\u0439\u043D.",
+ "\u0444\u0435\u0432.",
+ "\u043C\u0430\u0440.",
+ "\u0430\u043F\u0440.",
+ "\u043C\u0430\u0439",
+ "\u0438\u044E\u043D\u044C",
+ "\u0438\u044E\u043B\u044C",
+ "\u0430\u0432\u0433.",
+ "\u0441\u0435\u043D.",
+ "\u043E\u043A\u0442.",
+ "\u043D\u043E\u044F\u0431.",
+ "\u0434\u0435\u043A.",
+ }
+ // monthNamesTelugu list the month names in the Telugu.
+ monthNamesTelugu = []string{
+ "\u0C1C\u0C28\u0C35\u0C30\u0C3F",
+ "\u0C2B\u0C3F\u0C2C\u0C4D\u0C30\u0C35\u0C30\u0C3F",
+ "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F",
+ "\u0C0F\u0C2A\u0C4D\u0C30\u0C3F\u0C32\u0C4D",
+ "\u0C2E\u0C47",
+ "\u0C1C\u0C42\u0C28\u0C4D",
+ "\u0C1C\u0C41\u0C32\u0C48",
+ "\u0C06\u0C17\u0C38\u0C4D\u0C1F\u0C41",
+ "\u0C38\u0C46\u0C2A\u0C4D\u0C1F\u0C46\u0C02\u0C2C\u0C30\u0C4D",
+ "\u0C05\u0C15\u0C4D\u0C1F\u0C4B\u0C2C\u0C30\u0C4D",
+ "\u0C28\u0C35\u0C02\u0C2C\u0C30\u0C4D",
+ "\u0C21\u0C3F\u0C38\u0C46\u0C02\u0C2C\u0C30\u0C4D",
+ }
+ // monthNamesTeluguAbbr lists the month name abbreviations in the Telugu.
+ monthNamesTeluguAbbr = []string{
+ "\u0C1C\u0C28",
+ "\u0C2B\u0C3F\u0C2C\u0C4D\u0C30",
+ "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F",
+ "\u0C0F\u0C2A\u0C4D\u0C30\u0C3F",
+ "\u0C2E\u0C47",
+ "\u0C1C\u0C42\u0C28\u0C4D",
+ "\u0C1C\u0C41\u0C32\u0C48",
+ "\u0C06\u0C17",
+ "\u0C38\u0C46\u0C2A\u0C4D\u0C1F\u0C46\u0C02",
+ "\u0C05\u0C15\u0C4D\u0C1F\u0C4B",
+ "\u0C28\u0C35\u0C02",
+ "\u0C21\u0C3F\u0C38\u0C46\u0C02",
+ }
+ // monthNamesThai list the month names in the Thai.
+ monthNamesThai = []string{
+ "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21",
+ "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c",
+ "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21",
+ "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19",
+ "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21",
+ "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19",
+ "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21",
+ "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21",
+ "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19",
+ "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21",
+ "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19",
+ "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21",
+ }
+ // monthNamesTibetan list the month names in the Tibetan.
+ monthNamesTibetan = []string{
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b",
+ "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b",
+ "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b",
+ }
+ // monthNamesTibetanAbbr lists the month name abbreviations in the Tibetan.
+ monthNamesTibetanAbbr = []string{
+ "\u0f5f\u0fb3\u0f0b\u0f21",
+ "\u0f5f\u0fb3\u0f0b\u0f22",
+ "\u0f5f\u0fb3\u0f0b\u0f23",
+ "\u0f5f\u0fb3\u0f0b\u0f24",
+ "\u0f5f\u0fb3\u0f0b\u0f25",
+ "\u0f5f\u0fb3\u0f0b\u0f26",
+ "\u0f5f\u0fb3\u0f0b\u0f27",
+ "\u0f5f\u0fb3\u0f0b\u0f28",
+ "\u0f5f\u0fb3\u0f0b\u0f29",
+ "\u0f5f\u0fb3\u0f0b\u0f21\u0f20",
+ "\u0f5f\u0fb3\u0f0b\u0f21\u0f21",
+ "\u0f5f\u0fb3\u0f0b\u0f21\u0f22",
+ }
+ // monthNamesTigrinya list the month names in the Tigrinya.
+ monthNamesTigrinya = []string{
+ "\u1325\u122A",
+ "\u1208\u12AB\u1272\u1275",
+ "\u1218\u130B\u1262\u1275",
+ "\u121A\u12EB\u12DD\u12EB",
+ "\u130D\u1295\u1266\u1275",
+ "\u1230\u1290",
+ "\u1213\u121D\u1208",
+ "\u1290\u1213\u1230",
+ "\u1218\u1235\u12A8\u1228\u121D",
+ "\u1325\u1245\u121D\u1272",
+ "\u1215\u12F3\u122D",
+ "\u1273\u1215\u1233\u1235",
+ }
+ // monthNamesTigrinyaAbbr lists the month name abbreviations in the Tigrinya
+ monthNamesTigrinyaAbbr = []string{
+ "\u1325\u122A",
+ "\u1208\u12AB",
+ "\u1218\u130B",
+ "\u121A\u12EB",
+ "\u130D\u1295",
+ "\u1230\u1290",
+ "\u1213\u121D",
+ "\u1290\u1213",
+ "\u1218\u1235",
+ "\u1325\u1245",
+ "\u1215\u12F3",
+ "\u1273\u1215",
+ }
+ // monthNamesTsonga list the month names in the Tsonga.
+ monthNamesTsonga = []string{"Sunguti", "Nyenyenyani", "Nyenyankulu", "Dzivamisoko", "Mudyaxihi", "Khotavuxika", "Mawuwani", "Mhawuri", "Ndzhati", "Nhlangula", "Hukuri", "N’wendzamhala"}
+ // monthNamesTsongaAbbr lists the month name abbreviations in Tsonga, this prevents string concatenation.
+ monthNamesTsongaAbbr = []string{"Sun", "Yan", "Kul", "Dzi", "Mud", "Kho", "Maw", "Mha", "Ndz", "Nhl", "Huk", "N’w"}
+ // monthNamesTradMongolian lists the month number for use with traditional Mongolian.
+ monthNamesTradMongolian = []string{"M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12"}
+ // monthNamesTurkish list the month names in the Turkish.
+ monthNamesTurkish = []string{"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"}
+ // monthNamesTurkishAbbr lists the month name abbreviations in Turkish, this prevents string concatenation.
+ monthNamesTurkishAbbr = []string{"Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"}
+ // monthNamesTurkmen list the month names in the Turkmen.
+ monthNamesTurkmen = []string{"Ýanwar", "Fewral", "Mart", "Aprel", "Maý", "lýun", "lýul", "Awgust", "Sentýabr", "Oktýabr", "Noýabr", "Dekabr"}
+ // monthNamesTurkmenAbbr lists the month name abbreviations in Turkmen, this prevents string concatenation.
+ monthNamesTurkmenAbbr = []string{"Ýan", "Few", "Mart", "Apr", "Maý", "lýun", "lýul", "Awg", "Sen", "Okt", "Noý", "Dek"}
+ // monthNamesUkrainian list the month names in the Ukrainian.
+ monthNamesUkrainian = []string{
+ "\u0441\u0456\u0447\u0435\u043D\u044C",
+ "\u043B\u044E\u0442\u0438\u0439",
+ "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C",
+ "\u043A\u0432\u0456\u0442\u0435\u043D\u044C",
+ "\u0442\u0440\u0430\u0432\u0435\u043D\u044C",
+ "\u0447\u0435\u0440\u0432\u0435\u043D\u044C",
+ "\u043B\u0438\u043F\u0435\u043D\u044C",
+ "\u0441\u0435\u0440\u043F\u0435\u043D\u044C",
+ "\u0432\u0435\u0440\u0435\u0441\u0435\u043D\u044C",
+ "\u0436\u043E\u0432\u0442\u0435\u043D\u044C",
+ "\u043B\u0438\u0441\u0442\u043E\u043F\u0430\u0434",
+ "\u0433\u0440\u0443\u0434\u0435\u043D\u044C",
+ }
+ // monthNamesUkrainianAbbr lists the month name abbreviations in Ukrainian.
+ monthNamesUkrainianAbbr = []string{
+ "\u0421\u0456\u0447",
+ "\u041B\u044E\u0442",
+ "\u0411\u0435\u0440",
+ "\u041A\u0432\u0456",
+ "\u0422\u0440\u0430",
+ "\u0427\u0435\u0440",
+ "\u041B\u0438\u043F",
+ "\u0421\u0435\u0440",
+ "\u0412\u0435\u0440",
+ "\u0416\u043E\u0432",
+ "\u041B\u0438\u0441",
+ "\u0413\u0440\u0443",
+ }
+ // monthNamesUpperSorbian list the month names in the Upper Sorbian.
+ monthNamesUpperSorbian = []string{"januar", "februar", "měrc", "apryl", "meja", "junij", "julij", "awgust", "september", "oktober", "nowember", "december"}
+ // monthNamesUpperSorbianAbbr lists the month name abbreviations in the Upper Sorbian, this prevents string concatenation.
+ monthNamesUpperSorbianAbbr = []string{"jan", "feb", "měr", "apr", "mej", "jun", "jul", "awg", "sep", "okt", "now", "dec"}
+ // monthNamesUyghur list the month names in the Uyghur.
+ monthNamesUyghur = []string{
+ "\u064A\u0627\u0646\u06CB\u0627\u0631",
+ "\u0641\u06D0\u06CB\u0631\u0627\u0644",
+ "\u0645\u0627\u0631\u062A",
+ "\u0626\u0627\u067E\u0631\u06D0\u0644",
+ "\u0645\u0627\u064A",
+ "\u0626\u0649\u064A\u06C7\u0646",
+ "\u0626\u0649\u064A\u06C7\u0644",
+ "\u0626\u0627\u06CB\u063A\u06C7\u0633\u062A",
+ "\u0633\u06D0\u0646\u062A\u06D5\u0628\u0649\u0631",
+ "\u0626\u06C6\u0643\u062A\u06D5\u0628\u0649\u0631",
+ "\u0646\u0648\u064A\u0627\u0628\u0649\u0631",
+ "\u062F\u06D0\u0643\u0627\u0628\u0649\u0631",
+ }
+ // monthNamesUzbek list the month names in the Uzbek.
+ monthNamesUzbek = []string{"Yanvar", "Fevral", "Mart", "Aprel", "May", "Iyun", "Iyul", "Avgust", "Sentabr", "Oktabr", "Noyabr", "Dekabr"}
+ // monthNamesUzbekAbbr lists the month name abbreviations in the Uzbek, this prevents string concatenation.
+ monthNamesUzbekAbbr = []string{"Yan", "Fev", "Mar", "Apr", "May", "Iyn", "Iyl", "Avg", "Sen", "Okt", "Noy", "Dek"}
+ // monthNamesValencian list the month names in the Valencian.
+ monthNamesValencian = []string{"gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"}
+ // monthNamesValencianAbbr lists the month name abbreviations in the Valencian, this prevents string concatenation.
+ monthNamesValencianAbbr = []string{"gen.", "febr.", "març", "abr.", "maig", "juny", "jul.", "ag.", "set.", "oct.", "nov.", "des."}
+ // monthNamesVenda list the month names in the Venda.
+ monthNamesVenda = []string{"Phando", "Luhuhi", "Ṱhafamuhwe", "Lambamai", "Shundunthule", "Fulwi", "Fulwana", "Ṱhangule", "Khubvumedzi", "Tshimedzi", "Ḽara", "Nyendavhusiku"}
+ // monthNamesVendaAbbr lists the month name abbreviations in the Venda, this prevents string concatenation.
+ monthNamesVendaAbbr = []string{"Pha", "Luh", "Ṱhf", "Lam", "Shu", "Lwi", "Lwa", "Ṱha", "Khu", "Tsh", "Ḽar", "Nye"}
+ // monthNamesVietnamese list the month name used for Vietnamese
+ monthNamesVietnamese = []string{"Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12"}
+ // monthNamesVietnameseAbbr3 list the mid-form abbreviation for Vietnamese months.
+ monthNamesVietnameseAbbr3 = []string{"Thg 1", "Thg 2", "Thg 3", "Thg 4", "Thg 5", "Thg 6", "Thg 7", "Thg 8", "Thg 9", "Thg 10", "Thg 11", "Thg 12"}
+ // monthNamesVietnameseAbbr5 list the short-form abbreviation for Vietnamese months.
+ monthNamesVietnameseAbbr5 = []string{"T 1", "T 2", "T 3", "T 4", "T 5", "T 6", "T 7", "T 8", "T 9", "T 10", "T 11", "T 12"}
+ // monthNamesWelsh list the month names in the Welsh.
+ monthNamesWelsh = []string{"Ionawr", "Chwefror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorffennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"}
+ // monthNamesWelshAbbr lists the month name abbreviations in the Welsh, this prevents string concatenation.
+ monthNamesWelshAbbr = []string{"Ion", "Chwef", "Maw", "Ebr", "Mai", "Meh", "Gorff", "Awst", "Medi", "Hyd", "Tach", "Rhag"}
+ // monthNamesWolof list the month names in the Wolof.
+ monthNamesWolof = []string{"Samwiye", "Fewriye", "Maars", "Awril", "Me", "Suwe", "Sullet", "Ut", "Septàmbar", "Oktoobar", "Noowàmbar", "Desàmbar"}
+ // monthNamesWolofAbbr list the month name abbreviations in the Wolof, this prevents string concatenation.
+ monthNamesWolofAbbr = []string{"Sam.", "Few.", "Maa", "Awr.", "Me", "Suw", "Sul.", "Ut", "Sept.", "Okt.", "Now.", "Des."}
+ // monthNamesXhosa list the month names in the Xhosa.
+ monthNamesXhosa = []string{"uJanuwari", "uFebuwari", "uMatshi", "uAprili", "uMeyi", "uJuni", "uJulayi", "uAgasti", "uSeptemba", "uOktobha", "uNovemba", "uDisemba"}
+ // monthNamesXhosaAbbr list the month abbreviations in the Xhosa, this prevents string concatenation.
+ monthNamesXhosaAbbr = []string{"uJan.", "uFeb.", "uMat.", "uEpr.", "uMey.", "uJun.", "uJul.", "uAg.", "uSep.", "uOkt.", "uNov.", "uDis."}
+ // monthNamesYi list the month names in the Yi.
+ monthNamesYi = []string{"\ua2cd", "\ua44d", "\ua315", "\ua1d6", "\ua26c", "\ua0d8", "\ua3c3", "\ua246", "\ua22c", "\ua2b0", "\ua2b0\ua2aa", "\ua2b0\ua44b"}
+ // monthNamesYiSuffix lists the month names in Yi with the "\ua1aa" suffix.
+ monthNamesYiSuffix = []string{"\ua2cd\ua1aa", "\ua44d\ua1aa", "\ua315\ua1aa", "\ua1d6\ua1aa", "\ua26c\ua1aa", "\ua0d8\ua1aa", "\ua3c3\ua1aa", "\ua246\ua1aa", "\ua22c\ua1aa", "\ua2b0\ua1aa", "\ua2b0\ua2aa\ua1aa", "\ua2b0\ua44b\ua1aa"}
+ // monthNamesYiddish list the month names in the Yiddish.
+ monthNamesYiddish = []string{
+ "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8",
+ "\u05E4\u05BF\u05E2\u05D1\u05E8\u05D5\u05D0\u05B7\u05E8",
+ "\u05DE\u05E2\u05E8\u05E5",
+ "\u05D0\u05B7\u05E4\u05BC\u05E8\u05D9\u05DC",
+ "\u05DE\u05D9\u05D9",
+ "\u05D9\u05D5\u05E0\u05D9",
+ "\u05D9\u05D5\u05DC\u05D9",
+ "\u05D0\u05D5\u05D9\u05D2\u05D5\u05E1\u05D8",
+ "\u05E1\u05E2\u05E4\u05BC\u05D8\u05E2\u05DE\u05D1\u05E2\u05E8",
+ "\u05D0\u05E7\u05D8\u05D0\u05D1\u05E2\u05E8",
+ "\u05E0\u05D0\u05D5\u05D5\u05E2\u05DE\u05D1\u05E2\u05E8",
+ "\u05D3\u05E2\u05E6\u05E2\u05DE\u05D1\u05E2\u05E8",
+ }
+ // monthNamesYiddishAbbr lists the month name abbreviations in Yiddish.
+ monthNamesYiddishAbbr = []string{
+ "\u05D9\u05D0\u05B7\u05E0",
+ "\u05E4\u05BF\u05E2\u05D1",
+ "\u05DE\u05E2\u05E8\u05E5",
+ "\u05D0\u05B7\u05E4\u05BC\u05E8",
+ "\u05DE\u05D9\u05D9",
+ "\u05D9\u05D5\u05E0\u05D9",
+ "\u05D9\u05D5\u05DC\u05D9",
+ "\u05D0\u05D5\u05D9\u05D2",
+ "\u05E1\u05E2\u05E4\u05BC",
+ "\u05D0\u05E7\u05D8",
+ "\u05E0\u05D0\u05D5\u05D5",
+ "\u05D3\u05E2\u05E6",
+ }
+ // monthNamesYoruba list the month names in the Yoruba.
+ monthNamesYoruba = []string{
+ "\u1E62\u1EB9\u0301r\u1EB9\u0301",
+ "%C8r%E8l%E8",
+ "\u1EB8r\u1EB9\u0300n%E0",
+ "%CCgb%E9",
+ "\u1EB8\u0300bibi",
+ "%D2k%FAdu",
+ "Ag\u1EB9m\u1ECD",
+ "%D2g%FAn",
+ "Owewe",
+ "\u1ECC\u0300w%E0r%E0",
+ "B%E9l%FA",
+ "\u1ECC\u0300p\u1EB9\u0300",
+ }
+ // monthNamesYorubaAbbr lists the month name abbreviations in the Yoruba.
+ monthNamesYorubaAbbr = []string{
+ "\u1E62\u1EB9\u0301",
+ "%C8r",
+ "\u1EB8r",
+ "%CCg",
+ "\u1EB8\u0300b",
+ "%D2k",
+ "Ag",
+ "%D2g",
+ "Ow",
+ "\u1ECC\u0300w",
+ "B%E9",
+ "\u1ECC\u0300p",
+ }
+ // monthNamesZulu list the month names in the Zulu.
+ monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"}
+ // monthNamesZuluAbbr list the month name abbreviations in the Zulu.
+ monthNamesZuluAbbr = []string{"Jan", "Feb", "Mas", "Eph", "Mey", "Jun", "Jul", "Agas", "Sep", "Okt", "Nov", "Dis"}
+ // weekdayNamesAfrikaans list the weekday name in the Afrikaans.
+ weekdayNamesAfrikaans = []string{"Sondag", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrydag", "Saterdag"}
+ // weekdayNamesAfrikaansAbbr list the weekday name abbreviations in the Afrikaans.
+ weekdayNamesAfrikaansAbbr = []string{"So.", "Ma.", "Di.", "Wo.", "Do.", "Vr.", "Sa."}
+ // weekdayNamesAlbanian list the weekday name in the Albanian.
+ weekdayNamesAlbanian = []string{"e diel", "e hënë", "e martë", "e mërkurë", "e enjte", "e premte", "e shtunë"}
+ // weekdayNamesAlbanianAbbr list the weekday name abbreviations in the Albanian.
+ weekdayNamesAlbanianAbbr = []string{"die", "hën", "mar", "mër", "enj", "pre", "sht"}
+ // weekdayNamesAlsatian list the weekday name in the Alsatian.
+ weekdayNamesAlsatian = []string{"Sunntig", "Määntig", "Ziischtig", "Mittwuch", "Dunschtig", "Friitig", "Samschtig"}
+ // weekdayNamesAlsatianAbbr list the weekday name abbreviations in the Alsatian.
+ weekdayNamesAlsatianAbbr = []string{"Su.", "Mä.", "Zi.", "Mi.", "Du.", "Fr.", "Sa."}
+ // weekdayNamesAlsatianFrance list the weekday name in the Alsatian France.
+ weekdayNamesAlsatianFrance = []string{"Sundi", "Manti", "Zischti", "Mettwuch", "Dunnerschti", "Friti", "Sàmschti"}
+ // weekdayNamesAlsatianFranceAbbr list the weekday name abbreviations in the Alsatian France.
+ weekdayNamesAlsatianFranceAbbr = []string{"Su.", "Ma.", "Zi.", "Me.", "Du.", "Fr.", "Sà."}
+ // weekdayNamesAmharic list the weekday name in the Amharic.
+ weekdayNamesAmharic = []string{
+ "\u12A5\u1211\u12F5",
+ "\u1230\u129E",
+ "\u121B\u12AD\u1230\u129E",
+ "\u1228\u1261\u12D5",
+ "\u1210\u1219\u1235",
+ "\u12D3\u122D\u1265",
+ "\u1245\u12F3\u121C",
+ }
+ // weekdayNamesAmharicAbbr list the weekday name abbreviations in the Amharic.
+ weekdayNamesAmharicAbbr = []string{
+ "\u12A5\u1211\u12F5",
+ "\u1230\u129E",
+ "\u121B\u12AD\u1230",
+ "\u1228\u1261\u12D5",
+ "\u1210\u1219\u1235",
+ "\u12D3\u122D\u1265",
+ "\u1245\u12F3\u121C",
+ }
+ // weekdayNamesArabic list the weekday name in the Arabic.
+ weekdayNamesArabic = []string{
+ "\u0627\u0644\u0623\u062D\u062F",
+ "\u0627\u0644\u0625\u062B\u0646\u064A\u0646",
+ "\u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621",
+ "\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621",
+ "\u0627\u0644\u062E\u0645\u064A\u0633",
+ "\u0627\u0644\u062C\u0645\u0639\u0629",
+ "\u0627\u0644\u0633\u0628\u062A",
+ }
+ // weekdayNamesArabicAbbr list the weekday name abbreviations in the Arabic.
+ weekdayNamesArabicAbbr = []string{
+ "\u0627\u0644\u0623\u062D\u062F",
+ "\u0627\u0644\u0625\u062B\u0646\u064A\u0646",
+ "\u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621",
+ "\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621",
+ "\u0627\u0644\u062E\u0645\u064A\u0633",
+ "\u0627\u0644\u062C\u0645\u0639\u0629",
+ "\u0627\u0644\u0633\u0628\u062A",
+ }
+ // weekdayNamesArmenian list the weekday name in the Armenian.
+ weekdayNamesArmenian = []string{
+ "\u053F\u056B\u0580\u0561\u056F\u056B",
+ "\u0535\u0580\u056F\u0578\u0582\u0577\u0561\u0562\u0569\u056B",
+ "\u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B",
+ "\u0549\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B",
+ "\u0540\u056B\u0576\u0563\u0577\u0561\u0562\u0569\u056B",
+ "\u0548\u0582\u0580\u0562\u0561\u0569",
+ "\u0547\u0561\u0562\u0561\u0569",
+ }
+ // weekdayNamesArmenianAbbr list the weekday name abbreviations in the Armenian.
+ weekdayNamesArmenianAbbr = []string{
+ "\u053F\u056B\u0580",
+ "\u0535\u0580\u056F",
+ "\u0535\u0580\u0584",
+ "\u0549\u0580\u0584",
+ "\u0540\u0576\u0563",
+ "\u0548\u0582\u0580",
+ "\u0547\u0562\u0569",
+ }
+ // weekdayNamesAssamese list the weekday name in the Assamese.
+ weekdayNamesAssamese = []string{
+ "\u09F0\u09AC\u09BF\u09AC\u09BE\u09F0",
+ "\u09B8\u09CB\u09AE\u09AC\u09BE\u09F0",
+ "\u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0",
+ "\u09AC\u09C1\u09A7\u09AC\u09BE\u09F0",
+ "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF\u09AC\u09BE\u09F0",
+ "\u09B6\u09C1\u0995\u09CD\u09B0\u09AC\u09BE\u09F0",
+ "\u09B6\u09A8\u09BF\u09AC\u09BE\u09F0",
+ }
+ // weekdayNamesAssameseAbbr list the weekday name abbreviations in the Assamese.
+ weekdayNamesAssameseAbbr = []string{
+ "\u09F0\u09AC\u09BF.",
+ "\u09B8\u09CB\u09AE.",
+ "\u09AE\u0999\u09CD\u0997\u09B2.",
+ "\u09AC\u09C1\u09A7.",
+ "\u09AC\u09C3\u09B9.",
+ "\u09B6\u09C1\u0995\u09CD\u09B0.",
+ "\u09B6\u09A8\u09BF.",
+ }
+ // weekdayNamesAzerbaijaniCyrillic list the weekday name in the Azerbaijani (Cyrillic).
+ weekdayNamesAzerbaijaniCyrillic = []string{
+ "\u0431\u0430\u0437\u0430\u0440",
+ "\u0431\u0430\u0437\u0430\u0440%A0\u0435\u0440\u0442\u04D9\u0441\u0438",
+ "\u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B",
+ "\u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9",
+ "\u04B9\u04AF\u043C\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B",
+ "\u04B9\u04AF\u043C\u04D9",
+ "\u0448\u04D9\u043D\u0431\u04D9",
+ }
+ // weekdayNamesAzerbaijaniCyrillicAbbr list the weekday name abbreviations in the Azerbaijani (Cyrillic).
+ weekdayNamesAzerbaijaniCyrillicAbbr = []string{"\u0411", "\u0411\u0435", "\u0427\u0430", "\u0427", "\u04B8\u0430", "\u04B8", "\u0428"}
+ // weekdayNamesAzerbaijani list the weekday name in the Azerbaijani.
+ weekdayNamesAzerbaijani = []string{
+ "bazar",
+ "bazar%E7ert\u0259si",
+ "%E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131",
+ "%E7\u0259r\u015F\u0259nb\u0259",
+ "c%FCm\u0259%20ax\u015Fam\u0131",
+ "c%FCm\u0259",
+ "\u015F\u0259nb\u0259",
+ }
+ // weekdayNamesAzerbaijaniAbbr list the weekday name abbreviations in the Azerbaijani.
+ weekdayNamesAzerbaijaniAbbr = []string{"B.", "B.E.", "%C7.A.", "%C7.", "C.A.", "C.", "\u015E."}
+ // weekdayNamesBangla list the weekday name in the Bangla.
+ weekdayNamesBangla = []string{
+ "\u09B0\u09AC\u09BF\u09AC\u09BE\u09B0",
+ "\u09B8\u09CB\u09AE\u09AC\u09BE\u09B0",
+ "\u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0",
+ "\u09AC\u09C1\u09A7\u09AC\u09BE\u09B0",
+ "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF\u09AC\u09BE\u09B0",
+ "\u09B6\u09C1\u0995\u09CD\u09B0\u09AC\u09BE\u09B0",
+ "\u09B6\u09A8\u09BF\u09AC\u09BE\u09B0",
+ }
+ // weekdayNamesBanglaAbbr list the weekday name abbreviations in the Bangla.
+ weekdayNamesBanglaAbbr = []string{
+ "\u09B0\u09AC\u09BF.",
+ "\u09B8\u09CB\u09AE.",
+ "\u09AE\u0999\u09CD\u0997\u09B2.",
+ "\u09AC\u09C1\u09A7.",
+ "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF.",
+ "\u09B6\u09C1\u0995\u09CD\u09B0.",
+ "\u09B6\u09A8\u09BF.",
+ }
+ // weekdayNamesBashkir list the weekday name in the Bashkir.
+ weekdayNamesBashkir = []string{
+ "\u0419\u04D9\u043A\u0448\u04D9\u043C\u0431\u0435",
+ "\u0414\u04AF\u0448\u04D9\u043C\u0431\u0435",
+ "\u0428\u0438\u0448\u04D9\u043C\u0431\u0435",
+ "\u0428\u0430\u0440\u0448\u0430\u043C\u0431\u044B",
+ "\u041A\u0435\u0441\u0430\u0499\u043D\u0430",
+ "\u0419\u043E\u043C\u0430",
+ "\u0428\u04D9\u043C\u0431\u0435",
+ }
+ // weekdayNamesBashkirAbbr list the weekday name abbreviations in the Bashkir.
+ weekdayNamesBashkirAbbr = []string{
+ "\u0419\u0448",
+ "\u0414\u0448",
+ "\u0428\u0448",
+ "\u0428\u0440",
+ "\u041A\u0441",
+ "\u0419\u043C",
+ "\u0428\u0431",
+ }
+ // weekdayNamesBasque list the weekday name in the Basque.
+ weekdayNamesBasque = []string{"igandea", "astelehena", "asteartea", "asteazkena", "osteguna", "ostirala", "larunbata"}
+ // weekdayNamesBasqueAbbr list the weekday name abbreviations in the Basque.
+ weekdayNamesBasqueAbbr = []string{"ig.", "al.", "ar.", "az.", "og.", "or.", "lr."}
+ // weekdayNamesBelarusian list the weekday name in the Belarusian.
+ weekdayNamesBelarusian = []string{
+ "\u043D\u044F\u0434\u0437\u0435\u043B\u044F",
+ "\u043F\u0430\u043D\u044F\u0434\u0437\u0435\u043B\u0430\u043A",
+ "\u0430\u045E\u0442\u043E\u0440\u0430\u043A",
+ "\u0441\u0435\u0440\u0430\u0434\u0430",
+ "\u0447\u0430\u0446\u0432\u0435\u0440",
+ "\u043F\u044F\u0442\u043D\u0456\u0446\u0430",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesBelarusianAbbr list the weekday name abbreviations in the Belarusian.
+ weekdayNamesBelarusianAbbr = []string{
+ "\u043D\u0434",
+ "\u043F\u043D",
+ "\u0430\u045E\u0442",
+ "\u0441\u0440",
+ "\u0447\u0446",
+ "\u043F\u0442",
+ "\u0441\u0431",
+ }
+ // weekdayNamesBosnianCyrillic list the weekday name in the Bosnian (Cyrillic).
+ weekdayNamesBosnianCyrillic = []string{
+ "\u043D\u0435\u0434\u0458\u0435\u0459\u0430",
+ "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A",
+ "\u0443\u0442\u043E\u0440\u0430\u043A",
+ "\u0441\u0440\u0438\u0458\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A",
+ "\u043F\u0435\u0442\u0430\u043A",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesBosnianCyrillicAbbr list the weekday name abbreviations in the Bosnian (Cyrillic).
+ weekdayNamesBosnianCyrillicAbbr = []string{
+ "\u043D\u0435\u0434",
+ "\u043F\u043E\u043D",
+ "\u0443\u0442\u043E",
+ "\u0441\u0440\u0435",
+ "\u0447\u0435\u0442",
+ "\u043F\u0435\u0442",
+ "\u0441\u0443\u0431",
+ }
+ // weekdayNamesBosnian list the weekday name in the Bosnian.
+ weekdayNamesBosnian = []string{"nedjelja", "ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"}
+ // weekdayNamesBosnianAbbr list the weekday name abbreviations in the Bosnian.
+ weekdayNamesBosnianAbbr = []string{"ned", "pon", "uto", "sri", "čet", "pet", "sub"}
+ // weekdayNamesBreton list the weekday name in the Breton.
+ weekdayNamesBreton = []string{"Sul", "Lun", "Meurzh", "Merc'her", "Yaou", "Gwener", "Sadorn"}
+ // weekdayNamesBretonAbbr list the weekday name abbreviations in the Breton.
+ weekdayNamesBretonAbbr = []string{"Sul", "Lun", "Meu.", "Mer.", "Yaou", "Gwe.", "Sad."}
+ // weekdayNamesBulgarian list the weekday name in the Bulgarian.
+ weekdayNamesBulgarian = []string{
+ "\u043D\u0435\u0434\u0435\u043B\u044F",
+ "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u043D\u0438\u043A",
+ "\u0432\u0442\u043E\u0440\u043D\u0438\u043A",
+ "\u0441\u0440\u044F\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u044A\u0440\u0442\u044A\u043A",
+ "\u043F\u0435\u0442\u044A\u043A",
+ "\u0441\u044A\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesBulgarianAbbr list the weekday name abbreviations in the Bulgarian.
+ weekdayNamesBulgarianAbbr = []string{
+ "\u043D\u0435\u0434",
+ "\u043F\u043E\u043D",
+ "\u0432\u0442",
+ "\u0441\u0440",
+ "\u0447\u0435\u0442\u0432",
+ "\u043F\u0435\u0442",
+ "\u0441\u044A\u0431",
+ }
+ // weekdayNamesBurmese list the weekday name in the Burmese.
+ weekdayNamesBurmese = []string{
+ "\u1010\u1014\u1004\u103A\u1039\u1002\u1014\u103D\u1031",
+ "\u1010\u1014\u1004\u103A\u1039\u101C\u102C",
+ "\u1021\u1004\u103A\u1039\u1002\u102B",
+ "\u1017\u102F\u1012\u1039\u1013\u101F\u1030\u1038",
+ "\u1000\u103C\u102C\u101E\u1015\u1010\u1031\u1038",
+ "\u101E\u1031\u102C\u1000\u103C\u102C",
+ "\u1005\u1014\u1031",
+ }
+ // weekdayNamesCentralKurdish list the weekday name in the Central Kurdish.
+ weekdayNamesCentralKurdish = []string{
+ "\u06CC\u06D5\u06A9\u0634\u06D5\u0645\u0645\u06D5",
+ "\u062F\u0648\u0648\u0634\u06D5\u0645\u0645\u06D5",
+ "\u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5",
+ "\u0686\u0648\u0627\u0631\u0634\u06D5\u0645\u0645\u06D5",
+ "\u067E\u06CE\u0646\u062C\u0634\u06D5\u0645\u0645\u06D5",
+ "\u06BE\u06D5\u06CC\u0646\u06CC",
+ "\u0634\u06D5\u0645\u0645\u06D5",
+ }
+ // weekdayNamesCherokee list the weekday name in the Cherokee.
+ weekdayNamesCherokee = []string{
+ "\u13A4\u13BE\u13D9\u13D3\u13C6\u13CD\u13AC",
+ "\u13A4\u13BE\u13D9\u13D3\u13C9\u13C5\u13AF",
+ "\u13D4\u13B5\u13C1\u13A2\u13A6",
+ "\u13E6\u13A2\u13C1\u13A2\u13A6",
+ "\u13C5\u13A9\u13C1\u13A2\u13A6",
+ "\u13E7\u13BE\u13A9\u13B6\u13CD\u13D7",
+ "\u13A4\u13BE\u13D9\u13D3\u13C8\u13D5\u13BE",
+ }
+ // weekdayNamesCherokeeAbbr list the weekday name abbreviations in the Cherokee.
+ weekdayNamesCherokeeAbbr = []string{
+ "\u13C6\u13CD\u13AC",
+ "\u13C9\u13C5\u13AF",
+ "\u13D4\u13B5\u13C1",
+ "\u13E6\u13A2\u13C1",
+ "\u13C5\u13A9\u13C1",
+ "\u13E7\u13BE\u13A9",
+ "\u13C8\u13D5\u13BE",
+ }
+ // weekdayNamesChinese list the weekday name in the Chinese.
+ weekdayNamesChinese = []string{"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}
+ // weekdayNamesChineseAbbr list the weekday name abbreviations in the Chinese.
+ weekdayNamesChineseAbbr = []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
+ // weekdayNamesChineseAbbr list the weekday name abbreviations in the Chinese.
+ weekdayNamesChineseAbbr2 = []string{"週日", "週一", "週二", "週三", "週四", "週五", "週六"}
+ // weekdayNamesEnglish list the weekday name in the English.
+ weekdayNamesEnglish = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
+ // weekdayNamesEnglishAbbr list the weekday name abbreviations in the English.
+ weekdayNamesEnglishAbbr = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
+ // weekdayNamesEstonian list the weekday name in the Estonian.
+ weekdayNamesEstonian = []string{"pühapäev", "esmaspäev", "teisipäev", "kolmapäev", "neljapäev", "reede", "laupäev"}
+ // weekdayNamesEstonianAbbr list the weekday name abbreviations in the Estonian.
+ weekdayNamesEstonianAbbr = []string{"P", "E", "T", "K", "N", "R", "L"}
+ // weekdayNamesFaroese list the weekday name in the Faroese.
+ weekdayNamesFaroese = []string{"sunnudagur", "mánadagur", "týsdagur", "mikudagur", "hósdagur", "fríggjadagur", "leygardagur"}
+ // weekdayNamesFaroeseAbbr list the weekday name abbreviations in the Faroese.
+ weekdayNamesFaroeseAbbr = []string{"sun.", "mán.", "týs.", "mik.", "hós.", "frí.", "ley."}
+ // weekdayNamesFilipino list the weekday name in the Filipino.
+ weekdayNamesFilipino = []string{"Linggo", "Lunes", "Martes", "Miyerkules", "Huwebes", "Biyernes", "Sabado"}
+ // weekdayNamesFilipinoAbbr list the weekday name abbreviations in the Filipino.
+ weekdayNamesFilipinoAbbr = []string{"Lin", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab"}
+ // weekdayNamesFinnish list the weekday name in the Finnish
+ weekdayNamesFinnish = []string{"sunnuntai", "maanantai", "tiistai", "keskiviikko", "torstai", "perjantai", "lauantai"}
+ // weekdayNamesFinnishAbbr list the weekday name abbreviations in the Finnish
+ weekdayNamesFinnishAbbr = []string{"su", "ma", "ti", "ke", "to", "pe", "la"}
+ // weekdayNamesFrench list the weekday name in the French.
+ weekdayNamesFrench = []string{"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"}
+ // weekdayNamesFrenchAbbr list the weekday name abbreviations in the French.
+ weekdayNamesFrenchAbbr = []string{"dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."}
+ // weekdayNamesFrisian list the weekday name in the Frisian.
+ weekdayNamesFrisian = []string{"snein", "moandei", "tiisdei", "woansdei", "tongersdei", "freed", "sneon"}
+ // weekdayNamesFrisianAbbr list the weekday name abbreviations in the Frisian.
+ weekdayNamesFrisianAbbr = []string{"sni", "moa", "tii", "woa", "ton", "fre", "sno"}
+ // weekdayNamesFulah list the weekday name in the Fulah.
+ weekdayNamesFulah = []string{"dewo", "aaɓnde", "mawbaare", "njeslaare", "naasaande", "mawnde", "hoore-biir"}
+ // weekdayNamesFulahAbbr list the weekday name abbreviations in the Fulah
+ weekdayNamesFulahAbbr = []string{"dew", "aaɓ", "maw", "nje", "naa", "mwd", "hbi"}
+ // weekdayNamesNigeria list the weekday name in the Nigeria
+ weekdayNamesNigeria = []string{"alete", "altine", "talaata", "alarba", "alkamiisa", "aljumaa", "asete"}
+ // weekdayNamesNigeriaAbbr list the weekday name abbreviations in the Nigeria.
+ weekdayNamesNigeriaAbbr = []string{"alet", "alt.", "tal.", "alar.", "alk.", "alj.", "aset"}
+ // weekdayNamesGalician list the weekday name in the Galician.
+ weekdayNamesGalician = []string{"domingo", "luns", "martes", "mércores", "xoves", "venres", "sábado"}
+ // weekdayNamesGalicianAbbr list the weekday name abbreviations in the Galician.
+ weekdayNamesGalicianAbbr = []string{"dom.", "luns", "mar.", "mér.", "xov.", "ven.", "sáb."}
+ // weekdayNamesGeorgian list the weekday name in the Georgian.
+ weekdayNamesGeorgian = []string{
+ "\u10D9\u10D5\u10D8\u10E0\u10D0",
+ "\u10DD\u10E0\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8",
+ "\u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8",
+ "\u10DD\u10D7\u10EE\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8",
+ "\u10EE\u10E3\u10D7\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8",
+ "\u10DE\u10D0\u10E0\u10D0\u10E1\u10D9\u10D4\u10D5\u10D8",
+ "\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8",
+ }
+ // weekdayNamesGeorgianAbbr list the weekday name abbreviations in the Georgian.
+ weekdayNamesGeorgianAbbr = []string{
+ "\u10D9\u10D5.",
+ "\u10DD\u10E0\u10E8.",
+ "\u10E1\u10D0\u10DB\u10E8.",
+ "\u10DD\u10D7\u10EE\u10E8.",
+ "\u10EE\u10E3\u10D7\u10E8.",
+ "\u10DE\u10D0\u10E0.",
+ "\u10E8\u10D0\u10D1.",
+ }
+ // weekdayNamesGerman list the weekday name in the German.
+ weekdayNamesGerman = []string{"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}
+ // weekdayNamesGermanAbbr list the weekday name abbreviations in the German.
+ weekdayNamesGermanAbbr = []string{"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"}
+ // weekdayNamesGreek list the weekday name in the Greek.
+ weekdayNamesGreek = []string{
+ "\u039A\u03C5\u03C1\u03B9\u03B1\u03BA\u03AE",
+ "\u0394\u03B5\u03C5\u03C4\u03AD\u03C1\u03B1",
+ "\u03A4\u03C1\u03AF\u03C4\u03B7",
+ "\u03A4\u03B5\u03C4\u03AC\u03C1\u03C4\u03B7",
+ "\u03A0\u03AD\u03BC\u03C0\u03C4\u03B7",
+ "\u03A0\u03B1\u03C1\u03B1\u03C3\u03BA\u03B5\u03C5\u03AE",
+ "\u03A3\u03AC\u03B2\u03B2\u03B1\u03C4\u03BF",
+ }
+ // weekdayNamesGreekAbbr list the weekday name abbreviations in the Greek.
+ weekdayNamesGreekAbbr = []string{
+ "\u039A\u03C5\u03C1",
+ "\u0394\u03B5\u03C5",
+ "\u03A4\u03C1\u03B9",
+ "\u03A4\u03B5\u03C4",
+ "\u03A0\u03B5\u03BC",
+ "\u03A0\u03B1\u03C1",
+ "\u03A3\u03B1\u03B2",
+ }
+ // weekdayNamesGreenlandic list the weekday name in the Greenlandic.
+ weekdayNamesGreenlandic = []string{"sapaat", "ataasinngorneq", "marlunngorneq", "pingasunngorneq", "sisamanngorneq", "tallimanngorneq", "arfininngorneq"}
+ // weekdayNamesGreenlandicAbbr list the weekday name abbreviations in the Greenlandic.
+ weekdayNamesGreenlandicAbbr = []string{"sap.", "at.", "marl.", "ping.", "sis.", "tall.", "arf."}
+ // weekdayNamesGuarani list the weekday name in the Guarani.
+ weekdayNamesGuarani = []string{"arate\u0129", "arak%F5i", "araapy", "ararundy", "arapo", "arapote\u0129", "arapok%F5i"}
+ // weekdayNamesGuaraniAbbr list the weekday name abbreviations in the Guarani.
+ weekdayNamesGuaraniAbbr = []string{"te\u0129", "k%F5i", "apy", "ndy", "po", "ote\u0129", "ok%F5i"}
+ // weekdayNamesGujarati list the weekday name in the Gujarati.
+ weekdayNamesGujarati = []string{
+ "\u0AB0\u0AB5\u0ABF\u0AB5\u0ABE\u0AB0",
+ "\u0AB8\u0ACB\u0AAE\u0AB5\u0ABE\u0AB0",
+ "\u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0",
+ "\u0AAC\u0AC1\u0AA7\u0AB5\u0ABE\u0AB0",
+ "\u0A97\u0AC1\u0AB0\u0AC1\u0AB5\u0ABE\u0AB0",
+ "\u0AB6\u0AC1\u0A95\u0ACD\u0AB0\u0AB5\u0ABE\u0AB0",
+ "\u0AB6\u0AA8\u0ABF\u0AB5\u0ABE\u0AB0",
+ }
+ // weekdayNamesGujaratiAbbr list the weekday name abbreviations in the Gujarati.
+ weekdayNamesGujaratiAbbr = []string{
+ "\u0AB0\u0AB5\u0ABF",
+ "\u0AB8\u0ACB\u0AAE",
+ "\u0AAE\u0A82\u0A97\u0AB3",
+ "\u0AAC\u0AC1\u0AA7",
+ "\u0A97\u0AC1\u0AB0\u0AC1",
+ "\u0AB6\u0AC1\u0A95\u0ACD\u0AB0",
+ "\u0AB6\u0AA8\u0ABF",
+ }
+ // weekdayNamesHausa list the weekday name in the Hausa.
+ weekdayNamesHausa = []string{"Lahadi", "Litinin", "Talata", "Laraba", "Alhamis", "Jummaʼa", "Asabar"}
+ // weekdayNamesHausaAbbr list the weekday name abbreviations in the Hausa.
+ weekdayNamesHausaAbbr = []string{"Lah", "Lit", "Tal", "Lar", "Alh", "Jum", "Asa"}
+ // weekdayNamesHawaiian list the weekday name in the Hawaiian.
+ weekdayNamesHawaiian = []string{"Lāpule", "Poʻakahi", "Poʻalua", "Poʻakolu", "Poʻahā", "Poʻalima", "Poʻaono"}
+ // weekdayNamesHawaiianAbbr list the weekday name abbreviations in the Hawaiian.
+ weekdayNamesHawaiianAbbr = []string{"LP", "P1", "P2", "P3", "P4", "P5", "P6"}
+ // weekdayNamesHebrew list the weekday name in the Hebrew.
+ weekdayNamesHebrew = []string{
+ "\u05D9\u05D5\u05DD%A0\u05E8\u05D0\u05E9\u05D5\u05DF",
+ "\u05D9\u05D5\u05DD%A0\u05E9\u05E0\u05D9",
+ "\u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9",
+ "\u05D9\u05D5\u05DD%A0\u05E8\u05D1\u05D9\u05E2\u05D9",
+ "\u05D9\u05D5\u05DD%A0\u05D7\u05DE\u05D9\u05E9\u05D9",
+ "\u05D9\u05D5\u05DD%A0\u05E9\u05D9\u05E9\u05D9",
+ "\u05E9\u05D1\u05EA",
+ }
+ // weekdayNamesHebrewAbbr list the weekday name abbreviations in the Hebrew.
+ weekdayNamesHebrewAbbr = []string{
+ "\u05D9\u05D5\u05DD%A0\u05D0",
+ "\u05D9\u05D5\u05DD%A0\u05D1",
+ "\u05D9\u05D5\u05DD%A0\u05D2",
+ "\u05D9\u05D5\u05DD%A0\u05D3",
+ "\u05D9\u05D5\u05DD%A0\u05D4",
+ "\u05D9\u05D5\u05DD%A0\u05D5",
+ "\u05E9\u05D1\u05EA",
+ }
+ // weekdayNamesHindi list the weekday name in the Hindi.
+ weekdayNamesHindi = []string{
+ "\u0930\u0935\u093F\u0935\u093E\u0930",
+ "\u0938\u094B\u092E\u0935\u093E\u0930",
+ "\u092E\u0902\u0917\u0932\u0935\u093E\u0930",
+ "\u092C\u0941\u0927\u0935\u093E\u0930",
+ "\u0917\u0941\u0930\u0941\u0935\u093E\u0930",
+ "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930",
+ "\u0936\u0928\u093F\u0935\u093E\u0930",
+ }
+ // weekdayNamesHindiAbbr list the weekday name abbreviations in the Hindi.
+ weekdayNamesHindiAbbr = []string{
+ "\u0930\u0935\u093F.",
+ "\u0938\u094B\u092E.",
+ "\u092E\u0902\u0917\u0932.",
+ "\u092C\u0941\u0927.",
+ "\u0917\u0941\u0930\u0941.",
+ "\u0936\u0941\u0915\u094D\u0930.",
+ "\u0936\u0928\u093F.",
+ }
+ // weekdayNamesHungarian list the weekday name in the Hungarian.
+ weekdayNamesHungarian = []string{"vasárnap", "hétfő", "kedd", "szerda", "csütörtök", "péntek", "szombat"}
+ // weekdayNamesHungarianAbbr list the weekday name abbreviations in the Hungarian.
+ weekdayNamesHungarianAbbr = []string{"V", "H", "K", "Sze", "Cs", "P", "Szo"}
+ // weekdayNamesIcelandic list the weekday name in the Icelandic.
+ weekdayNamesIcelandic = []string{"sunnudagur", "mánudagur", "þriðjudagur", "miðvikudagur", "fimmtudagur", "föstudagur", "laugardagur"}
+ // weekdayNamesIcelandicAbbr list the weekday name abbreviations in the Icelandic.
+ weekdayNamesIcelandicAbbr = []string{"sun.", "mán.", "þri.", "mið.", "fim.", "fös.", "lau."}
+ // weekdayNamesIgbo list the weekday name in the Igbo.
+ weekdayNamesIgbo = []string{"Ụbọchị Ụka", "Mọnde", "Tiuzdee", "Wenezdee", "Tọọzdee", "Fraịdee", "Satọdee"}
+ // weekdayNamesIgboAbbr list the weekday name abbreviations in the Igbo.
+ weekdayNamesIgboAbbr = []string{"Ụka", "Mọn", "Tiu", "Wen", "Tọọ", "Fraị", "Satọdee"}
+ // weekdayNamesIndonesian list the weekday name in the Indonesian.
+ weekdayNamesIndonesian = []string{"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"}
+ // weekdayNamesIndonesianAbbr list the weekday name abbreviations in the Indonesian.
+ weekdayNamesIndonesianAbbr = []string{"Mgg", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"}
+ // weekdayNamesInuktitut list the weekday name in the Inuktitut.
+ weekdayNamesInuktitut = []string{"Naattiinguja", "Naggajjau", "Aippiq", "Pingatsiq", "Sitammiq", "Tallirmiq", "Sivataarvik"}
+ // weekdayNamesInuktitutAbbr list the weekday name abbreviations in the Inuktitut.
+ weekdayNamesInuktitutAbbr = []string{"Nat", "Nag", "Aip", "Pi", "Sit", "Tal", "Siv"}
+ // weekdayNamesSyllabics list the weekday name in the Syllabics.
+ weekdayNamesSyllabics = []string{
+ "\u14C8\u1466\u144F\u1591\u152D",
+ "\u14C7\u14A1\u1490\u153E\u152D\u1405",
+ "\u140A\u1403\u1449\u1431\u1585",
+ "\u1431\u1593\u1466\u14EF\u1585",
+ "\u14EF\u1455\u14BB\u14A5\u1585",
+ "\u1455\u14EA\u14D5\u1550\u14A5\u1585",
+ "\u14EF\u1559\u1456\u1550\u1555\u1483",
+ }
+ // weekdayNamesSyllabicsAbbr list the weekday name abbreviations in the Syllabics.
+ weekdayNamesSyllabicsAbbr = []string{
+ "\u14C8\u1466\u144F",
+ "\u14C7\u14A1\u1490",
+ "\u140A\u1403\u1449\u1431",
+ "\u1431\u1593\u1466\u14EF",
+ "\u14EF\u1455",
+ "\u1455\u14EA\u14D5",
+ "\u14EF\u1559\u1456\u1550\u1555\u1483",
+ }
+ // weekdayNamesIrish list the weekday name in the Irish.
+ weekdayNamesIrish = []string{"Dé Domhnaigh", "Dé Luain", "Dé Máirt", "Dé Céadaoin", "Déardaoin", "Dé hAoine", "Dé Sathairn"}
+ // weekdayNamesIrishAbbr list the weekday name abbreviations in the Irish.
+ weekdayNamesIrishAbbr = []string{"Domh", "Luan", "Máirt", "Céad", "Déar", "Aoine", "Sath"}
+ // weekdayNamesItalian list the weekday name in the Italian.
+ weekdayNamesItalian = []string{"domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"}
+ // weekdayNamesItalianAbbr list the weekday name abbreviations in the Italian.
+ weekdayNamesItalianAbbr = []string{"dom", "lun", "mar", "mer", "gio", "ven", "sab"}
+ // weekdayNamesJapanese list the weekday name in the Japanese.
+ weekdayNamesJapanese = []string{"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"}
+ // weekdayNamesJapaneseAbbr list the weekday name abbreviations in the Japanese.
+ weekdayNamesJapaneseAbbr = []string{"日", "月", "火", "水", "木", "金", "土"}
+ // weekdayNamesKannada list the weekday name in the Kannada.
+ weekdayNamesKannada = []string{
+ "\u0CAD\u0CBE\u0CA8\u0CC1\u0CB5\u0CBE\u0CB0",
+ "\u0CB8\u0CCB\u0CAE\u0CB5\u0CBE\u0CB0",
+ "\u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0",
+ "\u0CAC\u0CC1\u0CA7\u0CB5\u0CBE\u0CB0",
+ "\u0C97\u0CC1\u0CB0\u0CC1\u0CB5\u0CBE\u0CB0",
+ "\u0CB6\u0CC1\u0C95\u0CCD\u0CB0\u0CB5\u0CBE\u0CB0",
+ "\u0CB6\u0CA8\u0CBF\u0CB5\u0CBE\u0CB0",
+ }
+ // weekdayNamesKannadaAbbr list the weekday name abbreviations in the Kannada.
+ weekdayNamesKannadaAbbr = []string{
+ "\u0CAD\u0CBE\u0CA8\u0CC1.",
+ "\u0CB8\u0CCB\u0CAE.",
+ "\u0CAE\u0C82\u0C97\u0CB3.",
+ "\u0CAC\u0CC1\u0CA7.",
+ "\u0C97\u0CC1\u0CB0\u0CC1.",
+ "\u0CB6\u0CC1\u0C95\u0CCD\u0CB0.",
+ "\u0CB6\u0CA8\u0CBF.",
+ }
+ // weekdayNamesKashmiri list the weekday name in the Kashmiri.
+ weekdayNamesKashmiri = []string{
+ "\u0627\u064E\u062A\u06BE\u0648\u0627\u0631",
+ "\u0698\u0654\u0646\u062F\u0631\u0655\u0631\u0648\u0627\u0631",
+ "\u0628\u06C6\u0645\u0648\u0627\u0631",
+ "\u0628\u0648\u062F\u0648\u0627\u0631",
+ "\u0628\u0631\u0620\u0633\u0648\u0627\u0631",
+ "\u062C\u064F\u0645\u06C1",
+ "\u0628\u0679\u0648\u0627\u0631",
+ }
+ // weekdayNamesKashmiriAbbr list the weekday name abbreviations in the Kashmiri.
+ weekdayNamesKashmiriAbbr = []string{
+ "\u0622\u062A\u06BE\u0648\u0627\u0631",
+ "\u0698\u0654\u0646\u062F\u0655\u0631\u0648\u0627\u0631",
+ "\u0628\u06C6\u0645\u0648\u0627\u0631",
+ "\u0628\u0648\u062F\u0648\u0627\u0631",
+ "\u0628\u0631\u0620\u0633\u0648\u0627\u0631",
+ "\u062C\u064F\u0645\u06C1",
+ "\u0628\u0679\u0648\u0627\u0631",
+ }
+ // weekdayNamesKazakh list the weekday name in the Kazakh.
+ weekdayNamesKazakh = []string{
+ "\u0436\u0435\u043A\u0441\u0435\u043D\u0431\u0456",
+ "\u0434\u04AF\u0439\u0441\u0435\u043D\u0431\u0456",
+ "\u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456",
+ "\u0441\u04D9\u0440\u0441\u0435\u043D\u0431\u0456",
+ "\u0431\u0435\u0439\u0441\u0435\u043D\u0431\u0456",
+ "\u0436\u04B1\u043C\u0430",
+ "\u0441\u0435\u043D\u0431\u0456",
+ }
+ // weekdayNamesKazakhAbbr list the weekday name abbreviations in the Kazakh.
+ weekdayNamesKazakhAbbr = []string{
+ "\u0436\u0435\u043A",
+ "\u0434\u04AF\u0439",
+ "\u0441\u0435\u0439",
+ "\u0441\u04D9\u0440",
+ "\u0431\u0435\u0439",
+ "\u0436\u04B1\u043C",
+ "\u0441\u0435\u043D",
+ }
+ // weekdayNamesKhmer list the weekday name in the Khmer.
+ weekdayNamesKhmer = []string{
+ "\u1790\u17D2\u1784\u17C3\u17A2\u17B6\u1791\u17B7\u178F\u17D2\u1799",
+ "\u1790\u17D2\u1784\u17C3\u1785\u17D0\u1793\u17D2\u1791",
+ "\u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A",
+ "\u1790\u17D2\u1784\u17C3\u1796\u17BB\u1792",
+ "\u1790\u17D2\u1784\u17C3\u1796\u17D2\u179A\u17A0\u179F\u17D2\u1794\u178F\u17B7\u17CD",
+ "\u1790\u17D2\u1784\u17C3\u179F\u17BB\u1780\u17D2\u179A",
+ "\u1790\u17D2\u1784\u17C3\u179F\u17C5\u179A\u17CD",
+ }
+ // weekdayNamesKhmerAbbr list the weekday name abbreviations in the Khmer.
+ weekdayNamesKhmerAbbr = []string{
+ "\u17A2\u17B6\u1791\u17B7.",
+ "\u1785.",
+ "\u17A2.",
+ "\u1796\u17BB",
+ "\u1796\u17D2\u179A\u17A0.",
+ "\u179F\u17BB.",
+ "\u179F.",
+ }
+ // weekdayNamesKiche list the weekday name in the Kiche.
+ weekdayNamesKiche = []string{"juq'ij", "kaq'ij", "oxq'ij", "kajq'ij", "joq'ij", "waqq'ij", "wuqq'ij"}
+ // weekdayNamesKicheAbbr list the weekday name abbreviations in the Kiche.
+ weekdayNamesKicheAbbr = []string{"juq'", "kaq'", "oxq'", "kajq'", "joq'", "waqq'", "wuqq'"}
+ // weekdayNamesKinyarwanda list the weekday name in the Kinyarwanda.
+ weekdayNamesKinyarwanda = []string{"Ku cyumweru", "Ku wa mbere", "Ku wa kabiri", "Ku wa gatatu", "Ku wa kane", "Ku wa gatanu", "Ku wa gatandatu"}
+ // weekdayNamesKinyarwandaAbbr list the weekday name abbreviations in the Kinyarwanda.
+ weekdayNamesKinyarwandaAbbr = []string{"cyu.", "mbe.", "kab.", "gat.", "kan.", "gnu.", "gat."}
+ // weekdayNamesKiswahili list the weekday name in the Kiswahili.
+ weekdayNamesKiswahili = []string{"Jumapili", "Jumatatu", "Jumanne", "Jumatano", "Alhamisi", "Ijumaa", "Jumamosi"}
+ // weekdayNamesKiswahiliAbbr list the weekday name abbreviations in the Kiswahili.
+ weekdayNamesKiswahiliAbbr = []string{"Jpl", "Jtt", "Jnn", "Jtn", "Alh", "Ijm", "Jms"}
+ // weekdayNamesKonkani list the weekday name in the Konkani.
+ weekdayNamesKonkani = []string{
+ "\u0906\u092F\u0924\u093E\u0930",
+ "\u0938\u094B\u092E\u093E\u0930",
+ "\u092E\u0902\u0917\u0933\u093E\u0930",
+ "\u092C\u0941\u0927\u0935\u093E\u0930",
+ "\u092C\u093F\u0930\u0947\u0938\u094D\u0924\u093E\u0930",
+ "\u0938\u0941\u0915\u094D\u0930\u093E\u0930",
+ "\u0936\u0947\u0928\u0935\u093E\u0930",
+ }
+ // weekdayNamesKonkaniAbbr list the weekday name abbreviations in the Konkani.
+ weekdayNamesKonkaniAbbr = []string{
+ "\u0906\u092F.",
+ "\u0938\u094B\u092E.",
+ "\u092E\u0902\u0917\u0933.",
+ "\u092C\u0941\u0927.",
+ "\u092C\u093F\u0930\u0947.",
+ "\u0938\u0941\u0915\u094D\u0930.",
+ "\u0936\u0947\u0928.",
+ }
+ // weekdayNamesKorean list the weekday name in the Korean.
+ weekdayNamesKorean = []string{"일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"}
+ // weekdayNamesKoreanAbbr list the weekday name abbreviations in the Korean.
+ weekdayNamesKoreanAbbr = []string{"일", "월", "화", "수", "목", "금", "토"}
+ // weekdayNamesKyrgyz list the weekday name in the Kyrgyz.
+ weekdayNamesKyrgyz = []string{
+ "\u0436\u0435\u043A\u0448\u0435\u043C\u0431\u0438",
+ "\u0434\u04AF\u0439\u0448\u04E9\u043C\u0431\u04AF",
+ "\u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438",
+ "\u0448\u0430\u0440\u0448\u0435\u043C\u0431\u0438",
+ "\u0431\u0435\u0439\u0448\u0435\u043C\u0431\u0438",
+ "\u0436\u0443\u043C\u0430",
+ "\u0438\u0448\u0435\u043C\u0431\u0438",
+ }
+ // weekdayNamesKyrgyzAbbr list the weekday name abbreviations in the Kyrgyz.
+ weekdayNamesKyrgyzAbbr = []string{
+ "\u0436\u0435\u043A.",
+ "\u0434\u04AF\u0439.",
+ "\u0448\u0435\u0439\u0448.",
+ "\u0448\u0430\u0440\u0448.",
+ "\u0431\u0435\u0439\u0448.",
+ "\u0436\u0443\u043C\u0430",
+ "\u0438\u0448\u043C.",
+ }
+ // weekdayNamesLao list the weekday name in the Lao.
+ weekdayNamesLao = []string{
+ "\u0EA7\u0EB1\u0E99\u0EAD\u0EB2\u0E97\u0EB4\u0E94",
+ "\u0EA7\u0EB1\u0E99\u0E88\u0EB1\u0E99",
+ "\u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99",
+ "\u0EA7\u0EB1\u0E99\u0E9E\u0EB8\u0E94",
+ "\u0EA7\u0EB1\u0E99\u0E9E\u0EB0\u0EAB\u0EB1\u0E94",
+ "\u0EA7\u0EB1\u0E99\u0EAA\u0EB8\u0E81",
+ "\u0EA7\u0EB1\u0E99\u0EC0\u0EAA\u0EBB\u0EB2",
+ }
+ // weekdayNamesLaoAbbr list the weekday name abbreviations in the Lao.
+ weekdayNamesLaoAbbr = []string{
+ "\u0EAD\u0EB2\u0E97\u0EB4\u0E94",
+ "\u0E88\u0EB1\u0E99",
+ "\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99",
+ "\u0E9E\u0EB8\u0E94",
+ "\u0E9E\u0EB0\u0EAB\u0EB1\u0E94",
+ "\u0EAA\u0EB8\u0E81",
+ "\u0EC0\u0EAA\u0EBB\u0EB2",
+ }
+ // weekdayNamesLatin list the weekday name in the Latin.
+ weekdayNamesLatin = []string{"Solis", "Lunae", "Martis", "Mercurii", "Jovis", "Veneris", "Saturni"}
+ // weekdayNamesLatinAbbr list the weekday name abbreviations in the Latin.
+ weekdayNamesLatinAbbr = []string{"Sol", "Lun", "Mar", "Mer", "Jov", "Ven", "Sat"}
+ // weekdayNamesLatvian list the weekday name in the Latvian.
+ weekdayNamesLatvian = []string{"svētdiena", "pirmdiena", "otrdiena", "trešdiena", "ceturtdiena", "piektdiena", "sestdiena"}
+ // weekdayNamesLatvianAbbr list the weekday name abbreviations in the Latvian.
+ weekdayNamesLatvianAbbr = []string{"svētd.", "pirmd.", "otrd.", "trešd.", "ceturtd.", "piektd.", "sestd."}
+ // weekdayNamesLithuanian list the weekday name in the Lithuanian.
+ weekdayNamesLithuanian = []string{"sekmadienis", "pirmadienis", "antradienis", "trečiadienis", "ketvirtadienis", "penktadienis", "šeštadienis"}
+ // weekdayNamesLithuanianAbbr list the weekday name abbreviations in the Lithuanian.
+ weekdayNamesLithuanianAbbr = []string{"sk", "pr", "an", "tr", "kt", "pn", "št"}
+ // weekdayNamesLowerSorbian list the weekday name in the Lower Sorbian.
+ weekdayNamesLowerSorbian = []string{"nje\u017Aela", "ponje\u017Aele", "wa\u0142tora", "srjoda", "stw%F3rtk", "p\u011Btk", "sobota"}
+ // weekdayNamesLowerSorbianAbbr list the weekday name abbreviations in the Luxembourgish.
+ weekdayNamesLowerSorbianAbbr = []string{"nje", "pon", "wa\u0142", "srj", "stw", "p\u011Bt", "sob"}
+ // weekdayNamesLuxembourgish list the weekday name in the Luxembourgish
+ weekdayNamesLuxembourgish = []string{"Sonndeg", "Méindeg", "Dënschdeg", "Mëttwoch", "Donneschdeg", "Freideg", "Samschdeg"}
+ // weekdayNamesLuxembourgishAbbr list the weekday name abbreviations in the Lower Sorbian.
+ weekdayNamesLuxembourgishAbbr = []string{"Son", "Méi", "Dën", "Mët", "Don", "Fre", "Sam"}
+ // weekdayNamesMacedonian list the weekday name in the Macedonian.
+ weekdayNamesMacedonian = []string{
+ "\u043D\u0435\u0434\u0435\u043B\u0430",
+ "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u043D\u0438\u043A",
+ "\u0432\u0442\u043E\u0440\u043D\u0438\u043A",
+ "\u0441\u0440\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0440\u0442\u043E\u043A",
+ "\u043F\u0435\u0442\u043E\u043A",
+ "\u0441\u0430\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesMacedonianAbbr list the weekday name abbreviations in the Macedonian.
+ weekdayNamesMacedonianAbbr = []string{
+ "\u043D\u0435\u0434.",
+ "\u043F\u043E\u043D.",
+ "\u0432\u0442.",
+ "\u0441\u0440\u0435.",
+ "\u0447\u0435\u0442.",
+ "\u043F\u0435\u0442.",
+ "\u0441\u0430\u0431.",
+ }
+ // weekdayNamesMalay list the weekday name in the Malay.
+ weekdayNamesMalay = []string{"Ahad", "Isnin", "Selasa", "Rabu", "Khamis", "Jumaat", "Sabtu"}
+ // weekdayNamesMalayAbbr list the weekday name abbreviations in the Lower Sorbian.
+ weekdayNamesMalayAbbr = []string{"Ahd", "Isn", "Sel", "Rab", "Kha", "Jum", "Sab"}
+ // weekdayNamesMalayalam list the weekday name in the Malayalam.
+ weekdayNamesMalayalam = []string{
+ "\u0D1E\u0D3E\u0D2F\u0D31\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ "\u0D24\u0D3F\u0D19\u0D4D\u0D15\u0D33\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ "\u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A",
+ "\u0D2C\u0D41\u0D27\u0D28\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ "\u0D35\u0D4D\u0D2F\u0D3E\u0D34\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ "\u0D35\u0D46\u0D33\u0D4D\u0D33\u0D3F\u0D2F\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ "\u0D36\u0D28\u0D3F\u0D2F\u0D3E\u0D34\u0D4D\u200C\u0D1A",
+ }
+ // weekdayNamesMalayalamAbbr list the weekday name abbreviations in the Malayalam.
+ weekdayNamesMalayalamAbbr = []string{
+ "\u0D1E\u0D3E\u0D2F\u0D7C",
+ "\u0D24\u0D3F\u0D19\u0D4D\u0D15\u0D7E",
+ "\u0D1A\u0D4A\u0D35\u0D4D\u0D35",
+ "\u0D2C\u0D41\u0D27\u0D7B",
+ "\u0D35\u0D4D\u0D2F\u0D3E\u0D34\u0D02",
+ "\u0D35\u0D46\u0D33\u0D4D\u0D33\u0D3F",
+ "\u0D36\u0D28\u0D3F",
+ }
+ // weekdayNamesMaltese list the weekday name in the Maltese.
+ weekdayNamesMaltese = []string{"Il-\u0126add", "It-Tnejn", "It-Tlieta", "L-Erbg\u0127a", "Il-\u0126amis", "Il-\u0120img\u0127a", "Is-Sibt"}
+ // weekdayNamesMalteseAbbr list the weekday name abbreviations in the Maltese.
+ weekdayNamesMalteseAbbr = []string{"\u0126ad", "Tne", "Tli", "Erb", "\u0126am", "\u0120im", "Sib"}
+ // weekdayNamesMaori list the weekday name in the Maori.
+ weekdayNamesMaori = []string{"Rātapu", "Rāhina", "Rātū", "Rāapa", "Rāpare", "Rāmere", "Rāhoroi"}
+ // weekdayNamesMaoriAbbr list the weekday name abbreviations in the Maori.
+ weekdayNamesMaoriAbbr = []string{"Ta", "Hi", "Tū", "Apa", "Pa", "Me", "Ho"}
+ // weekdayNamesMapudungun list the weekday name in the Mapudungun.
+ weekdayNamesMapudungun = []string{"Kiñe Ante", "Epu Ante", "Kila Ante", "Meli Ante", "Kechu Ante", "Cayu Ante", "Regle Ante"}
+ // weekdayNamesMapudungunAbbr list the weekday name abbreviations in the Mapudungun.
+ weekdayNamesMapudungunAbbr = []string{"Kiñe", "Epu", "Kila", "Meli", "Kechu", "Cayu", "Regle"}
+ // weekdayNamesMarathi list the weekday name in the Marathi.
+ weekdayNamesMarathi = []string{
+ "\u0930\u0935\u093F\u0935\u093E\u0930",
+ "\u0938\u094B\u092E\u0935\u093E\u0930",
+ "\u092E\u0902\u0917\u0933\u0935\u093E\u0930",
+ "\u092C\u0941\u0927\u0935\u093E\u0930",
+ "\u0917\u0941\u0930\u0941\u0935\u093E\u0930",
+ "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930",
+ "\u0936\u0928\u093F\u0935\u093E\u0930",
+ }
+ // weekdayNamesMarathiAbbr list the weekday name abbreviations in the Marathi.
+ weekdayNamesMarathiAbbr = []string{
+ "\u0930\u0935\u093F.",
+ "\u0938\u094B\u092E.",
+ "\u092E\u0902\u0917\u0933.",
+ "\u092C\u0941\u0927.",
+ "\u0917\u0941\u0930\u0941.",
+ "\u0936\u0941\u0915\u094D\u0930.",
+ "\u0936\u0928\u093F.",
+ }
+ // weekdayNamesMohawk list the weekday name in the Mohawk.
+ weekdayNamesMohawk = []string{"Awentatokentì:ke", "Awentataón'ke", "Ratironhia'kehronòn:ke", "Soséhne", "Okaristiiáhne", "Ronwaia'tanentaktonhne", "Entákta"}
+ // weekdayNamesMongolian list the weekday name in the Mongolian.
+ weekdayNamesMongolian = []string{
+ "\u043D\u044F\u043C",
+ "\u0434\u0430\u0432\u0430\u0430",
+ "\u043C\u044F\u0433\u043C\u0430\u0440",
+ "\u043B\u0445\u0430\u0433\u0432\u0430",
+ "\u043F\u04AF\u0440\u044D\u0432",
+ "\u0431\u0430\u0430\u0441\u0430\u043D",
+ "\u0431\u044F\u043C\u0431\u0430",
+ }
+ // weekdayNamesMongolianAbbr list the weekday name abbreviations in the Mongolian.
+ weekdayNamesMongolianAbbr = []string{
+ "\u041D\u044F",
+ "\u0414\u0430",
+ "\u041C\u044F",
+ "\u041B\u0445",
+ "\u041F\u04AF",
+ "\u0411\u0430",
+ "\u0411\u044F",
+ }
+ // weekdayNamesMongolianCyrlAbbr list the weekday name abbreviations in the Mongolian (Cyrillic).
+ weekdayNamesMongolianCyrlAbbr = []string{
+ "\u041D\u044F",
+ "\u0414\u0430",
+ "\u041C\u044F",
+ "\u041B\u0445\u0430",
+ "\u041F\u04AF",
+ "\u0411\u0430",
+ "\u0411\u044F",
+ }
+ // weekdayNamesTraditionalMongolian list the weekday name abbreviations in the Traditional Mongolian.
+ weekdayNamesTraditionalMongolian = []string{
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1821\u1833\u1826\u1837",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1828\u1822\u182D\u1821\u1828",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u182C\u1823\u1836\u1820\u1837",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u182D\u1824\u1837\u182A\u1820\u1828",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1832\u1820\u182A\u1824\u1828",
+ "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828",
+ }
+ // weekdayNamesTraditionalMongolianMN list the weekday name abbreviations in the Traditional Mongolian MN.
+ weekdayNamesTraditionalMongolianMN = []string{
+ "\u1828\u1822\u182E\u180E\u1820",
+ "\u1833\u1820\u1838\u1820",
+ "\u182E\u1822\u182D\u182E\u1820\u1837",
+ "\u1840\u1820\u182D\u182A\u1820",
+ "\u182B\u1826\u1837\u182A\u1826",
+ "\u182A\u1820\u1830\u1820\u1829",
+ "\u182A\u1822\u182E\u182A\u1820",
+ }
+ // weekdayNamesNepali list the weekday name in the Nepali.
+ weekdayNamesNepali = []string{
+ "\u0906\u0907\u0924\u0935\u093E\u0930",
+ "\u0938\u094B\u092E\u0935\u093E\u0930",
+ "\u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930",
+ "\u092C\u0941\u0927\u0935\u093E\u0930",
+ "\u092C\u093F\u0939\u0940\u0935\u093E\u0930",
+ "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930",
+ "\u0936\u0928\u093F\u0935\u093E\u0930",
+ }
+ // weekdayNamesNepaliAbbr list the weekday name abbreviations in the Nepali.
+ weekdayNamesNepaliAbbr = []string{
+ "\u0906\u0907\u0924",
+ "\u0938\u094B\u092E",
+ "\u092E\u0919\u094D\u0917\u0932",
+ "\u092C\u0941\u0927",
+ "\u092C\u093F\u0939\u0940",
+ "\u0936\u0941\u0915\u094D\u0930",
+ "\u0936\u0928\u093F",
+ }
+ // weekdayNamesNepaliIN list the weekday name in the Nepali India.
+ weekdayNamesNepaliIN = []string{
+ "\u0906\u0907\u0924\u092C\u093E\u0930",
+ "\u0938\u094B\u092E\u092C\u093E\u0930",
+ "\u092E\u0919\u094D\u0917\u0932\u092C\u093E\u0930",
+ "\u092C\u0941\u0927\u092C\u093E\u0930",
+ "\u092C\u093F\u0939\u093F\u092C\u093E\u0930",
+ "\u0936\u0941\u0915\u094D\u0930\u092C\u093E\u0930",
+ "\u0936\u0928\u093F\u092C\u093E\u0930",
+ }
+ // weekdayNamesNepaliINAbbr list the weekday name abbreviations in the Nepali India.
+ weekdayNamesNepaliINAbbr = []string{
+ "\u0906\u0907\u0924",
+ "\u0938\u094B\u092E",
+ "\u092E\u0919\u094D\u0917\u0932",
+ "\u092C\u0941\u0927",
+ "\u092C\u093F\u0939\u093F",
+ "\u0936\u0941\u0915\u094D\u0930",
+ "\u0936\u0928\u093F",
+ }
+ // weekdayNamesNorwegian list the weekday name in the Norwegian.
+ weekdayNamesNorwegian = []string{"s%F8ndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "l%F8rdag"}
+ // weekdayNamesNorwegianAbbr list the weekday name abbreviations in the Norwegian.
+ weekdayNamesNorwegianAbbr = []string{"s%F8n.", "man.", "tir.", "ons.", "tor.", "fre.", "l%F8r."}
+ // weekdayNamesNorwegianNOAbbr list the weekday name abbreviations in the Norwegian Norway.
+ weekdayNamesNorwegianNOAbbr = []string{"s%F8n", "man", "tir", "ons", "tor", "fre", "l%F8r"}
+ // weekdayNamesNorwegianNynorsk list the weekday name abbreviations in the Norwegian Nynorsk.
+ weekdayNamesNorwegianNynorsk = []string{"s%F8ndag", "m%E5ndag", "tysdag", "onsdag", "torsdag", "fredag", "laurdag"}
+ // weekdayNamesNorwegianNynorskAbbr list the weekday name abbreviations in the Norwegian Nynorsk.
+ weekdayNamesNorwegianNynorskAbbr = []string{"s%F8n", "m%E5n", "tys", "ons", "tor", "fre", "lau"}
+ // weekdayNamesOccitan list the weekday name abbreviations in the Occitan.
+ weekdayNamesOccitan = []string{"dimenge", "diluns", "dimarts", "dimècres", "dijòus", "divendres", "dissabte"}
+ // weekdayNamesOccitanAbbr list the weekday name abbreviations in the Occitan.
+ weekdayNamesOccitanAbbr = []string{"dg.", "dl.", "dma.", "dmc.", "dj.", "dv.", "ds."}
+ // weekdayNamesOdia list the weekday name in the Odia.
+ weekdayNamesOdia = []string{
+ "\u0B30\u0B2C\u0B3F\u0B2C\u0B3E\u0B30",
+ "\u0B38\u0B4B\u0B2E\u0B2C\u0B3E\u0B30",
+ "\u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30",
+ "\u0B2C\u0B41\u0B27\u0B2C\u0B3E\u0B30",
+ "\u0B17\u0B41\u0B30\u0B41\u0B2C\u0B3E\u0B30",
+ "\u0B36\u0B41\u0B15\u0B4D\u0B30\u0B2C\u0B3E\u0B30",
+ "\u0B36\u0B28\u0B3F\u0B2C\u0B3E\u0B30",
+ }
+ // weekdayNamesOdiaAbbr list the weekday name abbreviations in the Odia.
+ weekdayNamesOdiaAbbr = []string{
+ "\u0B30\u0B2C\u0B3F.",
+ "\u0B38\u0B4B\u0B2E.",
+ "\u0B2E\u0B19\u0B4D\u0B17\u0B33.",
+ "\u0B2C\u0B41\u0B27.",
+ "\u0B17\u0B41\u0B30\u0B41.",
+ "\u0B36\u0B41\u0B15\u0B4D\u0B30.",
+ "\u0B36\u0B28\u0B3F.",
+ }
+ // weekdayNamesOromo list the weekday name abbreviations in the Oromo.
+ weekdayNamesOromo = []string{"Dilbata", "Wiixata", "Qibxata", "Roobii", "Kamiisa", "Jimaata", "Sanbata"}
+ // weekdayNamesOromoAbbr list the weekday name abbreviations in the Oromo.
+ weekdayNamesOromoAbbr = []string{"Dil", "Wix", "Qib", "Rob", "Kam", "Jim", "San"}
+ // weekdayNamesPashto list the weekday name in the Pashto.
+ weekdayNamesPashto = []string{
+ "\u064A\u0648\u0646\u06CD",
+ "\u062F\u0648\u0646\u06CD",
+ "\u062F\u0631\u06D0\u0646\u06CD",
+ "\u0685\u0644\u0631\u0646\u06CD",
+ "\u067E\u064A\u0646\u0681\u0646\u06CD",
+ "\u062C\u0645\u0639\u0647",
+ "\u0627\u0648\u0646\u06CD",
+ }
+ // weekdayNamesPersian list the weekday name in the Persian.
+ weekdayNamesPersian = []string{
+ "\u064A\u0643\u0634\u0646\u0628\u0647",
+ "\u062F\u0648\u0634\u0646\u0628\u0647",
+ "\u0633\u0647%A0\u0634\u0646\u0628\u0647",
+ "\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647",
+ "\u067E\u0646\u062C\u0634\u0646\u0628\u0647",
+ "\u062C\u0645\u0639\u0647",
+ "\u0634\u0646\u0628\u0647",
+ }
+ // weekdayNamesPolish list the weekday name abbreviations in the Polish.
+ weekdayNamesPolish = []string{"niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"}
+ // weekdayNamesPolishAbbr list the weekday name abbreviations in the Polish.
+ weekdayNamesPolishAbbr = []string{"niedz.", "pon.", "wt.", "śr.", "czw.", "pt.", "sob."}
+ // weekdayNamesPortuguese list the weekday name abbreviations in the Portuguese.
+ weekdayNamesPortuguese = []string{"domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"}
+ // weekdayNamesPortugueseAbbr list the weekday name abbreviations in the Portuguese.
+ weekdayNamesPortugueseAbbr = []string{"dom", "seg", "ter", "qua", "qui", "sex", "sáb"}
+ // weekdayNamesPunjabi list the weekday name in the Punjabi.
+ weekdayNamesPunjabi = []string{
+ "\u0A10\u0A24\u0A35\u0A3E\u0A30",
+ "\u0A38\u0A4B\u0A2E\u0A35\u0A3E\u0A30",
+ "\u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30",
+ "\u0A2C\u0A41\u0A71\u0A27\u0A35\u0A3E\u0A30",
+ "\u0A35\u0A40\u0A30\u0A35\u0A3E\u0A30",
+ "\u0A38\u0A3C\u0A41\u0A71\u0A15\u0A30\u0A35\u0A3E\u0A30",
+ "\u0A38\u0A3C\u0A28\u0A3F\u0A71\u0A1A\u0A30\u0A35\u0A3E\u0A30",
+ }
+ // weekdayNamesPunjabiAbbr list the weekday name abbreviations in the Punjabi.
+ weekdayNamesPunjabiAbbr = []string{
+ "\u0A10\u0A24.",
+ "\u0A38\u0A4B\u0A2E.",
+ "\u0A2E\u0A70\u0A17\u0A32.",
+ "\u0A2C\u0A41\u0A71\u0A27.",
+ "\u0A35\u0A40\u0A30.",
+ "\u0A38\u0A3C\u0A41\u0A15\u0A30.",
+ "\u0A38\u0A3C\u0A28\u0A3F\u0A71\u0A1A\u0A30.",
+ }
+ // weekdayNamesPunjabiArab list the weekday name in the Punjabi Arab.
+ weekdayNamesPunjabiArab = []string{
+ "\u067E\u064A\u0631",
+ "\u0645\u0646\u06AF\u0644",
+ "\u0628\u062F\u06BE",
+ "\u062C\u0645\u0639\u0631\u0627\u062A",
+ "\u062C\u0645\u0639\u0647",
+ "\u0647\u0641\u062A\u0647",
+ "\u0627\u062A\u0648\u0627\u0631",
+ }
+ // weekdayNamesQuechua list the weekday name abbreviations in the Quechua.
+ weekdayNamesQuechua = []string{"intichaw", "killachaw", "atipachaw", "quyllurchaw", "Ch' askachaw", "Illapachaw", "k'uychichaw"}
+ // weekdayNamesQuechuaAbbr list the weekday name abbreviations in the Quechua.
+ weekdayNamesQuechuaAbbr = []string{"int", "kil", "ati", "quy", "Ch'", "Ill", "k'u"}
+ // weekdayNamesQuechuaEcuador list the weekday name abbreviations in the Quechua Ecuador.
+ weekdayNamesQuechuaEcuador = []string{"inti", "awaki", "wanra", "chillay", "kullka", "chaska", "wakma"}
+ // weekdayNamesQuechuaEcuadorAbbr list the weekday name abbreviations in the Quechua Ecuador.
+ weekdayNamesQuechuaEcuadorAbbr = []string{"int", "awk", "wan", "chy", "kuk", "cha", "wak"}
+ // weekdayNamesQuechuaPeru list the weekday name abbreviations in the Quechua Peru.
+ weekdayNamesQuechuaPeru = []string{"Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"}
+ // weekdayNamesQuechuaPeruAbbr list the weekday name abbreviations in the Quechua Peru.
+ weekdayNamesQuechuaPeruAbbr = []string{"Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sab"}
+ // weekdayNamesRomanian list the weekday name abbreviations in the Romanian.
+ weekdayNamesRomanian = []string{"duminică", "luni", "marți", "miercuri", "joi", "vineri", "sâmbătă"}
+ // weekdayNamesRomanianAbbr list the weekday name abbreviations in the Romanian.
+ weekdayNamesRomanianAbbr = []string{"dum.", "lun.", "mar.", "mie.", "joi", "vin.", "sâm."}
+ // weekdayNamesRomanianMoldovaAbbr list the weekday name abbreviations in the Romanian Moldova.
+ weekdayNamesRomanianMoldovaAbbr = []string{"Du", "Lu", "Mar", "Mie", "Jo", "Vi", "Sâ"}
+ // weekdayNamesRomansh list the weekday name abbreviations in the Romansh.
+ weekdayNamesRomansh = []string{"dumengia", "glindesdi", "mardi", "mesemna", "gievgia", "venderdi", "sonda"}
+ // weekdayNamesRomanshAbbr list the weekday name abbreviations in the Romansh.
+ weekdayNamesRomanshAbbr = []string{"du", "gli", "ma", "me", "gie", "ve", "so"}
+ // weekdayNamesRussian list the weekday name abbreviations in the Russian.
+ weekdayNamesRussian = []string{
+ "\u0432\u043E\u0441\u043A\u0440\u0435\u0441\u0435\u043D\u044C\u0435",
+ "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A",
+ "\u0432\u0442\u043E\u0440\u043D\u0438\u043A",
+ "\u0441\u0440\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0435\u0440\u0433",
+ "\u043F\u044F\u0442\u043D\u0438\u0446\u0430",
+ "\u0441\u0443\u0431\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesRussianAbbr list the weekday name abbreviations in the Russian.
+ weekdayNamesRussianAbbr = []string{
+ "\u0412\u0441",
+ "\u041F\u043D",
+ "\u0412\u0442",
+ "\u0421\u0440",
+ "\u0427\u0442",
+ "\u041F\u0442",
+ "\u0421\u0431",
+ }
+ // weekdayNamesSakha list the weekday name abbreviations in the Sakha.
+ weekdayNamesSakha = []string{
+ "\u04E8\u0440\u04E9\u0431\u04AF\u043B",
+ "\u044D\u043D\u0438\u0434\u0438\u044D\u043D\u043D\u044C\u0438\u043A",
+ "\u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A",
+ "\u0421\u044D\u0440\u044D\u0434\u044D\u044D",
+ "\u0427\u044D\u043F\u043F\u0438\u044D\u0440",
+ "\u0411\u044D\u044D\u0442\u0438\u043D\u0441\u044D",
+ "\u0421\u0443\u0431\u0443\u043E\u0442\u0430",
+ }
+ // weekdayNamesSakhaAbbr list the weekday name abbreviations in the Sakha.
+ weekdayNamesSakhaAbbr = []string{
+ "\u04E8\u0440",
+ "\u0431\u043D",
+ "\u043E\u043F",
+ "\u0441\u044D",
+ "\u0447\u043F",
+ "\u0431\u044D",
+ "\u0441\u0431",
+ }
+ // weekdayNamesSami list the weekday name abbreviations in the Sami.
+ weekdayNamesSami = []string{"pasepeivi", "vuossargâ", "majebargâ", "koskokko", "tuorâstâh", "vástuppeivi", "lávurdâh"}
+ // weekdayNamesSamiAbbr list the weekday name abbreviations in the Sami.
+ weekdayNamesSamiAbbr = []string{"pas", "vuo", "maj", "kos", "tuo", "vás", "láv"}
+ // weekdayNamesSamiSamiLule list the weekday name abbreviations in the Sami (SamiLule).
+ weekdayNamesSamiSamiLule = []string{"ájllek", "mánnodahka", "dijstahka", "gasskavahkko", "duorastahka", "bierjjedahka", "lávvodahka"}
+ // weekdayNamesSamiSamiLuleAbbr list the weekday name abbreviations in the Sami (SamiLule).
+ weekdayNamesSamiSamiLuleAbbr = []string{"ájl", "mán", "dis", "gas", "duor", "bier", "láv"}
+ // weekdayNamesSamiSweden list the weekday name abbreviations in the Sami (Lule) Sweden.
+ weekdayNamesSamiSweden = []string{"sådnåbiejvve", "mánnodahka", "dijstahka", "gasskavahkko", "duorastahka", "bierjjedahka", "lávvodahka"}
+ // weekdayNamesSamiSwedenAbbr list the weekday name abbreviations in the Sami (Lule) Sweden.
+ weekdayNamesSamiSwedenAbbr = []string{"såd", "mán", "dis", "gas", "duor", "bier", "láv"}
+ // weekdayNamesSamiNorthern list the weekday name abbreviations in the Sami (Northern).
+ weekdayNamesSamiNorthern = []string{"sotnabeaivi", "vuossárga", "maŋŋebárga", "gaskavahkku", "duorasdat ", "bearjadat", "lávvardat"}
+ // weekdayNamesSamiNorthernFIAbbr list the weekday name abbreviations in the Sami (Northern).
+ weekdayNamesSamiNorthernAbbr = []string{"sotn", "vuos", "maŋ", "gask", "duor", "bear", "láv"}
+ // weekdayNamesSamiNorthernFI list the weekday name abbreviations in the Sami (Northern) Finland.
+ weekdayNamesSamiNorthernFI = []string{"sotnabeaivi", "vuossárga", "maŋŋebárga", "gaskavahkku", "duorastat", "bearjadat", "lávvardat"}
+ // weekdayNamesSamiNorthernFIAbbr list the weekday name abbreviations in the Sami (Northern) Finland.
+ weekdayNamesSamiNorthernFIAbbr = []string{"so", "má", "di", "ga", "du", "be", "lá"}
+ // weekdayNamesSamiNorthernSE list the weekday name abbreviations in the Sami (Northern) Sweden.
+ weekdayNamesSamiNorthernSE = []string{"sotnabeaivi", "mánnodat", "disdat", "gaskavahkku", "duorastat", "bearjadat", "lávvardat"}
+ // weekdayNamesSamiNorthernSEAbbr list the weekday name abbreviations in the Sami (Northern) Sweden.
+ weekdayNamesSamiNorthernSEAbbr = []string{"sotn", "mán", "dis", "gask", "duor", "bear", "láv"}
+ // weekdayNamesSamiSkolt list the weekday name abbreviations in the Sami (Skolt).
+ weekdayNamesSamiSkolt = []string{"p%E2%B4sspei%B4vv", "vu%F5ssargg", "m%E2%E2ibargg", "se%E4rad", "neljdpei%B4vv", "pi%E2tn%E2c", "sue%B4vet"}
+ // weekdayNamesSamiSkoltAbbr list the weekday name abbreviations in the Sami (Skolt).
+ weekdayNamesSamiSkoltAbbr = []string{"p%E2", "vu", "m%E2", "se", "ne", "pi", "su"}
+ // weekdayNamesSamiSouthern list the weekday name abbreviations in the Sami (Southern).
+ weekdayNamesSamiSouthern = []string{"aejlege", "m%E5anta", "d%E6jsta", "gaskev%E5hkoe", "duarsta", "bearjadahke", "laavvardahke"}
+ // weekdayNamesSamiSouthernAbbr list the weekday name abbreviations in the Sami (Southern).
+ weekdayNamesSamiSouthernAbbr = []string{"aej", "m%E5a", "d%E6j", "gask", "duar", "bearj", "laav"}
+ // weekdayNamesSanskrit list the weekday name abbreviations in the Sanskrit.
+ weekdayNamesSanskrit = []string{
+ "\u0930\u0935\u093F\u0935\u093E\u0938\u0930\u0903",
+ "\u0938\u094B\u092E\u0935\u093E\u0938\u0930\u0903",
+ "\u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903",
+ "\u092C\u0941\u0927\u0935\u093E\u0938\u0930\u0903",
+ "\u0917\u0941\u0930\u0941\u0935\u093E\u0938\u0930%3A",
+ "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0938\u0930\u0903",
+ "\u0936\u0928\u093F\u0935\u093E\u0938\u0930\u0903",
+ }
+ // weekdayNamesSanskritAbbr list the weekday name abbreviations in the Sanskrit.
+ weekdayNamesSanskritAbbr = []string{
+ "\u0930\u0935\u093F",
+ "\u0938\u094B\u092E",
+ "\u092E\u0919\u094D\u0917",
+ "\u092C\u0941\u0927",
+ "\u0917\u0941\u0930\u0941",
+ "\u0936\u0941\u0915\u094D\u0930",
+ "\u0936\u0928\u093F",
+ }
+ // weekdayNamesGaelic list the weekday name abbreviations in the Gaelic.
+ weekdayNamesGaelic = []string{"DiDòmhnaich", "DiLuain", "DiMàirt", "DiCiadain", "DiarDaoin", "DihAoine", "DiSathairne"}
+ // weekdayNamesGaelicAbbr list the weekday name abbreviations in the Gaelic
+ weekdayNamesGaelicAbbr = []string{"DiD", "DiL", "DiM", "DiC", "Dia", "Dih", "DiS"}
+ // weekdayNamesSerbian list the weekday name abbreviations in the Serbian.
+ weekdayNamesSerbian = []string{
+ "\u043D\u0435\u0434\u0435\u0459\u0430",
+ "\u043F\u043E\u043D\u0435\u0434\u0435\u0459\u0430\u043A",
+ "\u0443\u0442\u043E\u0440\u0430\u043A",
+ "\u0441\u0440\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A",
+ "\u043F\u0435\u0442\u0430\u043A",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesSerbianAbbr list the weekday name abbreviations in the Serbian.
+ weekdayNamesSerbianAbbr = []string{
+ "\u043D\u0435\u0434.",
+ "\u043F\u043E\u043D.",
+ "\u0443\u0442.",
+ "\u0441\u0440.",
+ "\u0447\u0435\u0442.",
+ "\u043F\u0435\u0442.",
+ "\u0441\u0443\u0431.",
+ }
+ // weekdayNamesSerbianBA list the weekday name abbreviations in the Serbian (Cyrillic) Bosnia and Herzegovina.
+ weekdayNamesSerbianBA = []string{
+ "\u043D\u0435\u0434\u0458\u0435\u0459\u0430",
+ "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A",
+ "\u0443\u0442\u043E\u0440\u0430\u043A",
+ "\u0441\u0440\u0438\u0458\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A",
+ "\u043F\u0435\u0442\u0430\u043A",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesSerbianBAAbbr list the weekday name abbreviations in the Serbian (Cyrillic) Bosnia and Herzegovina.
+ weekdayNamesSerbianBAAbbr = []string{
+ "\u043D\u0435\u0434",
+ "\u043F\u043E\u043D",
+ "\u0443\u0442\u043E",
+ "\u0441\u0440\u0438",
+ "\u0447\u0435\u0442",
+ "\u043F\u0435\u0442",
+ "\u0441\u0443\u0431",
+ }
+ // weekdayNamesSerbianLatin list the weekday name abbreviations in the Serbian (Latin).
+ weekdayNamesSerbianLatin = []string{"nedelja", "ponedeljak", "utorak", "sreda", "četvrtak", "petak", "subota"}
+ // weekdayNamesSerbianLatinAbbr list the weekday name abbreviations in the Serbian (Latin).
+ weekdayNamesSerbianLatinAbbr = []string{"ned", "pon", "uto", "sre", "čet", "pet", "sub"}
+ // weekdayNamesSerbianLatinBA list the weekday name abbreviations in the Serbian (Latin) Bosnia and Herzegovina.
+ weekdayNamesSerbianLatinBA = []string{"nedjelja", "ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"}
+ // weekdayNamesSerbianLatinBAAbbr list the weekday name abbreviations in the Serbian (Latin) Bosnia and Herzegovina.
+ weekdayNamesSerbianLatinBAAbbr = []string{"ned", "pon", "uto", "sri", "čet", "pet", "sub"}
+ // weekdayNamesSerbianLatinCSAbbr list the weekday name abbreviations in the Serbian (Latin) Serbia and Montenegro (Former).
+ weekdayNamesSerbianLatinCSAbbr = []string{"ned.", "pon.", "uto.", "sre.", "čet.", "pet.", "sub."}
+ // weekdayNamesSerbianLatinME list the weekday name abbreviations in the Serbian (Latin) Montenegro.
+ weekdayNamesSerbianLatinME = []string{"nedjelja", "ponedeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"}
+ // weekdayNamesSerbianME list the weekday name abbreviations in the Serbian (Cyrillic) Montenegro.
+ weekdayNamesSerbianME = []string{
+ "\u043D\u0435\u0434\u0435\u0459\u0430",
+ "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A",
+ "\u0443\u0442\u043E\u0440\u0430\u043A",
+ "\u0441\u0440\u0438\u0458\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A",
+ "\u043F\u0435\u0442\u0430\u043A",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesSesothoSaLeboa list the weekday name abbreviations in the Sesotho sa Leboa.
+ weekdayNamesSesothoSaLeboa = []string{"Lamorena", "Musopologo", "Labobedi", "Laboraro", "Labone", "Labohlano", "Mokibelo"}
+ // weekdayNamesSesothoSaLeboaAbbr list the weekday name abbreviations in the Sesotho sa Leboa.
+ weekdayNamesSesothoSaLeboaAbbr = []string{"Lam", "Moš", "Lbb", "Lbr", "Lbn", "Lbh", "Mok"}
+ // weekdayNamesSetswana list the weekday name abbreviations in the Setswana.
+ weekdayNamesSetswana = []string{"Sontaga", "Mosopulogo", "Labobedi", "Laboraro", "Labone", "Labotlhano", "Matlhatso"}
+ // weekdayNamesSetswanaAbbr list the weekday name abbreviations in the Setswana.
+ weekdayNamesSetswanaAbbr = []string{"Sont.", "Mos.", "Lab.", "Labr.", "Labn.", "Labt.", "Matlh."}
+ // weekdayNamesSindhi list the weekday name abbreviations in the Sindhi.
+ weekdayNamesSindhi = []string{
+ "\u0633\u0648\u0645\u0631",
+ "\u0627\u06B1\u0627\u0631\u0648",
+ "\u0627\u0631\u0628\u0639",
+ "\u062E\u0645\u064A\u0633",
+ "\u062C\u0645\u0639\u0648",
+ "\u0687\u0646\u0687\u0631",
+ "\u0622\u0686\u0631",
+ }
+ // weekdayNamesSindhiAbbr list the weekday name abbreviations in the Sindhi.
+ weekdayNamesSindhiAbbr = []string{
+ "\u0633\u0648",
+ "\u0627\u06B1",
+ "\u0627\u0631",
+ "\u062E\u0645",
+ "\u062C\u0645\u0639\u0648",
+ "\u0687\u0646",
+ "\u0622\u0686",
+ }
+ // weekdayNamesSlovak list the weekday name abbreviations in the Slovak.
+ weekdayNamesSlovak = []string{"nedeľa", "pondelok", "utorok", "streda", "štvrtok", "piatok", "sobota"}
+ // weekdayNamesSlovakAbbr list the weekday name abbreviations in the Slovak.
+ weekdayNamesSlovakAbbr = []string{"ne", "po", "ut", "st", "št", "pi", "so"}
+ // weekdayNamesSlovenian list the weekday name abbreviations in the Slovenian.
+ weekdayNamesSlovenian = []string{"nedelja", "ponedeljek", "torek", "sreda", "četrtek", "petek", "sobota"}
+ // weekdayNamesSlovenianAbbr list the weekday name abbreviations in the Slovenian.
+ weekdayNamesSlovenianAbbr = []string{"ned.", "pon.", "tor.", "sre.", "čet.", "pet.", "sob."}
+ // weekdayNamesSomali list the weekday name abbreviations in the Somali.
+ weekdayNamesSomali = []string{"Axad", "Isniin", "Talaado", "Arbaco", "Khamiis", "Jimco", "Sabti"}
+ // weekdayNamesSomaliAbbr list the weekday name abbreviations in the Somali.
+ weekdayNamesSomaliAbbr = []string{"Axd", "Isn", "Tldo", "Arbc", "Khms", "Jmc", "Sbti"}
+ // weekdayNamesSotho list the weekday name abbreviations in the Sotho.
+ weekdayNamesSotho = []string{"Sontaha", "Mmantaha", "Labobedi", "Laboraru", "Labone", "Labohlane", "Moqebelo"}
+ // weekdayNamesSothoAbbr list the weekday name abbreviations in the Sotho.
+ weekdayNamesSothoAbbr = []string{"Son", "Mma", "Bed", "Rar", "Ne", "Hla", "Moq"}
+ // weekdayNamesSpanish list the weekday name abbreviations in the Spanish.
+ weekdayNamesSpanish = []string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"}
+ // weekdayNamesSpanishAbbr list the weekday name abbreviations in the Spanish Argentina.
+ weekdayNamesSpanishAbbr = []string{"do.", "lu.", "ma.", "mi.", "ju.", "vi.", "sá."}
+ // weekdayNamesSpanishARAbbr list the weekday name abbreviations in the Spanish Argentina.
+ weekdayNamesSpanishARAbbr = []string{"dom.", "lun.", "mar.", "mié.", "jue.", "vie.", "sáb."}
+ // weekdayNamesSpanishUSAbbr list the weekday name abbreviations in the Spanish United States.
+ weekdayNamesSpanishUSAbbr = []string{"dom", "lun", "mar", "mié", "jue", "vie", "sáb"}
+ // weekdayNamesSwedish list the weekday name abbreviations in the Swedish.
+ weekdayNamesSwedish = []string{"söndag", "måndag", "tisdag", "onsdag", "torsdag", "fredag", "lördag"}
+ // weekdayNamesSwedishAbbr list the weekday name abbreviations in the Swedish Argentina.
+ weekdayNamesSwedishAbbr = []string{"sön", "mån", "tis", "ons", "tor", "fre", "lör"}
+ // weekdayNamesSyriac list the weekday name abbreviations in the Syriac.
+ weekdayNamesSyriac = []string{
+ "\u071A\u0715%A0\u0712\u072B\u0712\u0710",
+ "\u072C\u072A\u071D\u0722%A0\u0712\u072B\u0712\u0710",
+ "\u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710",
+ "\u0710\u072A\u0712\u0725\u0710%A0\u0712\u072B\u0712\u0710",
+ "\u071A\u0721\u072B\u0710%A0\u0712\u072B\u0712\u0710",
+ "\u0725\u072A\u0718\u0712\u072C\u0710",
+ "\u072B\u0712\u072C\u0710",
+ }
+ // weekdayNamesSyriacAbbr list the weekday name abbreviations in the Syriac.
+ weekdayNamesSyriacAbbr = []string{
+ "\u070F\u0710%A0\u070F\u0712\u072B",
+ "\u070F\u0712%A0\u070F\u0712\u072B",
+ "\u070F\u0713%A0\u070F\u0712\u072B",
+ "\u070F\u0715%A0\u070F\u0712\u072B",
+ "\u070F\u0717%A0\u070F\u0712\u072B",
+ "\u070F\u0725\u072A\u0718\u0712",
+ "\u070F\u072B\u0712",
+ }
+ // weekdayNamesTajik list the weekday name abbreviations in the Tajik.
+ weekdayNamesTajik = []string{
+ "\u042F\u043A\u0448\u0430\u043D\u0431\u0435",
+ "\u0434\u0443\u0448\u0430\u043D\u0431\u0435",
+ "\u0441\u0435\u0448\u0430\u043D\u0431\u0435",
+ "\u0447\u043E\u0440\u0448\u0430\u043D\u0431\u0435",
+ "\u043F\u0430\u043D\u04B7\u0448\u0430\u043D\u0431\u0435",
+ "\u04B7\u0443\u043C\u044A\u0430",
+ "\u0448\u0430\u043D\u0431\u0435",
+ }
+ // weekdayNamesTajikAbbr list the weekday name abbreviations in the Tajik.
+ weekdayNamesTajikAbbr = []string{
+ "\u043F\u043A\u0448",
+ "\u0434\u0448\u0431",
+ "\u0441\u0448\u0431",
+ "\u0447\u0448\u0431",
+ "\u043F\u0448\u0431",
+ "\u04B7\u0443\u043C",
+ "\u0448\u043D\u0431",
+ }
+ // weekdayNamesTamazight list the weekday name abbreviations in the Tamazight.
+ weekdayNamesTamazight = []string{"lh'ed", "letnayen", "ttlata", "larebâa", "lexmis", "ldjemâa", "ssebt"}
+ // weekdayNamesTamazightAbbr list the weekday name abbreviations in the Tamazight Argentina.
+ weekdayNamesTamazightAbbr = []string{"lh'd", "let", "ttl", "lar", "lex", "ldj", "sse"}
+ // weekdayNamesTamil list the weekday name abbreviations in the Tamil.
+ weekdayNamesTamil = []string{
+ "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BCD\u0BB1\u0BC1\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ "\u0B9A\u0BA9\u0BBF\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8",
+ }
+ // weekdayNamesTamilAbbr list the weekday name abbreviations in the Tamil.
+ weekdayNamesTamilAbbr = []string{
+ "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BC1",
+ "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
+ "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD",
+ "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD",
+ "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0BA9\u0BCD",
+ "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF",
+ "\u0B9A\u0BA9\u0BBF",
+ }
+ // weekdayNamesTamilLK list the weekday name abbreviations in the Tamil Sri Lanka.
+ weekdayNamesTamilLK = []string{
+ "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BC1",
+ "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
+ "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD",
+ "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD",
+ "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0BA9\u0BCD",
+ "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF",
+ "\u0B9A\u0BA9\u0BBF",
+ }
+ // weekdayNamesTamilLKAbbr list the weekday name abbreviations in the Tamil Sri Lanka.
+ weekdayNamesTamilLKAbbr = []string{
+ "\u0B9E\u0BBE\u0BAF\u0BBF.",
+ "\u0BA4\u0BBF\u0B99\u0BCD.",
+ "\u0B9A\u0BC6\u0BB5\u0BCD.",
+ "\u0BAA\u0BC1\u0BA4.",
+ "\u0BB5\u0BBF\u0BAF\u0BBE.",
+ "\u0BB5\u0BC6\u0BB3\u0BCD.",
+ "\u0B9A\u0BA9\u0BBF",
+ }
+ // weekdayNamesTatar list the weekday name abbreviations in the Tatar.
+ weekdayNamesTatar = []string{
+ "\u044F\u043A\u0448\u04D9\u043C\u0431\u0435",
+ "\u0434\u04AF\u0448\u04D9\u043C\u0431\u0435",
+ "\u0441\u0438\u0448\u04D9\u043C\u0431\u0435",
+ "\u0447\u04D9\u0440\u0448\u04D9\u043C\u0431\u0435",
+ "\u043F\u04D9\u043D\u0497\u0435\u0448\u04D9\u043C\u0431\u0435",
+ "\u0497\u043E\u043C\u0433\u0430",
+ "\u0448\u0438\u043C\u0431\u04D9",
+ }
+ // weekdayNamesTatarAbbr list the weekday name abbreviations in the Tatar.
+ weekdayNamesTatarAbbr = []string{
+ "\u044F\u043A\u0448.",
+ "\u0434\u04AF\u0448.",
+ "\u0441\u0438\u0448.",
+ "\u0447\u04D9\u0440\u0448.",
+ "\u043F\u04D9\u043D\u0497.",
+ "\u0497\u043E\u043C.",
+ "\u0448\u0438\u043C.",
+ }
+ // weekdayNamesTelugu list the weekday name abbreviations in the Telugu.
+ weekdayNamesTelugu = []string{
+ "\u0C06\u0C26\u0C3F\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C38\u0C4B\u0C2E\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C2C\u0C41\u0C27\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C17\u0C41\u0C30\u0C41\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C36\u0C41\u0C15\u0C4D\u0C30\u0C35\u0C3E\u0C30\u0C02",
+ "\u0C36\u0C28\u0C3F\u0C35\u0C3E\u0C30\u0C02",
+ }
+ // weekdayNamesTeluguAbbr list the weekday name abbreviations in the Telugu.
+ weekdayNamesTeluguAbbr = []string{
+ "\u0C06\u0C26\u0C3F",
+ "\u0C38\u0C4B\u0C2E",
+ "\u0C2E\u0C02\u0C17\u0C33",
+ "\u0C2C\u0C41\u0C27",
+ "\u0C17\u0C41\u0C30\u0C41",
+ "\u0C36\u0C41\u0C15\u0C4D\u0C30",
+ "\u0C36\u0C28\u0C3F",
+ }
+ // weekdayNamesThai list the weekday name abbreviations in the Thai.
+ weekdayNamesThai = []string{
+ "\u0E2D\u0E32\u0E17\u0E34\u0E15\u0E22\u0E4C",
+ "\u0E08\u0E31\u0E19\u0E17\u0E23\u0E4C",
+ "\u0E2D\u0E31\u0E07\u0E04\u0E32\u0E23",
+ "\u0E1E\u0E38\u0E18",
+ "\u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35",
+ "\u0E28\u0E38\u0E01\u0E23\u0E4C",
+ "\u0E40\u0E2A\u0E32\u0E23\u0E4C",
+ }
+ // weekdayNamesThaiAbbr list the weekday name abbreviations in the Thai.
+ weekdayNamesThaiAbbr = []string{
+ "\u0E2D\u0E32.",
+ "\u0E08.",
+ "\u0E2D.",
+ "\u0E1E.",
+ "\u0E1E\u0E24.",
+ "\u0E28.",
+ "\u0E2A.",
+ }
+ // weekdayNamesTibetan list the weekday name abbreviations in the Tibetan.
+ weekdayNamesTibetan = []string{
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F49\u0F72\u0F0B\u0F58\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F5F\u0FB3\u0F0B\u0F56\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F63\u0FB7\u0F42\u0F0B\u0F54\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F54\u0F0B\u0F66\u0F44\u0F66\u0F0D",
+ "\u0F42\u0F5F\u0F60\u0F0B\u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D",
+ }
+ // weekdayNamesTibetanAbbr list the weekday name abbreviations in the Tibetan.
+ weekdayNamesTibetanAbbr = []string{
+ "\u0F49\u0F72\u0F0B\u0F58\u0F0D",
+ "\u0F5F\u0FB3\u0F0B\u0F56\u0F0D",
+ "\u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D",
+ "\u0F63\u0FB7\u0F42\u0F0B\u0F54\u0F0D",
+ "\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D",
+ "\u0F54\u0F0B\u0F66\u0F44\u0F66\u0F0D",
+ "\u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D",
+ }
+ // weekdayNamesTigrinya list the weekday name abbreviations in the Tigrinya.
+ weekdayNamesTigrinya = []string{
+ "\u1230\u1295\u1260\u1275",
+ "\u1230\u1291\u12ED",
+ "\u1220\u1209\u1235",
+ "\u1228\u1261\u12D5",
+ "\u1283\u1219\u1235",
+ "\u12D3\u122D\u1262",
+ "\u1240\u12F3\u121D",
+ }
+ // weekdayNamesTigrinyaAbbr list the weekday name abbreviations in the Tigrinya.
+ weekdayNamesTigrinyaAbbr = []string{
+ "\u1230\u1295",
+ "\u1230\u1291",
+ "\u1230\u1209",
+ "\u1228\u1261",
+ "\u1213\u1219",
+ "\u12D3\u122D",
+ "\u1240\u12F3",
+ }
+ // weekdayNamesTsonga list the weekday name abbreviations in the Tsonga.
+ weekdayNamesTsonga = []string{"Sonta", "Musumbhunuku", "Ravumbirhi", "Ravunharhu", "Ravumune", "Ravuntlhanu", "Mugqivela"}
+ // weekdayNamesTsongaAbbr list the weekday name abbreviations in the Tsonga.
+ weekdayNamesTsongaAbbr = []string{"Son", "Mus", "Bir", "Har", "Ne", "Tlh", "Mug"}
+ // weekdayNamesTurkish list the weekday name abbreviations in the Turkish.
+ weekdayNamesTurkish = []string{"Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"}
+ // weekdayNamesTurkishAbbr list the weekday name abbreviations in the Turkish.
+ weekdayNamesTurkishAbbr = []string{"Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"}
+ // weekdayNamesTurkmen list the weekday name abbreviations in the Turkmen.
+ weekdayNamesTurkmen = []string{"Ýekşenbe", "Duşenbe", "Sişenbe", "Çarşenbe", "Penşenbe", "Anna", "Şenbe"}
+ // weekdayNamesTurkmenAbbr list the weekday name abbreviations in the Turkmen.
+ weekdayNamesTurkmenAbbr = []string{"Ýb", "Db", "Sb", "Çb", "Pb", "An", "Şb"}
+ // weekdayNamesUkrainian list the weekday name abbreviations in the Ukrainian.
+ weekdayNamesUkrainian = []string{
+ "\u043D\u0435\u0434\u0456\u043B\u044F",
+ "\u043F\u043E\u043D\u0435\u0434\u0456\u043B\u043E\u043A",
+ "\u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A",
+ "\u0441\u0435\u0440\u0435\u0434\u0430",
+ "\u0447\u0435\u0442\u0432\u0435\u0440",
+ "\u043F%27\u044F\u0442\u043D\u0438\u0446\u044F",
+ "\u0441\u0443\u0431\u043E\u0442\u0430",
+ }
+ // weekdayNamesUkrainianAbbr list the weekday name abbreviations in the Ukrainian.
+ weekdayNamesUkrainianAbbr = []string{
+ "\u041D\u0434",
+ "\u041F\u043D",
+ "\u0412\u0442",
+ "\u0421\u0440",
+ "\u0427\u0442",
+ "\u041F\u0442",
+ "\u0421\u0431",
+ }
+ // weekdayNamesSorbian list the weekday name abbreviations in the Sorbian.
+ weekdayNamesSorbian = []string{"njedźela", "póndźela", "wutora", "srjeda", "štwórtk", "pjatk", "sobota"}
+ // weekdayNamesSorbianAbbr list the weekday name abbreviations in the Sorbian.
+ weekdayNamesSorbianAbbr = []string{"nje", "pón", "wut", "srj", "štw", "pja", "sob"}
+ // weekdayNamesUrdu list the weekday name abbreviations in the Urdu.
+ weekdayNamesUrdu = []string{
+ "\u0627\u062A\u0648\u0627\u0631",
+ "\u067E\u064A\u0631",
+ "\u0645\u0646\u06AF\u0644",
+ "\u0628\u062F\u06BE",
+ "\u062C\u0645\u0639\u0631\u0627\u062A",
+ "\u062C\u0645\u0639\u0647",
+ "\u0647\u0641\u062A\u0647",
+ }
+ // weekdayNamesUrduIN list the weekday name abbreviations in the Urdu India.
+ weekdayNamesUrduIN = []string{
+ "\u0627\u062A\u0648\u0627\u0631",
+ "\u067E\u06CC\u0631",
+ "\u0645\u0646\u06AF\u0644",
+ "\u0628\u062F\u06BE",
+ "\u062C\u0645\u0639\u0631\u0627\u062A",
+ "\u062C\u0645\u0639\u06C1",
+ "\u06C1\u0641\u062A\u06C1",
+ }
+ // weekdayNamesUyghur list the weekday name abbreviations in the Uyghur.
+ weekdayNamesUyghur = []string{
+ "\u064A\u06D5\u0643\u0634\u06D5\u0646\u0628\u06D5",
+ "\u062F\u06C8\u0634\u06D5\u0646\u0628\u06D5",
+ "\u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5",
+ "\u0686\u0627\u0631\u0634\u06D5\u0646\u0628\u06D5",
+ "\u067E\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5",
+ "\u062C\u06C8\u0645\u06D5",
+ "\u0634\u06D5\u0646\u0628\u06D5",
+ }
+ // weekdayNamesUyghurAbbr list the weekday name abbreviations in the Uyghur.
+ weekdayNamesUyghurAbbr = []string{
+ "\u064A\u06D5",
+ "\u062F\u06C8",
+ "\u0633\u06D5",
+ "\u0686\u0627",
+ "\u067E\u06D5",
+ "\u062C\u06C8",
+ "\u0634\u06D5",
+ }
+ // weekdayNamesUzbekCyrillic list the weekday name abbreviations in the Uzbek Cyrillic.
+ weekdayNamesUzbekCyrillic = []string{
+ "\u044F\u043A\u0448\u0430\u043D\u0431\u0430",
+ "\u0434\u0443\u0448\u0430\u043D\u0431\u0430",
+ "\u0441\u0435\u0448\u0430\u043D\u0431\u0430",
+ "\u0447\u043E\u0440\u0448\u0430\u043D\u0431\u0430",
+ "\u043F\u0430\u0439\u0448\u0430\u043D\u0431\u0430",
+ "\u0436\u0443\u043C\u0430",
+ "\u0448\u0430\u043D\u0431\u0430",
+ }
+ // weekdayNamesUzbekCyrillicAbbr list the weekday name abbreviations in the Uzbek Cyrillic.
+ weekdayNamesUzbekCyrillicAbbr = []string{
+ "\u044F\u043A\u0448",
+ "\u0434\u0443\u0448",
+ "\u0441\u0435\u0448",
+ "\u0447\u043E\u0440",
+ "\u043F\u0430\u0439",
+ "\u0436\u0443\u043C",
+ "\u0448\u0430\u043D",
+ }
+ // weekdayNamesUzbek list the weekday name abbreviations in the Uzbek.
+ weekdayNamesUzbek = []string{"yakshanba", "dushanba", "seshanba", "chorshanba", "payshanba", "juma", "shanba"}
+ // weekdayNamesUzbekAbbr list the weekday name abbreviations in the Uzbek.
+ weekdayNamesUzbekAbbr = []string{"Yak", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan"}
+ // weekdayNamesValencian list the weekday name abbreviations in the Valencian.
+ weekdayNamesValencian = []string{"diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"}
+ // weekdayNamesValencianAbbr list the weekday name abbreviations in the Valencian.
+ weekdayNamesValencianAbbr = []string{"dg.", "dl.", "dt.", "dc.", "dj.", "dv.", "ds."}
+ // weekdayNamesVenda list the weekday name abbreviations in the Venda.
+ weekdayNamesVenda = []string{"Swondaha", "Musumbuluwo", "Ḽavhuvhili", "Ḽavhuraru", "Ḽavhuṋa", "Ḽavhuṱanu", "Mugivhela"}
+ // weekdayNamesVendaAbbr list the weekday name abbreviations in the Venda.
+ weekdayNamesVendaAbbr = []string{"Swo", "Mus", "Vhi", "Rar", "Ṋa", "Ṱan", "Mug"}
+ // weekdayNamesVietnamese list the weekday name abbreviations in the Vietnamese.
+ weekdayNamesVietnamese = []string{"Ch\u1EE7%20Nh\u1EADt", "Th\u1EE9%20Hai", "Th\u1EE9%20Ba", "Th\u1EE9%20T\u01B0", "Th\u1EE9%20N\u0103m", "Th\u1EE9%20S%E1u", "Th\u1EE9%20B\u1EA3y"}
+ // weekdayNamesVietnameseAbbr list the weekday name abbreviations in the Vietnamese.
+ weekdayNamesVietnameseAbbr = []string{"CN", "T2", "T3", "T4", "T5", "T6", "T7"}
+ // weekdayNamesWelsh list the weekday name abbreviations in the Welsh.
+ weekdayNamesWelsh = []string{"Dydd Sul", "Dydd Llun", "Dydd Mawrth", "Dydd Mercher", "Dydd Iau", "Dydd Gwener", "Dydd Sadwrn"}
+ // weekdayNamesWelshAbbr list the weekday name abbreviations in the Welsh.
+ weekdayNamesWelshAbbr = []string{"Sul", "Llun", "Maw", "Mer", "Iau", "Gwe", "Sad"}
+ // weekdayNamesWolof list the weekday name abbreviations in the Wolof.
+ weekdayNamesWolof = []string{"Dib%E9er", "Altine", "Talaata", "%C0llarba", "Alxames", "%C0jjuma", "Gaawu"}
+ // weekdayNamesWolofAbbr list the weekday name abbreviations in the Wolof.
+ weekdayNamesWolofAbbr = []string{"Dib.", "Alt.", "Tal.", "%C0ll.", "Alx.", "%C0jj.", "Gaa."}
+ // weekdayNamesXhosa list the weekday name abbreviations in the Xhosa.
+ weekdayNamesXhosa = []string{"Cawe", "Mvulo", "Lwesibini", "Lwesithathu", "Lwesine", "Lwesihlanu", "Mgqibelo"}
+ // weekdayNamesXhosaAbbr list the weekday name abbreviations in the Xhosa.
+ weekdayNamesXhosaAbbr = []string{"iCa.", "uMv.", "uLwesib.", "uLwesith.", "uLwesin.", "uLwesihl.", "uMgq."}
+ // weekdayNamesYi list the weekday name abbreviations in the Yi.
+ weekdayNamesYi = []string{
+ "\uA46D\uA18F\uA44D",
+ "\uA18F\uA282\uA494",
+ "\uA18F\uA282\uA44D",
+ "\uA18F\uA282\uA315",
+ "\uA18F\uA282\uA1D6",
+ "\uA18F\uA282\uA26C",
+ "\uA18F\uA282\uA0D8",
+ }
+ // weekdayNamesYiAbbr list the weekday name abbreviations in the Yi.
+ weekdayNamesYiAbbr = []string{
+ "\uA46D\uA18F",
+ "\uA18F\uA494",
+ "\uA18F\uA44D",
+ "\uA18F\uA315",
+ "\uA18F\uA1D6",
+ "\uA18F\uA26C",
+ "\uA18F\uA0D8",
+ }
+ // weekdayNamesYiddish list the weekday name abbreviations in the Yiddish.
+ weekdayNamesYiddish = []string{
+ "\u05D6\u05D5\u05E0\u05D8\u05D9\u05E7",
+ "\u05DE\u05D0\u05B8\u05E0\u05D8\u05D9\u05E7",
+ "\u05D3\u05D9\u05E0\u05E1\u05D8\u05D9\u05E7",
+ "\u05DE\u05D9\u05D8\u05D5\u05D5\u05D0\u05DA",
+ "\u05D3\u05D0\u05E0\u05E2\u05E8\u05E9\u05D8\u05D9\u05E7",
+ "\u05E4\u05BF\u05E8\u05F2\u05B7\u05D8\u05D9\u05E7",
+ "\u05E9\u05D1\u05EA",
+ }
+ // weekdayNamesYiddishAbbr list the weekday name abbreviations in the Yiddish.
+ weekdayNamesYiddishAbbr = []string{
+ "\u05D9\u05D5\u05DD%A0\u05D0",
+ "\u05D9\u05D5\u05DD%A0\u05D1",
+ "\u05D9\u05D5\u05DD%A0\u05D2",
+ "\u05D9\u05D5\u05DD%A0\u05D3",
+ "\u05D9\u05D5\u05DD%A0\u05D4",
+ "\u05D9\u05D5\u05DD%A0\u05D5",
+ "\u05E9\u05D1\u05EA",
+ }
+ // weekdayNamesYoruba list the weekday name abbreviations in the Yoruba.
+ weekdayNamesYoruba = []string{
+ "\u1ECCj\u1ECD\u0301%20%C0%ECk%FA",
+ "\u1ECCj\u1ECD\u0301%20Aj%E9",
+ "\u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun",
+ "\u1ECCj\u1ECD\u0301r%FA",
+ "\u1ECCj\u1ECD\u0301b\u1ECD",
+ "\u1ECCj\u1ECD\u0301%20\u1EB8t%EC",
+ "\u1ECCj\u1ECD\u0301%20%C0b%E1m\u1EB9\u0301ta",
+ }
+ // weekdayNamesYorubaAbbr list the weekday name abbreviations in the Yoruba.
+ weekdayNamesYorubaAbbr = []string{"%C0%ECk", "Aj", "%CC\u1E63g", "\u1ECCjr", "\u1ECCjb", "\u1EB8t", "%C0b%E1"}
+ // weekdayNamesZulu list the weekday name abbreviations in the Zulu.
+ weekdayNamesZulu = []string{"ISonto", "UMsombuluko", "ULwesibili", "ULwesithathu", "ULwesine", "ULwesihlanu", "UMgqibelo"}
+ // weekdayNamesZuluAbbr list the weekday name abbreviations in the Zulu.
+ weekdayNamesZuluAbbr = []string{"Son.", "Mso.", "Bi.", "Tha.", "Ne.", "Hla.", "Mgq."}
+ // apFmtAfrikaans defined the AM/PM name in the Afrikaans.
+ apFmtAfrikaans = "vm./nm."
+ // apFmtAlbanian defined the AM/PM name in the Albanian.
+ apFmtAlbanian = "p.d./m.d."
+ // apFmtAlsatian defined the AM/PM name in the Alsatian.
+ apFmtAlsatian = "vorm./nam."
+ // apFmtAmharic defined the AM/PM name in the Amharic.
+ apFmtAmharic = "\u1325\u12CB\u1275/\u12A8\u1230\u12D3\u1275"
+ // apFmtArabic defined the AM/PM name in the Arabic.
+ apFmtArabic = "\u0635/\u0645"
+ // apFmtAssamese defined the AM/PM name in the Assamese.
+ apFmtAssamese = "\u09F0\u09BE\u09A4\u09BF\u09AA\u09C1/\u0986\u09AC\u09C7\u09B2\u09BF"
+ // apFmtBreton defined the AM/PM name in the Assamese.
+ apFmtBreton = "A.M./G.M."
+ // apFmtBurmese defined the AM/PM name in the Assamese.
+ apFmtBurmese = "\u1014\u1036\u1014\u1000\u103A/\u100A\u1014\u1031"
+ // apFmtCameroon defined the AM/PM name in the Cameroon.
+ apFmtCameroon = "mat./soir"
+ // apFmtCentralKurdish defined the AM/PM name in the Central Kurdish.
+ apFmtCentralKurdish = "\u067E.\u0646/\u062F.\u0646"
+ // apFmtCuba defined the AM/PM name in the Cuba.
+ apFmtCuba = "a.m./p.m."
+ // apFmtFaroese defined the AM/PM name in the Faroese.
+ apFmtFaroese = "um fyr./um sein."
+ // apFmtFinnish defined the AM/PM name in the Finnish.
+ apFmtFinnish = "ap./ip."
+ // apFmtGreek defined the AM/PM name in the Greek.
+ apFmtGreek = "\u03C0\u03BC/\u03BC\u03BC"
+ // apFmtGujarati defined the AM/PM name in the Gujarati.
+ apFmtGujarati = "\u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8/\u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"
+ // apFmtHindi defined the AM/PM name in the Hindi.
+ apFmtHindi = "\u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928/\u0905\u092A\u0930\u093E\u0939\u094D\u0928"
+ // apFmtHungarian defined the AM/PM name in the Hungarian.
+ apFmtHungarian = "de./du."
+ // apFmtIcelandic defined the AM/PM name in the Icelandic.
+ apFmtIcelandic = "f.h./e.h."
+ // apFmtIgbo defined the AM/PM name in the Igbo.
+ apFmtIgbo = "A.M./P.M."
+ // apFmtIrish defined the AM/PM name in the Irish.
+ apFmtIrish = "r.n./i.n."
+ // apFmtJapanese defined the AM/PM name in the Japanese.
+ apFmtJapanese = "午前/午後"
+ // apFmtKannada defined the AM/PM name in the Kannada.
+ apFmtKannada = "\u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8/\u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"
+ // apFmtKhmer defined the AM/PM name in the Khmer.
+ apFmtKhmer = "\u1796\u17D2\u179A\u17B9\u1780/\u179B\u17D2\u1784\u17B6\u1785"
+ // apFmtKonkani defined the AM/PM name in the Konkani.
+ apFmtKonkani = "\u092E.\u092A\u0942./\u092E.\u0928\u0902."
+ // apFmtKorean defined the AM/PM name in the Korean.
+ apFmtKorean = "오전/오후"
+ // apFmtKyrgyz defined the AM/PM name in the Kyrgyz.
+ apFmtKyrgyz = "\u0442\u04A3/\u0442\u043A"
+ // apFmtLao defined the AM/PM name in the Lao.
+ apFmtLao = "\u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87/\u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"
+ // apFmtLatvian defined the AM/PM name in the Latvian.
+ apFmtLatvian = "priekšp./pēcp."
+ // apFmtLithuanian defined the AM/PM name in the Lithuanian.
+ apFmtLithuanian = "priešpiet/popiet"
+ // apFmtMacedonian defined the AM/PM name in the Macedonian.
+ apFmtMacedonian = "\u043F\u0440\u0435\u0442\u043F\u043B./\u043F\u043E\u043F\u043B."
+ // apFmtMalay defined the AM/PM name in the Malay.
+ apFmtMalay = "PG/PTG"
+ // apFmtMongolian defined the AM/PM name in the Mongolian.
+ apFmtMongolian = "\u04AF.\u04E9./\u04AF.\u0445."
+ // apFmtNigeria defined the AM/PM name in the Nigeria.
+ apFmtNigeria = "subaka/kikiiɗe"
+ // apFmtNorwegian defined the AM/PM name in the Norwegian.
+ apFmtNorwegian = "f.m./e.m."
+ // apFmtOromo defined the AM/PM name in the Oromo.
+ apFmtOromo = "WD/WB"
+ // apFmtPashto defined the AM/PM name in the Pashto.
+ apFmtPashto = "\u063A.\u0645./\u063A.\u0648."
+ // apFmtPersian defined the AM/PM name in the Persian.
+ apFmtPersian = "\u0642.\u0638/\u0628.\u0638"
+ // apFmtPunjabi defined the AM/PM name in the Punjabi.
+ apFmtPunjabi = "\u0A38\u0A35\u0A47\u0A30/\u0A38\u0A3C\u0A3E\u0A2E"
+ // apFmtSakha defined the AM/PM name in the Sakha.
+ apFmtSakha = "\u041A\u0418/\u041A\u041A"
+ // apFmtSamiNorthern defined the AM/PM name in the Sami (Northern).
+ apFmtSamiNorthern = "i.b./e.b."
+ // apFmtSanskrit defined the AM/PM name in the Sanskrit.
+ apFmtSanskrit = "\u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935/\u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"
+ // apFmtScottishGaelic defined the AM/PM name in the Scottish Gaelic.
+ apFmtScottishGaelic = "m/f"
+ // apFmtSerbianLatin defined the AM/PM name in the Serbian (Latin).
+ apFmtSerbianLatin = "pre podne/po podne"
+ // apFmtSerbianLatinBA defined the AM/PM name in the Serbian (Latin) Bosnia
+ // and Herzegovina.
+ apFmtSerbianLatinBA = "prije podne/po podne"
+ // apFmtSinhala defined the AM/PM name in the Sinhala.
+ apFmtSinhala = "\u0DB4\u0DD9.\u0DC0./\u0DB4.\u0DC0."
+ // apFmtSlovenian defined the AM/PM name in the Slovenian.
+ apFmtSlovenian = "dop./pop."
+ // apFmtSomali defined the AM/PM name in the Somali.
+ apFmtSomali = "GH/GD"
+ // apFmtSpanish defined the AM/PM name in the Spanish.
+ apFmtSpanish = "a. m./p. m."
+ // apFmtSpanishAR defined the AM/PM name in the Spanish Argentina.
+ apFmtSpanishAR = "a.%A0m./p.%A0m."
+ // apFmtSwedish defined the AM/PM name in the Swedish.
+ apFmtSwedish = "fm/em"
+ // apFmtSyriac defined the AM/PM name in the Syriac.
+ apFmtSyriac = "\u0729.\u071B/\u0712.\u071B"
+ // apFmtTamil defined the AM/PM name in the Tamil.
+ apFmtTamil = "\u0B95\u0BBE\u0BB2\u0BC8/\u0BAE\u0BBE\u0BB2\u0BC8"
+ // apFmtTibetan defined the AM/PM name in the Tibetan.
+ apFmtTibetan = "\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b/\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"
+ // apFmtTigrinya defined the AM/PM name in the Tigrinya.
+ apFmtTigrinya = "\u1295\u1309\u1206/\u12F5\u1215\u122A%20\u1250\u1275\u122A"
+ // apFmtTigrinyaER defined the AM/PM name in the Tigrinya Eritrea.
+ apFmtTigrinyaER = "\u1295\u1309\u1206%20\u1230\u12D3\u1270/\u12F5\u1215\u122D%20\u1230\u12D3\u1275"
+ // apFmtTurkish defined the AM/PM name in the Turkish.
+ apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053"
+ // apFmtUpperSorbian defined the AM/PM name in the Upper Sorbian.
+ apFmtUpperSorbian = "dopołdnja/popołdnju"
+ // apFmtUrdu defined the AM/PM name in the Urdu.
+ apFmtUrdu = "\u062F\u0646/\u0631\u0627\u062A"
+ // apFmtUyghur defined the AM/PM name in the Uyghur.
+ apFmtUyghur = "\u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646/\u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646"
+ // apFmtUzbek defined the AM/PM name in the Uzbek.
+ apFmtUzbek = "TO/TK"
+ // apFmtUzbekCyrillic defined the AM/PM name in the Uzbek Cyrillic.
+ apFmtUzbekCyrillic = "\u0422\u041E/\u0422\u041A"
+ // apFmtVietnamese defined the AM/PM name in the Vietnamese.
+ apFmtVietnamese = "SA/CH"
+ // apFmtWelsh defined the AM/PM name in the Welsh.
+ apFmtWelsh = "yb/yh"
+ // apFmtWolof defined the AM/PM name in the Wolof.
+ apFmtWolof = "Sub/Ngo"
+ // apFmtYi defined the AM/PM name in the Yi.
+ apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2"
+ // apFmtYiddish defined the AM/PM name in the Yiddish.
+ apFmtYiddish = "\u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2/\u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"
+ // apFmtYoruba defined the AM/PM name in the Yoruba.
+ apFmtYoruba = "%C0%E1r\u1ECD\u0300/\u1ECC\u0300s%E1n"
+ // switchArgumentFunc defined the switch argument printer function.
+ switchArgumentFunc = map[string]func(s string) string{
+ "[DBNum1]": func(s string) string {
+ r := strings.NewReplacer(
+ "0", "\u25cb", "1", "\u4e00", "2", "\u4e8c", "3", "\u4e09", "4", "\u56db",
+ "5", "\u4e94", "6", "\u516d", "7", "\u4e03", "8", "\u516b", "9", "\u4e5d",
+ )
+ return r.Replace(s)
+ },
+ "[DBNum2]": func(s string) string {
+ r := strings.NewReplacer(
+ "0", "\u96f6", "1", "\u58f9", "2", "\u8d30", "3", "\u53c1", "4", "\u8086",
+ "5", "\u4f0d", "6", "\u9646", "7", "\u67d2", "8", "\u634c", "9", "\u7396",
+ )
+ return r.Replace(s)
+ },
+ "[DBNum3]": func(s string) string {
+ r := strings.NewReplacer(
+ "0", "\uff10", "1", "\uff11", "2", "\uff12", "3", "\uff13", "4", "\uff14",
+ "5", "\uff15", "6", "\uff16", "7", "\uff17", "8", "\uff18", "9", "\uff19",
+ )
+ return r.Replace(s)
+ },
+ }
+ // langNumFmtFunc defines functions to apply language number format code.
+ langNumFmtFunc = map[CultureName]func(f *File, numFmtID int) string{
+ CultureNameEnUS: func(f *File, numFmtID int) string {
+ return f.langNumFmtFuncEnUS(numFmtID)
+ },
+ CultureNameJaJP: func(f *File, numFmtID int) string {
+ return f.langNumFmtFuncJaJP(numFmtID)
+ },
+ CultureNameKoKR: func(f *File, numFmtID int) string {
+ return f.langNumFmtFuncKoKR(numFmtID)
+ },
+ CultureNameZhCN: func(f *File, numFmtID int) string {
+ return f.langNumFmtFuncZhCN(numFmtID)
+ },
+ CultureNameZhTW: func(f *File, numFmtID int) string {
+ return f.langNumFmtFuncZhTW(numFmtID)
+ },
+ }
+)
+
+// getSupportedLanguageInfo returns language information by giving language code.
+// This function does not support different calendar type of the language
+// currently. For example: the hexadecimal language code 3010429 (fa-IR,301)
+// will be convert to 0429 (fa-IR).
+func getSupportedLanguageInfo(lang string) (languageInfo, bool) {
+ hex := lang
+ if len(hex) > 4 {
+ hex = hex[len(hex)-4:]
+ }
+ n := new(big.Int)
+ n.SetString(hex, 16)
+ if info, ok := supportedLanguageInfo[int(n.Int64())]; ok {
+ return info, ok
+ }
+ if info, ok := supportedLanguageCodeInfo[lang]; ok {
+ return info, ok
+ }
+ for _, info := range supportedLanguageInfo {
+ if inStrSlice(info.tags, lang, false) != -1 {
+ return info, true
+ }
+ }
+ return languageInfo{}, false
+}
+
+// applyBuiltInNumFmt provides a function to returns a value after formatted
+// with built-in number format code, or specified sort date format code.
+func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string {
+ if f.options != nil && f.options.ShortDatePattern != "" {
+ if numFmtID == 14 {
+ fmtCode = f.options.ShortDatePattern
+ }
+ if numFmtID == 22 {
+ fmtCode = fmt.Sprintf("%s hh:mm", f.options.ShortDatePattern)
+ }
+ }
+ return format(c.V, fmtCode, date1904, cellType, f.options)
+}
+
+// langNumFmtFuncEnUS returns number format code by given date and time pattern
+// for country code en-us.
+func (f *File) langNumFmtFuncEnUS(numFmtID int) string {
+ shortDatePattern, longTimePattern := "M/d/yy", "h:mm:ss"
+ if f.options.ShortDatePattern != "" {
+ shortDatePattern = f.options.ShortDatePattern
+ }
+ if f.options.LongTimePattern != "" {
+ longTimePattern = f.options.LongTimePattern
+ }
+ if 32 <= numFmtID && numFmtID <= 35 {
+ return longTimePattern
+ }
+ if (27 <= numFmtID && numFmtID <= 31) || (50 <= numFmtID && numFmtID <= 58) {
+ return shortDatePattern
+ }
+ return ""
+}
+
+// langNumFmtFuncJaJP returns number format code by given date and time pattern
+// for country code ja-jp.
+func (f *File) langNumFmtFuncJaJP(numFmtID int) string {
+ if numFmtID == 30 && f.options.ShortDatePattern != "" {
+ return f.options.ShortDatePattern
+ }
+ if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
+ return f.options.LongTimePattern
+ }
+ return langNumFmt["ja-jp"][numFmtID]
+}
+
+// langNumFmtFuncKoKR returns number format code by given date and time pattern
+// for country code ko-kr.
+func (f *File) langNumFmtFuncKoKR(numFmtID int) string {
+ if numFmtID == 30 && f.options.ShortDatePattern != "" {
+ return f.options.ShortDatePattern
+ }
+ if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
+ return f.options.LongTimePattern
+ }
+ return langNumFmt["ko-kr"][numFmtID]
+}
+
+// langNumFmtFuncZhCN returns number format code by given date and time pattern
+// for country code zh-cn.
+func (f *File) langNumFmtFuncZhCN(numFmtID int) string {
+ if numFmtID == 30 && f.options.ShortDatePattern != "" {
+ return f.options.ShortDatePattern
+ }
+ if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
+ return f.options.LongTimePattern
+ }
+ return langNumFmt["zh-cn"][numFmtID]
+}
+
+// langNumFmtFuncZhTW returns number format code by given date and time pattern
+// for country code zh-tw.
+func (f *File) langNumFmtFuncZhTW(numFmtID int) string {
+ if numFmtID == 30 && f.options.ShortDatePattern != "" {
+ return f.options.ShortDatePattern
+ }
+ if (32 <= numFmtID && numFmtID <= 33) && f.options.LongTimePattern != "" {
+ return f.options.LongTimePattern
+ }
+ return langNumFmt["zh-tw"][numFmtID]
+}
+
+// checkDateTimePattern check and validate date and time options field value.
+func (f *File) checkDateTimePattern() error {
+ for _, pattern := range []string{f.options.LongDatePattern, f.options.LongTimePattern, f.options.ShortDatePattern} {
+ p := nfp.NumberFormatParser()
+ for _, section := range p.Parse(pattern) {
+ for _, token := range section.Items {
+ if inStrSlice(supportedTokenTypes, token.TType, false) == -1 || inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 {
+ return ErrUnsupportedNumberFormat
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// extractNumFmtDecimal returns decimal places, if has a decimal point token and
+// zero place holder token from a number format code token list.
+func extractNumFmtDecimal(tokens []nfp.Token) (int, bool, bool) {
+ decimal, point, zero := 0, false, false
+ for _, token := range tokens {
+ if token.TType == nfp.TokenTypeDecimalPoint {
+ point = true
+ }
+ if token.TType == nfp.TokenTypeZeroPlaceHolder {
+ if point {
+ decimal = len(token.TValue)
+ }
+ zero = true
+ }
+ }
+ return decimal, point, zero
+}
+
+// extractNumFmtDecimal returns decimal places from a number format code that
+// has the same decimal places in positive part negative part or only positive
+// part, if the given number format code is not suitable for numeric this
+// function will return -1.
+func (f *File) extractNumFmtDecimal(fmtCode string) int {
+ var (
+ p = nfp.NumberFormatParser()
+ pos, neg, posPoint, negPoint, posZero, negZero bool
+ posDecimal, negDecimal int
+ )
+ for i, section := range p.Parse(fmtCode) {
+ if i == 0 {
+ pos = true
+ posDecimal, posPoint, posZero = extractNumFmtDecimal(section.Items)
+ }
+ if i == 1 {
+ neg = true
+ negDecimal, negPoint, negZero = extractNumFmtDecimal(section.Items)
+ }
+ }
+ if !pos {
+ return -1
+ }
+ equalPosNegDecimal := posPoint && negPoint && posDecimal == negDecimal
+ equalPosNegZero := !posPoint && !negPoint && posZero && negZero
+ if neg {
+ if equalPosNegDecimal {
+ return posDecimal
+ }
+ if equalPosNegZero {
+ return 0
+ }
+ return -1
+ }
+ if posPoint {
+ return posDecimal
+ }
+ if posZero {
+ return 0
+ }
+ return -1
+}
+
+// getBuiltInNumFmtCode convert number format index to number format code with
+// specified locale and language.
+func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) {
+ if fmtCode, ok := builtInNumFmt[numFmtID]; ok {
+ return fmtCode, true
+ }
+ if isLangNumFmt(numFmtID) {
+ if fn, ok := langNumFmtFunc[f.options.CultureInfo]; ok {
+ return fn(f, numFmtID), true
+ }
+ }
+ return "", false
+}
+
+// prepareNumberic split the number into two before and after parts by a
+// decimal point.
+func (nf *numberFormat) prepareNumberic(value string) {
+ if nf.cellType != CellTypeNumber && nf.cellType != CellTypeDate {
+ return
+ }
+ if nf.isNumeric, _, _ = isNumeric(value); !nf.isNumeric {
+ return
+ }
+}
+
+// format provides a function to return a string parse by number format
+// expression. If the given number format is not supported, this will return
+// the original cell value.
+func format(value, numFmt string, date1904 bool, cellType CellType, opts *Options) string {
+ p := nfp.NumberFormatParser()
+ nf := numberFormat{opts: opts, section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType}
+ nf.number, nf.valueSectionType = nf.getValueSectionType(value)
+ nf.prepareNumberic(value)
+ for i, section := range nf.section {
+ nf.sectionIdx = i
+ if section.Type != nf.valueSectionType {
+ continue
+ }
+ if nf.isNumeric {
+ switch section.Type {
+ case nfp.TokenSectionPositive:
+ return nf.alignmentHandler(nf.positiveHandler())
+ default:
+ return nf.alignmentHandler(nf.negativeHandler())
+ }
+ }
+ return nf.alignmentHandler(nf.textHandler())
+ }
+ return value
+}
+
+// getNumberPartLen returns the length of integer and fraction parts for the
+// numeric.
+func (nf *numberFormat) getNumberPartLen() (int, int) {
+ var intPart, fracPart, intLen, fracLen int
+ parts := strings.Split(strconv.FormatFloat(math.Abs(nf.number), 'f', -1, 64), ".")
+ intPart = len(parts[0])
+ if len(parts) == 2 {
+ fracPart = len(parts[1])
+ }
+ if nf.intHolder > intPart {
+ nf.intHolder = intPart
+ }
+ if intLen = intPart; nf.intPadding+nf.intHolder > intPart {
+ intLen = nf.intPadding + nf.intHolder
+ }
+ if fracLen = fracPart; fracPart > nf.fracHolder+nf.fracPadding {
+ fracLen = nf.fracHolder + nf.fracPadding
+ }
+ if nf.fracPadding > fracPart {
+ fracLen = nf.fracPadding
+ }
+ return intLen, fracLen
+}
+
+// getNumberFmtConf generate the number format padding and placeholder
+// configurations.
+func (nf *numberFormat) getNumberFmtConf() {
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeHashPlaceHolder {
+ if nf.usePointer {
+ nf.fracHolder += len(token.TValue)
+ continue
+ }
+ nf.intHolder += len(token.TValue)
+ }
+ if token.TType == nfp.TokenTypeExponential {
+ nf.useScientificNotation = true
+ }
+ if token.TType == nfp.TokenTypeThousandsSeparator {
+ nf.useCommaSep = true
+ }
+ if token.TType == nfp.TokenTypePercent {
+ nf.percent += len(token.TValue)
+ }
+ if token.TType == nfp.TokenTypeDecimalPoint {
+ nf.usePointer = true
+ }
+ if token.TType == nfp.TokenTypeFraction {
+ nf.useFraction = true
+ }
+ if token.TType == nfp.TokenTypeSwitchArgument {
+ nf.switchArgument = token.TValue
+ }
+ if token.TType == nfp.TokenTypeZeroPlaceHolder {
+ nf.intHolder = 0
+ if nf.usePointer {
+ if nf.useScientificNotation {
+ nf.expBaseLen += len(token.TValue)
+ continue
+ }
+ nf.fracPadding += len(token.TValue)
+ continue
+ }
+ nf.intPadding += len(token.TValue)
+ }
+ }
+}
+
+// handleDigitsLiteral apply hash and zero place holder tokens for the number
+// literal.
+func handleDigitsLiteral(text string, tokenValueLen, intPartLen, hashZeroPartLen int) (int, string) {
+ var result string
+ l := tokenValueLen
+ if intPartLen == 0 && len(text) > hashZeroPartLen {
+ l = len(text) + tokenValueLen - hashZeroPartLen
+ }
+ if len(text) < hashZeroPartLen {
+ intPartLen += len(text) - hashZeroPartLen
+ }
+ for i := 0; i < l; i++ {
+ j := i + intPartLen
+ if 0 <= j && j < len([]rune(text)) {
+ result += string([]rune(text)[j])
+ }
+ }
+ return l, result
+}
+
+// printNumberLiteral apply literal tokens for the pre-formatted text.
+func (nf *numberFormat) printNumberLiteral(text string) string {
+ var (
+ result string
+ frac float64
+ useFraction bool
+ intPartLen, hashZeroPartLen int
+ )
+ if nf.usePositive {
+ result += "-"
+ }
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder {
+ hashZeroPartLen += len(token.TValue)
+ }
+ }
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeCurrencyLanguage {
+ _, _ = nf.currencyLanguageHandler(token)
+ result += nf.currencyString
+ }
+ if token.TType == nfp.TokenTypeLiteral {
+ result += token.TValue
+ }
+ if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder || token.TType == nfp.TokenTypeDigitalPlaceHolder {
+ digits, str := handleDigitsLiteral(text, len(token.TValue), intPartLen, hashZeroPartLen)
+ intPartLen += digits
+ result += str
+ }
+ if token.TType == nfp.TokenTypeFraction {
+ _, frac = math.Modf(nf.number)
+ frac, useFraction = math.Abs(frac), true
+ }
+ if useFraction {
+ result += nf.fractionHandler(frac, token)
+ }
+ }
+ return nf.printSwitchArgument(result)
+}
+
+// fractionHandler handling fraction number format expression for positive and
+// negative numeric.
+func (nf *numberFormat) fractionHandler(frac float64, token nfp.Token) string {
+ var rat, result string
+ if token.TType == nfp.TokenTypeDigitalPlaceHolder {
+ fracPlaceHolder := len(token.TValue)
+ for i := 0; i < 5000; i++ {
+ if r := newRat(frac, int64(i), 0); len(r.Denom().String()) <= fracPlaceHolder {
+ if rat = r.String(); strings.HasPrefix(rat, "0/") {
+ rat = strings.Repeat(" ", 3)
+ }
+ continue
+ }
+ break
+ }
+ result += rat
+ }
+ if token.TType == nfp.TokenTypeDenominator {
+ denom, _ := strconv.ParseFloat(token.TValue, 64)
+ result += fmt.Sprintf("%d/%d", int(math.Round(frac*denom)), int(math.Round(denom)))
+ }
+ return result
+}
+
+// printCommaSep format number with thousands separator.
+func printCommaSep(text string) string {
+ var (
+ target strings.Builder
+ subStr = strings.Split(text, ".")
+ length = len(subStr[0])
+ )
+ for i := 0; i < length; i++ {
+ if i > 0 && (length-i)%3 == 0 {
+ target.WriteString(",")
+ }
+ target.WriteByte(text[i])
+ }
+ if len(subStr) == 2 {
+ target.WriteString(".")
+ target.WriteString(subStr[1])
+ }
+ return target.String()
+}
+
+// printSwitchArgument format number with switch argument.
+func (nf *numberFormat) printSwitchArgument(text string) string {
+ if nf.switchArgument == "" {
+ return text
+ }
+ if fn, ok := switchArgumentFunc[nf.switchArgument]; ok {
+ return fn(text)
+ }
+ return nf.value
+}
+
+// printBigNumber format number which precision great than 15 with fraction
+// zero padding and percentage symbol.
+func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string {
+ var exp float64
+ if nf.percent > 0 {
+ exp = 1
+ }
+ result := strings.TrimLeft(strconv.FormatFloat(decimal*math.Pow(100, exp), 'f', -1, 64), "-")
+ if nf.useCommaSep {
+ result = printCommaSep(result)
+ }
+ if fracLen > 0 {
+ if parts := strings.Split(result, "."); len(parts) == 2 {
+ fracPartLen := len(parts[1])
+ if fracPartLen < fracLen {
+ result = fmt.Sprintf("%s%s", result, strings.Repeat("0", fracLen-fracPartLen))
+ }
+ if fracPartLen > fracLen {
+ result = fmt.Sprintf("%s.%s", parts[0], parts[1][:fracLen])
+ }
+ } else {
+ result = fmt.Sprintf("%s.%s", result, strings.Repeat("0", fracLen))
+ }
+ }
+ if nf.percent > 0 {
+ return fmt.Sprintf("%s%%", result)
+ }
+ return result
+}
+
+// numberHandler handling number format expression for positive and negative
+// numeric.
+func (nf *numberFormat) numberHandler() string {
+ nf.getNumberFmtConf()
+ var (
+ num = nf.number
+ intLen, fracLen = nf.getNumberPartLen()
+ result string
+ )
+ if isNum, precision, decimal := isNumeric(nf.value); isNum {
+ if precision > 15 && intLen+fracLen > 15 && !nf.useScientificNotation {
+ return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen))
+ }
+ }
+ paddingLen := intLen + fracLen
+ if fracLen > 0 {
+ paddingLen++
+ }
+ fmtCode := fmt.Sprintf("%%0%d.%df%s", paddingLen, fracLen, strings.Repeat("%%", nf.percent))
+ if nf.useScientificNotation {
+ if nf.expBaseLen != 2 {
+ return nf.value
+ }
+ fmtCode = fmt.Sprintf("%%.%dE%s", fracLen, strings.Repeat("%%", nf.percent))
+ }
+ if nf.percent > 0 {
+ num *= math.Pow(100, float64(nf.percent))
+ }
+ if nf.useFraction {
+ num = math.Floor(math.Abs(num))
+ }
+ if !nf.useScientificNotation {
+ ratio := math.Pow(10, float64(fracLen))
+ num = math.Round(num*ratio) / ratio
+ }
+ if result = fmt.Sprintf(fmtCode, math.Abs(num)); nf.useCommaSep {
+ result = printCommaSep(result)
+ }
+ return nf.printNumberLiteral(result)
+}
+
+// dateTimeHandler handling data and time number format expression for a
+// positive numeric.
+func (nf *numberFormat) dateTimeHandler() string {
+ nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false
+ if !nf.useMillisecond {
+ nf.t = nf.t.Add(time.Duration(math.Round(float64(nf.t.Nanosecond())/1e9)) * time.Second)
+ }
+ for i, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeCurrencyLanguage {
+ if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode {
+ return nf.value
+ }
+ nf.result += nf.currencyString
+ }
+ if token.TType == nfp.TokenTypeDateTimes {
+ nf.dateTimesHandler(i, token)
+ }
+ if token.TType == nfp.TokenTypeElapsedDateTimes {
+ nf.elapsedDateTimesHandler(token)
+ }
+ if token.TType == nfp.TokenTypeLiteral {
+ nf.result += token.TValue
+ continue
+ }
+ if token.TType == nfp.TokenTypeDecimalPoint {
+ nf.result += "."
+ }
+ if token.TType == nfp.TokenTypeSwitchArgument {
+ nf.switchArgument = token.TValue
+ }
+ if token.TType == nfp.TokenTypeZeroPlaceHolder {
+ zeroHolderLen := len(token.TValue)
+ if zeroHolderLen > 3 {
+ zeroHolderLen = 3
+ }
+ nf.result += fmt.Sprintf("%03d", nf.t.Nanosecond()/1e6)[:zeroHolderLen]
+ }
+ }
+ return nf.printSwitchArgument(nf.result)
+}
+
+// alignmentHandler will be handling alignment token for each number format
+// selection for a number format expression.
+func (nf *numberFormat) alignmentHandler(result string) string {
+ tokens := nf.section[nf.sectionIdx].Items
+ if len(tokens) == 0 {
+ return result
+ }
+ if tokens[0].TType == nfp.TokenTypeAlignment {
+ result = nfp.Whitespace + result
+ }
+ if l := len(tokens); tokens[l-1].TType == nfp.TokenTypeAlignment {
+ result += nfp.Whitespace
+ }
+ return result
+}
+
+// positiveHandler will be handling positive selection for a number format
+// expression.
+func (nf *numberFormat) positiveHandler() string {
+ var fmtNum bool
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeGeneral {
+ if isNum, precision, _ := isNumeric(nf.value); isNum && precision > 11 {
+ return strconv.FormatFloat(nf.number, 'G', 10, 64)
+ }
+ return nf.value
+ }
+ if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 {
+ fmtNum = true
+ }
+ if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 {
+ if fmtNum || nf.number < 0 {
+ return nf.value
+ }
+ var useDateTimeTokens bool
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if inStrSlice(supportedDateTimeTokenTypes, token.TType, false) != -1 {
+ if useDateTimeTokens && nf.useMillisecond {
+ return nf.value
+ }
+ useDateTimeTokens = true
+ }
+ if inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 {
+ if token.TType == nfp.TokenTypeZeroPlaceHolder {
+ nf.useMillisecond = true
+ continue
+ }
+ return nf.value
+ }
+ }
+ return nf.dateTimeHandler()
+ }
+ }
+ return nf.numberHandler()
+}
+
+// currencyLanguageHandler will be handling currency and language types tokens
+// for a number format expression.
+func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) {
+ for _, part := range token.Parts {
+ if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 {
+ return false, ErrUnsupportedNumberFormat
+ }
+ if part.Token.TType == nfp.TokenSubTypeLanguageInfo {
+ if inStrSlice([]string{"F800", "x-sysdate", "1010000"}, part.Token.TValue, false) != -1 {
+ if nf.opts != nil && nf.opts.LongDatePattern != "" {
+ nf.value = format(nf.value, nf.opts.LongDatePattern, nf.date1904, nf.cellType, nf.opts)
+ return true, nil
+ }
+ part.Token.TValue = "409"
+ }
+ if inStrSlice([]string{"F400", "x-systime"}, part.Token.TValue, false) != -1 {
+ if nf.opts != nil && nf.opts.LongTimePattern != "" {
+ nf.value = format(nf.value, nf.opts.LongTimePattern, nf.date1904, nf.cellType, nf.opts)
+ return true, nil
+ }
+ part.Token.TValue = "409"
+ }
+ if _, ok := getSupportedLanguageInfo(strings.ToUpper(part.Token.TValue)); !ok {
+ return false, ErrUnsupportedNumberFormat
+ }
+ nf.localCode = strings.ToUpper(part.Token.TValue)
+ }
+ if part.Token.TType == nfp.TokenSubTypeCurrencyString {
+ nf.currencyString = part.Token.TValue
+ return false, nil
+ }
+ }
+ return false, nil
+}
+
+// localAmPm return AM/PM name by supported language ID.
+func (nf *numberFormat) localAmPm(ap string) string {
+ if languageInfo, ok := getSupportedLanguageInfo(nf.localCode); ok {
+ return languageInfo.apFmt
+ }
+ return ap
+}
+
+// localMonthsNameAfrikaans returns the Afrikaans name of the month.
+func localMonthsNameAfrikaans(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAfrikaansAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAfrikaans[int(t.Month())-1]
+ }
+ return monthNamesAfrikaansAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameAlbanian returns the Albanian name of the month.
+func localMonthsNameAlbanian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAlbanianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAlbanian[int(t.Month())-1]
+ }
+ return monthNamesAlbanianAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameAlsatian returns the Alsatian name of the month.
+func localMonthsNameAlsatian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAlsatianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAlsatian[int(t.Month())-1]
+ }
+ return monthNamesAlsatianAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameAlsatianFrance returns the Alsatian France name of the month.
+func localMonthsNameAlsatianFrance(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAlsatianFranceAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAlsatianFrance[int(t.Month())-1]
+ }
+ return monthNamesAlsatianFranceAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameAmharic returns the Amharic name of the month.
+func localMonthsNameAmharic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAmharicAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAmharic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesAmharic[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameArabic returns the Arabic name of the month.
+func localMonthsNameArabic(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesArabic[int(t.Month())-1])[:1])
+ }
+ return monthNamesArabic[int(t.Month())-1]
+}
+
+// localMonthsNameArabicIraq returns the Arabic Iraq name of the month.
+func localMonthsNameArabicIraq(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesArabicIraq[int(t.Month())-1])[:1])
+ }
+ return monthNamesArabicIraq[int(t.Month())-1]
+}
+
+// localMonthsNameArmenian returns the Armenian name of the month.
+func localMonthsNameArmenian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesArmenianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesArmenian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesArmenianAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameAssamese returns the Assamese name of the month.
+func localMonthsNameAssamese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAssameseAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAssamese[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesAssameseAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameAzerbaijaniCyrillic returns the Azerbaijani (Cyrillic) name of the month.
+func localMonthsNameAzerbaijaniCyrillic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAzerbaijaniCyrillicAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAzerbaijaniCyrillic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesAzerbaijaniCyrillic[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameAzerbaijani returns the Azerbaijani name of the month.
+func localMonthsNameAzerbaijani(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAzerbaijaniAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAzerbaijani[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesAzerbaijani[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameAustria returns the Austria name of the month.
+func localMonthsNameAustria(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesAustriaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesAustria[int(t.Month())-1]
+ }
+ return monthNamesAustriaAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameBangla returns the German name of the month.
+func localMonthsNameBangla(t time.Time, abbr int) string {
+ if abbr == 3 || abbr == 4 {
+ return monthNamesBangla[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBangla[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBashkir returns the Bashkir name of the month.
+func localMonthsNameBashkir(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBashkirAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBashkir[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBashkir[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBasque returns the Basque name of the month.
+func localMonthsNameBasque(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBasqueAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBasque[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBasque[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBelarusian returns the Belarusian name of the month.
+func localMonthsNameBelarusian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBelarusianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBelarusian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBelarusian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBosnianCyrillic returns the Bosnian (Cyrillic) name of the month.
+func localMonthsNameBosnianCyrillic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBosnianCyrillicAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBosnianCyrillic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBosnianCyrillic[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBosnian returns the Bosnian name of the month.
+func localMonthsNameBosnian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBosnianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBosnian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBosnian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBreton returns the Breton name of the month.
+func localMonthsNameBreton(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBretonAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBreton[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBreton[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBulgarian returns the Bulgarian name of the month.
+func localMonthsNameBulgarian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesBulgarian[int(t.Month())-1])[:3])
+ }
+ if abbr == 4 {
+ return monthNamesBulgarian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBulgarian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameBurmese returns the Burmese name of the month.
+func localMonthsNameBurmese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesBurmeseAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesBurmese[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesBurmese[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameCaribbean returns the Caribbean name of the month.
+func localMonthsNameCaribbean(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesCaribbeanAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesCaribbean[int(t.Month())-1]
+ }
+ return monthNamesCaribbeanAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameCentralKurdish returns the Central Kurdish name of the month.
+func localMonthsNameCentralKurdish(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesCentralKurdish[int(t.Month())-1])[:1])
+ }
+ return monthNamesCentralKurdish[int(t.Month())-1]
+}
+
+// localMonthsNameCherokee returns the Cherokee name of the month.
+func localMonthsNameCherokee(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesCherokeeAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesCherokee[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesCherokee[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameChinese1 returns the Chinese name of the month.
+func localMonthsNameChinese1(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesChineseNum[t.Month()]
+ }
+ if abbr == 4 {
+ return monthNamesChinese[int(t.Month())-1]
+ }
+ return monthNamesChineseAbbr[int(t.Month())-1]
+}
+
+// localMonthsNameChinese2 returns the Chinese name of the month.
+func localMonthsNameChinese2(t time.Time, abbr int) string {
+ if abbr == 3 || abbr == 4 {
+ return monthNamesChinese[int(t.Month())-1]
+ }
+ return monthNamesChineseAbbr[int(t.Month())-1]
+}
+
+// localMonthsNameChinese3 returns the Chinese name of the month.
+func localMonthsNameChinese3(t time.Time, abbr int) string {
+ if abbr == 3 || abbr == 4 {
+ return monthNamesChineseNum[t.Month()]
+ }
+ return strconv.Itoa(int(t.Month()))
+}
+
+// localMonthsNameEnglish returns the English name of the month.
+func localMonthsNameEnglish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return t.Month().String()[:3]
+ }
+ if abbr == 4 {
+ return t.Month().String()
+ }
+ return t.Month().String()[:1]
+}
+
+// localMonthsNameEstonian returns the Estonian name of the month.
+func localMonthsNameEstonian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesEstonianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesEstonian[int(t.Month())-1]
+ }
+ return monthNamesEstonianAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameFaroese returns the Faroese name of the month.
+func localMonthsNameFaroese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthsNameFaroeseAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesFaroese[int(t.Month())-1]
+ }
+ return monthsNameFaroeseAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameFilipino returns the Filipino name of the month.
+func localMonthsNameFilipino(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesFilipinoAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesFilipino[int(t.Month())-1]
+ }
+ return fmt.Sprintf("%02d", int(t.Month()))
+}
+
+// localMonthsNameFinnish returns the Finnish name of the month.
+func localMonthsNameFinnish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesFinnishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesFinnish[int(t.Month())-1]
+ }
+ return fmt.Sprintf("%02d", int(t.Month()))
+}
+
+// localMonthsNameFrench returns the French name of the month.
+func localMonthsNameFrench(t time.Time, abbr int) string {
+ if abbr == 3 {
+ month := monthNamesFrench[int(t.Month())-1]
+ if len([]rune(month)) <= 4 {
+ return monthNamesFrench[int(t.Month())-1]
+ }
+ return monthNamesFrenchAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesFrench[int(t.Month())-1]
+ }
+ return monthNamesFrenchAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameFrisian returns the Frisian name of the month.
+func localMonthsNameFrisian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesFrisianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesFrisian[int(t.Month())-1]
+ }
+ return monthNamesFrisian[int(t.Month())-1][:1]
+}
+
+// localMonthsNameFulah returns the Fulah name of the month.
+func localMonthsNameFulah(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesFulahAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesFulah[int(t.Month())-1]
+ }
+ return monthNamesFulah[int(t.Month())-1][:1]
+}
+
+// localMonthsNameGalician returns the Galician name of the month.
+func localMonthsNameGalician(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGalicianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesGalician[int(t.Month())-1]
+ }
+ return monthNamesGalician[int(t.Month())-1][:1]
+}
+
+// localMonthsNameGeorgian returns the Georgian name of the month.
+func localMonthsNameGeorgian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGeorgianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesGeorgian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesGeorgian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameGerman returns the German name of the month.
+func localMonthsNameGerman(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGermanAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesGerman[int(t.Month())-1]
+ }
+ return monthNamesGermanAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameGreek returns the Greek name of the month.
+func localMonthsNameGreek(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGreekAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesGreek[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesGreekAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameGreenlandic returns the Greenlandic name of the month.
+func localMonthsNameGreenlandic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGreenlandicAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesGreenlandic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesGreenlandicAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameGuarani returns the Guarani name of the month.
+func localMonthsNameGuarani(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGuaraniAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesGuarani[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesGuaraniAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameGujarati returns the Gujarati name of the month.
+func localMonthsNameGujarati(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesGujaratiAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesGujarati[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesGujaratiAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameHausa returns the Hausa name of the month.
+func localMonthsNameHausa(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesHausa[int(t.Month())-1])[:3])
+ }
+ if abbr == 4 {
+ return monthNamesHausa[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesHausa[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameHawaiian returns the Hawaiian name of the month.
+func localMonthsNameHawaiian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesHawaiianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesHawaiian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesHawaiianAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameHebrew returns the Hebrew name of the month.
+func localMonthsNameHebrew(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesHebrew[int(t.Month())-1])[:3])
+ }
+ if abbr == 4 {
+ return monthNamesHebrew[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesHebrew[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameHindi returns the Hindi name of the month.
+func localMonthsNameHindi(t time.Time, abbr int) string {
+ if abbr == 3 || abbr == 4 {
+ return monthNamesHindi[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesHindi[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameHungarian returns the Hungarian name of the month.
+func localMonthsNameHungarian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesHungarianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesHungarian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesHungarianAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameIcelandic returns the Icelandic name of the month.
+func localMonthsNameIcelandic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesIcelandicAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesIcelandic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesIcelandicAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameIgbo returns the Igbo name of the month.
+func localMonthsNameIgbo(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesIgboAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesIgbo[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesIgboAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameIndonesian returns the Indonesian name of the month.
+func localMonthsNameIndonesian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesIndonesianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesIndonesian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesIndonesianAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameInuktitut returns the Inuktitut name of the month.
+func localMonthsNameInuktitut(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesInuktitutAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesInuktitut[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesInuktitutAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameIrish returns the Irish name of the month.
+func localMonthsNameIrish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesIrishAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 {
+ return monthNamesIrish[int(t.Month())-1]
+ }
+ return monthNamesIrishAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameItalian returns the Italian name of the month.
+func localMonthsNameItalian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesItalianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesItalian[int(t.Month())-1]
+ }
+ return monthNamesItalianAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameKannada returns the Kannada name of the month.
+func localMonthsNameKannada(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKannadaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKannada[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKannada[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameKashmiri returns the Kashmiri name of the month.
+func localMonthsNameKashmiri(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesKashmiri[int(t.Month())-1])[:1])
+ }
+ return monthNamesKashmiri[int(t.Month())-1]
+}
+
+// localMonthsNameKazakh returns the Kazakh name of the month.
+func localMonthsNameKazakh(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKazakhAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKazakh[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKazakh[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameKhmer returns the Khmer name of the month.
+func localMonthsNameKhmer(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKhmerAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKhmer[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKhmerAbbr[int(t.Month())+11])[:1])
+}
+
+// localMonthsNameKiche returns the Kiche name of the month.
+func localMonthsNameKiche(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKicheAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKiche[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKicheAbbr[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameKinyarwanda returns the Kinyarwanda name of the month.
+func localMonthsNameKinyarwanda(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKinyarwandaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKinyarwanda[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKinyarwanda[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameKiswahili returns the Kiswahili name of the month.
+func localMonthsNameKiswahili(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKiswahiliAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKiswahili[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKiswahili[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameKonkani returns the Konkani name of the month.
+func localMonthsNameKonkani(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKonkaniAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKonkani[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKonkani[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameKorean returns the Korean name of the month.
+func localMonthsNameKorean(t time.Time, abbr int) string {
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKoreanAbbr[int(t.Month())-1]
+ }
+ return strconv.Itoa(int(t.Month()))
+}
+
+// localMonthsNameKyrgyz returns the Kyrgyz name of the month.
+func localMonthsNameKyrgyz(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesKyrgyzAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesKyrgyz[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesKyrgyz[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLao returns the Lao name of the month.
+func localMonthsNameLao(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLaoAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLao[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLao[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLatin returns the Latin name of the month.
+func localMonthsNameLatin(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLatinAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLatin[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLatin[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLatvian returns the Latvian name of the month.
+func localMonthsNameLatvian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLatvianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLatvian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLatvian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLithuanian returns the Lithuanian name of the month.
+func localMonthsNameLithuanian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLithuanianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLithuanian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLithuanian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLowerSorbian returns the LowerSorbian name of the month.
+func localMonthsNameLowerSorbian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLowerSorbianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLowerSorbian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLowerSorbian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameLuxembourgish returns the Luxembourgish name of the month.
+func localMonthsNameLuxembourgish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesLuxembourgishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesLuxembourgish[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesLuxembourgish[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMacedonian returns the Macedonian name of the month.
+func localMonthsNameMacedonian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMacedonianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMacedonian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMacedonian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMalay returns the Malay name of the month.
+func localMonthsNameMalay(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMalayAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMalay[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMalay[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMalayalam returns the Malayalam name of the month.
+func localMonthsNameMalayalam(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMalayalamAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMalayalam[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMalayalam[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMaltese returns the Maltese name of the month.
+func localMonthsNameMaltese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMalteseAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMaltese[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMaltese[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMaori returns the Maori name of the month.
+func localMonthsNameMaori(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMaoriAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMaori[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMaori[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMapudungun returns the Mapudungun name of the month.
+func localMonthsNameMapudungun(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesMapudungun[(t.Month() - 1)])[:1])
+ }
+ return monthNamesMapudungun[int(t.Month())-1]
+}
+
+// localMonthsNameMarathi returns the Marathi name of the month.
+func localMonthsNameMarathi(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMarathiAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMarathi[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMarathi[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMohawk returns the Mohawk name of the month.
+func localMonthsNameMohawk(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return t.Month().String()[:3]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMohawk[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMohawk[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMongolian returns the Mongolian name of the month.
+func localMonthsNameMongolian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMongolianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesMongolian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesMongolian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameMorocco returns the Morocco name of the month.
+func localMonthsNameMorocco(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesMoroccoAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesFrench[int(t.Month())-1]
+ }
+ return monthNamesFrench[int(t.Month())-1][:1]
+}
+
+// localMonthsNameNepali returns the Nepali name of the month.
+func localMonthsNameNepali(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesNepaliAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesNepali[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesNepali[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameNepaliIN returns the India Nepali name of the month.
+func localMonthsNameNepaliIN(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesNepaliINAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesNepaliIN[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesNepaliIN[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameNigeria returns the Nigeria name of the month.
+func localMonthsNameNigeria(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesNigeriaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesNigeria[int(t.Month())-1]
+ }
+ return monthNamesNigeria[int(t.Month())-1][:1]
+}
+
+// localMonthsNameNorwegian returns the Norwegian name of the month.
+func localMonthsNameNorwegian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthsNameFaroeseAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesNorwegian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesNorwegian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameOccitan returns the Occitan name of the month.
+func localMonthsNameOccitan(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesOccitanAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesOccitan[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesOccitan[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameOdia returns the Odia name of the month.
+func localMonthsNameOdia(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesOdia[(t.Month() - 1)])[:1])
+ }
+ return monthNamesOdia[int(t.Month())-1]
+}
+
+// localMonthsNameOromo returns the Oromo name of the month.
+func localMonthsNameOromo(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesOromoAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesOromo[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesOromo[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNamePashto returns the Pashto name of the month.
+func localMonthsNamePashto(t time.Time, abbr int) string {
+ if int(t.Month()) == 6 {
+ if abbr == 3 {
+ return "\u0686\u0646\u06AB\u0627 \u069A"
+ }
+ if abbr == 4 || abbr > 6 {
+ return "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649"
+ }
+ }
+ return monthNamesPashto[int(t.Month())-1]
+}
+
+// localMonthsNamePersian returns the Persian name of the month.
+func localMonthsNamePersian(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesPersian[(t.Month() - 1)])[:1])
+ }
+ return monthNamesPersian[int(t.Month())-1]
+}
+
+// localMonthsNamePolish returns the Polish name of the month.
+func localMonthsNamePolish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesPolish[(t.Month() - 1)])[:3])
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesPolish[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesPolish[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNamePortuguese returns the Portuguese name of the month.
+func localMonthsNamePortuguese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesPortuguese[(t.Month() - 1)])[:3])
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesPortuguese[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesPortuguese[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNamePunjabi returns the Punjabi name of the month.
+func localMonthsNamePunjabi(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesPunjabi[(t.Month() - 1)])[:1])
+ }
+ return monthNamesPunjabi[int(t.Month())-1]
+}
+
+// localMonthsNamePunjabiArab returns the Punjabi Arab name of the month.
+func localMonthsNamePunjabiArab(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesPunjabiArab[(t.Month() - 1)])[:1])
+ }
+ return monthNamesPunjabiArab[int(t.Month())-1]
+}
+
+// localMonthsNameQuechua returns the Quechua name of the month.
+func localMonthsNameQuechua(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesQuechua[(t.Month() - 1)])[:3])
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesQuechua[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesQuechua[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameQuechuaEcuador returns the QuechuaEcuador name of the month.
+func localMonthsNameQuechuaEcuador(t time.Time, abbr int) string {
+ if abbr == 3 {
+ if int(t.Month()) == 1 {
+ return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:4])
+ }
+ return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:3])
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesQuechuaEcuador[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameRomanian returns the Romanian name of the month.
+func localMonthsNameRomanian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesRomanianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesRomanian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesRomanian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameRomansh returns the Romansh name of the month.
+func localMonthsNameRomansh(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesRomanshAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesRomansh[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesRomansh[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameRussian returns the Russian name of the month.
+func localMonthsNameRussian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ month := monthNamesRussian[int(t.Month())-1]
+ if len([]rune(month)) <= 4 {
+ return month
+ }
+ return monthNamesRussianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesRussian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesRussian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameSakha returns the Sakha name of the month.
+func localMonthsNameSakha(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSakhaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSakha[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSakha[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSami returns the Sami name of the month.
+func localMonthsNameSami(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSamiAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSami[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSami[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSamiLule returns the Sami (Lule) name of the month.
+func localMonthsNameSamiLule(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSamiLuleAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSamiLule[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSamiLule[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSamiNorthern returns the Sami (Northern) name of the month.
+func localMonthsNameSamiNorthern(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSamiNorthernAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSamiNorthern[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSamiNorthern[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSamiNorthernFI returns the Sami (Northern) Finland name of the
+// month.
+func localMonthsNameSamiNorthernFI(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSamiNorthernAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ if int(t.Month()) == 1 {
+ return "ođđajagemánu"
+ }
+ return monthNamesSamiNorthern[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSamiNorthern[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSamiSkolt returns the Sami (Skolt) name of the month.
+func localMonthsNameSamiSkolt(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesSamiSkolt[(t.Month() - 1)])[:1])
+ }
+ return monthNamesSamiSkolt[int(t.Month())-1]
+}
+
+// localMonthsNameSamiSouthern returns the Sami (Southern) name of the month.
+func localMonthsNameSamiSouthern(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSamiSouthernAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSamiSouthern[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSamiSouthern[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSanskrit returns the Sanskrit name of the month.
+func localMonthsNameSanskrit(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesSanskrit[(t.Month() - 1)])[:1])
+ }
+ return monthNamesSanskrit[int(t.Month())-1]
+}
+
+// localMonthsNameScottishGaelic returns the Scottish Gaelic name of the month.
+func localMonthsNameScottishGaelic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesScottishGaelicAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesScottishGaelic[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesScottishGaelic[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSerbian returns the Serbian (Cyrillic) name of the month.
+func localMonthsNameSerbian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSerbianAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSerbian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSerbian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSerbianBA returns the Serbian (Cyrillic) Bosnia and
+// Herzegovina name of the month.
+func localMonthsNameSerbianBA(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSerbianBAAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSerbianBA[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSerbianBA[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSerbianLatin returns the Serbian (Latin) name of the month.
+func localMonthsNameSerbianLatin(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:3])
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSerbianLatin[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSerbianLatinCS returns the Serbian (Latin) name of the month.
+func localMonthsNameSerbianLatinCS(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSerbianLatinAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSerbianLatin[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSesothoSaLeboa returns the Sesotho sa Leboa name of the month.
+func localMonthsNameSesothoSaLeboa(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSesothoSaLeboaAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSesothoSaLeboa[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSesothoSaLeboa[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSetswana returns the Setswana name of the month.
+func localMonthsNameSetswana(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSetswanaAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSetswana[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSetswana[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSindhi returns the Sindhi name of the month.
+func localMonthsNameSindhi(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesSindhi[(t.Month() - 1)])[:1])
+ }
+ return monthNamesSindhi[int(t.Month())-1]
+}
+
+// localMonthsNameSinhala returns the Sinhala name of the month.
+func localMonthsNameSinhala(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSinhalaAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSinhala[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSinhala[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSlovak returns the Slovak name of the month.
+func localMonthsNameSlovak(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return strconv.Itoa(int(t.Month()))
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSlovak[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSlovak[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSlovenian returns the Slovenian name of the month.
+func localMonthsNameSlovenian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSlovenianAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSlovenian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSlovenian[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSomali returns the Somali name of the month.
+func localMonthsNameSomali(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSomaliAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSomali[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSomali[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSotho returns the Sotho name of the month.
+func localMonthsNameSotho(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSothoAbbr[(t.Month() - 1)]
+ }
+ if abbr == 4 || abbr > 6 {
+ return monthNamesSotho[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSotho[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSpanish returns the Spanish name of the month.
+func localMonthsNameSpanish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSpanishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSpanish[int(t.Month())-1]
+ }
+ return monthNamesSpanishAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameSpanishPE returns the Spanish Peru name of the month.
+func localMonthsNameSpanishPE(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSpanishPEAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSpanishPE[int(t.Month())-1]
+ }
+ return monthNamesSpanishPEAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameSwedish returns the Swedish name of the month.
+func localMonthsNameSwedish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSwedishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSwedish[int(t.Month())-1]
+ }
+ return monthNamesSwedishAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameSwedishFI returns the Swedish Finland name of the month.
+func localMonthsNameSwedishFI(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSwedishFIAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSwedish[int(t.Month())-1]
+ }
+ return monthNamesSwedishFIAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameSyriac returns the Syriac name of the month.
+func localMonthsNameSyriac(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSyriacAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSyriac[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSyriac[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTajik returns the Tajik name of the month.
+func localMonthsNameTajik(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTajikAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTajik[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTajik[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTamazight returns the Tamazight name of the month.
+func localMonthsNameTamazight(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTamazightAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTamazight[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTamazight[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTamil returns the Tamil name of the month.
+func localMonthsNameTamil(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return string([]rune(monthNamesTamil[(t.Month() - 1)])[:1])
+ }
+ return monthNamesTamil[int(t.Month())-1]
+}
+
+// localMonthsNameTamilLK returns the Tamil Sri Lanka name of the month.
+func localMonthsNameTamilLK(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTamilAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTamil[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTamil[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTatar returns the Tatar name of the month.
+func localMonthsNameTatar(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTatarAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTatar[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTatar[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTelugu returns the Telugu name of the month.
+func localMonthsNameTelugu(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTeluguAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTelugu[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTelugu[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameSyllabics returns the Syllabics name of the month.
+func localMonthsNameSyllabics(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesSyllabicsAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesSyllabics[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesSyllabics[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameThai returns the Thai name of the month.
+func localMonthsNameThai(t time.Time, abbr int) string {
+ if abbr == 3 {
+ r := []rune(monthNamesThai[int(t.Month())-1])
+ return string(r[:1]) + "." + string(r[len(r)-2:len(r)-1]) + "."
+ }
+ if abbr == 4 {
+ return monthNamesThai[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesThai[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameTibetan returns the Tibetan name of the month.
+func localMonthsNameTibetan(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTibetanAbbr[int(t.Month())-1]
+ }
+ if abbr == 5 {
+ if t.Month() == 10 {
+ return "\u0f66"
+ }
+ return "\u0f5f"
+ }
+ return monthNamesTibetan[int(t.Month())-1]
+}
+
+// localMonthsNameTigrinya returns the Tigrinya name of the month.
+func localMonthsNameTigrinya(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTigrinyaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTigrinya[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTigrinya[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTsonga returns the Tsonga name of the month.
+func localMonthsNameTsonga(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTsongaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTsonga[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTsonga[(t.Month() - 1)])[:1])
+}
+
+// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of
+// the month.
+func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string {
+ if abbr == 5 {
+ return "M"
+ }
+ return monthNamesTradMongolian[t.Month()-1]
+}
+
+// localMonthsNameTurkish returns the Turkish name of the month.
+func localMonthsNameTurkish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTurkishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTurkish[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTurkishAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameTurkmen returns the Turkmen name of the month.
+func localMonthsNameTurkmen(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTurkmenAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTurkmen[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTurkmenAbbr[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameUkrainian returns the Ukrainian name of the month.
+func localMonthsNameUkrainian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesUkrainianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesUkrainian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesUkrainian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameUpperSorbian returns the Upper Sorbian name of the month.
+func localMonthsNameUpperSorbian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesUpperSorbianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesUpperSorbian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesUpperSorbian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameUyghur returns the Uyghur name of the month.
+func localMonthsNameUyghur(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return fmt.Sprintf("%d-\u0626\u0627\u064A", int(t.Month()))
+ }
+ if abbr == 4 {
+ return monthNamesUyghur[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesUyghur[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameUzbek returns the Uzbek name of the month.
+func localMonthsNameUzbek(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesUzbekAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesUzbek[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesUzbek[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameUzbekCyrillic returns the Uzbek (Cyrillic) name of the month.
+func localMonthsNameUzbekCyrillic(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesTajikAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesTajik[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesTajik[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameValencian returns the Valencian name of the month.
+func localMonthsNameValencian(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesValencianAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesValencian[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesValencian[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameVenda returns the Venda name of the month.
+func localMonthsNameVenda(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesVendaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesVenda[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesVenda[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameVietnamese returns the Vietnamese name of the month.
+func localMonthsNameVietnamese(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesVietnameseAbbr3[t.Month()-1]
+ }
+ if abbr == 5 {
+ return monthNamesVietnameseAbbr5[t.Month()-1]
+ }
+ return monthNamesVietnamese[t.Month()-1]
+}
+
+// localMonthsNameWelsh returns the Welsh name of the month.
+func localMonthsNameWelsh(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesWelshAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesWelsh[int(t.Month())-1]
+ }
+ return monthNamesWelshAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsNameWolof returns the Wolof name of the month.
+func localMonthsNameWolof(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesWolofAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesWolof[int(t.Month())-1]
+ }
+ return monthNamesWolof[int(t.Month())-1][:1]
+}
+
+// localMonthsNameXhosa returns the Xhosa name of the month.
+func localMonthsNameXhosa(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesXhosaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesXhosa[int(t.Month())-1]
+ }
+ return "u"
+}
+
+// localMonthsNameYi returns the Yi name of the month.
+func localMonthsNameYi(t time.Time, abbr int) string {
+ if abbr == 3 || abbr == 4 {
+ return monthNamesYiSuffix[t.Month()-1]
+ }
+ return string([]rune(monthNamesYi[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameYiddish returns the Yiddish name of the month.
+func localMonthsNameYiddish(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesYiddishAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesYiddish[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesYiddish[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameYoruba returns the Yoruba name of the month.
+func localMonthsNameYoruba(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesYorubaAbbr[int(t.Month())-1]
+ }
+ if abbr == 4 {
+ return monthNamesYoruba[int(t.Month())-1]
+ }
+ return string([]rune(monthNamesYoruba[int(t.Month())-1])[:1])
+}
+
+// localMonthsNameZulu returns the Zulu name of the month.
+func localMonthsNameZulu(t time.Time, abbr int) string {
+ if abbr == 3 {
+ return monthNamesZuluAbbr[t.Month()-1]
+ }
+ if abbr == 4 {
+ return monthNamesZulu[int(t.Month())-1]
+ }
+ return monthNamesZuluAbbr[int(t.Month())-1][:1]
+}
+
+// localMonthsName return months name by supported language ID.
+func (nf *numberFormat) localMonthsName(abbr int) string {
+ if languageInfo, ok := getSupportedLanguageInfo(nf.localCode); ok {
+ return languageInfo.localMonth(nf.t, abbr)
+ }
+ return localMonthsNameEnglish(nf.t, abbr)
+}
+
+// dateTimesHandler will be handling date and times types tokens for a number
+// format expression.
+func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
+ if idx := inStrSlice(nfp.AmPm, strings.ToUpper(token.TValue), false); idx != -1 {
+ if nf.ap == "" {
+ nextHours := nf.hoursNext(i)
+ aps := strings.Split(nf.localAmPm(token.TValue), "/")
+ nf.ap = aps[0]
+ if nextHours >= 12 {
+ nf.ap = aps[1]
+ }
+ }
+ nf.result += nf.ap
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ l := len(token.TValue)
+ if l == 1 && nf.isMonthToken(i) {
+ nf.result += strconv.Itoa(int(nf.t.Month()))
+ return
+ }
+ if l == 2 && nf.isMonthToken(i) {
+ nf.result += fmt.Sprintf("%02d", int(nf.t.Month()))
+ return
+ }
+ if l == 3 {
+ nf.result += nf.localMonthsName(3)
+ return
+ }
+ if l == 4 || l > 5 {
+ nf.result += nf.localMonthsName(4)
+ return
+ }
+ if l == 5 {
+ nf.result += nf.localMonthsName(5)
+ return
+ }
+ }
+ nf.yearsHandler(token)
+ nf.daysHandler(token)
+ nf.hoursHandler(i, token)
+ nf.minutesHandler(token)
+ nf.secondsHandler(token)
+}
+
+// eraYear convert time to the Japanese era years.
+func eraYear(t time.Time) (int, int) {
+ i, year := 0, -1
+ for i = len(japaneseEraYears) - 1; i > 0; i-- {
+ if y := japaneseEraYears[i]; t.After(y) {
+ year = t.Year() - y.Year() + 1
+ break
+ }
+ }
+ return i, year
+}
+
+// japaneseYearHandler handling the Japanease calendar years.
+func (nf *numberFormat) japaneseYearHandler(token nfp.Token, langInfo languageInfo) {
+ if strings.Contains(strings.ToUpper(token.TValue), "G") {
+ i, year := eraYear(nf.t)
+ if year == -1 {
+ return
+ }
+ nf.useGannen = langInfo.useGannen
+ switch len(token.TValue) {
+ case 1:
+ nf.useGannen = false
+ nf.result += japaneseEraSymbols[i]
+ case 2:
+ nf.result += japaneseEraNames[i][:3]
+ default:
+ nf.result += japaneseEraNames[i]
+ }
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "E") {
+ _, year := eraYear(nf.t)
+ if year == -1 {
+ nf.result += strconv.Itoa(nf.t.Year())
+ return
+ }
+ if year == 1 && nf.useGannen {
+ nf.result += "\u5143"
+ return
+ }
+ if len(token.TValue) == 1 && !nf.useGannen {
+ nf.result += strconv.Itoa(year)
+ return
+ }
+ if len(token.TValue) == 2 {
+ nf.result += fmt.Sprintf("%02d", year)
+ }
+ }
+}
+
+// republicOfChinaYearHandler handling the Republic of China calendar years.
+func (nf *numberFormat) republicOfChinaYearHandler(token nfp.Token, langInfo languageInfo) {
+ if strings.Contains(strings.ToUpper(token.TValue), "G") {
+ year := nf.t.Year() - republicOfChinaYear.Year() + 1
+ if year == 1 {
+ nf.useGannen = langInfo.useGannen
+ }
+ var name string
+ if name = republicOfChinaEraName[0]; len(token.TValue) < 3 {
+ name = republicOfChinaEraName[1]
+ }
+ if year < 0 {
+ name += republicOfChinaEraName[2]
+ }
+ nf.result += name
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "E") {
+ year := nf.t.Year() - republicOfChinaYear.Year() + 1
+ if year < 0 {
+ year = republicOfChinaYear.Year() - nf.t.Year()
+ }
+ if year == 1 && nf.useGannen {
+ nf.result += "\u5143"
+ return
+ }
+ if len(token.TValue) == 1 && !nf.useGannen {
+ nf.result += strconv.Itoa(year)
+ }
+ }
+}
+
+// yearsHandler will be handling years in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) yearsHandler(token nfp.Token) {
+ langInfo, _ := getSupportedLanguageInfo(nf.localCode)
+ if strings.Contains(strings.ToUpper(token.TValue), "Y") {
+ year := nf.t.Year()
+ if nf.opts != nil && nf.opts.CultureInfo == CultureNameKoKR {
+ year += 2333
+ }
+ if len(token.TValue) <= 2 {
+ nf.result += strconv.Itoa(year)[2:]
+ return
+ }
+ nf.result += strconv.Itoa(year)
+ return
+ }
+ if inStrSlice(langInfo.tags, "zh-TW", false) != -1 ||
+ nf.opts != nil && nf.opts.CultureInfo == CultureNameZhTW {
+ nf.republicOfChinaYearHandler(token, langInfo)
+ return
+ }
+ if inStrSlice(langInfo.tags, "ja-JP", false) != -1 ||
+ nf.opts != nil && nf.opts.CultureInfo == CultureNameJaJP {
+ nf.japaneseYearHandler(token, langInfo)
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "E") {
+ nf.result += strconv.Itoa(nf.t.Year())
+ return
+ }
+}
+
+// daysHandler will be handling days in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) daysHandler(token nfp.Token) {
+ info, _ := getSupportedLanguageInfo(nf.localCode)
+ l := len(token.TValue)
+ weekdayNames, weekdayNamesAbbr := info.weekdayNames, info.weekdayNamesAbbr
+ if len(weekdayNames) != 7 {
+ weekdayNames = weekdayNamesEnglish
+ }
+ if len(weekdayNamesAbbr) != 7 {
+ weekdayNamesAbbr = weekdayNamesEnglishAbbr
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "A") {
+ if l == 3 {
+ nf.result += weekdayNamesAbbr[nf.t.Weekday()]
+ }
+ if l > 3 {
+ nf.result += weekdayNames[nf.t.Weekday()]
+ }
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "D") {
+ switch l {
+ case 1:
+ nf.result += strconv.Itoa(nf.t.Day())
+ case 2:
+ nf.result += fmt.Sprintf("%02d", nf.t.Day())
+ case 3:
+ nf.result += weekdayNamesAbbr[nf.t.Weekday()]
+ default:
+ nf.result += weekdayNames[nf.t.Weekday()]
+ }
+ }
+}
+
+// hoursHandler will be handling hours in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) hoursHandler(i int, token nfp.Token) {
+ if nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H"); nf.hours {
+ h := nf.t.Hour()
+ ap, ok := nf.apNext(i)
+ if ok {
+ nf.ap = ap[0]
+ if h >= 12 {
+ nf.ap = ap[1]
+ }
+ if h > 12 {
+ h -= 12
+ }
+ }
+ if nf.ap != "" {
+ if nf.hoursNext(i) == -1 && h > 12 {
+ h -= 12
+ }
+ if h == 0 {
+ h = 12
+ }
+ }
+ switch len(token.TValue) {
+ case 1:
+ nf.result += strconv.Itoa(h)
+ return
+ default:
+ nf.result += fmt.Sprintf("%02d", h)
+ return
+ }
+ }
+}
+
+// minutesHandler will be handling minutes in the date and times types tokens
+// for a number format expression.
+func (nf *numberFormat) minutesHandler(token nfp.Token) {
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ nf.hours = false
+ switch len(token.TValue) {
+ case 1:
+ nf.result += strconv.Itoa(nf.t.Minute())
+ return
+ default:
+ nf.result += fmt.Sprintf("%02d", nf.t.Minute())
+ }
+ }
+}
+
+// secondsHandler will be handling seconds in the date and times types tokens
+// for a number format expression.
+func (nf *numberFormat) secondsHandler(token nfp.Token) {
+ if nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S"); !nf.seconds {
+ return
+ }
+ if len(token.TValue) == 1 {
+ nf.result += strconv.Itoa(nf.t.Second())
+ return
+ }
+ nf.result += fmt.Sprintf("%02d", nf.t.Second())
+}
+
+// elapsedDateTimesHandler will be handling elapsed date and times types tokens
+// for a number format expression.
+func (nf *numberFormat) elapsedDateTimesHandler(token nfp.Token) {
+ if strings.Contains(strings.ToUpper(token.TValue), "H") {
+ nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Hours()))
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Minutes()))
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "S") {
+ nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Seconds()))
+ return
+ }
+}
+
+// hoursNext detects if a token of type hours exists after a given tokens list.
+func (nf *numberFormat) hoursNext(i int) int {
+ tokens := nf.section[nf.sectionIdx].Items
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
+ t := timeFromExcelTime(nf.number, false)
+ return t.Hour()
+ }
+ }
+ }
+ return -1
+}
+
+// apNext detects if a token of type AM/PM exists after a given tokens list.
+func (nf *numberFormat) apNext(i int) ([]string, bool) {
+ tokens := nf.section[nf.sectionIdx].Items
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
+ return nil, false
+ }
+ if i := inStrSlice(nfp.AmPm, tokens[idx].TValue, false); i != -1 {
+ return strings.Split(nf.localAmPm(tokens[idx].TValue), "/"), true
+ }
+ }
+ }
+ return nil, false
+}
+
+// isMonthToken detects if the given token represents minutes, if no hours and
+// seconds tokens before the given token or not seconds after the given token,
+// the current token is a minutes token.
+func (nf *numberFormat) isMonthToken(i int) bool {
+ tokens := nf.section[nf.sectionIdx].Items
+ var timePrevious, secondsNext bool
+ for idx := i - 1; idx >= 0; idx-- {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ timePrevious = strings.ContainsAny(strings.ToUpper(tokens[idx].TValue), "HS")
+ break
+ }
+ if tokens[idx].TType == nfp.TokenTypeElapsedDateTimes {
+ timePrevious = true
+ break
+ }
+ }
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ secondsNext = strings.Contains(strings.ToUpper(tokens[idx].TValue), "S")
+ break
+ }
+ }
+ return !(timePrevious || secondsNext)
+}
+
+// negativeHandler will be handling negative selection for a number format
+// expression.
+func (nf *numberFormat) negativeHandler() (result string) {
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral {
+ return nf.value
+ }
+ if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 {
+ return nf.value
+ }
+ }
+ return nf.numberHandler()
+}
+
+// textHandler will be handling text selection for a number format expression.
+func (nf *numberFormat) textHandler() (result string) {
+ for _, token := range nf.section[nf.sectionIdx].Items {
+ if token.TType == nfp.TokenTypeLiteral {
+ result += token.TValue
+ }
+ if token.TType == nfp.TokenTypeTextPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder {
+ result += nf.value
+ }
+ }
+ return result
+}
+
+// getValueSectionType returns its applicable number format expression section
+// based on the given value.
+func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
+ if nf.cellType != CellTypeNumber && nf.cellType != CellTypeDate {
+ return 0, nfp.TokenSectionText
+ }
+ isNum, _, _ := isNumeric(value)
+ if !isNum {
+ return 0, nfp.TokenSectionText
+ }
+ number, _ := strconv.ParseFloat(value, 64)
+ if number >= 0 {
+ return number, nfp.TokenSectionPositive
+ }
+ var hasNeg bool
+ for _, sec := range nf.section {
+ if sec.Type == nfp.TokenSectionNegative {
+ hasNeg = true
+ }
+ }
+ if !hasNeg {
+ nf.usePositive = true
+ return number, nfp.TokenSectionPositive
+ }
+ return number, nfp.TokenSectionNegative
+}
diff --git a/numfmt_test.go b/numfmt_test.go
new file mode 100644
index 0000000000..5380c1bc89
--- /dev/null
+++ b/numfmt_test.go
@@ -0,0 +1,3689 @@
+package excelize
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/xuri/nfp"
+)
+
+func TestNumFmt(t *testing.T) {
+ for _, item := range [][]string{
+ {"123", "general", "123"},
+ {"-123", ";general", "-123"},
+ {"12345678901", "General", "12345678901"},
+ {"43543.5448726851", "General", "43543.54487"},
+ {"-43543.5448726851", "General", "-43543.54487"},
+ {"1234567890.12345", "General", "1234567890"},
+ {"43528", "y", "19"},
+ {"43528", "Y", "19"},
+ {"43528", "yy", "19"},
+ {"43528", "YY", "19"},
+ {"43528", "yyy", "2019"},
+ {"43528", "YYY", "2019"},
+ {"43528", "yyyy", "2019"},
+ {"43528", "YYYY", "2019"},
+ {"43528", "yyyyy", "2019"},
+ {"43528", "YYYYY", "2019"},
+ {"43528", "m", "3"},
+ {"43528", "mm", "03"},
+ {"43528", "mmm", "Mar"},
+ {"43528", "mmmm", "March"},
+ {"43528", "mmmmm", "M"},
+ {"43528", "mmmmmm", "March"},
+ {"43528", "d", "4"},
+ {"43528", "dd", "04"},
+ {"43528", "ddd", "Mon"},
+ {"43528", "dddd", "Monday"},
+ {"43528", "h", "0"},
+ {"43528", "hh", "00"},
+ {"43528", "hhh", "00"},
+ {"43543.544872685183", "hhmm", "1304"},
+ {"43543.544872685183", "mmhhmmmm", "0313March"},
+ {"43543.544872685183", "mm hh mm mm", "03 13 04 03"},
+ {"43543.544872685183", "mm hh m m", "03 13 4 3"},
+ {"43543.544872685183", "m s", "4 37"},
+ {"43528", "[h]", "1044672"},
+ {"43528", "[m]", "62680320"},
+ {"43528", "s", "0"},
+ {"43528", "ss", "00"},
+ {"43528", "[s]", "3760819200"},
+ {"43543.544872685183", "h:mm:ss AM/PM", "1:04:37 PM"},
+ {"43543.544872685183", "AM/PM h:mm:ss", "PM 1:04:37"},
+ {"43543.086539351854", "hh:mm:ss AM/PM", "02:04:37 AM"},
+ {"43543.086539351854", "AM/PM hh:mm:ss", "AM 02:04:37"},
+ {"43543.086539351854", "AM/PM hh:mm:ss a/p", "AM 02:04:37 a"},
+ {"0.609375", "[HH]:mm:ss", "14:37:30"},
+ {"43528", "YYYY", "2019"},
+ {"43528", "", "43528"},
+ {"43528.2123", "YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:43"},
+ {"43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:43"},
+ {"43528.2123", "M/D/YYYY h:m:s", "3/4/2019 5:5:43"},
+ {"43528.003958333335", "m/d/yyyy h:m:s", "3/4/2019 0:5:42"},
+ {"43528.003958333335", "M/D/YYYY h:mm:s", "3/4/2019 0:05:42"},
+ {"0.64583333333333337", "h:mm:ss am/pm", "3:30:00 pm"},
+ {"43528.003958333335", "h:mm", "0:05"},
+ {"6.9444444444444444E-5", "h:m", "0:0"},
+ {"6.9444444444444444E-5", "h:mm", "0:00"},
+ {"6.9444444444444444E-5", "h:m", "0:0"},
+ {"0.50070601851851848", "h:m", "12:1"},
+ {"0.97952546296296295", "h:m", "23:30"},
+ {"43528", "mmmm", "March"},
+ {"43528", "dddd", "Monday"},
+ {"0", ";;;", ""},
+ {"0", "0%", "0%"},
+ {"0", "0.0%", "0.0%"},
+ {"0", "0.00%", "0.00%"},
+ {"43528", "[$-409]MM/DD/YYYY", "03/04/2019"},
+ {"43528", "[$-409]MM/DD/YYYY am/pm", "03/04/2019 AM"},
+ {"43528", "[$-111]MM/DD/YYYY", "43528"},
+ {"43528", "[$US-409]MM/DD/YYYY", "US03/04/2019"},
+ {"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"},
+ {"45186", "DD.MM.YYYY", "17.09.2023"},
+ {"text", "AM/PM h h:mm", "text"},
+ {"43466.189571759256", "[$-404]aaa;@", "週二"},
+ {"43466.189571759256", "[$-404]aaaa;@", "星期二"},
+ {"43466.189571759256", "[$-zh-TW]aaa;@", "週二"},
+ {"43466.189571759256", "[$-zh-TW]aaaa;@", "星期二"},
+ {"43466.189571759256", "[$-804]aaa;@", "周二"},
+ {"43466.189571759256", "[$-804]aaaa;@", "星期二"},
+ {"43466.189571759256", "[$-0804]aaa;@", "周二"},
+ {"43466.189571759256", "[$-0804]aaaa;@", "星期二"},
+ {"43466.189571759256", "[$-zh-CN]aaa;@", "周二"},
+ {"43466.189571759256", "[$-zh-CN]aaaa;@", "星期二"},
+ {"43466.189571759256", "[$-435]aaa;@", "Bi."},
+ {"43466.189571759256", "[$-435]aaaa;@", "ULwesibili"},
+ {"43466.189571759256", "[$-0435]aaa;@", "Bi."},
+ {"43466.189571759256", "[$-0435]aaaa;@", "ULwesibili"},
+ {"43466.189571759256", "[$-zu-ZA]aaa;@", "Bi."},
+ {"43466.189571759256", "[$-zu-ZA]aaaa;@", "ULwesibili"},
+ {"43466.189571759256", "[$-404]ddd;@", "週二"},
+ {"43466.189571759256", "[$-404]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-0404]ddd;@", "週二"},
+ {"43466.189571759256", "[$-0404]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-zh-TW]ddd;@", "週二"},
+ {"43466.189571759256", "[$-zh-TW]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-804]ddd;@", "周二"},
+ {"43466.189571759256", "[$-804]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-0804]ddd;@", "周二"},
+ {"43466.189571759256", "[$-0804]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-zh-CN]ddd;@", "周二"},
+ {"43466.189571759256", "[$-zh-CN]dddd;@", "星期二"},
+ {"43466.189571759256", "[$-435]ddd;@", "Bi."},
+ {"43466.189571759256", "[$-435]dddd;@", "ULwesibili"},
+ {"43466.189571759256", "[$-0435]ddd;@", "Bi."},
+ {"43466.189571759256", "[$-0435]dddd;@", "ULwesibili"},
+ {"43466.189571759256", "[$-zu-ZA]ddd;@", "Bi."},
+ {"43466.189571759256", "[$-zu-ZA]dddd;@", "ULwesibili"},
+ {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"},
+ {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"},
+ {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."},
+ {"44682.18957170139", "[$-36]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"},
+ {"44682.18957170139", "[$-36]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."},
+ {"44682.18957170139", "[$-36]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"},
+ {"44562.189571759256", "[$-036]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"},
+ {"44562.189571759256", "[$-036]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"},
+ {"44562.189571759256", "[$-036]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."},
+ {"44682.18957170139", "[$-036]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"},
+ {"44682.18957170139", "[$-036]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."},
+ {"44682.18957170139", "[$-036]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"},
+ {"44562.189571759256", "[$-0036]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"},
+ {"44562.189571759256", "[$-0036]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"},
+ {"44562.189571759256", "[$-0036]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."},
+ {"44682.18957170139", "[$-0036]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"},
+ {"44682.18957170139", "[$-0036]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."},
+ {"44682.18957170139", "[$-0036]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"},
+ {"44562.189571759256", "[$-af]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"},
+ {"44562.189571759256", "[$-af]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"},
+ {"44562.189571759256", "[$-af]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."},
+ {"44682.18957170139", "[$-af]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"},
+ {"44682.18957170139", "[$-af]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."},
+ {"44682.18957170139", "[$-af]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"},
+ {"44562.189571759256", "[$-436]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"},
+ {"44562.189571759256", "[$-436]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"},
+ {"44562.189571759256", "[$-436]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."},
+ {"44682.18957170139", "[$-436]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"},
+ {"44682.18957170139", "[$-436]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."},
+ {"44682.18957170139", "[$-436]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"},
+ {"44562.189571759256", "[$-1C]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-1C]mmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-1C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-1C]mmmmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."},
+ {"43543.503206018519", "[$-1C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 m.d."},
+ {"43543.503206018519", "[$-1C]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 m.d. mar"},
+ {"43543.503206018519", "[$-1C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 m.d. mar"},
+ {"43543.503206018519", "[$-1C]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 m.d. e martë"},
+ {"44562.189571759256", "[$-41C]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-41C]mmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-41C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 p.d."},
+ {"44562.189571759256", "[$-41C]mmmmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."},
+ {"43543.503206018519", "[$-41C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 m.d."},
+ {"43543.503206018519", "[$-41C]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 m.d. mar"},
+ {"43543.503206018519", "[$-41C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 m.d. mar"},
+ {"43543.503206018519", "[$-41C]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 m.d. e martë"},
+ {"44562.189571759256", "[$-84]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 vorm."},
+ {"44562.189571759256", "[$-84]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 vorm."},
+ {"44562.189571759256", "[$-84]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 vorm."},
+ {"44562.189571759256", "[$-84]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 vorm."},
+ {"43543.503206018519", "[$-84]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 nam."},
+ {"43543.503206018519", "[$-84]mmmm dd yyyy h:mm AM/PM aaa", "März 19 2019 12:04 nam. Zi."},
+ {"43543.503206018519", "[$-84]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 nam. Zi."},
+ {"43543.503206018519", "[$-84]mmmmmm dd yyyy h:mm AM/PM dddd", "März 19 2019 12:04 nam. Ziischtig"},
+ {"44562.189571759256", "[$-484]mmm dd yyyy h:mm AM/PM", "Jän. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-484]mmmm dd yyyy h:mm AM/PM", "Jänner 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-484]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-484]mmmmmm dd yyyy h:mm AM/PM", "Jänner 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-484]mmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-484]mmmm dd yyyy h:mm AM/PM aaa", "März 19 2019 12:04 PM Zi."},
+ {"43543.503206018519", "[$-484]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Zi."},
+ {"43543.503206018519", "[$-484]mmmmmm dd yyyy h:mm AM/PM dddd", "März 19 2019 12:04 PM Zischti"},
+ {"44562.189571759256", "[$-5E]mmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-5E]mmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-5E]mmmmm dd yyyy h:mm AM/PM", "\u1303 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-5E]mmmmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"43543.503206018519", "[$-5E]mmm dd yyyy h:mm AM/PM", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275"},
+ {"43543.503206018519", "[$-5E]mmmm dd yyyy h:mm AM/PM aaa", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"},
+ {"43543.503206018519", "[$-5E]mmmmm dd yyyy h:mm AM/PM ddd", "\u121B 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"},
+ {"43543.503206018519", "[$-5E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230\u129E"},
+ {"44562.189571759256", "[$-45E]mmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-45E]mmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-45E]mmmmm dd yyyy h:mm AM/PM", "\u1303 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"44562.189571759256", "[$-45E]mmmmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"},
+ {"43543.503206018519", "[$-45E]mmm dd yyyy h:mm AM/PM", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275"},
+ {"43543.503206018519", "[$-45E]mmmm dd yyyy h:mm AM/PM aaa", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"},
+ {"43543.503206018519", "[$-45E]mmmmm dd yyyy h:mm AM/PM ddd", "\u121B 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"},
+ {"43543.503206018519", "[$-45E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230\u129E"},
+ {"44562.189571759256", "[$-1]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-1]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-1]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-1401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-1401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-1401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-3C01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3C01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3C01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3C01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-3C01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-3C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-c01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-c01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-c01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-c01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-c01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-c01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-c01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-c01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-801]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-801]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-801]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-801]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-801]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-801]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-2C01]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2C01]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2C01]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2C01]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-2C01]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-2C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-3401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-3401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-3401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-3001]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3001]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3001]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3001]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-3001]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-3001]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-1801]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1801]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1801]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1801]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-1801]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-1801]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-2001]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2001]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2001]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2001]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-2001]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-2001]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-4001]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-4001]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-4001]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-4001]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-4001]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-4001]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-4001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-4001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43466.189571759256", "[$-404]g\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5e74\u0031\u6708\u0031\u65e5"},
+ {"43466.189571759256", "[$-404]e\"年\"m\"月\"d\"日\";@", "\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
+ {"43466.189571759256", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
+ {"43466.189571759256", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
+ {"43466.189571759256", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
+ {"43466.189571759256", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"},
+ {"4385.5083333333332", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
+ {"4385.5083333333332", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
+ {"4385.5083333333332", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
+ {"4385.5083333333332", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"},
+ {"123", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
+ {"123", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
+ {"123", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
+ {"123", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"},
+ {"44562.189571759256", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1010401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1010401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1010401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-1010401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1010401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1010401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-2801]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2801]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2801]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2801]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-2801]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-2801]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-1C01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1C01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1C01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-1C01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-1C01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-1C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-1C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-3801]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3801]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3801]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-3801]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-3801]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-3801]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-3801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-2401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-2401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-2401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-2401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-2401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-2B]mmm dd yyyy h:mm AM/PM", "\u0540\u0576\u057E 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2B]mmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2B]mmmmm dd yyyy h:mm AM/PM", "\u0540 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2B]mmmmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-2B]mmm dd yyyy h:mm AM/PM", "\u0544\u0580\u057F 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-2B]mmmm dd yyyy h:mm AM/PM aaa", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0584"},
+ {"43543.503206018519", "[$-2B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0544 19 2019 12:04 PM \u0535\u0580\u0584"},
+ {"43543.503206018519", "[$-2B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B"},
+ {"44562.189571759256", "[$-42B]mmm dd yyyy h:mm AM/PM", "\u0540\u0576\u057E 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42B]mmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42B]mmmmm dd yyyy h:mm AM/PM", "\u0540 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42B]mmmmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-42B]mmm dd yyyy h:mm AM/PM", "\u0544\u0580\u057F 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-42B]mmmm dd yyyy h:mm AM/PM aaa", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0584"},
+ {"43543.503206018519", "[$-42B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0544 19 2019 12:04 PM \u0535\u0580\u0584"},
+ {"43543.503206018519", "[$-42B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B"},
+ {"44562.189571759256", "[$-4D]mmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-4D]mmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-4D]mmmmm dd yyyy h:mm AM/PM", "\u099C 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-4D]mmmmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"43543.503206018519", "[$-4D]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF"},
+ {"43543.503206018519", "[$-4D]mmmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-4D]mmmmm dd yyyy h:mm AM/PM ddd", "\u09AE 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-4D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0"},
+ {"44562.189571759256", "[$-44D]mmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-44D]mmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-44D]mmmmm dd yyyy h:mm AM/PM", "\u099C 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"44562.189571759256", "[$-44D]mmmmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"},
+ {"43543.503206018519", "[$-44D]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF"},
+ {"43543.503206018519", "[$-44D]mmmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-44D]mmmmm dd yyyy h:mm AM/PM ddd", "\u09AE 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-44D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0"},
+ {"44562.189571759256", "[$-742C]mmm dd yyyy h:mm AM/PM", "\u0408\u0430\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-742C]mmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-742C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-742C]mmmmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-742C]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-742C]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0427\u0430"},
+ {"43543.503206018519", "[$-742C]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0427\u0430"},
+ {"43543.503206018519", "[$-742C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B"},
+ {"44562.189571759256", "[$-82C]mmm dd yyyy h:mm AM/PM", "\u0408\u0430\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82C]mmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82C]mmmmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-82C]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-82C]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0427\u0430"},
+ {"43543.503206018519", "[$-82C]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0427\u0430"},
+ {"43543.503206018519", "[$-82C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B"},
+ {"44562.189571759256", "[$-2C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-2C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-2C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-2C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-2C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"},
+ {"44562.189571759256", "[$-782C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-782C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-782C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-782C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-782C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-782C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-782C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-782C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"},
+ {"44562.189571759256", "[$-42C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-42C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-42C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-42C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."},
+ {"43543.503206018519", "[$-42C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"},
+ {"43543.503206018519", "[$-45]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-45]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-45]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"},
+ {"43543.503206018519", "[$-845]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-845]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-845]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"},
+ {"43543.503206018519", "[$-445]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-445]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."},
+ {"43543.503206018519", "[$-445]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"},
+ {"44562.189571759256", "[$-6D]mmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6D]mmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6D]mmmmm dd yyyy h:mm AM/PM", "\u0493 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6D]mmmmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-6D]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-6D]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0448"},
+ {"43543.503206018519", "[$-6D]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0428\u0448"},
+ {"43543.503206018519", "[$-6D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0438\u0448\u04D9\u043C\u0431\u0435"},
+ {"44562.189571759256", "[$-46D]mmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46D]mmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46D]mmmmm dd yyyy h:mm AM/PM", "\u0493 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46D]mmmmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-46D]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-46D]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0448"},
+ {"43543.503206018519", "[$-46D]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0428\u0448"},
+ {"43543.503206018519", "[$-46D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0438\u0448\u04D9\u043C\u0431\u0435"},
+ {"44562.189571759256", "[$-2D]mmm dd yyyy h:mm AM/PM", "urt. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2D]mmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2D]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2D]mmmmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-2D]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-2D]mmmm dd yyyy h:mm AM/PM aaa", "martxoa 19 2019 12:04 PM ar."},
+ {"43543.503206018519", "[$-2D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ar."},
+ {"43543.503206018519", "[$-2D]mmmmmm dd yyyy h:mm AM/PM dddd", "martxoa 19 2019 12:04 PM asteartea"},
+ {"44562.189571759256", "[$-42D]mmm dd yyyy h:mm AM/PM", "urt. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42D]mmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42D]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42D]mmmmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-42D]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-42D]mmmm dd yyyy h:mm AM/PM aaa", "martxoa 19 2019 12:04 PM ar."},
+ {"43543.503206018519", "[$-42D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ar."},
+ {"43543.503206018519", "[$-42D]mmmmmm dd yyyy h:mm AM/PM dddd", "martxoa 19 2019 12:04 PM asteartea"},
+ {"44562.189571759256", "[$-23]mmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-23]mmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-23]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-23]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-23]mmm dd yyyy h:mm AM/PM", "\u0441\u0430\u043A 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-23]mmmm dd yyyy h:mm AM/PM aaa", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442"},
+ {"43543.503206018519", "[$-23]mmmmm dd yyyy h:mm AM/PM ddd", "\u0441 19 2019 12:04 PM \u0430\u045E\u0442"},
+ {"43543.503206018519", "[$-23]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-423]mmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-423]mmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-423]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-423]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-423]mmm dd yyyy h:mm AM/PM", "\u0441\u0430\u043A 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-423]mmmm dd yyyy h:mm AM/PM aaa", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442"},
+ {"43543.503206018519", "[$-423]mmmmm dd yyyy h:mm AM/PM ddd", "\u0441 19 2019 12:04 PM \u0430\u045E\u0442"},
+ {"43543.503206018519", "[$-423]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-641A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-641A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-641A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-641A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-641A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-641A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-641A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-641A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-201A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-201A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-201A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-201A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-201A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-201A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-201A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-201A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-681A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-681A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-681A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-681A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-681A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-681A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-681A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-681A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"},
+ {"44562.189571759256", "[$-781A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-781A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-781A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-781A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-781A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-781A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-781A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-781A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"},
+ {"44562.189571759256", "[$-141A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-141A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-141A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-141A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-141A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-141A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-141A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"},
+ {"43543.503206018519", "[$-141A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"},
+ {"44562.189571759256", "[$-7E]mmm dd yyyy h:mm AM/PM", "Gen. 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-7E]mmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-7E]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-7E]mmmmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."},
+ {"43543.503206018519", "[$-7E]mmm dd yyyy h:mm AM/PM", "Meur. 19 2019 12:04 G.M."},
+ {"43543.503206018519", "[$-7E]mmmm dd yyyy h:mm AM/PM aaa", "Meurzh 19 2019 12:04 G.M. Meu."},
+ {"43543.503206018519", "[$-7E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 G.M. Meu."},
+ {"43543.503206018519", "[$-7E]mmmmmm dd yyyy h:mm AM/PM dddd", "Meurzh 19 2019 12:04 G.M. Meurzh"},
+ {"44562.189571759256", "[$-47E]mmm dd yyyy h:mm AM/PM", "Gen. 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-47E]mmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-47E]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-47E]mmmmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."},
+ {"43543.503206018519", "[$-47E]mmm dd yyyy h:mm AM/PM", "Meur. 19 2019 12:04 G.M."},
+ {"43543.503206018519", "[$-47E]mmmm dd yyyy h:mm AM/PM aaa", "Meurzh 19 2019 12:04 G.M. Meu."},
+ {"43543.503206018519", "[$-47E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 G.M. Meu."},
+ {"43543.503206018519", "[$-47E]mmmmmm dd yyyy h:mm AM/PM dddd", "Meurzh 19 2019 12:04 G.M. Meurzh"},
+ {"44562.189571759256", "[$-2]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-2]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-2]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-2]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442"},
+ {"43543.503206018519", "[$-2]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0432\u0442"},
+ {"43543.503206018519", "[$-2]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"44562.189571759256", "[$-402]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-402]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-402]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-402]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-402]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-402]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442"},
+ {"43543.503206018519", "[$-402]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0432\u0442"},
+ {"43543.503206018519", "[$-402]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"44562.189571759256", "[$-55]mmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-55]mmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-55]mmmmm dd yyyy h:mm AM/PM", "\u1007 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-55]mmmmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"43543.503206018519", "[$-55]mmm dd yyyy h:mm AM/PM", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031"},
+ {"43543.503206018519", "[$-55]mmmm dd yyyy h:mm AM/PM aaa", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"43543.503206018519", "[$-55]mmmmm dd yyyy h:mm AM/PM ddd", "\u1019 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"43543.503206018519", "[$-55]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"44562.189571759256", "[$-455]mmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-455]mmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-455]mmmmm dd yyyy h:mm AM/PM", "\u1007 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"44562.189571759256", "[$-455]mmmmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"},
+ {"43543.503206018519", "[$-455]mmm dd yyyy h:mm AM/PM", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031"},
+ {"43543.503206018519", "[$-455]mmmm dd yyyy h:mm AM/PM aaa", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"43543.503206018519", "[$-455]mmmmm dd yyyy h:mm AM/PM ddd", "\u1019 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"43543.503206018519", "[$-455]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"},
+ {"44562.189571759256", "[$-3]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-3]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-3]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-3]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-3]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."},
+ {"43543.503206018519", "[$-3]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-3]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-3]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"},
+ {"44562.189571759256", "[$-403]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-403]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-403]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-403]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-403]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."},
+ {"43543.503206018519", "[$-403]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-403]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-403]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"},
+ {"44562.189571759256", "[$-45F]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-45F]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-45F]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"},
+ {"44562.189571759256", "[$-45F]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"},
+ {"43543.503206018519", "[$-45F]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"},
+ {"43543.503206018519", "[$-45F]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-45F]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"43543.503206018519", "[$-45F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"},
+ {"44562.189571759256", "[$-92]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-92]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-92]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-92]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"43543.503206018519", "[$-92]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"},
+ {"43543.503206018519", "[$-92]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-92]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-92]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"44562.189571759256", "[$-7C92]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-7C92]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-7C92]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-7C92]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"43543.503206018519", "[$-7C92]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"},
+ {"43543.503206018519", "[$-7C92]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-7C92]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-7C92]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+
+ {"44562.189571759256", "[$-492]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-492]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-492]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"},
+ {"44562.189571759256", "[$-492]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"},
+ {"43543.503206018519", "[$-492]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"},
+ {"43543.503206018519", "[$-492]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-492]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"43543.503206018519", "[$-492]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"},
+ {"44562.189571759256", "[$-5C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-5C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-5C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-5C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-5C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"},
+ {"44562.189571759256", "[$-7C5C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C5C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C5C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-7C5C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-7C5C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"},
+ {"44562.189571759256", "[$-45C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-45C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-45C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-45C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"},
+ {"43543.503206018519", "[$-45C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"},
+ {"43543.503206018519", "[$-4]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-4]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-4]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-7804]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-7804]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-7804]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-804]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 周二"},
+ {"43543.503206018519", "[$-804]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 周二"},
+ {"43543.503206018519", "[$-804]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-1004]mmm dd yyyy h:mm AM/PM aaa", "三月 19 2019 12:04 下午 周二"},
+ {"43543.503206018519", "[$-1004]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 周二"},
+ {"43543.503206018519", "[$-1004]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-7C04]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-7C04]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-7C04]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-C04]mmm dd yyyy h:mm AM/PM aaa", "三月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-C04]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-C04]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-1404]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-1404]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-1404]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-404]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-404]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"},
+ {"43543.503206018519", "[$-404]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"},
+ {"43543.503206018519", "[$-9]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-9]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-9]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-1000]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1000]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1000]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"},
+ {"43543.503206018519", "[$-c09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-c09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-c09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"},
+ {"43543.503206018519", "[$-2809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-1009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-2409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-3C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-4009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-1809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-1809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-1809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"},
+ {"43543.503206018519", "[$-2009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-4409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-1409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-3409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-4809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-1C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-1C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-2C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-2C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-4C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-4C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"},
+ {"43543.503206018519", "[$-809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"},
+ {"43543.503206018519", "[$-3009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-3009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"},
+ {"43543.503206018519", "[$-25]mmm dd yyyy h:mm AM/PM aaa", "märts 19 2019 12:04 PM T"},
+ {"43543.503206018519", "[$-25]mmmm dd yyyy h:mm AM/PM ddd", "märts 19 2019 12:04 PM T"},
+ {"43543.503206018519", "[$-25]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM teisipäev"},
+ {"43543.503206018519", "[$-425]mmm dd yyyy h:mm AM/PM aaa", "märts 19 2019 12:04 PM T"},
+ {"43543.503206018519", "[$-425]mmmm dd yyyy h:mm AM/PM ddd", "märts 19 2019 12:04 PM T"},
+ {"43543.503206018519", "[$-425]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM teisipäev"},
+ {"43543.503206018519", "[$-38]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 um sein. týs."},
+ {"43543.503206018519", "[$-38]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 um sein. týs."},
+ {"43543.503206018519", "[$-38]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 um sein. týsdagur"},
+ {"43543.503206018519", "[$-438]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 um sein. týs."},
+ {"43543.503206018519", "[$-438]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 um sein. týs."},
+ {"43543.503206018519", "[$-438]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 um sein. týsdagur"},
+ {"43543.503206018519", "[$-64]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-64]mmmm dd yyyy h:mm AM/PM ddd", "Marso 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-64]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 PM Martes"},
+ {"43543.503206018519", "[$-464]mmm dd yyyy h:mm AM/PM ddd", "Mar 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-464]mmmm dd yyyy h:mm AM/PM ddd", "Marso 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-464]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 PM Martes"},
+ {"43543.503206018519", "[$-B]mmm dd yyyy h:mm AM/PM aaa", "maalis 19 2019 12:04 ip. ti"},
+ {"43543.503206018519", "[$-B]mmmm dd yyyy h:mm AM/PM ddd", "maaliskuu 19 2019 12:04 ip. ti"},
+ {"43543.503206018519", "[$-B]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 ip. tiistai"},
+ {"43543.503206018519", "[$-40B]mmm dd yyyy h:mm AM/PM aaa", "maalis 19 2019 12:04 ip. ti"},
+ {"43543.503206018519", "[$-40B]mmmm dd yyyy h:mm AM/PM ddd", "maaliskuu 19 2019 12:04 ip. ti"},
+ {"43543.503206018519", "[$-40B]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 ip. tiistai"},
+ {"44562.189571759256", "[$-C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-80C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-80C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-80C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-80C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-80C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-80C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-2c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 mat."},
+ {"44562.189571759256", "[$-2c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 mat."},
+ {"44562.189571759256", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 mat."},
+ {"43543.503206018519", "[$-2c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 soir mar."},
+ {"43543.503206018519", "[$-2c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 soir mar."},
+ {"43543.503206018519", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 soir mardi"},
+ {"44562.189571759256", "[$-c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-1C0C]mmm dd yyyy h:mm AM/PM", "Janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C0C]mmmm dd yyyy h:mm AM/PM", "Janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-1C0C]mmm dd yyyy h:mm AM/PM aaa", "Mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-1C0C]mmmm dd yyyy h:mm AM/PM ddd", "Mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-240C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-240C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-240C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-240C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-240C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-240C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-300C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-300C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-300C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-300C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-300C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-300C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-40C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-40C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-40C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-40C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-40C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-40C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-3c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-3c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-3c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-140C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-140C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-140C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-140C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-140C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-140C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-340C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-340C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-340C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-340C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-340C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-340C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-380C]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-380C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-380C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-380C]mmm dd yyyy h:mm AM/PM aaa", "mar. 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-380C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-380C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-180C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-180C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-180C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-180C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-180C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-180C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-200C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-200C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-200C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-200C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-200C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-200C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-280C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-280C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-280C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-280C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-280C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."},
+ {"43543.503206018519", "[$-280C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-62]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-62]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-62]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-62]mmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-62]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-62]mmmmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-62]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-62]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-62]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-62]mmmm dd yyyy h:mm AM/PM aaa", "Maart 19 2019 12:04 PM tii"},
+ {"43543.503206018519", "[$-62]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM tii"},
+ {"43543.503206018519", "[$-62]mmmmmm dd yyyy h:mm AM/PM dddd", "Maart 19 2019 12:04 PM tiisdei"},
+ {"44562.189571759256", "[$-462]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-462]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-462]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-462]mmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-462]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-462]mmmmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-462]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-462]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-462]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-462]mmmm dd yyyy h:mm AM/PM aaa", "Maart 19 2019 12:04 PM tii"},
+ {"43543.503206018519", "[$-462]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM tii"},
+ {"43543.503206018519", "[$-462]mmmmmm dd yyyy h:mm AM/PM dddd", "Maart 19 2019 12:04 PM tiisdei"},
+ {"44562.189571759256", "[$-67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-67]mmmm dd yyyy h:mm AM/PM aaa", "mbooy 19 2019 12:04 PM maw"},
+ {"43543.503206018519", "[$-67]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM maw"},
+ {"43543.503206018519", "[$-67]mmmmmm dd yyyy h:mm AM/PM dddd", "mbooy 19 2019 12:04 PM mawbaare"},
+ {"44562.189571759256", "[$-7C67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C67]mmmm dd yyyy h:mm AM/PM aaa", "mbooy 19 2019 12:04 PM maw"},
+ {"43543.503206018519", "[$-7C67]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM maw"},
+ {"43543.503206018519", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM dddd", "mbooy 19 2019 12:04 PM mawbaare"},
+ {"44562.189571759256", "[$-467]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 subaka"},
+ {"44562.189571759256", "[$-467]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"},
+ {"44562.189571759256", "[$-467]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 subaka"},
+ {"44562.189571759256", "[$-467]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"},
+ {"43543.503206018519", "[$-467]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 kikiiɗe"},
+ {"43543.503206018519", "[$-467]mmmm dd yyyy h:mm AM/PM aaa", "marsa 19 2019 12:04 kikiiɗe tal."},
+ {"43543.503206018519", "[$-467]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 kikiiɗe tal."},
+ {"43543.503206018519", "[$-467]mmmmmm dd yyyy h:mm AM/PM dddd", "marsa 19 2019 12:04 kikiiɗe talaata"},
+ {"44562.189571759256", "[$-867]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-867]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-867]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-867]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-867]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-867]mmmm dd yyyy h:mm AM/PM aaa", "marsa 19 2019 12:04 PM tal."},
+ {"43543.503206018519", "[$-867]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tal."},
+ {"43543.503206018519", "[$-867]mmmmmm dd yyyy h:mm AM/PM dddd", "marsa 19 2019 12:04 PM talaata"},
+ {"44562.189571759256", "[$-56]mmm dd yyyy h:mm AM/PM", "Xan. 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."},
+ {"44562.189571759256", "[$-56]mmm dd yyyy h:mm AM/PM", "Xan. 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM aaa", "Mar. 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM ddd", "Marzo 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 p.m. martes"},
+ {"43543.503206018519", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-456]mmm dd yyyy h:mm AM/PM aaa", "Mar. 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-456]mmmm dd yyyy h:mm AM/PM ddd", "Marzo 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-456]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 p.m. martes"},
+ {"44562.189571759256", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-37]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-37]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-37]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-37]mmmm dd yyyy h:mm AM/PM aaa", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."},
+ {"43543.503206018519", "[$-37]mmmmm dd yyyy h:mm AM/PM ddd", "\u10DB 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."},
+ {"43543.503206018519", "[$-37]mmmmmm dd yyyy h:mm AM/PM dddd", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8"},
+ {"44562.189571759256", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-437]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-437]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-437]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-437]mmmm dd yyyy h:mm AM/PM aaa", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."},
+ {"43543.503206018519", "[$-437]mmmmm dd yyyy h:mm AM/PM ddd", "\u10DB 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."},
+ {"43543.503206018519", "[$-437]mmmmmm dd yyyy h:mm AM/PM dddd", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8"},
+ {"43543.503206018519", "[$-7]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-7]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-7]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"},
+ {"44562.189571759256", "[$-C07]mmm dd yyyy h:mm AM/PM aaa", "Jän 01 2022 4:32 AM Sa"},
+ {"44562.189571759256", "[$-C07]mmmm dd yyyy h:mm AM/PM ddd", "Jänner 01 2022 4:32 AM Sa"},
+ {"44562.189571759256", "[$-C07]mmmmm dd yyyy h:mm AM/PM dddd", "J 01 2022 4:32 AM Samstag"},
+ {"43543.503206018519", "[$-407]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-407]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-407]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"},
+ {"43543.503206018519", "[$-1407]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-1407]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-1407]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"},
+ {"43543.503206018519", "[$-807]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-807]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"},
+ {"43543.503206018519", "[$-807]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"},
+ {"44562.189571759256", "[$-8]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-8]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-8]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-8]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"},
+ {"43543.503206018519", "[$-8]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"},
+ {"43543.503206018519", "[$-8]mmmm dd yyyy h:mm AM/PM aaa", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"},
+ {"43543.503206018519", "[$-8]mmmmm dd yyyy h:mm AM/PM ddd", "\u039C 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"},
+ {"43543.503206018519", "[$-8]mmmmmm dd yyyy h:mm AM/PM dddd", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03AF\u03C4\u03B7"},
+ {"44562.189571759256", "[$-408]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-408]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-408]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"},
+ {"44562.189571759256", "[$-408]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"},
+ {"43543.503206018519", "[$-408]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"},
+ {"43543.503206018519", "[$-408]mmmm dd yyyy h:mm AM/PM aaa", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"},
+ {"43543.503206018519", "[$-408]mmmmm dd yyyy h:mm AM/PM ddd", "\u039C 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"},
+ {"43543.503206018519", "[$-408]mmmmmm dd yyyy h:mm AM/PM dddd", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03AF\u03C4\u03B7"},
+ {"44562.189571759256", "[$-6F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-6F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-6F]mmmm dd yyyy h:mm AM/PM aaa", "marsi 19 2019 12:04 PM marl."},
+ {"43543.503206018519", "[$-6F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM marl."},
+ {"43543.503206018519", "[$-6F]mmmmmm dd yyyy h:mm AM/PM dddd", "marsi 19 2019 12:04 PM marlunngorneq"},
+ {"44562.189571759256", "[$-46F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-46F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-46F]mmmm dd yyyy h:mm AM/PM aaa", "marsi 19 2019 12:04 PM marl."},
+ {"43543.503206018519", "[$-46F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM marl."},
+ {"43543.503206018519", "[$-46F]mmmmmm dd yyyy h:mm AM/PM dddd", "marsi 19 2019 12:04 PM marlunngorneq"},
+ {"44562.189571759256", "[$-74]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-74]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-74]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-74]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-74]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-74]mmmm dd yyyy h:mm AM/PM aaa", "jasyapy 19 2019 12:04 p.m. apy"},
+ {"43543.503206018519", "[$-74]mmmmm dd yyyy h:mm AM/PM ddd", "j 19 2019 12:04 p.m. apy"},
+ {"43543.503206018519", "[$-74]mmmmmm dd yyyy h:mm AM/PM dddd", "jasyapy 19 2019 12:04 p.m. araapy"},
+ {"44562.189571759256", "[$-474]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-474]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-474]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-474]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-474]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-474]mmmm dd yyyy h:mm AM/PM aaa", "jasyapy 19 2019 12:04 p.m. apy"},
+ {"43543.503206018519", "[$-474]mmmmm dd yyyy h:mm AM/PM ddd", "j 19 2019 12:04 p.m. apy"},
+ {"43543.503206018519", "[$-474]mmmmmm dd yyyy h:mm AM/PM dddd", "jasyapy 19 2019 12:04 p.m. araapy"},
+ {"44562.189571759256", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-47]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-47]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-47]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"43543.503206018519", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"43543.503206018519", "[$-47]mmmm dd yyyy h:mm AM/PM aaa", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"},
+ {"43543.503206018519", "[$-47]mmmmm dd yyyy h:mm AM/PM ddd", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"},
+ {"43543.503206018519", "[$-47]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0"},
+ {"44562.189571759256", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-447]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-447]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"44562.189571759256", "[$-447]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"43543.503206018519", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"},
+ {"43543.503206018519", "[$-447]mmmm dd yyyy h:mm AM/PM aaa", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"},
+ {"43543.503206018519", "[$-447]mmmmm dd yyyy h:mm AM/PM ddd", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"},
+ {"43543.503206018519", "[$-447]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0"},
+ {"44562.189571759256", "[$-68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-68]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-68]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-68]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"},
+ {"44562.189571759256", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C68]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-7C68]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"},
+ {"44562.189571759256", "[$-468]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-468]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-468]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-468]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-468]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-468]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-468]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"},
+ {"43543.503206018519", "[$-468]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"},
+ {"44562.189571759256", "[$-75]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-75]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-75]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-75]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-75]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-75]mmmm dd yyyy h:mm AM/PM aaa", "Malaki 19 2019 12:04 PM P2"},
+ {"43543.503206018519", "[$-75]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM P2"},
+ {"43543.503206018519", "[$-75]mmmmmm dd yyyy h:mm AM/PM dddd", "Malaki 19 2019 12:04 PM Poʻalua"},
+ {"44562.189571759256", "[$-475]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-475]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-475]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-475]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-475]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-475]mmmm dd yyyy h:mm AM/PM aaa", "Malaki 19 2019 12:04 PM P2"},
+ {"43543.503206018519", "[$-475]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM P2"},
+ {"43543.503206018519", "[$-475]mmmmmm dd yyyy h:mm AM/PM dddd", "Malaki 19 2019 12:04 PM Poʻalua"},
+ {"44562.189571759256", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9"},
+ {"44562.189571759256", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-40D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-40D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-40D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-40D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-40D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-40D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9"},
+ {"44562.189571759256", "[$-39]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-39]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-39]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-39]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-39]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-39]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."},
+ {"43543.503206018519", "[$-39]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."},
+ {"43543.503206018519", "[$-39]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932\u0935\u093E\u0930"},
+ {"44562.189571759256", "[$-439]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-439]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-439]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-439]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-439]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-439]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."},
+ {"43543.503206018519", "[$-439]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."},
+ {"43543.503206018519", "[$-439]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932\u0935\u093E\u0930"},
+ {"44562.189571759256", "[$-E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."},
+ {"43543.503206018519", "[$-E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."},
+ {"43543.503206018519", "[$-E]mmmm dd yyyy h:mm AM/PM aaa", "március 19 2019 12:04 du. K"},
+ {"43543.503206018519", "[$-E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 du. K"},
+ {"43543.503206018519", "[$-E]mmmmmm dd yyyy h:mm AM/PM dddd", "március 19 2019 12:04 du. kedd"},
+ {"44562.189571759256", "[$-40E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-40E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-40E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."},
+ {"44562.189571759256", "[$-40E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."},
+ {"43543.503206018519", "[$-40E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."},
+ {"43543.503206018519", "[$-40E]mmmm dd yyyy h:mm AM/PM aaa", "március 19 2019 12:04 du. K"},
+ {"43543.503206018519", "[$-40E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 du. K"},
+ {"43543.503206018519", "[$-40E]mmmmmm dd yyyy h:mm AM/PM dddd", "március 19 2019 12:04 du. kedd"},
+ {"44562.189571759256", "[$-F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."},
+ {"43543.503206018519", "[$-F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."},
+ {"43543.503206018519", "[$-F]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.h. þri."},
+ {"43543.503206018519", "[$-F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.h. þri."},
+ {"43543.503206018519", "[$-F]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.h. þriðjudagur"},
+ {"44562.189571759256", "[$-40F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-40F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-40F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."},
+ {"44562.189571759256", "[$-40F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."},
+ {"43543.503206018519", "[$-40F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."},
+ {"43543.503206018519", "[$-40F]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.h. þri."},
+ {"43543.503206018519", "[$-40F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.h. þri."},
+ {"43543.503206018519", "[$-40F]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.h. þriðjudagur"},
+ {"44562.189571759256", "[$-70]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-70]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-70]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-70]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."},
+ {"43543.503206018519", "[$-70]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."},
+ {"43543.503206018519", "[$-70]mmmm dd yyyy h:mm AM/PM aaa", "Machị 19 2019 12:04 P.M. Tiu"},
+ {"43543.503206018519", "[$-70]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 P.M. Tiu"},
+ {"43543.503206018519", "[$-70]mmmmmm dd yyyy h:mm AM/PM dddd", "Machị 19 2019 12:04 P.M. Tiuzdee"},
+ {"44562.189571759256", "[$-470]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-470]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-470]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."},
+ {"44562.189571759256", "[$-470]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."},
+ {"43543.503206018519", "[$-470]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."},
+ {"43543.503206018519", "[$-470]mmmm dd yyyy h:mm AM/PM aaa", "Machị 19 2019 12:04 P.M. Tiu"},
+ {"43543.503206018519", "[$-470]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 P.M. Tiu"},
+ {"43543.503206018519", "[$-470]mmmmmm dd yyyy h:mm AM/PM dddd", "Machị 19 2019 12:04 P.M. Tiuzdee"},
+ {"44562.189571759256", "[$-21]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-21]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-21]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-21]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-21]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-21]mmmm dd yyyy h:mm AM/PM aaa", "Maret 19 2019 12:04 PM Sel"},
+ {"43543.503206018519", "[$-21]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sel"},
+ {"43543.503206018519", "[$-21]mmmmmm dd yyyy h:mm AM/PM dddd", "Maret 19 2019 12:04 PM Selasa"},
+ {"44562.189571759256", "[$-421]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-421]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-421]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-421]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-421]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-421]mmmm dd yyyy h:mm AM/PM aaa", "Maret 19 2019 12:04 PM Sel"},
+ {"43543.503206018519", "[$-421]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sel"},
+ {"43543.503206018519", "[$-421]mmmmmm dd yyyy h:mm AM/PM dddd", "Maret 19 2019 12:04 PM Selasa"},
+ {"44562.189571759256", "[$-5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-5D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-5D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-5D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"},
+ {"44562.189571759256", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C5D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"},
+ {"44562.189571759256", "[$-85D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-85D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-85D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-85D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"},
+ {"43543.503206018519", "[$-85D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"},
+ {"44562.189571759256", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-785D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-785D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-785D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-785D]mmmm dd yyyy h:mm AM/PM aaa", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431"},
+ {"43543.503206018519", "[$-785D]mmmmm dd yyyy h:mm AM/PM ddd", "\u14AB 19 2019 12:04 PM \u140A\u1403\u1449\u1431"},
+ {"43543.503206018519", "[$-785D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431\u1585"},
+ {"44562.189571759256", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-45D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-45D]mmmm dd yyyy h:mm AM/PM aaa", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431"},
+ {"43543.503206018519", "[$-45D]mmmmm dd yyyy h:mm AM/PM ddd", "\u14AB 19 2019 12:04 PM \u140A\u1403\u1449\u1431"},
+ {"43543.503206018519", "[$-45D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431\u1585"},
+ {"44562.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."},
+ {"44593.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."},
+ {"44621.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."},
+ {"44652.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."},
+ {"44682.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."},
+ {"44713.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."},
+ {"44743.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."},
+ {"44774.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."},
+ {"44805.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."},
+ {"44835.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."},
+ {"44866.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."},
+ {"44896.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."},
+ {"44562.189571759256", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."},
+ {"44593.189571759256", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."},
+ {"44621.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."},
+ {"44652.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."},
+ {"44682.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."},
+ {"44713.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."},
+ {"44743.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."},
+ {"44774.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."},
+ {"44805.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."},
+ {"44835.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."},
+ {"44866.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."},
+ {"44896.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."},
+ {"44562.189571759256", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 r.n."},
+ {"44593.189571759256", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 r.n."},
+ {"44621.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."},
+ {"44652.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 r.n."},
+ {"44682.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "B 01 2022 4:32 r.n."},
+ {"44713.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."},
+ {"44743.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 r.n."},
+ {"44774.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "L 01 2022 4:32 r.n."},
+ {"44805.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."},
+ {"44835.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM aaa", "D 01 2022 4:32 r.n. Sath"},
+ {"44866.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM ddd", "S 01 2022 4:32 r.n. Máirt"},
+ {"44896.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM dddd", "N 01 2022 4:32 r.n. Déardaoin"},
+ {"44562.189571759256", "[$-83C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."},
+ {"44593.189571759256", "[$-83C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."},
+ {"44621.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."},
+ {"44652.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."},
+ {"44682.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."},
+ {"44713.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."},
+ {"44743.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."},
+ {"44774.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."},
+ {"44805.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."},
+ {"44835.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."},
+ {"44866.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."},
+ {"44896.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."},
+ {"44562.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."},
+ {"44593.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."},
+ {"44621.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."},
+ {"44652.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."},
+ {"44682.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."},
+ {"44713.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."},
+ {"44743.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."},
+ {"44774.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."},
+ {"44805.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."},
+ {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM aaa", "Deireadh Fómhair 01 2022 4:32 r.n. Sath"},
+ {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM ddd", "Samhain 01 2022 4:32 r.n. Máirt"},
+ {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM dddd", "Nollaig 01 2022 4:32 r.n. Déardaoin"},
+ {"43543.503206018519", "[$-10]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-10]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-10]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"},
+ {"43543.503206018519", "[$-410]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-410]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-410]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"},
+ {"43543.503206018519", "[$-810]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-810]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-810]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"},
+ {"43543.503206018519", "[$-11]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 午後 火"},
+ {"43543.503206018519", "[$-11]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 午後 火"},
+ {"43543.503206018519", "[$-11]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 午後 火曜日"},
+ {"43543.503206018519", "[$-411]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 午後 火"},
+ {"43543.503206018519", "[$-411]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 午後 火"},
+ {"43543.503206018519", "[$-411]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 午後 火曜日"},
+ {"44562.189571759256", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-4B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-4B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-4B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"43543.503206018519", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"43543.503206018519", "[$-4B]mmmm dd yyyy h:mm AM/PM aaa", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."},
+ {"43543.503206018519", "[$-4B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."},
+ {"43543.503206018519", "[$-4B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0"},
+ {"44562.189571759256", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-44B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-44B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"44562.189571759256", "[$-44B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"43543.503206018519", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"},
+ {"43543.503206018519", "[$-44B]mmmm dd yyyy h:mm AM/PM aaa", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."},
+ {"43543.503206018519", "[$-44B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."},
+ {"43543.503206018519", "[$-44B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0"},
+ {"44562.189571759256", "[$-471]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-471]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-471]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-471]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-471]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-471]mmmm dd yyyy h:mm AM/PM aaa", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-471]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-471]mmmmmm dd yyyy h:mm AM/PM dddd", "March 19 2019 12:04 PM Tuesday"},
+ {"44562.189571759256", "[$-60]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-60]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-60]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-60]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-60]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-60]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"43543.503206018519", "[$-60]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"43543.503206018519", "[$-60]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"44562.189571759256", "[$-460]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-460]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-460]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-460]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-460]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-460]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"43543.503206018519", "[$-460]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"43543.503206018519", "[$-460]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"},
+ {"44562.189571759256", "[$-860]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-860]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-860]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-860]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-860]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-860]mmmm dd yyyy h:mm AM/PM aaa", "March 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-860]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-860]mmmmmm dd yyyy h:mm AM/PM dddd", "March 19 2019 12:04 PM Tuesday"},
+ {"44562.189571759256", "[$-3F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-3F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-3F]mmmm dd yyyy h:mm AM/PM aaa", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439"},
+ {"43543.503206018519", "[$-3F]mmmmm dd yyyy h:mm AM/PM ddd", "Н 19 2019 12:04 PM \u0441\u0435\u0439"},
+ {"43543.503206018519", "[$-3F]mmmmmm dd yyyy h:mm AM/PM dddd", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456"},
+ {"44562.189571759256", "[$-43F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-43F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-43F]mmmm dd yyyy h:mm AM/PM aaa", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439"},
+ {"43543.503206018519", "[$-43F]mmmmm dd yyyy h:mm AM/PM ddd", "Н 19 2019 12:04 PM \u0441\u0435\u0439"},
+ {"43543.503206018519", "[$-43F]mmmmmm dd yyyy h:mm AM/PM dddd", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456"},
+ {"44562.189571759256", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-53]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-53]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-53]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"43543.503206018519", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"},
+ {"43543.503206018519", "[$-53]mmmm dd yyyy h:mm AM/PM aaa", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."},
+ {"43543.503206018519", "[$-53]mmmmm dd yyyy h:mm AM/PM ddd", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."},
+ {"43543.503206018519", "[$-53]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A"},
+ {"44562.189571759256", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-453]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-453]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"44562.189571759256", "[$-453]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"},
+ {"43543.503206018519", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"},
+ {"43543.503206018519", "[$-453]mmmm dd yyyy h:mm AM/PM aaa", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."},
+ {"43543.503206018519", "[$-453]mmmmm dd yyyy h:mm AM/PM ddd", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."},
+ {"43543.503206018519", "[$-453]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A"},
+ {"44562.189571759256", "[$-86]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-86]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-86]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-86]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-86]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-86]mmmm dd yyyy h:mm AM/PM aaa", "urox ik' 19 2019 12:04 p.m. oxq'"},
+ {"43543.503206018519", "[$-86]mmmmm dd yyyy h:mm AM/PM ddd", "u 19 2019 12:04 p.m. oxq'"},
+ {"43543.503206018519", "[$-86]mmmmmm dd yyyy h:mm AM/PM dddd", "urox ik' 19 2019 12:04 p.m. oxq'ij"},
+ {"44562.189571759256", "[$-486]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-486]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-486]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-486]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-486]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-486]mmmm dd yyyy h:mm AM/PM aaa", "urox ik' 19 2019 12:04 p.m. oxq'"},
+ {"43543.503206018519", "[$-486]mmmmm dd yyyy h:mm AM/PM ddd", "u 19 2019 12:04 p.m. oxq'"},
+ {"43543.503206018519", "[$-486]mmmmmm dd yyyy h:mm AM/PM dddd", "urox ik' 19 2019 12:04 p.m. oxq'ij"},
+ {"44562.189571759256", "[$-87]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-87]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-87]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-87]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-87]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-87]mmmm dd yyyy h:mm AM/PM aaa", "Werurwe 19 2019 12:04 PM kab."},
+ {"43543.503206018519", "[$-87]mmmmm dd yyyy h:mm AM/PM ddd", "W 19 2019 12:04 PM kab."},
+ {"43543.503206018519", "[$-87]mmmmmm dd yyyy h:mm AM/PM dddd", "Werurwe 19 2019 12:04 PM Ku wa kabiri"},
+ {"44562.189571759256", "[$-487]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-487]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-487]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-487]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-487]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-487]mmmm dd yyyy h:mm AM/PM aaa", "Werurwe 19 2019 12:04 PM kab."},
+ {"43543.503206018519", "[$-487]mmmmm dd yyyy h:mm AM/PM ddd", "W 19 2019 12:04 PM kab."},
+ {"43543.503206018519", "[$-487]mmmmmm dd yyyy h:mm AM/PM dddd", "Werurwe 19 2019 12:04 PM Ku wa kabiri"},
+ {"44562.189571759256", "[$-41]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-41]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-41]mmmm dd yyyy h:mm AM/PM aaa", "Machi 19 2019 12:04 PM Jnn"},
+ {"43543.503206018519", "[$-41]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Jnn"},
+ {"43543.503206018519", "[$-41]mmmmmm dd yyyy h:mm AM/PM dddd", "Machi 19 2019 12:04 PM Jumanne"},
+ {"44562.189571759256", "[$-441]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-441]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-441]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-441]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-441]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-441]mmmm dd yyyy h:mm AM/PM aaa", "Machi 19 2019 12:04 PM Jnn"},
+ {"43543.503206018519", "[$-441]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Jnn"},
+ {"43543.503206018519", "[$-441]mmmmmm dd yyyy h:mm AM/PM dddd", "Machi 19 2019 12:04 PM Jumanne"},
+ {"44562.189571759256", "[$-57]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-57]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-57]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-57]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"43543.503206018519", "[$-57]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-57]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-57]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-57]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u093E\u0930"},
+ {"44562.189571759256", "[$-457]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-457]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-457]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-457]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"43543.503206018519", "[$-457]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-457]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-457]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-457]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u093E\u0930"},
+ {"43543.503206018519", "[$-12]mmm dd yyyy h:mm AM/PM aaa", "3 19 2019 12:04 오후 화"},
+ {"43543.503206018519", "[$-12]mmmm dd yyyy h:mm AM/PM ddd", "3월 19 2019 12:04 오후 화"},
+ {"43543.503206018519", "[$-12]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 오후 화요일"},
+ {"43543.503206018519", "[$-412]mmm dd yyyy h:mm AM/PM aaa", "3 19 2019 12:04 오후 화"},
+ {"43543.503206018519", "[$-412]mmmm dd yyyy h:mm AM/PM ddd", "3월 19 2019 12:04 오후 화"},
+ {"43543.503206018519", "[$-412]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 오후 화요일"},
+ {"44562.189571759256", "[$-40]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-40]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-40]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-40]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"},
+ {"43543.503206018519", "[$-40]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"},
+ {"43543.503206018519", "[$-40]mmmm dd yyyy h:mm AM/PM aaa", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."},
+ {"43543.503206018519", "[$-40]mmmmm dd yyyy h:mm AM/PM ddd", "\u041C 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."},
+ {"43543.503206018519", "[$-40]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438"},
+ {"44562.189571759256", "[$-440]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-440]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-440]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"},
+ {"44562.189571759256", "[$-440]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"},
+ {"43543.503206018519", "[$-440]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"},
+ {"43543.503206018519", "[$-440]mmmm dd yyyy h:mm AM/PM aaa", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."},
+ {"43543.503206018519", "[$-440]mmmmm dd yyyy h:mm AM/PM ddd", "\u041C 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."},
+ {"43543.503206018519", "[$-440]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438"},
+ {"44562.189571759256", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-54]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-54]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-54]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"43543.503206018519", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"},
+ {"43543.503206018519", "[$-54]mmmm dd yyyy h:mm AM/PM aaa", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"43543.503206018519", "[$-54]mmmmm dd yyyy h:mm AM/PM ddd", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"43543.503206018519", "[$-54]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"44562.189571759256", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-454]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-454]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"44562.189571759256", "[$-454]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"},
+ {"43543.503206018519", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"},
+ {"43543.503206018519", "[$-454]mmmm dd yyyy h:mm AM/PM aaa", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"43543.503206018519", "[$-454]mmmmm dd yyyy h:mm AM/PM ddd", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"43543.503206018519", "[$-454]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"},
+ {"44562.189571759256", "[$-476]mmm dd yyyy h:mm AM/PM", "Ian 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-476]mmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-476]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-476]mmmmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-476]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-476]mmmm dd yyyy h:mm AM/PM aaa", "Martius 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-476]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Mar"},
+ {"43543.503206018519", "[$-476]mmmmmm dd yyyy h:mm AM/PM dddd", "Martius 19 2019 12:04 PM Martis"},
+ {"44562.189571759256", "[$-26]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-26]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-26]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-26]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."},
+ {"43543.503206018519", "[$-26]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."},
+ {"43543.503206018519", "[$-26]mmmm dd yyyy h:mm AM/PM aaa", "marts 19 2019 12:04 pēcp. otrd."},
+ {"43543.503206018519", "[$-26]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pēcp. otrd."},
+ {"43543.503206018519", "[$-26]mmmmmm dd yyyy h:mm AM/PM dddd", "marts 19 2019 12:04 pēcp. otrdiena"},
+ {"44562.189571759256", "[$-426]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-426]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-426]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."},
+ {"44562.189571759256", "[$-426]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."},
+ {"43543.503206018519", "[$-426]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."},
+ {"43543.503206018519", "[$-426]mmmm dd yyyy h:mm AM/PM aaa", "marts 19 2019 12:04 pēcp. otrd."},
+ {"43543.503206018519", "[$-426]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pēcp. otrd."},
+ {"43543.503206018519", "[$-426]mmmmmm dd yyyy h:mm AM/PM dddd", "marts 19 2019 12:04 pēcp. otrdiena"},
+ {"44562.189571759256", "[$-27]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-27]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-27]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-27]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"},
+ {"43543.503206018519", "[$-27]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"},
+ {"43543.503206018519", "[$-27]mmmm dd yyyy h:mm AM/PM aaa", "kovas 19 2019 12:04 popiet an"},
+ {"43543.503206018519", "[$-27]mmmmm dd yyyy h:mm AM/PM ddd", "k 19 2019 12:04 popiet an"},
+ {"43543.503206018519", "[$-27]mmmmmm dd yyyy h:mm AM/PM dddd", "kovas 19 2019 12:04 popiet antradienis"},
+ {"44562.189571759256", "[$-427]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-427]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-427]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"},
+ {"44562.189571759256", "[$-427]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"},
+ {"43543.503206018519", "[$-427]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"},
+ {"43543.503206018519", "[$-427]mmmm dd yyyy h:mm AM/PM aaa", "kovas 19 2019 12:04 popiet an"},
+ {"43543.503206018519", "[$-427]mmmmm dd yyyy h:mm AM/PM ddd", "k 19 2019 12:04 popiet an"},
+ {"43543.503206018519", "[$-427]mmmmmm dd yyyy h:mm AM/PM dddd", "kovas 19 2019 12:04 popiet antradienis"},
+ {"44562.189571759256", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C2E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C2E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 PM wa\u0142"},
+ {"43543.503206018519", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wa\u0142"},
+ {"43543.503206018519", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 PM wa\u0142tora"},
+ {"44562.189571759256", "[$-82E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-82E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-82E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 PM wa\u0142"},
+ {"43543.503206018519", "[$-82E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wa\u0142"},
+ {"43543.503206018519", "[$-82E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 PM wa\u0142tora"},
+ {"44562.189571759256", "[$-6E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-6E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-6E]mmmm dd yyyy h:mm AM/PM aaa", "Mäerz 19 2019 12:04 PM Dën"},
+ {"43543.503206018519", "[$-6E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Dën"},
+ {"43543.503206018519", "[$-6E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mäerz 19 2019 12:04 PM Dënschdeg"},
+ {"44562.189571759256", "[$-46E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-46E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-46E]mmmm dd yyyy h:mm AM/PM aaa", "Mäerz 19 2019 12:04 PM Dën"},
+ {"43543.503206018519", "[$-46E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Dën"},
+ {"43543.503206018519", "[$-46E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mäerz 19 2019 12:04 PM Dënschdeg"},
+ {"44562.189571759256", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-2F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-2F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-2F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"43543.503206018519", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."},
+ {"43543.503206018519", "[$-2F]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."},
+ {"43543.503206018519", "[$-2F]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."},
+ {"43543.503206018519", "[$-2F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"44562.189571759256", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-42F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-42F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"44562.189571759256", "[$-42F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."},
+ {"43543.503206018519", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."},
+ {"43543.503206018519", "[$-42F]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."},
+ {"43543.503206018519", "[$-42F]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."},
+ {"43543.503206018519", "[$-42F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"44562.189571759256", "[$-3E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-3E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-3E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-3E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"43543.503206018519", "[$-3E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"},
+ {"43543.503206018519", "[$-3E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-3E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-3E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"},
+ {"44562.189571759256", "[$-83E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-83E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-83E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-83E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"43543.503206018519", "[$-83E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"},
+ {"43543.503206018519", "[$-83E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-83E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-83E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"},
+ {"44562.189571759256", "[$-43E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-43E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-43E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"},
+ {"44562.189571759256", "[$-43E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"},
+ {"43543.503206018519", "[$-43E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"},
+ {"43543.503206018519", "[$-43E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-43E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"},
+ {"43543.503206018519", "[$-43E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"},
+ {"44562.189571759256", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-4C]mmmm dd yyyy h:mm AM/PM aaa", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"},
+ {"43543.503206018519", "[$-4C]mmmmm dd yyyy h:mm AM/PM ddd", "\u0D2E 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"},
+ {"43543.503206018519", "[$-4C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A"},
+ {"44562.189571759256", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-44C]mmmm dd yyyy h:mm AM/PM aaa", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"},
+ {"43543.503206018519", "[$-44C]mmmmm dd yyyy h:mm AM/PM ddd", "\u0D2E 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"},
+ {"43543.503206018519", "[$-44C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A"},
+ {"44562.189571759256", "[$-3A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-3A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-3A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-3A]mmmm dd yyyy h:mm AM/PM aaa", "Marzu 19 2019 12:04 PM Tli"},
+ {"43543.503206018519", "[$-3A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tli"},
+ {"43543.503206018519", "[$-3A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzu 19 2019 12:04 PM It-Tlieta"},
+ {"44562.189571759256", "[$-43A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-43A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-43A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-43A]mmmm dd yyyy h:mm AM/PM aaa", "Marzu 19 2019 12:04 PM Tli"},
+ {"43543.503206018519", "[$-43A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tli"},
+ {"43543.503206018519", "[$-43A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzu 19 2019 12:04 PM It-Tlieta"},
+ {"44562.189571759256", "[$-81]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-81]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-81]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-81]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-81]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-81]mmmm dd yyyy h:mm AM/PM aaa", "Poutūterangi 19 2019 12:04 p.m. Tū"},
+ {"43543.503206018519", "[$-81]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Tū"},
+ {"43543.503206018519", "[$-81]mmmmmm dd yyyy h:mm AM/PM dddd", "Poutūterangi 19 2019 12:04 p.m. Rātū"},
+ {"44562.189571759256", "[$-481]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-481]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-481]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-481]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-481]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-481]mmmm dd yyyy h:mm AM/PM aaa", "Poutūterangi 19 2019 12:04 p.m. Tū"},
+ {"43543.503206018519", "[$-481]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Tū"},
+ {"43543.503206018519", "[$-481]mmmmmm dd yyyy h:mm AM/PM dddd", "Poutūterangi 19 2019 12:04 p.m. Rātū"},
+ {"44562.189571759256", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7A]mmmm dd yyyy h:mm AM/PM aaa", "Kila 19 2019 12:04 PM Kila"},
+ {"43543.503206018519", "[$-7A]mmmmm dd yyyy h:mm AM/PM ddd", "K 19 2019 12:04 PM Kila"},
+ {"43543.503206018519", "[$-7A]mmmmmm dd yyyy h:mm AM/PM dddd", "Kila 19 2019 12:04 PM Kila Ante"},
+ {"44562.189571759256", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-47A]mmmm dd yyyy h:mm AM/PM aaa", "Kila 19 2019 12:04 PM Kila"},
+ {"43543.503206018519", "[$-47A]mmmmm dd yyyy h:mm AM/PM ddd", "K 19 2019 12:04 PM Kila"},
+ {"43543.503206018519", "[$-47A]mmmmmm dd yyyy h:mm AM/PM dddd", "Kila 19 2019 12:04 PM Kila Ante"},
+ {"44562.189571759256", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-4E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-4E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-4E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"43543.503206018519", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-4E]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-4E]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-4E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u0935\u093E\u0930"},
+ {"44562.189571759256", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"44562.189571759256", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."},
+ {"43543.503206018519", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."},
+ {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u0935\u093E\u0930"},
+ {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."},
+ {"44562.189571759256", "[$-7C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C]mmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C]mmmm dd yyyy h:mm AM/PM aaa", "Enniskó:Wa 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-7C]mmmmm dd yyyy h:mm AM/PM ddd", "E 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-7C]mmmmmm dd yyyy h:mm AM/PM dddd", "Enniskó:Wa 19 2019 12:04 PM Ratironhia'kehronòn:ke"},
+ {"44562.189571759256", "[$-47C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47C]mmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-47C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-47C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-47C]mmmm dd yyyy h:mm AM/PM aaa", "Enniskó:Wa 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-47C]mmmmm dd yyyy h:mm AM/PM ddd", "E 19 2019 12:04 PM Tue"},
+ {"43543.503206018519", "[$-47C]mmmmmm dd yyyy h:mm AM/PM dddd", "Enniskó:Wa 19 2019 12:04 PM Ratironhia'kehronòn:ke"},
+ {"44562.189571759256", "[$-50]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-50]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-50]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-50]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"43543.503206018519", "[$-50]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."},
+ {"43543.503206018519", "[$-50]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-50]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-50]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"},
+ {"44562.189571759256", "[$-7850]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-7850]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-7850]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-7850]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"43543.503206018519", "[$-7850]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."},
+ {"43543.503206018519", "[$-7850]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-7850]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-7850]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"},
+ {"44562.189571759256", "[$-450]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-450]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-450]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."},
+ {"44562.189571759256", "[$-450]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."},
+ {"43543.503206018519", "[$-450]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."},
+ {"43543.503206018519", "[$-450]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-450]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"},
+ {"43543.503206018519", "[$-450]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"},
+ {"44562.189571759256", "[$-7C50]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-7C50]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C50]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-7C50]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"},
+ {"44562.189571759256", "[$-7C50]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828"},
+ {"44896.18957170139", "[$-7C50]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"},
+ {"44562.189571759256", "[$-850]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-850]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-850]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-850]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"},
+ {"44562.189571759256", "[$-850]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828"},
+ {"44896.18957170139", "[$-850]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"},
+ {"44562.189571759256", "[$-C50]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-C50]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-C50]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182B\u1826\u1837\u182A\u1826"},
+ {"44562.189571759256", "[$-C50]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182A\u1822\u182E\u182A\u1820"},
+ {"44896.18957170139", "[$-C50]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182B\u1826\u1837\u182A\u1826"},
+ {"44562.189571759256", "[$-61]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-61]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-61]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-61]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-61]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-61]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-61]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-61]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930"},
+ {"44562.189571759256", "[$-861]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-861]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-861]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-861]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-861]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-861]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-861]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-861]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u092C\u093E\u0930"},
+ {"44562.189571759256", "[$-461]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-461]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-461]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"44562.189571759256", "[$-461]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-461]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"},
+ {"43543.503206018519", "[$-461]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-461]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"},
+ {"43543.503206018519", "[$-461]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930"},
+ {"44562.189571759256", "[$-14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-14]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir."},
+ {"43543.503206018519", "[$-14]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir."},
+ {"43543.503206018519", "[$-14]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"},
+ {"44562.189571759256", "[$-7C14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-7C14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-7C14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-7C14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-7C14]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir"},
+ {"43543.503206018519", "[$-7C14]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir"},
+ {"43543.503206018519", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"},
+ {"44562.189571759256", "[$-414]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-414]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-414]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-414]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-414]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-414]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir"},
+ {"43543.503206018519", "[$-414]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir"},
+ {"43543.503206018519", "[$-414]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"},
+ {"44562.189571759256", "[$-7814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-7814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-7814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-7814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."},
+ {"43543.503206018519", "[$-7814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."},
+ {"43543.503206018519", "[$-7814]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.m. tys"},
+ {"43543.503206018519", "[$-7814]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.m. tys"},
+ {"43543.503206018519", "[$-7814]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.m. tysdag"},
+ {"44562.189571759256", "[$-814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."},
+ {"44562.189571759256", "[$-814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."},
+ {"43543.503206018519", "[$-814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."},
+ {"43543.503206018519", "[$-814]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.m. tys"},
+ {"43543.503206018519", "[$-814]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.m. tys"},
+ {"43543.503206018519", "[$-814]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.m. tysdag"},
+ {"44562.189571759256", "[$-82]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-82]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-82]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-82]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 PM dma."},
+ {"43543.503206018519", "[$-82]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM dma."},
+ {"43543.503206018519", "[$-82]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 PM dimarts"},
+ {"44562.189571759256", "[$-482]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-482]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-482]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-482]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-482]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-482]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 PM dma."},
+ {"43543.503206018519", "[$-482]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM dma."},
+ {"43543.503206018519", "[$-482]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 PM dimarts"},
+ {"44562.189571759256", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-48]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-48]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-48]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-48]mmmm dd yyyy h:mm AM/PM aaa", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."},
+ {"43543.503206018519", "[$-48]mmmmm dd yyyy h:mm AM/PM ddd", "\u0B2E 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."},
+ {"43543.503206018519", "[$-48]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30"},
+ {"44562.189571759256", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-448]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-448]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-448]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-448]mmmm dd yyyy h:mm AM/PM aaa", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."},
+ {"43543.503206018519", "[$-448]mmmmm dd yyyy h:mm AM/PM ddd", "\u0B2E 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."},
+ {"43543.503206018519", "[$-448]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30"},
+ {"44562.189571759256", "[$-72]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-72]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-72]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-72]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"},
+ {"43543.503206018519", "[$-72]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"},
+ {"43543.503206018519", "[$-72]mmmm dd yyyy h:mm AM/PM aaa", "Bitooteessa 19 2019 12:04 WB Qib"},
+ {"43543.503206018519", "[$-72]mmmmm dd yyyy h:mm AM/PM ddd", "B 19 2019 12:04 WB Qib"},
+ {"43543.503206018519", "[$-72]mmmmmm dd yyyy h:mm AM/PM dddd", "Bitooteessa 19 2019 12:04 WB Qibxata"},
+ {"44562.189571759256", "[$-472]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-472]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-472]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"},
+ {"44562.189571759256", "[$-472]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"},
+ {"43543.503206018519", "[$-472]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"},
+ {"43543.503206018519", "[$-472]mmmm dd yyyy h:mm AM/PM aaa", "Bitooteessa 19 2019 12:04 WB Qib"},
+ {"43543.503206018519", "[$-472]mmmmm dd yyyy h:mm AM/PM ddd", "B 19 2019 12:04 WB Qib"},
+ {"43543.503206018519", "[$-472]mmmmmm dd yyyy h:mm AM/PM dddd", "Bitooteessa 19 2019 12:04 WB Qibxata"},
+ {"44562.189571759256", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"43543.503206018519", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."},
+ {"43543.503206018519", "[$-63]mmmm dd yyyy h:mm AM/PM aaa", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"43543.503206018519", "[$-63]mmmmm dd yyyy h:mm AM/PM ddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"43543.503206018519", "[$-63]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"44562.189571759256", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44562.189571759256", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"44713.188888888886", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."},
+ {"43543.503206018519", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."},
+ {"43543.503206018519", "[$-463]mmmm dd yyyy h:mm AM/PM aaa", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"43543.503206018519", "[$-463]mmmmm dd yyyy h:mm AM/PM ddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"43543.503206018519", "[$-463]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"},
+ {"44562.189571759256", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-29]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-29]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-29]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"43543.503206018519", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"},
+ {"43543.503206018519", "[$-29]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"43543.503206018519", "[$-29]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"43543.503206018519", "[$-29]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"44562.189571759256", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-429]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-429]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"},
+ {"44562.189571759256", "[$-429]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"},
+ {"43543.503206018519", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"},
+ {"43543.503206018519", "[$-429]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"43543.503206018519", "[$-429]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"43543.503206018519", "[$-429]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"},
+ {"44562.189571759256", "[$-15]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-15]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-15]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-15]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-15]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-15]mmmm dd yyyy h:mm AM/PM aaa", "marzec 19 2019 12:04 PM wt."},
+ {"43543.503206018519", "[$-15]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wt."},
+ {"43543.503206018519", "[$-15]mmmmmm dd yyyy h:mm AM/PM dddd", "marzec 19 2019 12:04 PM wtorek"},
+ {"44562.189571759256", "[$-415]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-415]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-415]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-415]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-415]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-415]mmmm dd yyyy h:mm AM/PM aaa", "marzec 19 2019 12:04 PM wt."},
+ {"43543.503206018519", "[$-415]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wt."},
+ {"43543.503206018519", "[$-415]mmmmmm dd yyyy h:mm AM/PM dddd", "marzec 19 2019 12:04 PM wtorek"},
+ {"44562.189571759256", "[$-16]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-16]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-16]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-16]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-16]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-16]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-16]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-16]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"},
+ {"44562.189571759256", "[$-416]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-416]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-416]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-416]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-416]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-416]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-416]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-416]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"},
+ {"44562.189571759256", "[$-816]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-816]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-816]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-816]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-816]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-816]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-816]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"},
+ {"43543.503206018519", "[$-816]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"},
+ {"44562.189571759256", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-46]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-46]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-46]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"43543.503206018519", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"},
+ {"43543.503206018519", "[$-46]mmmm dd yyyy h:mm AM/PM aaa", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."},
+ {"43543.503206018519", "[$-46]mmmmm dd yyyy h:mm AM/PM ddd", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."},
+ {"43543.503206018519", "[$-46]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30"},
+ {"44562.189571759256", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C46]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C46]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C46]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"43543.503206018519", "[$-7C46]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"43543.503206018519", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"44562.189571759256", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-446]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-446]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"44562.189571759256", "[$-446]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"},
+ {"43543.503206018519", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"},
+ {"43543.503206018519", "[$-446]mmmm dd yyyy h:mm AM/PM aaa", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."},
+ {"43543.503206018519", "[$-446]mmmmm dd yyyy h:mm AM/PM ddd", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."},
+ {"43543.503206018519", "[$-446]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30"},
+ {"44562.189571759256", "[$-846]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-846]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-846]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-846]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-846]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-846]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"43543.503206018519", "[$-846]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"43543.503206018519", "[$-846]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"},
+ {"44562.189571759256", "[$-6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-6B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. ati"},
+ {"43543.503206018519", "[$-6B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. ati"},
+ {"43543.503206018519", "[$-6B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. atipachaw"},
+ {"44562.189571759256", "[$-46B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-46B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-46B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-46B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-46B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-46B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. ati"},
+ {"43543.503206018519", "[$-46B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. ati"},
+ {"43543.503206018519", "[$-46B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. atipachaw"},
+ {"44562.189571759256", "[$-86B]mmm dd yyyy h:mm AM/PM", "kull 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-86B]mmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-86B]mmmmm dd yyyy h:mm AM/PM", "k 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-86B]mmmmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-86B]mmm dd yyyy h:mm AM/PM", "paw 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-86B]mmmm dd yyyy h:mm AM/PM aaa", "pawkar 19 2019 12:04 PM wan"},
+ {"43543.503206018519", "[$-86B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM wan"},
+ {"43543.503206018519", "[$-86B]mmmmmm dd yyyy h:mm AM/PM dddd", "pawkar 19 2019 12:04 PM wanra"},
+ {"44562.189571759256", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-C6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-C6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-C6B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. Mar"},
+ {"43543.503206018519", "[$-C6B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Mar"},
+ {"43543.503206018519", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. Martes"},
+ {"44562.189571759256", "[$-18]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-18]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-18]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-18]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-18]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-18]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-18]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-18]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"},
+ {"44562.189571759256", "[$-818]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-818]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-818]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-818]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-818]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-818]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. Mar"},
+ {"43543.503206018519", "[$-818]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. Mar"},
+ {"43543.503206018519", "[$-818]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"},
+ {"44562.189571759256", "[$-418]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-418]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-418]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-418]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-418]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."},
+ {"43543.503206018519", "[$-418]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-418]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-418]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"},
+ {"44562.189571759256", "[$-17]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-17]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-17]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-17]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-17]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-17]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM ma"},
+ {"43543.503206018519", "[$-17]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ma"},
+ {"43543.503206018519", "[$-17]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-417]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-417]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-417]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-417]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-417]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-417]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM ma"},
+ {"43543.503206018519", "[$-417]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ma"},
+ {"43543.503206018519", "[$-417]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM mardi"},
+ {"44562.189571759256", "[$-19]mmm dd yyyy h:mm AM/PM", "янв. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-19]mmmm dd yyyy h:mm AM/PM", "январь 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-19]mmmmm dd yyyy h:mm AM/PM", "я 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-19]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-19]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-19]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"43543.503206018519", "[$-819]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-819]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-819]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"43543.503206018519", "[$-419]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-419]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-419]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"},
+ {"44562.189571759256", "[$-85]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-85]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-85]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-85]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"},
+ {"43543.503206018519", "[$-85]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"},
+ {"43543.503206018519", "[$-85]mmmm dd yyyy h:mm AM/PM aaa", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u043E\u043F"},
+ {"43543.503206018519", "[$-85]mmmmm dd yyyy h:mm AM/PM ddd", "\u041A 19 2019 12:04 \u041A\u041A \u043E\u043F"},
+ {"43543.503206018519", "[$-85]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A"},
+ {"44562.189571759256", "[$-485]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-485]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-485]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"},
+ {"44562.189571759256", "[$-485]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"},
+ {"43543.503206018519", "[$-485]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"},
+ {"43543.503206018519", "[$-485]mmmm dd yyyy h:mm AM/PM aaa", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u043E\u043F"},
+ {"43543.503206018519", "[$-485]mmmmm dd yyyy h:mm AM/PM ddd", "\u041A 19 2019 12:04 \u041A\u041A \u043E\u043F"},
+ {"43543.503206018519", "[$-485]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A"},
+ {"44562.189571759256", "[$-703B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-703B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-703B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-703B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-703B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-703B]mmmm dd yyyy h:mm AM/PM aaa", "njuhčâmáánu 19 2019 12:04 PM maj"},
+ {"43543.503206018519", "[$-703B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM maj"},
+ {"43543.503206018519", "[$-703B]mmmmmm dd yyyy h:mm AM/PM dddd", "njuhčâmáánu 19 2019 12:04 PM majebargâ"},
+ {"44562.189571759256", "[$-243B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-243B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-243B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-243B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-243B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-243B]mmmm dd yyyy h:mm AM/PM aaa", "njuhčâmáánu 19 2019 12:04 PM maj"},
+ {"43543.503206018519", "[$-243B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM maj"},
+ {"43543.503206018519", "[$-243B]mmmmmm dd yyyy h:mm AM/PM dddd", "njuhčâmáánu 19 2019 12:04 PM majebargâ"},
+ {"44562.189571759256", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C3B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C3B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"},
+ {"44562.189571759256", "[$-103B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-103B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-103B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-103B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-103B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-103B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-103B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-103B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"},
+ {"44562.189571759256", "[$-143B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-143B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-143B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-143B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-143B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-143B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-143B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-143B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"},
+ {"44562.189571759256", "[$-3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."},
+ {"43543.503206018519", "[$-3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."},
+ {"43543.503206018519", "[$-3B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 e.b. maŋ"},
+ {"43543.503206018519", "[$-3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 e.b. maŋ"},
+ {"43543.503206018519", "[$-3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 e.b. maŋŋebárga"},
+ {"44562.189571759256", "[$-C3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-C3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-C3B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 PM di"},
+ {"43543.503206018519", "[$-C3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM di"},
+ {"43543.503206018519", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 PM maŋŋebárga"},
+ {"44562.189571759256", "[$-43B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-43B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-43B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."},
+ {"44562.189571759256", "[$-43B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."},
+ {"43543.503206018519", "[$-43B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."},
+ {"43543.503206018519", "[$-43B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 e.b. maŋ"},
+ {"43543.503206018519", "[$-43B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 e.b. maŋ"},
+ {"43543.503206018519", "[$-43B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 e.b. maŋŋebárga"},
+ {"44562.189571759256", "[$-83B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-83B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-83B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-83B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-83B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-83B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-83B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM dis"},
+ {"43543.503206018519", "[$-83B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 PM disdat"},
+ {"44562.189571759256", "[$-743B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-743B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-743B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-743B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-743B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-743B]mmmm dd yyyy h:mm AM/PM aaa", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2"},
+ {"43543.503206018519", "[$-743B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM m%E2"},
+ {"43543.503206018519", "[$-743B]mmmmmm dd yyyy h:mm AM/PM dddd", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2%E2ibargg"},
+ {"44562.189571759256", "[$-203B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-203B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-203B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-203B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-203B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-203B]mmmm dd yyyy h:mm AM/PM aaa", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2"},
+ {"43543.503206018519", "[$-203B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM m%E2"},
+ {"43543.503206018519", "[$-203B]mmmmmm dd yyyy h:mm AM/PM dddd", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2%E2ibargg"},
+ {"44562.189571759256", "[$-783B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-783B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-783B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-783B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-783B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-783B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-783B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-783B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"},
+ {"44562.189571759256", "[$-183B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-183B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-183B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-183B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-183B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-183B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-183B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-183B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"},
+ {"44562.189571759256", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C3B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-1C3B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"},
+ {"43543.503206018519", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"},
+ {"44562.189571759256", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-4F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-4F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-4F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"43543.503206018519", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"},
+ {"43543.503206018519", "[$-4F]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"},
+ {"43543.503206018519", "[$-4F]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"},
+ {"43543.503206018519", "[$-4F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903"},
+ {"44562.189571759256", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-44F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-44F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"44562.189571759256", "[$-44F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"},
+ {"43543.503206018519", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"},
+ {"43543.503206018519", "[$-44F]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"},
+ {"43543.503206018519", "[$-44F]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"},
+ {"43543.503206018519", "[$-44F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903"},
+ {"44562.189571759256", "[$-91]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-91]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-91]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-91]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"},
+ {"43543.503206018519", "[$-91]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"},
+ {"43543.503206018519", "[$-91]mmmm dd yyyy h:mm AM/PM aaa", "Am Màrt 19 2019 12:04 f DiM"},
+ {"43543.503206018519", "[$-91]mmmmm dd yyyy h:mm AM/PM ddd", "A 19 2019 12:04 f DiM"},
+ {"43543.503206018519", "[$-91]mmmmmm dd yyyy h:mm AM/PM dddd", "Am Màrt 19 2019 12:04 f DiMàirt"},
+ {"44562.189571759256", "[$-491]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-491]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-491]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"},
+ {"44562.189571759256", "[$-491]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"},
+ {"43543.503206018519", "[$-491]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"},
+ {"43543.503206018519", "[$-491]mmmm dd yyyy h:mm AM/PM aaa", "Am Màrt 19 2019 12:04 f DiM"},
+ {"43543.503206018519", "[$-491]mmmmm dd yyyy h:mm AM/PM ddd", "A 19 2019 12:04 f DiM"},
+ {"43543.503206018519", "[$-491]mmmmmm dd yyyy h:mm AM/PM dddd", "Am Màrt 19 2019 12:04 f DiMàirt"},
+ {"44562.189571759256", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-6C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-1C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-301A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-301A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-301A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-301A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-301A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"},
+ {"43543.503206018519", "[$-301A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-281A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-281A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-281A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-281A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-281A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-281A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."},
+ {"43543.503206018519", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"},
+ {"44562.189571759256", "[$-701A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-701A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-701A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-701A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"43543.503206018519", "[$-701A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"},
+ {"43543.503206018519", "[$-701A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-701A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-701A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"},
+ {"44562.189571759256", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-7C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"43543.503206018519", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"},
+ {"43543.503206018519", "[$-7C1A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"},
+ {"44562.189571759256", "[$-181A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-181A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-181A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-181A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"},
+ {"43543.503206018519", "[$-181A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"},
+ {"43543.503206018519", "[$-181A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-181A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-181A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"},
+ {"44562.189571759256", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-2C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"},
+ {"44562.189571759256", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"},
+ {"43543.503206018519", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"},
+ {"43543.503206018519", "[$-2C1A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"},
+ {"44562.189571759256", "[$-241A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-241A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-241A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"},
+ {"44562.189571759256", "[$-241A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"},
+ {"43543.503206018519", "[$-241A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"},
+ {"43543.503206018519", "[$-241A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-241A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"},
+ {"43543.503206018519", "[$-241A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"},
+ {"44562.189571759256", "[$-81A]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-81A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-81A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-81A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-81A]mmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-81A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto."},
+ {"43543.503206018519", "[$-81A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto."},
+ {"43543.503206018519", "[$-81A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"},
+ {"44562.189571759256", "[$-6C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-6C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-6C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-6C]mmmm dd yyyy h:mm AM/PM aaa", "Matšhe 19 2019 12:04 PM Lbb"},
+ {"43543.503206018519", "[$-6C]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lbb"},
+ {"43543.503206018519", "[$-6C]mmmmmm dd yyyy h:mm AM/PM dddd", "Matšhe 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-46C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-46C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-46C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-46C]mmmm dd yyyy h:mm AM/PM aaa", "Matšhe 19 2019 12:04 PM Lbb"},
+ {"43543.503206018519", "[$-46C]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lbb"},
+ {"43543.503206018519", "[$-46C]mmmmmm dd yyyy h:mm AM/PM dddd", "Matšhe 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-32]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-32]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-32]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-32]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-32]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-32]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-32]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-32]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-832]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-832]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-832]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-832]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-832]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-832]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-832]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-832]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-432]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-432]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-432]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-432]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-432]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-432]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-432]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."},
+ {"43543.503206018519", "[$-432]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-59]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-59]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-59]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"},
+ {"44562.189571759256", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C59]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-7C59]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"},
+ {"44562.189571759256", "[$-859]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-859]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-859]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-859]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-859]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-859]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-859]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"},
+ {"43543.503206018519", "[$-859]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"},
+ {"44562.189571759256", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-5B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-5B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-5B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"43543.503206018519", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."},
+ {"43543.503206018519", "[$-5B]mmmm dd yyyy h:mm AM/PM aaa", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"},
+ {"43543.503206018519", "[$-5B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"},
+ {"43543.503206018519", "[$-5B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631\u0628\u0639"},
+ {"44562.189571759256", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-45B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-45B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"44562.189571759256", "[$-45B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."},
+ {"43543.503206018519", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."},
+ {"43543.503206018519", "[$-45B]mmmm dd yyyy h:mm AM/PM aaa", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"},
+ {"43543.503206018519", "[$-45B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"},
+ {"43543.503206018519", "[$-45B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631\u0628\u0639"},
+ {"44562.189571759256", "[$-1B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-1B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-1B]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 PM ut"},
+ {"43543.503206018519", "[$-1B]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ut"},
+ {"43543.503206018519", "[$-1B]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 PM utorok"},
+ {"44562.189571759256", "[$-41B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-41B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-41B]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 PM ut"},
+ {"43543.503206018519", "[$-41B]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ut"},
+ {"43543.503206018519", "[$-41B]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 PM utorok"},
+ {"44562.189571759256", "[$-24]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-24]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-24]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-24]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."},
+ {"43543.503206018519", "[$-24]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."},
+ {"43543.503206018519", "[$-24]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 pop. tor."},
+ {"43543.503206018519", "[$-24]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pop. tor."},
+ {"43543.503206018519", "[$-24]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 pop. torek"},
+ {"44562.189571759256", "[$-424]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-424]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-424]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."},
+ {"44562.189571759256", "[$-424]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."},
+ {"43543.503206018519", "[$-424]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."},
+ {"43543.503206018519", "[$-424]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 pop. tor."},
+ {"43543.503206018519", "[$-424]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pop. tor."},
+ {"43543.503206018519", "[$-424]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 pop. torek"},
+ {"44562.189571759256", "[$-77]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-77]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-77]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-77]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"},
+ {"43543.503206018519", "[$-77]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"},
+ {"43543.503206018519", "[$-77]mmmm dd yyyy h:mm AM/PM aaa", "Maarso 19 2019 12:04 GD Tldo"},
+ {"43543.503206018519", "[$-77]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 GD Tldo"},
+ {"43543.503206018519", "[$-77]mmmmmm dd yyyy h:mm AM/PM dddd", "Maarso 19 2019 12:04 GD Talaado"},
+ {"44562.189571759256", "[$-477]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-477]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-477]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"},
+ {"44562.189571759256", "[$-477]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"},
+ {"43543.503206018519", "[$-477]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"},
+ {"43543.503206018519", "[$-477]mmmm dd yyyy h:mm AM/PM aaa", "Maarso 19 2019 12:04 GD Tldo"},
+ {"43543.503206018519", "[$-477]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 GD Tldo"},
+ {"43543.503206018519", "[$-477]mmmmmm dd yyyy h:mm AM/PM dddd", "Maarso 19 2019 12:04 GD Talaado"},
+ {"44562.189571759256", "[$-30]mmm dd yyyy h:mm AM/PM", "Phe 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-30]mmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-30]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-30]mmmmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-30]mmm dd yyyy h:mm AM/PM", "Ube 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-30]mmmm dd yyyy h:mm AM/PM aaa", "Hlakubele 19 2019 12:04 PM Bed"},
+ {"43543.503206018519", "[$-30]mmmmm dd yyyy h:mm AM/PM ddd", "H 19 2019 12:04 PM Bed"},
+ {"43543.503206018519", "[$-30]mmmmmm dd yyyy h:mm AM/PM dddd", "Hlakubele 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-430]mmm dd yyyy h:mm AM/PM", "Phe 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-430]mmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-430]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-430]mmmmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-430]mmm dd yyyy h:mm AM/PM", "Ube 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-430]mmmm dd yyyy h:mm AM/PM aaa", "Hlakubele 19 2019 12:04 PM Bed"},
+ {"43543.503206018519", "[$-430]mmmmm dd yyyy h:mm AM/PM ddd", "H 19 2019 12:04 PM Bed"},
+ {"43543.503206018519", "[$-430]mmmmmm dd yyyy h:mm AM/PM dddd", "Hlakubele 19 2019 12:04 PM Labobedi"},
+ {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."},
+ {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."},
+ {"44562.189571759256", "[$-A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p. m. ma."},
+ {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p. m. ma."},
+ {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p. m. martes"},
+ {"44562.189571759256", "[$-2C0A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-2C0A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-2C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-2C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-200A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-200A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-200A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-200A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-200A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-200A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-400A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-400A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-400A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-400A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-400A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-400A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-340A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-340A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-340A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-340A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-340A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-340A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-240A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-240A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-240A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-240A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-240A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-240A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-140A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-140A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-140A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-140A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-140A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-140A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-5C0A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-5C0A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.m."},
+ {"44562.189571759256", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.m."},
+ {"43543.503206018519", "[$-5C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-5C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.m. martes"},
+ {"43543.503206018519", "[$-1C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-1C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-1C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-300A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-300A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-300A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-440A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-440A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-440A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-100A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-100A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-100A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-480A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-480A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-480A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-580A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-580A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.m. mar."},
+ {"43543.503206018519", "[$-580A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.m. martes"},
+ {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."},
+ {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."},
+ {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."},
+ {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."},
+ {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."},
+ {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p. m. mar."},
+ {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p. m. mar."},
+ {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p. m. martes"},
+ {"43543.503206018519", "[$-4C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-4C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-4C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-180A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-180A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-180A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-3C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-3C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-3C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-280A]mmm dd yyyy h:mm AM/PM", "Ene. 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-280A]mmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-280A]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-280A]mmmmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-280A]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.%A0m."},
+ {"43543.503206018519", "[$-280A]mmmm dd yyyy h:mm AM/PM aaa", "Marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-280A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-280A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzo 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-500A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-500A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-500A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"},
+ {"43543.503206018519", "[$-40A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM ma."},
+ {"43543.503206018519", "[$-40A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM ma."},
+ {"43543.503206018519", "[$-40A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"},
+ {"43543.503206018519", "[$-C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM ma."},
+ {"43543.503206018519", "[$-C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM ma."},
+ {"43543.503206018519", "[$-C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"},
+ {"43543.503206018519", "[$-540A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-540A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"},
+ {"43543.503206018519", "[$-540A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"},
+ {"44562.189571759256", "[$-380A]mmm dd yyyy h:mm AM/PM", "Ene. 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-380A]mmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-380A]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-380A]mmmmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-380A]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.%A0m."},
+ {"43543.503206018519", "[$-380A]mmmm dd yyyy h:mm AM/PM aaa", "Marzo 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-380A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 p.%A0m. mar."},
+ {"43543.503206018519", "[$-380A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzo 19 2019 12:04 p.%A0m. martes"},
+ {"44562.189571759256", "[$-1D]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-1D]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-1D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM tis"},
+ {"43543.503206018519", "[$-1D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tis"},
+ {"43543.503206018519", "[$-1D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM tisdag"},
+ {"44562.189571759256", "[$-81D]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 fm"},
+ {"44562.189571759256", "[$-81D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 fm"},
+ {"44562.189571759256", "[$-81D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 fm"},
+ {"44562.189571759256", "[$-81D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 fm"},
+ {"43543.503206018519", "[$-81D]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 em"},
+ {"43543.503206018519", "[$-81D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 em tis"},
+ {"43543.503206018519", "[$-81D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 em tis"},
+ {"43543.503206018519", "[$-81D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 em tisdag"},
+ {"44562.189571759256", "[$-41D]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-41D]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-41D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM tis"},
+ {"43543.503206018519", "[$-41D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tis"},
+ {"43543.503206018519", "[$-41D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM tisdag"},
+ {"44562.189571759256", "[$-5A]mmm dd yyyy h:mm AM/PM", "\u071F\u0722%A0\u070F\u0712 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-5A]mmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-5A]mmmmm dd yyyy h:mm AM/PM", "\u071F 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-5A]mmmmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"},
+ {"43543.503206018519", "[$-5A]mmm dd yyyy h:mm AM/PM", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B"},
+ {"43543.503206018519", "[$-5A]mmmm dd yyyy h:mm AM/PM aaa", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"},
+ {"43543.503206018519", "[$-5A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0710 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"},
+ {"43543.503206018519", "[$-5A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710"},
+ {"44562.189571759256", "[$-45A]mmm dd yyyy h:mm AM/PM", "\u071F\u0722%A0\u070F\u0712 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-45A]mmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-45A]mmmmm dd yyyy h:mm AM/PM", "\u071F 01 2022 4:32 \u0729.\u071B"},
+ {"44562.189571759256", "[$-45A]mmmmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"},
+ {"43543.503206018519", "[$-45A]mmm dd yyyy h:mm AM/PM", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B"},
+ {"43543.503206018519", "[$-45A]mmmm dd yyyy h:mm AM/PM aaa", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"},
+ {"43543.503206018519", "[$-45A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0710 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"},
+ {"43543.503206018519", "[$-45A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710"},
+ {"44562.189571759256", "[$-28]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-28]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-28]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-28]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-28]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-28]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-28]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-28]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"},
+ {"44562.189571759256", "[$-7C28]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C28]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C28]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C28]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C28]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C28]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-7C28]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-7C28]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"},
+ {"44562.189571759256", "[$-428]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-428]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-428]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-428]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-428]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-428]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-428]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"},
+ {"43543.503206018519", "[$-428]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"},
+ {"44562.189571759256", "[$-5F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-5F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-5F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-5F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-5F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-5F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"},
+ {"44562.189571759256", "[$-7C5F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-7C5F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-7C5F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-7C5F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-7C5F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-7C5F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"},
+ {"44562.189571759256", "[$-85F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-85F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-85F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-85F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-85F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"},
+ {"43543.503206018519", "[$-85F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"},
+ {"44562.189571759256", "[$-49]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-49]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-49]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-49]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-49]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-49]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"},
+ {"43543.503206018519", "[$-49]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"},
+ {"43543.503206018519", "[$-49]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8"},
+ {"44562.189571759256", "[$-449]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-449]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-449]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-449]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-449]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-449]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"},
+ {"43543.503206018519", "[$-449]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"},
+ {"43543.503206018519", "[$-449]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8"},
+ {"44562.189571759256", "[$-849]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9. 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-849]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-849]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"44562.189571759256", "[$-849]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-849]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD. 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"},
+ {"43543.503206018519", "[$-849]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD."},
+ {"43543.503206018519", "[$-849]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD."},
+ {"43543.503206018519", "[$-849]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"},
+ {"44562.189571759256", "[$-44]mmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44]mmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44]mmmmm dd yyyy h:mm AM/PM", "\u0433 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44]mmmmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-44]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-44]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448."},
+ {"43543.503206018519", "[$-44]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0438\u0448."},
+ {"43543.503206018519", "[$-44]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448\u04D9\u043C\u0431\u0435"},
+ {"44562.189571759256", "[$-444]mmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-444]mmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-444]mmmmm dd yyyy h:mm AM/PM", "\u0433 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-444]mmmmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-444]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-444]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448."},
+ {"43543.503206018519", "[$-444]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0438\u0448."},
+ {"43543.503206018519", "[$-444]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448\u04D9\u043C\u0431\u0435"},
+ {"44562.189571759256", "[$-4A]mmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4A]mmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4A]mmmmm dd yyyy h:mm AM/PM", "\u0C1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-4A]mmmmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-4A]mmm dd yyyy h:mm AM/PM", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-4A]mmmm dd yyyy h:mm AM/PM aaa", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"},
+ {"43543.503206018519", "[$-4A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0C2E 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"},
+ {"43543.503206018519", "[$-4A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02"},
+ {"44562.189571759256", "[$-44A]mmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44A]mmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44A]mmmmm dd yyyy h:mm AM/PM", "\u0C1C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-44A]mmmmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-44A]mmm dd yyyy h:mm AM/PM", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-44A]mmmm dd yyyy h:mm AM/PM aaa", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"},
+ {"43543.503206018519", "[$-44A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0C2E 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"},
+ {"43543.503206018519", "[$-44A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02"},
+ {"44562.189571759256", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e18. 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e40.\u0e22. 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e04. 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e22. 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e04. 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e2a.\u0e04. 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e22. 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e15.\u0e04. 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e22. 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e18.\u0e04. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e40 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."},
+ {"44866.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."},
+ {"44896.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"},
+ {"44562.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e18. 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e40.\u0e22. 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e04. 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e22. 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e04. 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e2a.\u0e04. 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e22. 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e15.\u0e04. 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e22. 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e18.\u0e04. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e40 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."},
+ {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."},
+ {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"},
+ {"100", "g\"年\"m\"月\"d\"日\";@", "年4月9日"},
+ {"100", "e\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
+ {"100", "ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
+ {"100", "[$-411]ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"},
+ {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
+ {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"},
+ {"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"},
+ {"43709", "[$-411]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"},
+ {"43709", "[$-411]ggee\"年\"m\"月\"d\"日\";@", "\u4EE401年9月1日"},
+ {"43709", "[$-411]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C01年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]ggee\"年\"m\"月\"d\"日\";@", "\u4EE401年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C01年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]ggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"},
+ {"43709", "[$-ja-JP-x-gannen,80]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"},
+ {"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"},
+ {"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"},
+ {"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"},
+ {"44896.18957170139", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R4年12月1日"},
+ {"44896.18957170139", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE44年12月1日"},
+ {"44896.18957170139", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C4年12月1日"},
+ {"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f24 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f25 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f26 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f27 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f28 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f29 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f20 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44866.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44896.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44562.189571759256", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44866.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44896.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44562.189571759256", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM aaa", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D"},
+ {"44866.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM ddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D"},
+ {"44896.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM dddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D"},
+ {"44562.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f24 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f25 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f26 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f27 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f28 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f29 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f20 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44866.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44896.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44562.189571759256", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44866.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44896.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44562.189571759256", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44593.189571759256", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44621.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44652.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44682.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44713.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44743.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44774.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44805.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"},
+ {"44835.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM aaa", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D"},
+ {"44866.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM ddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D"},
+ {"44896.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM dddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D"},
+ {"44562.189571759256", "[$-73]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-73]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-73]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-73]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"43543.503206018519", "[$-73]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A"},
+ {"43543.503206018519", "[$-73]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"},
+ {"43543.503206018519", "[$-73]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"},
+ {"43543.503206018519", "[$-73]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1220\u1209\u1235"},
+ {"44562.189571759256", "[$-873]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"},
+ {"44562.189571759256", "[$-873]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"},
+ {"44562.189571759256", "[$-873]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"},
+ {"44562.189571759256", "[$-873]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"},
+ {"43543.503206018519", "[$-873]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275"},
+ {"43543.503206018519", "[$-873]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1230\u1209"},
+ {"43543.503206018519", "[$-873]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1230\u1209"},
+ {"43543.503206018519", "[$-873]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1220\u1209\u1235"},
+ {"44562.189571759256", "[$-473]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-473]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-473]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206"},
+ {"44562.189571759256", "[$-473]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"},
+ {"43543.503206018519", "[$-473]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A"},
+ {"43543.503206018519", "[$-473]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"},
+ {"43543.503206018519", "[$-473]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"},
+ {"43543.503206018519", "[$-473]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1220\u1209\u1235"},
+ {"44562.189571759256", "[$-31]mmm dd yyyy h:mm AM/PM", "Sun 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-31]mmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-31]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-31]mmmmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-31]mmm dd yyyy h:mm AM/PM", "Kul 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-31]mmmm dd yyyy h:mm AM/PM aaa", "Nyenyankulu 19 2019 12:04 PM Bir"},
+ {"43543.503206018519", "[$-31]mmmmm dd yyyy h:mm AM/PM ddd", "N 19 2019 12:04 PM Bir"},
+ {"43543.503206018519", "[$-31]mmmmmm dd yyyy h:mm AM/PM dddd", "Nyenyankulu 19 2019 12:04 PM Ravumbirhi"},
+ {"44562.189571759256", "[$-431]mmm dd yyyy h:mm AM/PM", "Sun 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-431]mmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-431]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-431]mmmmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-431]mmm dd yyyy h:mm AM/PM", "Kul 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-431]mmmm dd yyyy h:mm AM/PM aaa", "Nyenyankulu 19 2019 12:04 PM Bir"},
+ {"43543.503206018519", "[$-431]mmmmm dd yyyy h:mm AM/PM ddd", "N 19 2019 12:04 PM Bir"},
+ {"43543.503206018519", "[$-431]mmmmmm dd yyyy h:mm AM/PM dddd", "Nyenyankulu 19 2019 12:04 PM Ravumbirhi"},
+ {"44562.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Nis 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "May 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Haz 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Tem 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Ağu 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Eyl 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Eki 01 2022 4:32 \u00F6\u00F6"},
+ {"44866.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Kas 01 2022 4:32 \u00F6\u00F6"},
+ {"44896.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Ara 01 2022 4:32 \u00F6\u00F6"},
+ {"44562.189571759256", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ocak 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Şubat 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Mart 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Nisan 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Mayıs 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Haziran 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Temmuz 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ağustos 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Eylül 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ekim 01 2022 4:32 \u00F6\u00F6"},
+ {"44866.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Kasım 01 2022 4:32 \u00F6\u00F6"},
+ {"44896.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Aralık 01 2022 4:32 \u00F6\u00F6"},
+ {"44562.189571759256", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "Ş 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM aaa", "E 01 2022 4:32 \u00F6\u00F6 Cmt"},
+ {"44866.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM ddd", "K 01 2022 4:32 \u00F6\u00F6 Sal"},
+ {"44896.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM dddd", "A 01 2022 4:32 \u00F6\u00F6 Perşembe"},
+ {"44562.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Nis 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "May 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Haz 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Tem 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Ağu 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Eyl 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Eki 01 2022 4:32 \u00F6\u00F6"},
+ {"44866.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Kas 01 2022 4:32 \u00F6\u00F6"},
+ {"44896.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Ara 01 2022 4:32 \u00F6\u00F6"},
+ {"44562.189571759256", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ocak 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Şubat 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Mart 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Nisan 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Mayıs 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Haziran 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Temmuz 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ağustos 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Eylül 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ekim 01 2022 4:32 \u00F6\u00F6"},
+ {"44866.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Kasım 01 2022 4:32 \u00F6\u00F6"},
+ {"44896.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Aralık 01 2022 4:32 \u00F6\u00F6"},
+ {"44562.189571759256", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 \u00F6\u00F6"},
+ {"44593.189571759256", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "Ş 01 2022 4:32 \u00F6\u00F6"},
+ {"44621.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"},
+ {"44652.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 \u00F6\u00F6"},
+ {"44682.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"},
+ {"44713.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 \u00F6\u00F6"},
+ {"44743.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"},
+ {"44774.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"},
+ {"44805.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"},
+ {"44835.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM aaa", "E 01 2022 4:32 \u00F6\u00F6 Cmt"},
+ {"44866.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM ddd", "K 01 2022 4:32 \u00F6\u00F6 Sal"},
+ {"44896.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM dddd", "A 01 2022 4:32 \u00F6\u00F6 Perşembe"},
+ {"44562.189571759256", "[$-42]mmm dd yyyy h:mm AM/PM", "Ýan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42]mmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42]mmmmm dd yyyy h:mm AM/PM", "Ý 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-42]mmmmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-42]mmm dd yyyy h:mm AM/PM", "Mart 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-42]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 PM Sb"},
+ {"43543.503206018519", "[$-42]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sb"},
+ {"43543.503206018519", "[$-42]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 PM Sişenbe"},
+ {"44562.189571759256", "[$-442]mmm dd yyyy h:mm AM/PM", "Ýan 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-442]mmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-442]mmmmm dd yyyy h:mm AM/PM", "Ý 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-442]mmmmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-442]mmm dd yyyy h:mm AM/PM", "Mart 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-442]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 PM Sb"},
+ {"43543.503206018519", "[$-442]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sb"},
+ {"43543.503206018519", "[$-442]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 PM Sişenbe"},
+ {"44562.189571759256", "[$-22]mmm dd yyyy h:mm AM/PM", "\u0421\u0456\u0447 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-22]mmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-22]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-22]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-22]mmm dd yyyy h:mm AM/PM", "\u0411\u0435\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-22]mmmm dd yyyy h:mm AM/PM aaa", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-22]mmmmm dd yyyy h:mm AM/PM ddd", "\u0431 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-22]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A"},
+ {"44562.189571759256", "[$-422]mmm dd yyyy h:mm AM/PM", "\u0421\u0456\u0447 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-422]mmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-422]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-422]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-422]mmm dd yyyy h:mm AM/PM", "\u0411\u0435\u0440 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-422]mmmm dd yyyy h:mm AM/PM aaa", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-422]mmmmm dd yyyy h:mm AM/PM ddd", "\u0431 19 2019 12:04 PM \u0412\u0442"},
+ {"43543.503206018519", "[$-422]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A"},
+ {"44562.189571759256", "[$-2E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-2E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-2E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-2E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"},
+ {"43543.503206018519", "[$-2E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 popołdnju"},
+ {"43543.503206018519", "[$-2E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 popołdnju wut"},
+ {"43543.503206018519", "[$-2E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 popołdnju wut"},
+ {"43543.503206018519", "[$-2E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 popołdnju wutora"},
+ {"44562.189571759256", "[$-42E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-42E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-42E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dopołdnja"},
+ {"44562.189571759256", "[$-42E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"},
+ {"43543.503206018519", "[$-42E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 popołdnju"},
+ {"43543.503206018519", "[$-42E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 popołdnju wut"},
+ {"43543.503206018519", "[$-42E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 popołdnju wut"},
+ {"43543.503206018519", "[$-42E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 popołdnju wutora"},
+ {"44562.189571759256", "[$-20]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-20]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-20]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-20]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-20]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-20]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-20]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-20]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"44562.189571759256", "[$-820]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"},
+ {"44562.189571759256", "[$-820]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"},
+ {"44562.189571759256", "[$-820]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 \u062F\u0646"},
+ {"44562.189571759256", "[$-820]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"},
+ {"43543.503206018519", "[$-820]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A"},
+ {"43543.503206018519", "[$-820]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-820]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-820]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"},
+ {"44562.189571759256", "[$-420]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-420]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-420]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-420]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-420]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-420]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-420]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"43543.503206018519", "[$-420]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"},
+ {"44562.189571759256", "[$-80]mmm dd yyyy h:mm AM/PM", "1-\u0626\u0627\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-80]mmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-80]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-80]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"43543.503206018519", "[$-80]mmm dd yyyy h:mm AM/PM", "3-\u0626\u0627\u064A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646"},
+ {"43543.503206018519", "[$-80]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"},
+ {"43543.503206018519", "[$-80]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"},
+ {"43543.503206018519", "[$-80]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5"},
+ {"44562.189571759256", "[$-480]mmm dd yyyy h:mm AM/PM", "1-\u0626\u0627\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-480]mmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-480]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"44562.189571759256", "[$-480]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"},
+ {"43543.503206018519", "[$-480]mmm dd yyyy h:mm AM/PM", "3-\u0626\u0627\u064A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646"},
+ {"43543.503206018519", "[$-480]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"},
+ {"43543.503206018519", "[$-480]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"},
+ {"43543.503206018519", "[$-480]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5"},
+ {"44562.189571759256", "[$-7843]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-7843]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-7843]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-7843]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"},
+ {"43543.503206018519", "[$-7843]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 \u0422\u041A"},
+ {"43543.503206018519", "[$-7843]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"},
+ {"43543.503206018519", "[$-7843]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"},
+ {"43543.503206018519", "[$-7843]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448\u0430\u043D\u0431\u0430"},
+ {"44562.189571759256", "[$-843]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-843]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-843]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 \u0422\u041E"},
+ {"44562.189571759256", "[$-843]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"},
+ {"43543.503206018519", "[$-843]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 \u0422\u041A"},
+ {"43543.503206018519", "[$-843]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"},
+ {"43543.503206018519", "[$-843]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"},
+ {"43543.503206018519", "[$-843]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448\u0430\u043D\u0431\u0430"},
+ {"44562.189571759256", "[$-43]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-43]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-43]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-43]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"43543.503206018519", "[$-43]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"},
+ {"43543.503206018519", "[$-43]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-43]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-43]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"},
+ {"44562.189571759256", "[$-7C43]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-7C43]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-7C43]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-7C43]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"43543.503206018519", "[$-7C43]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"},
+ {"43543.503206018519", "[$-7C43]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-7C43]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-7C43]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"},
+ {"44562.189571759256", "[$-443]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-443]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-443]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"},
+ {"44562.189571759256", "[$-443]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"},
+ {"43543.503206018519", "[$-443]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"},
+ {"43543.503206018519", "[$-443]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-443]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"},
+ {"43543.503206018519", "[$-443]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"},
+ {"44562.189571759256", "[$-803]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-803]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-803]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."},
+ {"44562.189571759256", "[$-803]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."},
+ {"43543.503206018519", "[$-803]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."},
+ {"43543.503206018519", "[$-803]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-803]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."},
+ {"43543.503206018519", "[$-803]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"},
+ {"44562.189571759256", "[$-33]mmm dd yyyy h:mm AM/PM", "Pha 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-33]mmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-33]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-33]mmmmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-33]mmm dd yyyy h:mm AM/PM", "Ṱhf 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-33]mmmm dd yyyy h:mm AM/PM aaa", "Ṱhafamuhwe 19 2019 12:04 PM Vhi"},
+ {"43543.503206018519", "[$-33]mmmmm dd yyyy h:mm AM/PM ddd", "Ṱ 19 2019 12:04 PM Vhi"},
+ {"43543.503206018519", "[$-33]mmmmmm dd yyyy h:mm AM/PM dddd", "Ṱhafamuhwe 19 2019 12:04 PM Ḽavhuvhili"},
+ {"44562.189571759256", "[$-433]mmm dd yyyy h:mm AM/PM", "Pha 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-433]mmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-433]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-433]mmmmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"},
+ {"43543.503206018519", "[$-433]mmm dd yyyy h:mm AM/PM", "Ṱhf 19 2019 12:04 PM"},
+ {"43543.503206018519", "[$-433]mmmm dd yyyy h:mm AM/PM aaa", "Ṱhafamuhwe 19 2019 12:04 PM Vhi"},
+ {"43543.503206018519", "[$-433]mmmmm dd yyyy h:mm AM/PM ddd", "Ṱ 19 2019 12:04 PM Vhi"},
+ {"43543.503206018519", "[$-433]mmmmmm dd yyyy h:mm AM/PM dddd", "Ṱhafamuhwe 19 2019 12:04 PM Ḽavhuvhili"},
+ {"44562.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 10 01 2022 4:32 SA"},
+ {"44866.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 11 01 2022 4:32 SA"},
+ {"44896.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 12 01 2022 4:32 SA"},
+ {"44562.189571759256", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 10 01 2022 4:32 SA"},
+ {"44866.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 11 01 2022 4:32 SA"},
+ {"44896.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 12 01 2022 4:32 SA"},
+ {"44562.189571759256", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM aaa", "T 10 01 2022 4:32 SA T7"},
+ {"44866.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM ddd", "T 11 01 2022 4:32 SA T3"},
+ {"44896.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM dddd", "T 12 01 2022 4:32 SA Th\u1EE9%20N\u0103m"},
+ {"44562.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 10 01 2022 4:32 SA"},
+ {"44866.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 11 01 2022 4:32 SA"},
+ {"44896.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 12 01 2022 4:32 SA"},
+ {"44562.189571759256", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 10 01 2022 4:32 SA"},
+ {"44866.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 11 01 2022 4:32 SA"},
+ {"44896.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 12 01 2022 4:32 SA"},
+ {"44562.189571759256", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 1 01 2022 4:32 SA"},
+ {"44593.189571759256", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 2 01 2022 4:32 SA"},
+ {"44621.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 3 01 2022 4:32 SA"},
+ {"44652.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 4 01 2022 4:32 SA"},
+ {"44682.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 5 01 2022 4:32 SA"},
+ {"44713.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 6 01 2022 4:32 SA"},
+ {"44743.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"},
+ {"44774.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"},
+ {"44805.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"},
+ {"44835.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM aaa", "T 10 01 2022 4:32 SA T7"},
+ {"44866.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM ddd", "T 11 01 2022 4:32 SA T3"},
+ {"44896.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM dddd", "T 12 01 2022 4:32 SA Th\u1EE9%20N\u0103m"},
+ {"44562.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Ebr 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Meh 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Gorff 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Hyd 01 2022 4:32 yb"},
+ {"44866.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Tach 01 2022 4:32 yb"},
+ {"44896.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Rhag 01 2022 4:32 yb"},
+ {"44562.189571759256", "[$-52]mmmm dd yyyy h:mm AM/PM", "Ionawr 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-52]mmmm dd yyyy h:mm AM/PM", "Chwefror 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mawrth 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Ebrill 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mehefin 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Gorffennaf 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Hydref 01 2022 4:32 yb"},
+ {"44866.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Tachwedd 01 2022 4:32 yb"},
+ {"44896.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Rhagfyr 01 2022 4:32 yb"},
+ {"44562.189571759256", "[$-52]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-52]mmmmm dd yyyy h:mm AM/PM", "C 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM aaa", "H 01 2022 4:32 yb Sad"},
+ {"44866.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM ddd", "T 01 2022 4:32 yb Maw"},
+ {"44896.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM dddd", "R 01 2022 4:32 yb Dydd Iau"},
+ {"44562.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Ebr 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Meh 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Gorff 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Hyd 01 2022 4:32 yb"},
+ {"44866.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Tach 01 2022 4:32 yb"},
+ {"44896.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Rhag 01 2022 4:32 yb"},
+ {"44562.189571759256", "[$-452]mmmm dd yyyy h:mm AM/PM", "Ionawr 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-452]mmmm dd yyyy h:mm AM/PM", "Chwefror 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mawrth 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Ebrill 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mehefin 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Gorffennaf 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Hydref 01 2022 4:32 yb"},
+ {"44866.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Tachwedd 01 2022 4:32 yb"},
+ {"44896.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Rhagfyr 01 2022 4:32 yb"},
+ {"44562.189571759256", "[$-452]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 yb"},
+ {"44593.189571759256", "[$-452]mmmmm dd yyyy h:mm AM/PM", "C 01 2022 4:32 yb"},
+ {"44621.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44652.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 yb"},
+ {"44682.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44713.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44743.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"},
+ {"44774.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"},
+ {"44805.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"},
+ {"44835.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM aaa", "H 01 2022 4:32 yb Sad"},
+ {"44866.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM ddd", "T 01 2022 4:32 yb Maw"},
+ {"44896.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM dddd", "R 01 2022 4:32 yb Dydd Iau"},
+ {"44562.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Awr. 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Suw 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Sul. 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Sept. 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Okt. 01 2022 4:32 Sub"},
+ {"44866.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Now. 01 2022 4:32 Sub"},
+ {"44896.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Des. 01 2022 4:32 Sub"},
+ {"44562.189571759256", "[$-88]mmmm dd yyyy h:mm AM/PM", "Samwiye 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-88]mmmm dd yyyy h:mm AM/PM", "Fewriye 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Maars 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Awril 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Suwe 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Sullet 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Septàmbar 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Oktoobar 01 2022 4:32 Sub"},
+ {"44866.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Noowàmbar 01 2022 4:32 Sub"},
+ {"44896.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Desàmbar 01 2022 4:32 Sub"},
+ {"44562.189571759256", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-88]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 Sub Gaa."},
+ {"44866.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 Sub Tal."},
+ {"44896.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 Sub Alxames"},
+ {"44562.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Awr. 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Suw 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Sul. 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Sept. 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Okt. 01 2022 4:32 Sub"},
+ {"44866.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Now. 01 2022 4:32 Sub"},
+ {"44896.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Des. 01 2022 4:32 Sub"},
+ {"44562.189571759256", "[$-488]mmmm dd yyyy h:mm AM/PM", "Samwiye 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-488]mmmm dd yyyy h:mm AM/PM", "Fewriye 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Maars 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Awril 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Suwe 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Sullet 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Septàmbar 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Oktoobar 01 2022 4:32 Sub"},
+ {"44866.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Noowàmbar 01 2022 4:32 Sub"},
+ {"44896.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Desàmbar 01 2022 4:32 Sub"},
+ {"44562.189571759256", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44593.189571759256", "[$-488]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 Sub"},
+ {"44621.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"},
+ {"44652.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 Sub"},
+ {"44682.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"},
+ {"44713.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44743.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44774.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"},
+ {"44805.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"},
+ {"44835.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 Sub Gaa."},
+ {"44866.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 Sub Tal."},
+ {"44896.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 Sub Alxames"},
+ {"44562.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uEpr. 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uMey. 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uJun. 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uJul. 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uAg. 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uSep. 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uOkt. 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uNov. 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uDis. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJanuwari 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-34]mmmm dd yyyy h:mm AM/PM", "uFebuwari 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uMatshi 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uAprili 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uMeyi 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJuni 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJulayi 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uAgasti 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uSeptemba 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uOktobha 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uNovemba 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uDisemba 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM aaa", "u 01 2022 4:32 AM uMgq."},
+ {"44866.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM ddd", "u 01 2022 4:32 AM uLwesib."},
+ {"44896.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM dddd", "u 01 2022 4:32 AM Lwesine"},
+ {"44562.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uEpr. 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uMey. 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uJun. 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uJul. 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uAg. 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uSep. 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uOkt. 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uNov. 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uDis. 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJanuwari 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-434]mmmm dd yyyy h:mm AM/PM", "uFebuwari 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uMatshi 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uAprili 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uMeyi 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJuni 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJulayi 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uAgasti 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uSeptemba 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uOktobha 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uNovemba 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uDisemba 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM aaa", "u 01 2022 4:32 AM uMgq."},
+ {"44866.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM ddd", "u 01 2022 4:32 AM uLwesib."},
+ {"44896.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM dddd", "u 01 2022 4:32 AM Lwesine"},
+ {"44562.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44866.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44896.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44562.189571759256", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44866.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44896.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44562.189571759256", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2cd 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua44d 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua315 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua1d6 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua26c 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua0d8 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM aaa", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA0D8"},
+ {"44866.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM ddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA44D"},
+ {"44896.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM dddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA282\uA1D6"},
+ {"44562.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44866.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44896.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44562.189571759256", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44866.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44896.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"},
+ {"44562.189571759256", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2cd 01 2022 4:32 \ua3b8\ua111"},
+ {"44593.189571759256", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua44d 01 2022 4:32 \ua3b8\ua111"},
+ {"44621.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua315 01 2022 4:32 \ua3b8\ua111"},
+ {"44652.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua1d6 01 2022 4:32 \ua3b8\ua111"},
+ {"44682.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua26c 01 2022 4:32 \ua3b8\ua111"},
+ {"44713.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua0d8 01 2022 4:32 \ua3b8\ua111"},
+ {"44743.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"},
+ {"44774.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"},
+ {"44805.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"},
+ {"44835.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM aaa", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA0D8"},
+ {"44866.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM ddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA44D"},
+ {"44896.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM dddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA282\uA1D6"},
+ {"44562.189571759256", "[$-43D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"},
+ {"44562.189571759256", "[$-43D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"},
+ {"44562.189571759256", "[$-43D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"},
+ {"44562.189571759256", "[$-43D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"},
+ {"43543.503206018519", "[$-43D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"},
+ {"43543.503206018519", "[$-43D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-43D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D9\u05D5\u05DD%A0\u05D2"},
+ {"43543.503206018519", "[$-43D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D3\u05D9\u05E0\u05E1\u05D8\u05D9\u05E7"},
+ {"44562.189571759256", "[$-6A]mmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-6A]mmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-6A]mmmmm dd yyyy h:mm AM/PM", "\u1E62 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-6A]mmmmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"43543.503206018519", "[$-6A]mmm dd yyyy h:mm AM/PM", "\u1EB8r 19 2019 12:04 \u1ECC\u0300s%E1n"},
+ {"43543.503206018519", "[$-6A]mmmm dd yyyy h:mm AM/PM aaa", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"},
+ {"43543.503206018519", "[$-6A]mmmmm dd yyyy h:mm AM/PM ddd", "\u1EB8 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"},
+ {"43543.503206018519", "[$-6A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n \u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun"},
+ {"44562.189571759256", "[$-46A]mmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-46A]mmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-46A]mmmmm dd yyyy h:mm AM/PM", "\u1E62 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"44562.189571759256", "[$-46A]mmmmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"},
+ {"43543.503206018519", "[$-46A]mmm dd yyyy h:mm AM/PM", "\u1EB8r 19 2019 12:04 \u1ECC\u0300s%E1n"},
+ {"43543.503206018519", "[$-46A]mmmm dd yyyy h:mm AM/PM aaa", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"},
+ {"43543.503206018519", "[$-46A]mmmmm dd yyyy h:mm AM/PM ddd", "\u1EB8 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"},
+ {"43543.503206018519", "[$-46A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n \u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun"},
+ {"44562.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Eph 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Mey 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Jun 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Jul 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Agas 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Sep 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Okt 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Nov 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Dis 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-35]mmmm dd yyyy h:mm AM/PM", "Januwari 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-35]mmmm dd yyyy h:mm AM/PM", "Febhuwari 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Mashi 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Ephreli 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Meyi 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Juni 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Julayi 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Agasti 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Septemba 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Okthoba 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Novemba 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Disemba 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-35]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 AM Mgq."},
+ {"44866.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 AM Bi."},
+ {"44896.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 AM ULwesine"},
+ {"44562.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Eph 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Mey 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Jun 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Jul 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Agas 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Sep 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Okt 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Nov 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Dis 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-435]mmmm dd yyyy h:mm AM/PM", "Januwari 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-435]mmmm dd yyyy h:mm AM/PM", "Febhuwari 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Mashi 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Ephreli 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Meyi 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Juni 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Julayi 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Agasti 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Septemba 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Okthoba 01 2022 4:32 AM"},
+ {"44866.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Novemba 01 2022 4:32 AM"},
+ {"44896.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Disemba 01 2022 4:32 AM"},
+ {"44562.189571759256", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44593.189571759256", "[$-435]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"},
+ {"44621.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44652.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 AM"},
+ {"44682.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"},
+ {"44713.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44743.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"},
+ {"44774.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"},
+ {"44805.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"},
+ {"44835.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 AM Mgq."},
+ {"44866.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 AM Bi."},
+ {"44896.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 AM ULwesine"},
+ {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "Tuesday, March 19, 2019"},
+ {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37 PM"},
+ {"text_", "General", "text_"},
+ {"text_", "\"=====\"@@@\"--\"@\"----\"", "=====text_text_text_--text_----"},
+ {"0.0450685976001E+21", "0_);[Red]\\(0\\)", "45068597600100000000 "},
+ {"8.0450685976001E+21", "0_);[Red]\\(0\\)", "8045068597600100000000 "},
+ {"8.0450685976001E-21", "0_);[Red]\\(0\\)", "0 "},
+ {"8.04506", "0_);[Red]\\(0\\)", "8 "},
+ {"-0.0450685976001E+21", "0_);[Red]\\(0\\)", "(45068597600100000000)"},
+ {"-8.0450685976001E+21", "0_);[Red]\\(0\\)", "(8045068597600100000000)"},
+ {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"},
+ {"-8.04506", "0_);[Red]\\(0\\)", "(8)"},
+ {"-8.04506", "$#,##0.00_);[Red]($#,##0.00)", "($8.05)"},
+ {"43543.5448726851", `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, " $43,543.54 "},
+ {"1234.5678", "0", "1235"},
+ {"1234.125", "0.00", "1234.13"},
+ {"1234.5678", "0.00", "1234.57"},
+ {"1234.5678", "#,##0", "1,235"},
+ {"1234.5678", "#,##0.00", "1,234.57"},
+ {"1234.5678", "0%", "123457%"},
+ {"1234.5678", "#,##0 ;(#,##0)", "1,235 "},
+ {"1234.5678", "#,##0 ;[red](#,##0)", "1,235 "},
+ {"1234.5678", "#,##0.00;(#,##0.00)", "1,234.57"},
+ {"1234.5678", "#,##0.00;[red](#,##0.00)", "1,234.57"},
+ {"1234.5678", "#", "1235"},
+ {"1234.5678", "#0", "1235"},
+ {"1234.5678", "##", "1235"},
+ {"1234.5678", "00000.00#", "01234.568"},
+ {"1234.5678", "00000####", "000001235"},
+ {"1234.5678", "00000######", "000001235"},
+ {"-1234.5678", "0.00", "-1234.57"},
+ {"-1234.5678", "0.00;-0.00", "-1234.57"},
+ {"-1234.5678", "0.00%%", "-12345678.00%%"},
+ {"2.1", "mmss.0000", "2400.000"},
+ {"0.007", "[h]:mm:ss.0", "0:10:04.8"},
+ {"0.007", "[h]:mm:ss.00", "0:10:04.80"},
+ {"0.007", "[h]:mm:ss.000", "0:10:04.800"},
+ {"0.007", "[h]:mm:ss.0000", "0:10:04.800"},
+ {"0.3270833333", "[h]:mm", "7:51"},
+ {"0.5347222222", "[h]:mm", "12:50"},
+ {"0.5833333333", "[h]:mm", "14:00"},
+ {"0.5833333333", "hh", "14"},
+ {"123", "[h]:mm,:ss.0", "2952:00,:00.0"},
+ {"123", "yy-.dd", "00-.02"},
+ {"123", "[DBNum1][$-804]yyyy\"年\"m\"月\";@", "\u4e00\u4e5d\u25cb\u25cb\u5e74\u4e94\u6708"},
+ {"123", "[DBNum2][$-804]yyyy\"年\"m\"月\";@", "\u58f9\u7396\u96f6\u96f6\u5e74\u4f0d\u6708"},
+ {"123", "[DBNum3][$-804]yyyy\"年\"m\"月\";@", "\uff11\uff19\uff10\uff10\u5e74\uff15\u6708"},
+ {"1234567890", "[DBNum1][$-804]0.00", "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u25cb.\u25cb\u25cb"},
+ {"1234567890", "[DBNum2][$-804]0.00", "\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u96f6.\u96f6\u96f6"},
+ {"1234567890", "[DBNum3][$-804]0.00", "\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19\uff10.\uff10\uff10"},
+ {"1234.5678", "0.00###", "1234.5678"},
+ {"1234.5678", "00000.00###", "01234.5678"},
+ {"-1234.5678", "00000.00###;;", ""},
+ {"1234.5678", "0.00000", "1234.56780"},
+ {"8.8888666665555487", "0.00000", "8.88887"},
+ {"8.8888666665555493e+19", "#,000.00", "88,888,666,665,555,500,000.00"},
+ {"8.8888666665555493e+19", "0.00000", "88888666665555500000.00000"},
+ {"37947.7500001", "0.00000000E+00", "3.79477500E+04"},
+ {"2312312321.1231198", "0.00E+00", "2.31E+09"},
+ {"3.2234623764278598E+33", "0.00E+00", "3.22E+33"},
+ {"1.234E-16", "0.00000000000000000000", "0.00000000000000012340"},
+ {"1.234E-16", "0.000000000000000000", "0.000000000000000123"},
+ {"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"},
+ {"1.234E-16", "0.000000000000000000%%%%", "0.000000000000012340%"},
+ {"-123.4567", "# ?/?", "-123 1/2"},
+ {"123.4567", "# ??/??", "123 37/81"},
+ {"123.4567", "#\\ ???/???", "123 58/127"},
+ {"123.4567", "#\\ ?/2", "123 1/2"},
+ {"123.4567", "#\\ ?/4", "123 2/4"},
+ {"123.4567", "#\\ ?/8", "123 4/8"},
+ {"123.4567", "#\\ ?/16", "123 7/16"},
+ {"123.4567", "#\\ ?/10", "123 5/10"},
+ {"-123.4567", "#\\ ?/100", "-123 46/100"},
+ {"123.4567", "#\\ ?/1000", "123 457/1000"},
+ {"1234.5678", "[$$-409]#,##0.00", "$1,234.57"},
+ {"123", "[$x.-unknown]#,##0.00", "x.123.00"},
+ {"123", "[$x.-unknown]MM/DD/YYYY", "x.05/02/1900"},
+ {"1234.5678", "0.0xxx00", "1234.5xxx68"},
+ {"80145.899999999994", "[$¥-8004]\" \"#\" \"####\"\"", "¥ 8 0146"},
+ {"1", "?", "1"},
+ // Unsupported number format
+ {"37947.7500001", "0.00000000E+000", "37947.7500001"},
+ {"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"},
+ // Invalid number format
+ {"123", "x0.00s", "123"},
+ {"123", "[h]:m00m:ss", "123"},
+ {"123", "yy-00dd", "123"},
+ {"123", "yy-##dd", "123"},
+ {"123", "xx[h]:mm,:ss.0xx", "xx2952:00,:00.0xx"},
+ {"-123", "x0.00s", "-123"},
+ {"-1234.5678", ";E+;", "-1234.5678"},
+ {"1234.5678", "E+;", "1234.5678"},
+ {"1234.5678", "00000.00###s", "1234.5678"},
+ {"-1234.5678", "00000.00###;s;", "-1234.5678"},
+ } {
+ result := format(item[0], item[1], false, CellTypeNumber, nil)
+ assert.Equal(t, item[2], result, item)
+ }
+ // Test format number with specified date and time format code
+ for _, item := range [][]string{
+ {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "2019年3月19日"},
+ {"43543.503206018519", "[$-x-sysdate]dddd, mmmm dd, yyyy", "2019年3月19日"},
+ {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"},
+ {"43543.503206018519", "[$-x-systime]h:mm:ss AM/PM", "12:04:37"},
+ } {
+ result := format(item[0], item[1], false, CellTypeNumber, &Options{
+ ShortDatePattern: "yyyy/m/d",
+ LongDatePattern: "yyyy\"年\"M\"月\"d\"日\"",
+ LongTimePattern: "H:mm:ss",
+ })
+ assert.Equal(t, item[2], result, item)
+ }
+ // Test format number with string data type cell value
+ for _, cellType := range []CellType{CellTypeSharedString, CellTypeInlineString} {
+ for _, item := range [][]string{
+ {"1234.5678", "General", "1234.5678"},
+ {"1234.5678", "yyyy\"年\"m\"月\"d\"日\";@", "1234.5678"},
+ {"1234.5678", "h\"时\"mm\"分\"ss\"秒\";@", "1234.5678"},
+ {"1234.5678", "\"¥\"#,##0.00_);\\(\"¥\"#,##0.00\\)", "1234.5678"},
+ {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"},
+ {"1234.5678", "\"text\"@", "text1234.5678"},
+ } {
+ result := format(item[0], item[1], false, cellType, nil)
+ assert.Equal(t, item[2], result, item)
+ }
+ }
+ nf := numberFormat{}
+ changeNumFmtCode, err := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}})
+ assert.Equal(t, ErrUnsupportedNumberFormat, err)
+ assert.False(t, changeNumFmtCode)
+}
diff --git a/picture.go b/picture.go
index 468cccdd81..de0d555870 100644
--- a/picture.go
+++ b/picture.go
@@ -1,25 +1,21 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
- "encoding/json"
"encoding/xml"
- "errors"
- "fmt"
"image"
"io"
- "io/ioutil"
"os"
"path"
"path/filepath"
@@ -27,177 +23,284 @@ import (
"strings"
)
-// parseFormatPictureSet provides a function to parse the format settings of
+// PictureInsertType defines the type of the picture has been inserted into the
+// worksheet.
+type PictureInsertType byte
+
+// Insert picture types.
+const (
+ PictureInsertTypePlaceOverCells PictureInsertType = iota
+ PictureInsertTypePlaceInCell
+ PictureInsertTypeIMAGE
+ PictureInsertTypeDISPIMG
+)
+
+// parseGraphicOptions provides a function to parse the format settings of
// the picture with default value.
-func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
- format := formatPicture{
- FPrintsWithSheet: true,
- FLocksWithSheet: false,
- NoChangeAspect: false,
- Autofit: false,
- OffsetX: 0,
- OffsetY: 0,
- XScale: 1.0,
- YScale: 1.0,
- }
- err := json.Unmarshal(parseFormatSet(formatSet), &format)
- return &format, err
+func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
+ if opts == nil {
+ return &GraphicOptions{
+ PrintObject: boolPtr(true),
+ Locked: boolPtr(true),
+ ScaleX: defaultDrawingScale,
+ ScaleY: defaultDrawingScale,
+ }
+ }
+ if opts.PrintObject == nil {
+ opts.PrintObject = boolPtr(true)
+ }
+ if opts.Locked == nil {
+ opts.Locked = boolPtr(true)
+ }
+ if opts.ScaleX == 0 {
+ opts.ScaleX = defaultDrawingScale
+ }
+ if opts.ScaleY == 0 {
+ opts.ScaleY = defaultDrawingScale
+ }
+ return opts
}
// AddPicture provides the method to add picture in a sheet by given picture
// format set (such as offset, scale, aspect ratio setting and print settings)
-// and file path. For example:
+// and file path, supported image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG,
+// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency-safe. Note that
+// this function only supports adding pictures placed over the cells currently,
+// and doesn't support adding pictures placed in cells or creating the Kingsoft
+// WPS Office embedded image cells. For example:
+//
+// package main
+//
+// import (
+// "fmt"
+// _ "image/gif"
+// _ "image/jpeg"
+// _ "image/png"
+//
+// "github.com/xuri/excelize/v2"
+// )
+//
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// // Insert a picture.
+// if err := f.AddPicture("Sheet1", "A2", "image.jpg", nil); err != nil {
+// fmt.Println(err)
+// return
+// }
+// // Insert a picture scaling in the cell with location hyperlink.
+// enable := true
+// if err := f.AddPicture("Sheet1", "D2", "image.png",
+// &excelize.GraphicOptions{
+// ScaleX: 0.5,
+// ScaleY: 0.5,
+// Hyperlink: "#Sheet2!D8",
+// HyperlinkType: "Location",
+// },
+// ); err != nil {
+// fmt.Println(err)
+// return
+// }
+// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
+// if err := f.AddPicture("Sheet1", "H2", "image.gif",
+// &excelize.GraphicOptions{
+// PrintObject: &enable,
+// LockAspectRatio: false,
+// OffsetX: 15,
+// OffsetY: 10,
+// Hyperlink: "https://github.com/xuri/excelize",
+// HyperlinkType: "External",
+// Positioning: "oneCell",
+// },
+// ); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
+//
+// The optional parameter "AltText" is used to add alternative text to a graph
+// object.
+//
+// The optional parameter "PrintObject" indicates whether the graph object is
+// printed when the worksheet is printed, the default value of that is 'true'.
//
-// package main
+// The optional parameter "Locked" indicates whether lock the graph object.
+// Locking an object has no effect unless the sheet is protected.
//
-// import (
-// _ "image/gif"
-// _ "image/jpeg"
-// _ "image/png"
+// The optional parameter "LockAspectRatio" indicates whether lock aspect ratio
+// for the graph object, the default value of that is 'false'.
//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
+// The optional parameter "AutoFit" specifies if you make graph object size
+// auto-fits the cell, the default value of that is 'false'.
//
-// func main() {
-// f := excelize.NewFile()
-// // Insert a picture.
-// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil {
-// fmt.Println(err)
-// }
-// // Insert a picture scaling in the cell with location hyperlink.
-// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil {
-// fmt.Println(err)
-// }
-// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
-// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil {
-// fmt.Println(err)
-// }
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
+// The optional parameter "AutoFitIgnoreAspect" specifies if fill the cell with
+// the image and ignore its aspect ratio, the default value of that is 'false'.
+// This option only works when the "AutoFit" is enabled.
//
-// LinkType defines two types of hyperlink "External" for web site or
-// "Location" for moving to one of cell in this workbook. When the
-// "hyperlink_type" is "Location", coordinates need to start with "#".
+// The optional parameter "OffsetX" specifies the horizontal offset of the graph
+// object with the cell, the default value of that is 0.
//
-// Positioning defines two types of the position of a picture in an Excel
-// spreadsheet, "oneCell" (Move but don't size with cells) or "absolute"
-// (Don't move or size with cells). If you don't set this parameter, default
-// positioning is move and size with cells.
-func (f *File) AddPicture(sheet, cell, picture, format string) error {
+// The optional parameter "OffsetY" specifies the vertical offset of the graph
+// object with the cell, the default value of that is 0.
+//
+// The optional parameter "ScaleX" specifies the horizontal scale of graph
+// object, the default value of that is 1.0 which presents 100%.
+//
+// The optional parameter "ScaleY" specifies the vertical scale of graph object,
+// the default value of that is 1.0 which presents 100%.
+//
+// The optional parameter "Hyperlink" specifies the hyperlink of the graph
+// object.
+//
+// The optional parameter "HyperlinkType" defines two types of
+// hyperlink "External" for website or "Location" for moving to one of the
+// cells in this workbook. When the "HyperlinkType" is "Location",
+// coordinates need to start with "#".
+//
+// The optional parameter "Positioning" defines 3 types of the position of a
+// graph object in a spreadsheet: "oneCell" (Move but don't size with
+// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or
+// size with cells). If you don't set this parameter, the default positioning
+// is to move and size with cells.
+func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error {
var err error
// Check picture exists first.
- if _, err = os.Stat(picture); os.IsNotExist(err) {
+ if _, err = os.Stat(name); os.IsNotExist(err) {
return err
}
- ext, ok := supportImageTypes[path.Ext(picture)]
+ ext, ok := supportedImageTypes[strings.ToLower(path.Ext(name))]
if !ok {
- return errors.New("unsupported image extension")
+ return ErrImgExt
}
- file, _ := ioutil.ReadFile(picture)
- _, name := filepath.Split(picture)
- return f.AddPictureFromBytes(sheet, cell, format, name, ext, file)
+ file, _ := os.ReadFile(filepath.Clean(name))
+ return f.AddPictureFromBytes(sheet, cell, &Picture{Extension: ext, File: file, Format: opts})
}
// AddPictureFromBytes provides the method to add picture in a sheet by given
// picture format set (such as offset, scale, aspect ratio setting and print
-// settings), file base name, extension name and file bytes. For example:
-//
-// package main
-//
-// import (
-// "fmt"
-// _ "image/jpeg"
-// "io/ioutil"
+// settings), file base name, extension name and file bytes, supported image
+// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. Note that
+// this function only supports adding pictures placed over the cells currently,
+// and doesn't support adding pictures placed in cells or creating the Kingsoft
+// WPS Office embedded image cells. For example:
//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
+// package main
//
-// func main() {
-// f := excelize.NewFile()
+// import (
+// "fmt"
+// _ "image/jpeg"
+// "os"
//
-// file, err := ioutil.ReadFile("image.jpg")
-// if err != nil {
-// fmt.Println(err)
-// }
-// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil {
-// fmt.Println(err)
-// }
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
+// "github.com/xuri/excelize/v2"
+// )
//
-func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error {
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// file, err := os.ReadFile("image.jpg")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.AddPictureFromBytes("Sheet1", "A2", &excelize.Picture{
+// Extension: ".jpg",
+// File: file,
+// Format: &excelize.GraphicOptions{AltText: "Excel Logo"},
+// }); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
+func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
var drawingHyperlinkRID int
var hyperlinkType string
- ext, ok := supportImageTypes[extension]
+ ext, ok := supportedImageTypes[strings.ToLower(pic.Extension)]
if !ok {
- return errors.New("unsupported image extension")
+ return ErrImgExt
}
- formatSet, err := parseFormatPictureSet(format)
- if err != nil {
- return err
+ if pic.InsertType != PictureInsertTypePlaceOverCells {
+ return ErrParameterInvalid
}
- img, _, err := image.DecodeConfig(bytes.NewReader(file))
+ options := parseGraphicOptions(pic.Format)
+ img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
if err != nil {
return err
}
- // Read sheet data.
- xlsx, err := f.workSheetReader(sheet)
+ // Read sheet data
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return err
}
+ f.mu.Unlock()
+ ws.mu.Lock()
// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
drawingID := f.countDrawings() + 1
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
- drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML)
+ drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
- mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl")
- drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType)
+ mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl")
+ var drawingRID int
+ if rels, _ := f.relsReader(drawingRels); rels != nil {
+ for _, rel := range rels.Relationships {
+ if rel.Type == SourceRelationshipImage && rel.Target == mediaStr {
+ drawingRID, _ = strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
+ break
+ }
+ }
+ }
+ if drawingRID == 0 {
+ drawingRID = f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType)
+ }
// Add picture with hyperlink.
- if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" {
- if formatSet.HyperlinkType == "External" {
- hyperlinkType = formatSet.HyperlinkType
+ if options.Hyperlink != "" && options.HyperlinkType != "" {
+ if options.HyperlinkType == "External" {
+ hyperlinkType = options.HyperlinkType
}
- drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType)
+ drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType)
}
- err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet)
+ ws.mu.Unlock()
+ err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options)
if err != nil {
return err
}
- f.addContentTypePart(drawingID, "drawings")
+ if err = f.addContentTypePart(drawingID, "drawings"); err != nil {
+ return err
+ }
f.addSheetNameSpace(sheet, SourceRelationship)
return err
}
-// deleteSheetRelationships provides a function to delete relationships in
-// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
-// relationship index.
-func (f *File) deleteSheetRelationships(sheet, rID string) {
- name, ok := f.sheetMap[trimSheetName(sheet)]
- if !ok {
- name = strings.ToLower(sheet) + ".xml"
- }
- var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
- sheetRels := f.relsReader(rels)
- if sheetRels == nil {
- sheetRels = &xlsxRelationships{}
- }
- for k, v := range sheetRels.Relationships {
- if v.ID == rID {
- sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
- }
- }
- f.Relationships[rels] = sheetRels
-}
-
// addSheetLegacyDrawing provides a function to add legacy drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
- xlsx, _ := f.workSheetReader(sheet)
- xlsx.LegacyDrawing = &xlsxLegacyDrawing{
+ ws, _ := f.workSheetReader(sheet)
+ ws.LegacyDrawing = &xlsxLegacyDrawing{
+ RID: "rId" + strconv.Itoa(rID),
+ }
+}
+
+// addSheetLegacyDrawingHF provides a function to add legacy drawing
+// header/footer element to xl/worksheets/sheet%d.xml by given
+// worksheet name and relationship index.
+func (f *File) addSheetLegacyDrawingHF(sheet string, rID int) {
+ ws, _ := f.workSheetReader(sheet)
+ ws.LegacyDrawingHF = &xlsxLegacyDrawingHF{
RID: "rId" + strconv.Itoa(rID),
}
}
@@ -205,67 +308,76 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
// addSheetDrawing provides a function to add drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetDrawing(sheet string, rID int) {
- xlsx, _ := f.workSheetReader(sheet)
- xlsx.Drawing = &xlsxDrawing{
+ ws, _ := f.workSheetReader(sheet)
+ ws.Drawing = &xlsxDrawing{
RID: "rId" + strconv.Itoa(rID),
}
}
// addSheetPicture provides a function to add picture element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
-func (f *File) addSheetPicture(sheet string, rID int) {
- xlsx, _ := f.workSheetReader(sheet)
- xlsx.Picture = &xlsxPicture{
+func (f *File) addSheetPicture(sheet string, rID int) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ ws.Picture = &xlsxPicture{
RID: "rId" + strconv.Itoa(rID),
}
+ return err
}
// countDrawings provides a function to get drawing files count storage in the
// folder xl/drawings.
func (f *File) countDrawings() int {
- c1, c2 := 0, 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/drawings/drawing") {
- c1++
+ drawings := map[string]struct{}{}
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/drawings/drawing") {
+ drawings[k.(string)] = struct{}{}
}
- }
- for rel := range f.Drawings {
- if strings.Contains(rel, "xl/drawings/drawing") {
- c2++
+ return true
+ })
+ f.Drawings.Range(func(rel, value interface{}) bool {
+ if strings.Contains(rel.(string), "xl/drawings/drawing") {
+ drawings[rel.(string)] = struct{}{}
}
- }
- if c1 < c2 {
- return c2
- }
- return c1
+ return true
+ })
+ return len(drawings)
}
// addDrawingPicture provides a function to add picture by given sheet,
// drawingXML, cell, file name, width, height relationship index and format
// sets.
-func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) error {
+func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyperlinkRID int, img image.Config, opts *GraphicOptions) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
- if formatSet.Autofit {
- width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), formatSet)
- if err != nil {
+ if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 {
+ return newInvalidOptionalValue("Positioning", opts.Positioning, supportedPositioning)
+ }
+ width, height := img.Width, img.Height
+ if opts.AutoFit {
+ if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
return err
}
+ } else {
+ width = int(float64(width) * opts.ScaleX)
+ height = int(float64(height) * opts.ScaleY)
+ }
+ colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
+ content, cNvPrID, err := f.drawingParser(drawingXML)
+ if err != nil {
+ return err
}
- col--
- row--
- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 :=
- f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height)
- content, cNvPrID := f.drawingParser(drawingXML)
twoCellAnchor := xdrCellAnchor{}
- twoCellAnchor.EditAs = formatSet.Positioning
+ twoCellAnchor.EditAs = opts.Positioning
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = formatSet.OffsetX * EMU
+ from.ColOff = opts.OffsetX * EMU
from.Row = rowStart
- from.RowOff = formatSet.OffsetY * EMU
+ from.RowOff = opts.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
@@ -274,9 +386,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
twoCellAnchor.From = &from
twoCellAnchor.To = &to
pic := xlsxPic{}
- pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
+ pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = opts.LockAspectRatio
pic.NvPicPr.CNvPr.ID = cNvPrID
- pic.NvPicPr.CNvPr.Descr = file
+ pic.NvPicPr.CNvPr.Descr = opts.AltText
pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID)
if hyperlinkRID != 0 {
pic.NvPicPr.CNvPr.HlinkClick = &xlsxHlinkClick{
@@ -286,15 +398,30 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
}
pic.BlipFill.Blip.R = SourceRelationship.Value
pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID)
+ if ext == ".svg" {
+ pic.BlipFill.Blip.ExtList = &xlsxEGOfficeArtExtensionList{
+ Ext: []xlsxCTOfficeArtExtension{
+ {
+ URI: ExtURISVG,
+ SVGBlip: xlsxCTSVGBlip{
+ XMLNSaAVG: NameSpaceDrawing2016SVG.Value,
+ Embed: pic.BlipFill.Blip.Embed,
+ },
+ },
+ },
+ }
+ }
pic.SpPr.PrstGeom.Prst = "rect"
twoCellAnchor.Pic = &pic
twoCellAnchor.ClientData = &xdrClientData{
- FLocksWithSheet: formatSet.FLocksWithSheet,
- FPrintsWithSheet: formatSet.FPrintsWithSheet,
+ FLocksWithSheet: *opts.Locked,
+ FPrintsWithSheet: *opts.PrintObject,
}
+ content.mu.Lock()
+ defer content.mu.Unlock()
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
- f.Drawings[drawingXML] = content
+ f.Drawings.Store(drawingXML, content)
return err
}
@@ -302,11 +429,12 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
// folder xl/media/image.
func (f *File) countMedia() int {
count := 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/media/image") {
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/media/image") {
count++
}
- }
+ return true
+ })
return count
}
@@ -315,259 +443,263 @@ func (f *File) countMedia() int {
// and drawings that use it will reference the same image.
func (f *File) addMedia(file []byte, ext string) string {
count := f.countMedia()
- for name, existing := range f.XLSX {
- if !strings.HasPrefix(name, "xl/media/image") {
- continue
+ var name string
+ f.Pkg.Range(func(k, existing interface{}) bool {
+ if !strings.HasPrefix(k.(string), "xl/media/image") {
+ return true
}
- if bytes.Equal(file, existing) {
- return name
+ if bytes.Equal(file, existing.([]byte)) {
+ name = k.(string)
+ return false
}
+ return true
+ })
+ if name != "" {
+ return name
}
media := "xl/media/image" + strconv.Itoa(count+1) + ext
- f.XLSX[media] = file
+ f.Pkg.Store(media, file)
return media
}
-// setContentTypePartImageExtensions provides a function to set the content
-// type for relationship parts and the Main Document part.
-func (f *File) setContentTypePartImageExtensions() {
- var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false}
- content := f.contentTypesReader()
- for _, v := range content.Defaults {
- _, ok := imageTypes[v.Extension]
- if ok {
- imageTypes[v.Extension] = true
- }
+// GetPictures provides a function to get picture meta info and raw content
+// embed in spreadsheet by given worksheet and cell name. This function
+// returns the image contents as []byte data types. This function is
+// concurrency safe. For example:
+//
+// f, err := excelize.OpenFile("Book1.xlsx")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// pics, err := f.GetPictures("Sheet1", "A2")
+// if err != nil {
+// fmt.Println(err)
+// }
+// for idx, pic := range pics {
+// name := fmt.Sprintf("image%d%s", idx+1, pic.Extension)
+// if err := os.WriteFile(name, pic.File, 0644); err != nil {
+// fmt.Println(err)
+// }
+// }
+func (f *File) GetPictures(sheet, cell string) ([]Picture, error) {
+ col, row, err := CellNameToCoordinates(cell)
+ if err != nil {
+ return nil, err
}
- for k, v := range imageTypes {
- if !v {
- content.Defaults = append(content.Defaults, xlsxDefault{
- Extension: k,
- ContentType: "image/" + k,
- })
- }
+ col--
+ row--
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ f.mu.Unlock()
+ return nil, err
}
-}
-
-// setContentTypePartVMLExtensions provides a function to set the content type
-// for relationship parts and the Main Document part.
-func (f *File) setContentTypePartVMLExtensions() {
- vml := false
- content := f.contentTypesReader()
- for _, v := range content.Defaults {
- if v.Extension == "vml" {
- vml = true
- }
+ f.mu.Unlock()
+ if ws.Drawing == nil {
+ return f.getCellImages(sheet, cell)
+ }
+ target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
+ drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
+ drawingRelationships := strings.ReplaceAll(
+ strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
+ imgs, err := f.getCellImages(sheet, cell)
+ if err != nil {
+ return nil, err
}
- if !vml {
- content.Defaults = append(content.Defaults, xlsxDefault{
- Extension: "vml",
- ContentType: ContentTypeVML,
- })
+ pics, err := f.getPicture(row, col, drawingXML, drawingRelationships)
+ if err != nil {
+ return nil, err
}
+ return append(imgs, pics...), err
}
-// addContentTypePart provides a function to add content type part
-// relationships in the file [Content_Types].xml by given index.
-func (f *File) addContentTypePart(index int, contentType string) {
- setContentType := map[string]func(){
- "comments": f.setContentTypePartVMLExtensions,
- "drawings": f.setContentTypePartImageExtensions,
- }
- partNames := map[string]string{
- "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
- "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
- "comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
- "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
- "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
- "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
- "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
- "sharedStrings": "/xl/sharedStrings.xml",
- }
- contentTypes := map[string]string{
- "chart": ContentTypeDrawingML,
- "chartsheet": ContentTypeSpreadSheetMLChartsheet,
- "comments": ContentTypeSpreadSheetMLComments,
- "drawings": ContentTypeDrawing,
- "table": ContentTypeSpreadSheetMLTable,
- "pivotTable": ContentTypeSpreadSheetMLPivotTable,
- "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
- "sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
- }
- s, ok := setContentType[contentType]
- if ok {
- s()
- }
- content := f.contentTypesReader()
- for _, v := range content.Overrides {
- if v.PartName == partNames[contentType] {
- return
- }
- }
- content.Overrides = append(content.Overrides, xlsxOverride{
- PartName: partNames[contentType],
- ContentType: contentTypes[contentType],
- })
-}
-
-// getSheetRelationshipsTargetByID provides a function to get Target attribute
-// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
-// relationship index.
-func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
- name, ok := f.sheetMap[trimSheetName(sheet)]
- if !ok {
- name = strings.ToLower(sheet) + ".xml"
- }
- var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
- sheetRels := f.relsReader(rels)
- if sheetRels == nil {
- sheetRels = &xlsxRelationships{}
+// GetPictureCells returns all picture cell references in a worksheet by a
+// specific worksheet name.
+func (f *File) GetPictureCells(sheet string) ([]string, error) {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ f.mu.Unlock()
+ return nil, err
}
- for _, v := range sheetRels.Relationships {
- if v.ID == rID {
- return v.Target
- }
+ f.mu.Unlock()
+ if ws.Drawing == nil {
+ return f.getImageCells(sheet)
}
- return ""
-}
+ target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
+ drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
+ drawingRelationships := strings.ReplaceAll(
+ strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
-// GetPicture provides a function to get picture base name and raw content
-// embed in XLSX by given worksheet and cell name. This function returns the
-// file name in XLSX and file contents as []byte data types. For example:
-//
-// f, err := excelize.OpenFile("Book1.xlsx")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// file, raw, err := f.GetPicture("Sheet1", "A2")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// if err := ioutil.WriteFile(file, raw, 0644); err != nil {
-// fmt.Println(err)
-// }
-//
-func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
- col, row, err := CellNameToCoordinates(cell)
+ embeddedImageCells, err := f.getImageCells(sheet)
if err != nil {
- return "", nil, err
+ return nil, err
}
- col--
- row--
- xlsx, err := f.workSheetReader(sheet)
+ imageCells, err := f.getPictureCells(drawingXML, drawingRelationships)
if err != nil {
- return "", nil, err
- }
- if xlsx.Drawing == nil {
- return "", nil, err
+ return nil, err
}
- target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
- drawingXML := strings.Replace(target, "..", "xl", -1)
- _, ok := f.XLSX[drawingXML]
- if !ok {
- return "", nil, err
- }
- drawingRelationships := strings.Replace(
- strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1)
-
- return f.getPicture(row, col, drawingXML, drawingRelationships)
+ return append(embeddedImageCells, imageCells...), err
}
-// DeletePicture provides a function to delete charts in spreadsheet by given
-// worksheet and cell name. Note that the image file won't be deleted from the
-// document currently.
-func (f *File) DeletePicture(sheet, cell string) (err error) {
+// DeletePicture provides a function to delete all pictures in a cell by given
+// worksheet name and cell reference.
+func (f *File) DeletePicture(sheet, cell string) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
- return
+ return err
}
col--
row--
ws, err := f.workSheetReader(sheet)
if err != nil {
- return
+ return err
}
if ws.Drawing == nil {
- return
+ return err
+ }
+ drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
+ drawingRels := "xl/drawings/_rels/" + filepath.Base(drawingXML) + ".rels"
+ rIDs, err := f.deleteDrawing(col, row, drawingXML, "Pic")
+ if err != nil {
+ return err
+ }
+ for _, rID := range rIDs {
+ rels := f.getDrawingRelationships(drawingRels, rID)
+ if rels == nil {
+ return err
+ }
+ var used bool
+ checkPicRef := func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/drawings/_rels/drawing") {
+ if k.(string) == drawingRels {
+ return true
+ }
+ r, err := f.relsReader(k.(string))
+ if err != nil {
+ return true
+ }
+ for _, rel := range r.Relationships {
+ if rel.Type == SourceRelationshipImage &&
+ filepath.Base(rel.Target) == filepath.Base(rels.Target) {
+ used = true
+ }
+ }
+ }
+ return true
+ }
+ f.Relationships.Range(checkPicRef)
+ f.Pkg.Range(checkPicRef)
+ if !used {
+ f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1))
+ }
+ f.deleteDrawingRels(drawingRels, rID)
}
- drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1)
- return f.deleteDrawing(col, row, drawingXML, "Pic")
+ return err
}
// getPicture provides a function to get picture base name and raw content
// embed in spreadsheet by given coordinates and drawing relationships.
-func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) {
- var (
- wsDr *xlsxWsDr
- ok bool
- deWsDr *decodeWsDr
- drawRel *xlsxRelationship
- deTwoCellAnchor *decodeTwoCellAnchor
- )
-
- wsDr, _ = f.drawingParser(drawingXML)
- if ret, buf = f.getPictureFromWsDr(row, col, drawingRelationships, wsDr); len(buf) > 0 {
+func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) {
+ var wsDr *xlsxWsDr
+ if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return
}
- deWsDr = new(decodeWsDr)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))).
- Decode(deWsDr); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
- return
+ wsDr.mu.Lock()
+ defer wsDr.mu.Unlock()
+ cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row }
+ cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row }
+ cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
+ pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
+ if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil {
+ pic.File = buffer.([]byte)
+ pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
+ pics = append(pics, pic)
+ }
}
- err = nil
- for _, anchor := range deWsDr.TwoCellAnchor {
- deTwoCellAnchor = new(decodeTwoCellAnchor)
- if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")).
- Decode(deTwoCellAnchor); err != nil && err != io.EOF {
- err = fmt.Errorf("xml decode error: %s", err)
- return
- }
- if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil {
- if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
- drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed)
- if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
- ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
- return
- }
- }
+ cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
+ var target string
+ if strings.HasPrefix(r.Target, "/") {
+ target = strings.TrimPrefix(r.Target, "/")
+ } else {
+ target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
+ }
+
+ pic := Picture{Extension: filepath.Ext(target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
+ if buffer, _ := f.Pkg.Load(target); buffer != nil {
+ pic.File = buffer.([]byte)
+ pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
+ pics = append(pics, pic)
}
}
+ for _, anchor := range wsDr.TwoCellAnchor {
+ f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
+ }
+ for _, anchor := range wsDr.OneCellAnchor {
+ f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
+ }
return
}
-// getPictureFromWsDr provides a function to get picture base name and raw
-// content in worksheet drawing by given coordinates and drawing
-// relationships.
-func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (ret string, buf []byte) {
- var (
- ok bool
- anchor *xdrCellAnchor
- drawRel *xlsxRelationship
- )
- for _, anchor = range wsDr.TwoCellAnchor {
+// extractCellAnchor extract drawing object from cell anchor by giving drawing
+// cell anchor, drawing relationships part path, conditional and callback
+// function.
+func (f *File) extractCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
+ cond func(from *xlsxFrom) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship),
+ cond2 func(from *decodeFrom) bool, cb2 func(anchor *decodeCellAnchor, rels *xlsxRelationship),
+) {
+ var drawRel *xlsxRelationship
+ if anchor.GraphicFrame == "" {
if anchor.From != nil && anchor.Pic != nil {
- if anchor.From.Col == col && anchor.From.Row == row {
+ if cond(anchor.From) {
if drawRel = f.getDrawingRelationships(drawingRelationships,
anchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
- if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
- ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
- return
+ if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
+ cb(anchor, drawRel)
}
}
}
}
+ return
+ }
+ f.extractDecodeCellAnchor(anchor, drawingRelationships, cond2, cb2)
+}
+
+// extractDecodeCellAnchor extract drawing object from cell anchor by giving
+// decoded drawing cell anchor, drawing relationships part path, conditional and
+// callback function.
+func (f *File) extractDecodeCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
+ cond func(from *decodeFrom) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship),
+) {
+ var (
+ drawRel *xlsxRelationship
+ deCellAnchor = new(decodeCellAnchor)
+ )
+ _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor)
+ if deCellAnchor.From != nil && deCellAnchor.Pic != nil {
+ if cond(deCellAnchor.From) {
+ if drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
+ if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
+ cb(deCellAnchor, drawRel)
+ }
+ }
+ }
}
- return
}
// getDrawingRelationships provides a function to get drawing relationships
// from xl/drawings/_rels/drawing%s.xml.rels by given file name and
// relationship ID.
func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
- if drawingRels := f.relsReader(rels); drawingRels != nil {
+ if drawingRels, _ := f.relsReader(rels); drawingRels != nil {
+ drawingRels.mu.Lock()
+ defer drawingRels.mu.Unlock()
for _, v := range drawingRels.Relationships {
if v.ID == rID {
return &v
@@ -580,16 +712,17 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
// drawingsWriter provides a function to save xl/drawings/drawing%d.xml after
// serialize structure.
func (f *File) drawingsWriter() {
- for path, d := range f.Drawings {
+ f.Drawings.Range(func(path, d interface{}) bool {
if d != nil {
- v, _ := xml.Marshal(d)
- f.saveFileList(path, v)
+ v, _ := xml.Marshal(d.(*xlsxWsDr))
+ f.saveFileList(path.(string), v)
}
- }
+ return true
+ })
}
// drawingResize calculate the height and width after resizing.
-func (f *File) drawingResize(sheet string, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) {
+func (f *File) drawingResize(sheet, cell string, width, height float64, opts *GraphicOptions) (w, h, c, r int, err error) {
var mergeCells []MergeCell
mergeCells, err = f.GetMergeCells(sheet)
if err != nil {
@@ -602,21 +735,21 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f
}
cellWidth, cellHeight := f.getColWidth(sheet, c), f.getRowHeight(sheet, r)
for _, mergeCell := range mergeCells {
- if inMergeCell, err = f.checkCellInArea(cell, mergeCell[0]); err != nil {
- return
- }
if inMergeCell {
- rng, _ = areaRangeToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis())
- sortCoordinates(rng)
+ continue
+ }
+ if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err == nil {
+ rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis())
+ _ = sortCoordinates(rng)
}
}
if inMergeCell {
cellWidth, cellHeight = 0, 0
c, r = rng[0], rng[1]
- for col := rng[0] - 1; col < rng[2]; col++ {
+ for col := rng[0]; col <= rng[2]; col++ {
cellWidth += f.getColWidth(sheet, col)
}
- for row := rng[1] - 1; row < rng[3]; row++ {
+ for row := rng[1]; row <= rng[3]; row++ {
cellHeight += f.getRowHeight(sheet, row)
}
}
@@ -628,7 +761,244 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f
asp := float64(cellHeight) / height
height, width = float64(cellHeight), width*asp
}
- width, height = width-float64(formatSet.OffsetX), height-float64(formatSet.OffsetY)
- w, h = int(width*formatSet.XScale), int(height*formatSet.YScale)
+ if opts.AutoFitIgnoreAspect {
+ width, height = float64(cellWidth), float64(cellHeight)
+ }
+ width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY)
+ w, h = int(width*opts.ScaleX), int(height*opts.ScaleY)
return
}
+
+// getPictureCells provides a function to get all picture cell references in a
+// worksheet by given drawing part path and drawing relationships path.
+func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]string, error) {
+ var (
+ cells []string
+ err error
+ wsDr *xlsxWsDr
+ )
+ if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
+ return cells, err
+ }
+ wsDr.mu.Lock()
+ defer wsDr.mu.Unlock()
+ cond := func(from *xlsxFrom) bool { return true }
+ cond2 := func(from *decodeFrom) bool { return true }
+ cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
+ if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok {
+ if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
+ cells = append(cells, cell)
+ }
+ }
+ }
+ cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
+ var target string
+ if strings.HasPrefix(r.Target, "/") {
+ target = strings.TrimPrefix(r.Target, "/")
+ } else {
+ target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
+ }
+
+ if _, ok := f.Pkg.Load(target); ok {
+ if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
+ cells = append(cells, cell)
+ }
+ }
+ }
+ for _, anchor := range wsDr.TwoCellAnchor {
+ f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
+ }
+ for _, anchor := range wsDr.OneCellAnchor {
+ f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
+ }
+ return cells, err
+}
+
+// cellImagesReader provides a function to get the pointer to the structure
+// after deserialization of xl/cellimages.xml.
+func (f *File) cellImagesReader() (*decodeCellImages, error) {
+ if f.DecodeCellImages == nil {
+ f.DecodeCellImages = new(decodeCellImages)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCellImages)))).
+ Decode(f.DecodeCellImages); err != nil && err != io.EOF {
+ return f.DecodeCellImages, err
+ }
+ }
+ return f.DecodeCellImages, nil
+}
+
+// getImageCells returns all the cell images and the Kingsoft WPS
+// Office embedded image cells reference by given worksheet name.
+func (f *File) getImageCells(sheet string) ([]string, error) {
+ var (
+ err error
+ cells []string
+ )
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return cells, err
+ }
+ for _, row := range ws.SheetData.Row {
+ for _, c := range row.C {
+ if c.F != nil && c.F.Content != "" &&
+ strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(c.F.Content, "="), "_xlfn."), "DISPIMG") {
+ if _, err = f.CalcCellValue(sheet, c.R); err != nil {
+ return cells, err
+ }
+ cells = append(cells, c.R)
+ }
+ r, err := f.getImageCellRel(&c, &Picture{})
+ if err != nil {
+ return cells, err
+ }
+ if r != nil {
+ cells = append(cells, c.R)
+ }
+
+ }
+ }
+ return cells, err
+}
+
+// getRichDataRichValueRel returns relationship of the cell image by given meta
+// blocks value.
+func (f *File) getRichDataRichValueRel(val string) (*xlsxRelationship, error) {
+ var r *xlsxRelationship
+ idx, err := strconv.Atoi(val)
+ if err != nil {
+ return r, err
+ }
+ richValueRel, err := f.richValueRelReader()
+ if err != nil {
+ return r, err
+ }
+ if idx >= len(richValueRel.Rels) {
+ return r, err
+ }
+ rID := richValueRel.Rels[idx].ID
+ if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
+ return nil, err
+ }
+ return r, err
+}
+
+// getRichDataWebImagesRel returns relationship of a web image by given meta
+// blocks value.
+func (f *File) getRichDataWebImagesRel(val string) (*xlsxRelationship, error) {
+ var r *xlsxRelationship
+ idx, err := strconv.Atoi(val)
+ if err != nil {
+ return r, err
+ }
+ richValueWebImages, err := f.richValueWebImageReader()
+ if err != nil {
+ return r, err
+ }
+ if idx >= len(richValueWebImages.WebImageSrd) {
+ return r, err
+ }
+ rID := richValueWebImages.WebImageSrd[idx].Blip.RID
+ if r = f.getRichValueWebImageRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
+ return nil, err
+ }
+ return r, err
+}
+
+// getImageCellRel returns the cell image relationship.
+func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error) {
+ var r *xlsxRelationship
+ if c.Vm == nil || c.V != formulaErrorVALUE {
+ return r, nil
+ }
+ metaData, err := f.metadataReader()
+ if err != nil {
+ return r, err
+ }
+ vmd := metaData.ValueMetadata
+ if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 {
+ return r, err
+ }
+ richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V
+ richValue, err := f.richValueReader()
+ if err != nil {
+ return r, err
+ }
+ if richValueIdx >= len(richValue.Rv) {
+ return r, err
+ }
+ rv := richValue.Rv[richValueIdx].V
+ if len(rv) == 2 && rv[1] == "5" {
+ pic.InsertType = PictureInsertTypePlaceInCell
+ return f.getRichDataRichValueRel(rv[0])
+ }
+ // cell image inserted by IMAGE formula function
+ if len(rv) > 3 && rv[1]+rv[2] == "10" {
+ pic.InsertType = PictureInsertTypeIMAGE
+ return f.getRichDataWebImagesRel(rv[0])
+ }
+ return r, err
+}
+
+// getCellImages provides a function to get the cell images and
+// the Kingsoft WPS Office embedded cell images by given worksheet name and cell
+// reference.
+func (f *File) getCellImages(sheet, cell string) ([]Picture, error) {
+ pics, err := f.getDispImages(sheet, cell)
+ if err != nil {
+ return pics, err
+ }
+ _, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
+ pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell}
+ r, err := f.getImageCellRel(c, &pic)
+ if err != nil || r == nil {
+ return "", true, err
+ }
+ pic.Extension = filepath.Ext(r.Target)
+ if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil {
+ pic.File = buffer.([]byte)
+ pics = append(pics, pic)
+ }
+ return "", true, nil
+ })
+ return pics, err
+}
+
+// getDispImages provides a function to get the Kingsoft WPS Office embedded
+// cell images by given worksheet name and cell reference.
+func (f *File) getDispImages(sheet, cell string) ([]Picture, error) {
+ formula, err := f.GetCellFormula(sheet, cell)
+ if err != nil {
+ return nil, err
+ }
+ if !strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(formula, "="), "_xlfn."), "DISPIMG") {
+ return nil, err
+ }
+ imgID, err := f.CalcCellValue(sheet, cell)
+ if err != nil {
+ return nil, err
+ }
+ cellImages, err := f.cellImagesReader()
+ if err != nil {
+ return nil, err
+ }
+ rels, err := f.relsReader(defaultXMLPathCellImagesRels)
+ if rels == nil {
+ return nil, err
+ }
+ var pics []Picture
+ for _, cellImg := range cellImages.CellImage {
+ if cellImg.Pic.NvPicPr.CNvPr.Name == imgID {
+ for _, r := range rels.Relationships {
+ if r.ID == cellImg.Pic.BlipFill.Blip.Embed {
+ pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypeDISPIMG}
+ if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil {
+ pic.File = buffer.([]byte)
+ pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr
+ pics = append(pics, pic)
+ }
+ }
+ }
+ }
+ }
+ return pics, err
+}
diff --git a/picture_test.go b/picture_test.go
index f6f716efde..c0c9075583 100644
--- a/picture_test.go
+++ b/picture_test.go
@@ -1,31 +1,31 @@
package excelize
import (
+ "fmt"
+ "image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
-
- _ "golang.org/x/image/tiff"
-
- "fmt"
- "io/ioutil"
+ "io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
+ _ "golang.org/x/image/bmp"
+ _ "golang.org/x/image/tiff"
)
func BenchmarkAddPictureFromBytes(b *testing.B) {
f := NewFile()
- imgFile, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png"))
+ imgFile, err := os.ReadFile(filepath.Join("test", "images", "excel.png"))
if err != nil {
b.Error("unable to load image for benchmark")
}
b.ResetTimer()
for i := 1; i <= b.N; i++ {
- if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile); err != nil {
+ if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "Excel"}}); err != nil {
b.Error(err)
}
}
@@ -33,93 +33,156 @@ func BenchmarkAddPictureFromBytes(b *testing.B) {
func TestAddPicture(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- // Test add picture to worksheet with offset and location hyperlink.
+ // Test add picture to worksheet with offset and location hyperlink
assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"),
- `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`))
- // Test add picture to worksheet with offset, external hyperlink and positioning.
+ &GraphicOptions{OffsetX: 140, OffsetY: 120, Hyperlink: "#Sheet2!D8", HyperlinkType: "Location"}))
+ // Test add picture to worksheet with offset, external hyperlink and positioning
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
- `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`))
+ &GraphicOptions{OffsetX: 10, OffsetY: 10, Hyperlink: "https://github.com/xuri/excelize", HyperlinkType: "External", Positioning: "oneCell"}))
- file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png"))
+ file, err := os.ReadFile(filepath.Join("test", "images", "excel.png"))
assert.NoError(t, err)
- // Test add picture to worksheet with autofit.
- assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`))
- assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "autofit": true}`))
- f.NewSheet("AddPicture")
+ // Test add picture to worksheet with autofit
+ assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
+ assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{OffsetX: 10, OffsetY: 10, AutoFit: true}))
+ assert.NoError(t, f.AddPicture("Sheet1", "C30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true, AutoFitIgnoreAspect: true}))
+ _, err = f.NewSheet("AddPicture")
+ assert.NoError(t, err)
assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30))
assert.NoError(t, f.MergeCell("AddPicture", "B3", "D9"))
- assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`))
- assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`))
+ assert.NoError(t, f.MergeCell("AddPicture", "B1", "D1"))
+ assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
+ assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
+
+ // Test add picture to worksheet from bytes
+ assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
+ // Test add picture to worksheet from bytes with unsupported insert type
+ assert.Equal(t, ErrParameterInvalid, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}, InsertType: PictureInsertTypePlaceInCell}))
+ // Test add picture to worksheet from bytes with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
+
+ for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} {
+ assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil))
+ }
- // Test add picture to worksheet from bytes.
- assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file))
- // Test add picture to worksheet from bytes with illegal cell coordinates.
- assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ // Test write file to given path
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx")))
+ assert.NoError(t, f.Close())
- assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), ""))
- assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), ""))
- assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), ""))
+ // Test get pictures after inserting a new picture from a workbook which contains existing pictures
+ f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), nil))
+ pics, err := f.GetPictures("Sheet1", "A30")
+ assert.NoError(t, err)
+ assert.Len(t, pics, 2)
- // Test write file to given path.
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx")))
+ // Test get picture cells
+ cells, err := f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
+ assert.NoError(t, err)
+ path := "xl/drawings/drawing1.xml"
+ f.Drawings.Delete(path)
+ cells, err = f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
+ // Test get picture cells with unsupported charset
+ f.Drawings.Delete(path)
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err = f.GetPictureCells("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
+ assert.NoError(t, err)
+ // Test get picture cells with unsupported charset
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err = f.GetPictureCells("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test add picture with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test add picture with invalid sheet name
+ assert.EqualError(t, f.AddPicture("Sheet:1", "A1", filepath.Join("test", "images", "excel.jpg"), nil), ErrSheetNameInvalid.Error())
}
func TestAddPictureErrors(t *testing.T) {
- xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
- // Test add picture to worksheet with invalid file path.
- err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "")
- if assert.Error(t, err) {
- assert.True(t, os.IsNotExist(err), "Expected os.IsNotExist(err) == true")
- }
+ // Test add picture to worksheet with invalid file path
+ assert.Error(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), nil))
- // Test add picture to worksheet with unsupport file type.
- err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "")
- assert.EqualError(t, err, "unsupported image extension")
+ // Test add picture to worksheet with unsupported file type
+ assert.EqualError(t, f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), nil), ErrImgExt.Error())
- err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1))
- assert.EqualError(t, err, "unsupported image extension")
+ assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", &Picture{Extension: "jpg", File: make([]byte, 1), Format: &GraphicOptions{AltText: "Excel Logo"}}), ErrImgExt.Error())
- // Test add picture to worksheet with invalid file data.
- err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1))
- assert.EqualError(t, err, "image: unknown format")
+ // Test add picture to worksheet with invalid file data
+ assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "G21", &Picture{Extension: ".jpg", File: make([]byte, 1), Format: &GraphicOptions{AltText: "Excel Logo"}}), image.ErrFormat.Error())
+
+ // Test add picture with custom image decoder and encoder
+ decode := func(r io.Reader) (image.Image, error) { return nil, nil }
+ decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil }
+ for cell, ext := range map[string]string{"Q1": "emf", "Q7": "wmf", "Q13": "emz", "Q19": "wmz"} {
+ image.RegisterFormat(ext, "", decode, decodeConfig)
+ assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil))
+ }
+ assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.8}))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx")))
+ assert.NoError(t, f.Close())
}
func TestGetPicture(t *testing.T) {
- f, err := prepareTestBook1()
+ f := NewFile()
+ assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
+ pics, err := f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Len(t, pics[0].File, 13233)
+ assert.Empty(t, pics[0].Format.AltText)
+ assert.Equal(t, PictureInsertTypePlaceOverCells, pics[0].InsertType)
+
+ f, err = prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}
- file, raw, err := f.GetPicture("Sheet1", "F21")
+ pics, err = f.GetPictures("Sheet1", "F21")
assert.NoError(t, err)
- if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) ||
- !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) {
-
+ if !assert.NotEmpty(t, filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension))) || !assert.NotEmpty(t, pics[0].File) ||
+ !assert.NoError(t, os.WriteFile(filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension)), pics[0].File, 0o644)) {
t.FailNow()
}
- // Try to get picture from a worksheet with illegal cell coordinates.
- _, _, err = f.GetPicture("Sheet1", "A")
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ // Try to get picture from a worksheet with illegal cell reference
+ _, err = f.GetPictures("Sheet1", "A")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
- // Try to get picture from a worksheet that doesn't contain any images.
- file, raw, err = f.GetPicture("Sheet3", "I9")
- assert.EqualError(t, err, "sheet Sheet3 is not exist")
- assert.Empty(t, file)
- assert.Empty(t, raw)
+ // Try to get picture from a worksheet that doesn't contain any images
+ pics, err = f.GetPictures("Sheet3", "I9")
+ assert.EqualError(t, err, "sheet Sheet3 does not exist")
+ assert.Len(t, pics, 0)
- // Try to get picture from a cell that doesn't contain an image.
- file, raw, err = f.GetPicture("Sheet2", "A2")
+ // Try to get picture from a cell that doesn't contain an image
+ pics, err = f.GetPictures("Sheet2", "A2")
assert.NoError(t, err)
- assert.Empty(t, file)
- assert.Empty(t, raw)
+ assert.Len(t, pics, 0)
+
+ // Test get picture with invalid sheet name
+ _, err = f.GetPictures("Sheet:1", "A2")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8")
f.getDrawingRelationships("", "")
@@ -132,78 +195,413 @@ func TestGetPicture(t *testing.T) {
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
assert.NoError(t, err)
- file, raw, err = f.GetPicture("Sheet1", "F21")
+ pics, err = f.GetPictures("Sheet1", "F21")
assert.NoError(t, err)
- if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) ||
- !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) {
-
+ if !assert.NotEmpty(t, filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension))) || !assert.NotEmpty(t, pics[0].File) ||
+ !assert.NoError(t, os.WriteFile(filepath.Join("test", fmt.Sprintf("image1%s", pics[0].Extension)), pics[0].File, 0o644)) {
t.FailNow()
}
- // Try to get picture from a local storage file that doesn't contain an image.
- file, raw, err = f.GetPicture("Sheet1", "F22")
+ // Try to get picture from a local storage file that doesn't contain an image
+ pics, err = f.GetPictures("Sheet1", "F22")
+ assert.NoError(t, err)
+ assert.Len(t, pics, 0)
+ assert.NoError(t, f.Close())
+
+ // Try to get picture with one cell anchor
+ f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/drawing2.xml", []byte(`10151322`))
+ pics, err = f.GetPictures("Sheet2", "K16")
+ assert.NoError(t, err)
+ assert.Len(t, pics, 1)
+ // Try to get picture cells with one cell anchor
+ cells, err := f.GetPictureCells("Sheet2")
assert.NoError(t, err)
- assert.Empty(t, file)
- assert.Empty(t, raw)
+ assert.Equal(t, []string{"K16"}, cells)
- // Test get picture from none drawing worksheet.
+ // Try to get picture cells with absolute target path in the drawing relationship
+ rels, err := f.relsReader("xl/drawings/_rels/drawing2.xml.rels")
+ assert.NoError(t, err)
+ rels.Relationships[0].Target = "/xl/media/image2.jpeg"
+ cells, err = f.GetPictureCells("Sheet2")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"K16"}, cells)
+ // Try to get pictures with absolute target path in the drawing relationship
+ pics, err = f.GetPictures("Sheet2", "K16")
+ assert.NoError(t, err)
+ assert.Len(t, pics, 1)
+
+ assert.NoError(t, f.Close())
+
+ // Test get picture from none drawing worksheet
f = NewFile()
- file, raw, err = f.GetPicture("Sheet1", "F22")
+ pics, err = f.GetPictures("Sheet1", "F22")
assert.NoError(t, err)
- assert.Empty(t, file)
- assert.Empty(t, raw)
+ assert.Len(t, pics, 0)
f, err = prepareTestBook1()
assert.NoError(t, err)
- f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset
- _, _, err = f.getPicture(20, 5, "xl/drawings/drawing1.xml", "xl/drawings/_rels/drawing2.xml.rels")
- assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
+
+ // Test get pictures with unsupported charset
+ path := "xl/drawings/drawing1.xml"
+ f.Drawings.Delete(path)
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "F21")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ f.Drawings.Delete(path)
+ _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test get embedded cell pictures
+ f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
+ f.Pkg.Store(defaultXMLPathCellImages, []byte(``))
+ f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(``, SourceRelationshipImage)))
+ pics, err = f.GetPictures("Sheet1", "F21")
+ assert.NoError(t, err)
+ assert.Len(t, pics, 2)
+ assert.Equal(t, "CellImage1", pics[0].Format.AltText)
+ assert.Equal(t, PictureInsertTypeDISPIMG, pics[0].InsertType)
+
+ // Test get embedded cell pictures with invalid formula
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()"))
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
+
+ // Test get embedded cell pictures with unsupported charset
+ f.Relationships.Delete(defaultXMLPathCellImagesRels)
+ f.Pkg.Store(defaultXMLPathCellImagesRels, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "F21")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ f.Pkg.Store(defaultXMLPathCellImages, MacintoshCyrillicCharset)
+ f.DecodeCellImages = nil
+ _, err = f.GetPictures("Sheet1", "F21")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
}
func TestAddDrawingPicture(t *testing.T) {
- // testing addDrawingPicture with illegal cell coordinates.
+ // Test addDrawingPicture with illegal cell reference
f := NewFile()
- assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}
+ assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ // Test addDrawingPicture with invalid positioning types
+ assert.Equal(t, newInvalidOptionalValue("Positioning", "x", supportedPositioning),
+ f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}))
+
+ path := "xl/drawings/drawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addDrawingPicture("sheet1", path, "A1", "", 0, 0, image.Config{}, opts), "XML syntax error on line 1: invalid UTF-8")
}
func TestAddPictureFromBytes(t *testing.T) {
f := NewFile()
- imgFile, err := ioutil.ReadFile("logo.png")
+ imgFile, err := os.ReadFile("logo.png")
assert.NoError(t, err, "Unable to load logo for test")
- assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile))
- assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile))
+
+ assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}))
+ assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}))
imageCount := 0
- for fileName := range f.XLSX {
- if strings.Contains(fileName, "media/image") {
+ f.Pkg.Range(func(fileName, v interface{}) bool {
+ if strings.Contains(fileName.(string), "media/image") {
imageCount++
}
- }
+ return true
+ })
assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.")
- assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist")
+ assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}), "sheet SheetN does not exist")
+ // Test add picture from bytes with invalid sheet name
+ assert.EqualError(t, f.AddPictureFromBytes("Sheet:1", fmt.Sprint("A", 1), &Picture{Extension: ".png", File: imgFile, Format: &GraphicOptions{AltText: "logo"}}), ErrSheetNameInvalid.Error())
}
func TestDeletePicture(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
+ // Test delete picture on a worksheet which does not contains any pictures
assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
- assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), ""))
- assert.NoError(t, f.DeletePicture("Sheet1", "P1"))
+ // Add same pictures on different worksheets
+ assert.NoError(t, f.AddPicture("Sheet1", "F20", filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.NoError(t, f.AddPicture("Sheet1", "I20", filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.NoError(t, f.AddPicture("Sheet2", "F1", filepath.Join("test", "images", "excel.jpg"), nil))
+ // Test delete picture on a worksheet, the images should be preserved
+ assert.NoError(t, f.DeletePicture("Sheet1", "F20"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx")))
- // Test delete picture on not exists worksheet.
- assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist")
- // Test delete picture with invalid coordinates.
- assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`)
- // Test delete picture on no chart worksheet.
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
+ assert.NoError(t, err)
+ // Test delete same picture on different worksheet, the images should be removed
+ assert.NoError(t, f.DeletePicture("Sheet1", "F20"))
+ assert.NoError(t, f.DeletePicture("Sheet1", "I20"))
+ assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture2.xlsx")))
+
+ // Test delete picture on not exists worksheet
+ assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist")
+ // Test delete picture with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1"))
+ // Test delete picture with invalid coordinates
+ assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", ""))
+ assert.NoError(t, f.Close())
+ // Test delete picture on no chart worksheet
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))
+
+ f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
+ assert.NoError(t, err)
+ // Test delete picture with unsupported charset drawing
+ f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeletePicture("Sheet1", "F10"), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
+ assert.NoError(t, err)
+ // Test delete picture with unsupported charset drawing relationships
+ f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels")
+ f.Pkg.Store("xl/drawings/_rels/drawing1.xml.rels", MacintoshCyrillicCharset)
+ assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
+ assert.NoError(t, err)
+ // Test delete picture without drawing relationships
+ f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels")
+ f.Pkg.Delete("xl/drawings/_rels/drawing1.xml.rels")
+ assert.NoError(t, f.DeletePicture("Sheet1", "I20"))
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), nil))
+ assert.NoError(t, f.AddPicture("Sheet1", "G1", filepath.Join("test", "images", "excel.jpg"), nil))
+ drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
+ assert.True(t, ok)
+ // Made two picture reference the same drawing relationship ID
+ drawing.(*xlsxWsDr).TwoCellAnchor[1].Pic.BlipFill.Blip.Embed = "rId1"
+ assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
+ assert.NoError(t, f.Close())
}
func TestDrawingResize(t *testing.T) {
f := NewFile()
- // Test calculate drawing resize on not exists worksheet.
+ // Test calculate drawing resize on not exists worksheet
_, _, _, _, err := f.drawingResize("SheetN", "A1", 1, 1, nil)
- assert.EqualError(t, err, "sheet SheetN is not exist")
- // Test calculate drawing resize with invalid coordinates.
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test calculate drawing resize with invalid coordinates
_, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil)
- assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`)
- f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
- assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
+}
+
+func TestSetContentTypePartRelsExtensions(t *testing.T) {
+ f := NewFile()
+ f.ContentTypes = &xlsxTypes{}
+ assert.NoError(t, f.setContentTypePartRelsExtensions())
+
+ // Test set content type part relationships extensions with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.setContentTypePartRelsExtensions(), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestSetContentTypePartImageExtensions(t *testing.T) {
+ f := NewFile()
+ // Test set content type part image extensions with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.setContentTypePartImageExtensions(), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestSetContentTypePartVMLExtensions(t *testing.T) {
+ f := NewFile()
+ // Test set content type part VML extensions with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.setContentTypePartVMLExtensions(), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestAddContentTypePart(t *testing.T) {
+ f := NewFile()
+ // Test add content type part with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestGetPictureCells(t *testing.T) {
+ f := NewFile()
+ // Test get picture cells on a worksheet which not contains any pictures
+ cells, err := f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Empty(t, cells)
+ // Test get picture cells on not exists worksheet
+ _, err = f.GetPictureCells("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
+
+ // Test get embedded picture cells
+ f = NewFile()
+ assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
+ cells, err = f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"A2", "A1"}, cells)
+
+ // Test get embedded cell pictures with invalid formula
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG()"))
+ _, err = f.GetPictureCells("Sheet1")
+ assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
+ assert.NoError(t, f.Close())
+}
+
+func TestExtractDecodeCellAnchor(t *testing.T) {
+ f := NewFile()
+ cond := func(a *decodeFrom) bool { return true }
+ cb := func(a *decodeCellAnchor, r *xlsxRelationship) {}
+ f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb)
+}
+
+func TestGetCellImages(t *testing.T) {
+ f := NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ _, err := f.getCellImages("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test get the cell images
+ prepareWorkbook := func() *File {
+ f := NewFile()
+ assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
+ f.Pkg.Store(defaultXMLMetadata, []byte(``))
+ f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`05`))
+ f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``))
+ f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage)))
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
+ SheetData: xlsxSheetData{Row: []xlsxRow{
+ {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}},
+ }},
+ })
+ return f
+ }
+ f = prepareWorkbook()
+ pics, err := f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(pics))
+ assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType)
+ cells, err := f.GetPictureCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"A1"}, cells)
+
+ // Test get the cell images without image relationships parts
+ f.Relationships.Delete(defaultXMLRdRichValueRelRels)
+ f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink)))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images with unsupported charset rich data rich value relationships
+ f.Relationships.Delete(defaultXMLRdRichValueRelRels)
+ f.Pkg.Store(defaultXMLRdRichValueRelRels, MacintoshCyrillicCharset)
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images with unsupported charset rich data rich value
+ f.Pkg.Store(defaultXMLRdRichValueRel, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get the image cells without block of metadata records
+ cells, err = f.GetPictureCells("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.Empty(t, cells)
+ // Test get the cell images with rich data rich value relationships
+ f.Pkg.Store(defaultXMLMetadata, []byte(``))
+ f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images with unsupported charset meta data
+ f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get the cell images without block of metadata records
+ f.Pkg.Store(defaultXMLMetadata, []byte(``))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+
+ f = prepareWorkbook()
+ // Test get the cell images with empty image cell rich value
+ f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`5`))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
+ assert.Empty(t, pics)
+ // Test get the cell images without image cell rich value
+ f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`01`))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images with unsupported charset rich value
+ f.Pkg.Store(defaultXMLRdRichValuePart, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ f = prepareWorkbook()
+ // Test get the cell images with invalid rich value index
+ f.Pkg.Store(defaultXMLMetadata, []byte(``))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+
+ f = prepareWorkbook()
+ // Test get the cell images inserted by IMAGE formula function
+ f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`0100`))
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`
+ `))
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipImage)))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(pics))
+ assert.Equal(t, PictureInsertTypeIMAGE, pics[0].InsertType)
+
+ // Test get the cell images inserted by IMAGE formula function with unsupported charset web images relationships
+ f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, MacintoshCyrillicCharset)
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+
+ // Test get the cell images inserted by IMAGE formula function without image part
+ f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipHyperLink)))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images inserted by IMAGE formula function with unsupported charset web images part
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePart, MacintoshCyrillicCharset)
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get the cell images inserted by IMAGE formula function with empty charset web images part
+ f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(``))
+ pics, err = f.GetPictures("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Empty(t, pics)
+ // Test get the cell images inserted by IMAGE formula function with invalid rich value index
+ f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`100`))
+ _, err = f.GetPictures("Sheet1", "A1")
+ assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
+}
+
+func TestGetImageCells(t *testing.T) {
+ f := NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ _, err := f.getImageCells("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
}
diff --git a/pivotTable.go b/pivotTable.go
index f820d76026..4825bef05a 100644
--- a/pivotTable.go
+++ b/pivotTable.go
@@ -1,107 +1,165 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
+ "bytes"
"encoding/xml"
- "errors"
"fmt"
+ "io"
+ "path/filepath"
+ "reflect"
"strconv"
"strings"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
-// PivotTableOption directly maps the format settings of the pivot table.
-type PivotTableOption struct {
- DataRange string
- PivotTableRange string
- Rows []PivotTableField
- Columns []PivotTableField
- Data []PivotTableField
- Filter []PivotTableField
+// PivotTableOptions directly maps the format settings of the pivot table.
+//
+// PivotTableStyleName: The built-in pivot table style names
+//
+// PivotStyleLight1 - PivotStyleLight28
+// PivotStyleMedium1 - PivotStyleMedium28
+// PivotStyleDark1 - PivotStyleDark28
+type PivotTableOptions struct {
+ pivotTableXML string
+ pivotCacheXML string
+ pivotSheetName string
+ pivotDataRange string
+ namedDataRange bool
+ DataRange string
+ PivotTableRange string
+ Name string
+ Rows []PivotTableField
+ Columns []PivotTableField
+ Data []PivotTableField
+ Filter []PivotTableField
+ RowGrandTotals bool
+ ColGrandTotals bool
+ ShowDrill bool
+ UseAutoFormatting bool
+ PageOverThenDown bool
+ MergeItem bool
+ ClassicLayout bool
+ CompactData bool
+ ShowError bool
+ ShowRowHeaders bool
+ ShowColHeaders bool
+ ShowRowStripes bool
+ ShowColStripes bool
+ ShowLastColumn bool
+ FieldPrintTitles bool
+ ItemPrintTitles bool
+ PivotTableStyleName string
}
// PivotTableField directly maps the field settings of the pivot table.
+//
+// Name specifies the name of the data field. Maximum 255 characters
+// are allowed in data field name, excess characters will be truncated.
+//
// Subtotal specifies the aggregation function that applies to this data
// field. The default value is sum. The possible values for this attribute
// are:
//
-// Average
-// Count
-// CountNums
-// Max
-// Min
-// Product
-// StdDev
-// StdDevp
-// Sum
-// Var
-// Varp
+// Average
+// Count
+// CountNums
+// Max
+// Min
+// Product
+// StdDev
+// StdDevp
+// Sum
+// Var
+// Varp
//
-// Name specifies the name of the data field. Maximum 255 characters
-// are allowed in data field name, excess characters will be truncated.
+// NumFmt specifies the number format ID of the data field, this filed only
+// accepts built-in number format ID and does not support custom number format
+// expression currently.
type PivotTableField struct {
- Data string
- Name string
- Subtotal string
+ Compact bool
+ Data string
+ Name string
+ Outline bool
+ ShowAll bool
+ InsertBlankRow bool
+ Subtotal string
+ DefaultSubtotal bool
+ NumFmt int
}
// AddPivotTable provides the method to add pivot table by given pivot table
-// options.
+// options. Note that the same fields can not in Columns, Rows and Filter
+// fields at the same time.
//
-// For example, create a pivot table on the Sheet1!$G$2:$M$34 area with the
-// region Sheet1!$A$1:$E$31 as the data source, summarize by sum for sales:
+// For example, create a pivot table on the range reference Sheet1!G2:M34 with
+// the range reference Sheet1!A1:E31 as the data source, summarize by sum for
+// sales:
//
-// package main
+// package main
//
-// import (
-// "fmt"
-// "math/rand"
+// import (
+// "fmt"
+// "math/rand"
//
-// "github.com/360EntSecGroup-Skylar/excelize"
-// )
+// "github.com/xuri/excelize/v2"
+// )
//
-// func main() {
-// f := excelize.NewFile()
-// // Create some data in a sheet
-// month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
-// year := []int{2017, 2018, 2019}
-// types := []string{"Meat", "Dairy", "Beverages", "Produce"}
-// region := []string{"East", "West", "North", "South"}
-// f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})
-// for i := 0; i < 30; i++ {
-// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)])
-// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)])
-// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)])
-// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))
-// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])
-// }
-// if err := f.AddPivotTable(&excelize.PivotTableOption{
-// DataRange: "Sheet1!$A$1:$E$31",
-// PivotTableRange: "Sheet1!$G$2:$M$34",
-// Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}},
-// Filter: []excelize.PivotTableField{{Data: "Region"}},
-// Columns: []excelize.PivotTableField{{Data: "Type"}},
-// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}},
-// }); err != nil {
-// fmt.Println(err)
-// }
-// if err := f.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
-// }
-//
-func (f *File) AddPivotTable(opt *PivotTableOption) error {
+// func main() {
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// // Create some data in a sheet
+// month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
+// year := []int{2017, 2018, 2019}
+// types := []string{"Meat", "Dairy", "Beverages", "Produce"}
+// region := []string{"East", "West", "North", "South"}
+// f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})
+// for row := 2; row < 32; row++ {
+// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)])
+// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)])
+// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)])
+// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000))
+// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])
+// }
+// if err := f.AddPivotTable(&excelize.PivotTableOptions{
+// DataRange: "Sheet1!A1:E31",
+// PivotTableRange: "Sheet1!G2:M34",
+// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+// Filter: []excelize.PivotTableField{{Data: "Region"}},
+// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}},
+// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}},
+// RowGrandTotals: true,
+// ColGrandTotals: true,
+// ShowDrill: true,
+// ShowRowHeaders: true,
+// ShowColHeaders: true,
+// ShowLastColumn: true,
+// }); err != nil {
+// fmt.Println(err)
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
+// }
+func (f *File) AddPivotTable(opts *PivotTableOptions) error {
// parameter validation
- dataSheet, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt)
+ _, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opts)
if err != nil {
return err
}
@@ -110,53 +168,61 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error {
pivotCacheID := f.countPivotCache() + 1
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
- pivotTableXML := strings.Replace(sheetRelationshipsPivotTableXML, "..", "xl", -1)
- pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
- err = f.addPivotCache(pivotCacheID, pivotCacheXML, opt, dataSheet)
- if err != nil {
+ opts.pivotTableXML = strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
+ opts.pivotCacheXML = "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
+ if err = f.addPivotCache(opts); err != nil {
return err
}
// workbook pivot cache
- workBookPivotCacheRID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipPivotCache, fmt.Sprintf("pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
+ workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(opts.pivotCacheXML, "xl/"), "")
cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID)
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
// rId not used
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
- err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opt)
- if err != nil {
+ if err = f.addPivotTable(cacheID, pivotTableID, opts); err != nil {
return err
}
pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels"
f.addRels(pivotTableSheetRels, SourceRelationshipPivotTable, sheetRelationshipsPivotTableXML, "")
- f.addContentTypePart(pivotTableID, "pivotTable")
- f.addContentTypePart(pivotCacheID, "pivotCache")
-
- return nil
+ if err = f.addContentTypePart(pivotTableID, "pivotTable"); err != nil {
+ return err
+ }
+ return f.addContentTypePart(pivotCacheID, "pivotCache")
}
// parseFormatPivotTableSet provides a function to validate pivot table
// properties.
-func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) {
- if opt == nil {
- return nil, "", errors.New("parameter is required")
+func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet, string, error) {
+ if opts == nil {
+ return nil, "", ErrParameterRequired
}
- dataSheetName, _, err := f.adjustRange(opt.DataRange)
+ pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange)
if err != nil {
- return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
+ return nil, "", newPivotTableRangeError(err.Error())
}
- pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange)
+ if len(opts.Name) > MaxFieldLength {
+ return nil, "", ErrNameLength
+ }
+ opts.pivotSheetName = pivotTableSheetName
+ if err = f.getPivotTableDataRange(opts); err != nil {
+ return nil, "", err
+ }
+ dataSheetName, _, err := f.adjustRange(opts.pivotDataRange)
if err != nil {
- return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
+ return nil, "", newPivotTableDataRangeError(err.Error())
}
dataSheet, err := f.workSheetReader(dataSheetName)
if err != nil {
return dataSheet, "", err
}
- pivotTableSheetPath, ok := f.sheetMap[trimSheetName(pivotTableSheetName)]
+ pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName)
if !ok {
- return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s is not exist", pivotTableSheetName)
+ return dataSheet, pivotTableSheetPath, ErrSheetNotExist{pivotTableSheetName}
+ }
+ if opts.CompactData && opts.ClassicLayout {
+ return nil, "", ErrPivotTableClassicLayout
}
return dataSheet, pivotTableSheetPath, err
}
@@ -164,23 +230,23 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet,
// adjustRange adjust range, for example: adjust Sheet1!$E$31:$A$1 to Sheet1!$A$1:$E$31
func (f *File) adjustRange(rangeStr string) (string, []int, error) {
if len(rangeStr) < 1 {
- return "", []int{}, errors.New("parameter is required")
+ return "", []int{}, ErrParameterRequired
}
rng := strings.Split(rangeStr, "!")
if len(rng) != 2 {
- return "", []int{}, errors.New("parameter is invalid")
+ return "", []int{}, ErrParameterInvalid
}
- trimRng := strings.Replace(rng[1], "$", "", -1)
- coordinates, err := f.areaRefToCoordinates(trimRng)
+ trimRng := strings.ReplaceAll(rng[1], "$", "")
+ coordinates, err := rangeRefToCoordinates(trimRng)
if err != nil {
return rng[0], []int{}, err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if x1 == x2 && y1 == y2 {
- return rng[0], []int{}, errors.New("parameter is invalid")
+ return rng[0], []int{}, ErrParameterInvalid
}
- // Correct the coordinate area, such correct C1:B3 to B1:C3.
+ // Correct the range, such correct C1:B3 to B1:C3.
if x2 < x1 {
x1, x2 = x2, x1
}
@@ -191,13 +257,16 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
return rng[0], []int{x1, y1, x2, y2}, nil
}
-// getPivotFieldsOrder provides a function to get order list of pivot table
+// getTableFieldsOrder provides a function to get order list of pivot table
// fields.
-func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) {
- order := []string{}
- dataSheet, coordinates, err := f.adjustRange(dataRange)
+func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) {
+ var order []string
+ if err := f.getPivotTableDataRange(opts); err != nil {
+ return order, err
+ }
+ dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
if err != nil {
- return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
+ return order, newPivotTableDataRangeError(err.Error())
}
for col := coordinates[0]; col <= coordinates[2]; col++ {
coordinate, _ := CoordinatesToCellName(col, coordinates[1])
@@ -205,66 +274,95 @@ func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) {
if err != nil {
return order, err
}
+ if name == "" {
+ return order, ErrParameterInvalid
+ }
order = append(order, name)
}
return order, nil
}
// addPivotCache provides a function to create a pivot cache by given properties.
-func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotTableOption, ws *xlsxWorksheet) error {
+func (f *File) addPivotCache(opts *PivotTableOptions) error {
// validate data range
- dataSheet, coordinates, err := f.adjustRange(opt.DataRange)
+ dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
if err != nil {
- return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
+ return newPivotTableDataRangeError(err.Error())
}
- // data range has been checked
- order, _ := f.getPivotFieldsOrder(opt.DataRange)
- hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
- vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
+ order, err := f.getTableFieldsOrder(opts)
+ if err != nil {
+ return newPivotTableDataRangeError(err.Error())
+ }
+ topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
+ bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{
- SaveData: false,
- RefreshOnLoad: true,
+ SaveData: false,
+ RefreshOnLoad: true,
+ CreatedVersion: pivotTableVersion,
+ RefreshedVersion: pivotTableRefreshedVersion,
+ MinRefreshableVersion: pivotTableVersion,
CacheSource: &xlsxCacheSource{
Type: "worksheet",
WorksheetSource: &xlsxWorksheetSource{
- Ref: hcell + ":" + vcell,
+ Ref: topLeftCell + ":" + bottomRightCell,
Sheet: dataSheet,
},
},
CacheFields: &xlsxCacheFields{},
}
+ if opts.namedDataRange {
+ pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange}
+ }
for _, name := range order {
pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{
- Name: name,
- SharedItems: &xlsxSharedItems{
- Count: 0,
- },
+ Name: name,
+ SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}},
})
}
pc.CacheFields.Count = len(pc.CacheFields.CacheField)
pivotCache, err := xml.Marshal(pc)
- f.saveFileList(pivotCacheXML, pivotCache)
+ f.saveFileList(opts.pivotCacheXML, pivotCache)
return err
}
// addPivotTable provides a function to create a pivot table by given pivot
// table ID and properties.
-func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opt *PivotTableOption) error {
+func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) error {
// validate pivot table range
- _, coordinates, err := f.adjustRange(opt.PivotTableRange)
+ _, coordinates, err := f.adjustRange(opts.PivotTableRange)
if err != nil {
- return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
+ return newPivotTableRangeError(err.Error())
}
- hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
- vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
+ topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
+ bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
+ pivotTableStyle := func() string {
+ if opts.PivotTableStyleName == "" {
+ return "PivotStyleLight16"
+ }
+ return opts.PivotTableStyleName
+ }
pt := xlsxPivotTableDefinition{
- Name: fmt.Sprintf("Pivot Table%d", pivotTableID),
- CacheID: cacheID,
- DataCaption: "Values",
+ Name: opts.Name,
+ CacheID: cacheID,
+ RowGrandTotals: &opts.RowGrandTotals,
+ ColGrandTotals: &opts.ColGrandTotals,
+ UpdatedVersion: pivotTableRefreshedVersion,
+ MinRefreshableVersion: pivotTableVersion,
+ ShowDrill: &opts.ShowDrill,
+ UseAutoFormatting: &opts.UseAutoFormatting,
+ PageOverThenDown: &opts.PageOverThenDown,
+ MergeItem: &opts.MergeItem,
+ CreatedVersion: pivotTableVersion,
+ CompactData: &opts.CompactData,
+ GridDropZones: opts.ClassicLayout,
+ ShowError: &opts.ShowError,
+ FieldPrintTitles: opts.FieldPrintTitles,
+ ItemPrintTitles: opts.ItemPrintTitles,
+ DataCaption: "Values",
Location: &xlsxLocation{
- Ref: hcell + ":" + vcell,
+ Ref: topLeftCell + ":" + bottomRightCell,
FirstDataCol: 1,
FirstDataRow: 1,
FirstHeaderRow: 1,
@@ -283,35 +381,45 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
I: []*xlsxI{{}},
},
PivotTableStyleInfo: &xlsxPivotTableStyleInfo{
- Name: "PivotStyleLight16",
- ShowRowHeaders: true,
- ShowColHeaders: true,
- ShowLastColumn: true,
+ Name: pivotTableStyle(),
+ ShowRowHeaders: opts.ShowRowHeaders,
+ ShowColHeaders: opts.ShowColHeaders,
+ ShowRowStripes: opts.ShowRowStripes,
+ ShowColStripes: opts.ShowColStripes,
+ ShowLastColumn: opts.ShowLastColumn,
},
}
+ if pt.Name == "" {
+ pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID)
+ }
+
+ // set classic layout
+ if opts.ClassicLayout {
+ pt.Compact, pt.CompactData = boolPtr(false), boolPtr(false)
+ }
// pivot fields
- _ = f.addPivotFields(&pt, opt)
+ _ = f.addPivotFields(&pt, opts)
// count pivot fields
pt.PivotFields.Count = len(pt.PivotFields.PivotField)
// data range has been checked
- _ = f.addPivotRowFields(&pt, opt)
- _ = f.addPivotColFields(&pt, opt)
- _ = f.addPivotPageFields(&pt, opt)
- _ = f.addPivotDataFields(&pt, opt)
+ _ = f.addPivotRowFields(&pt, opts)
+ _ = f.addPivotColFields(&pt, opts)
+ _ = f.addPivotPageFields(&pt, opts)
+ _ = f.addPivotDataFields(&pt, opts)
pivotTable, err := xml.Marshal(pt)
- f.saveFileList(pivotTableXML, pivotTable)
+ f.saveFileList(opts.pivotTableXML, pivotTable)
return err
}
// addPivotRowFields provides a method to add row fields for pivot table by
// given pivot table options.
-func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
+func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
// row fields
- rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt)
+ rowFieldsIndex, err := f.getPivotFieldsIndex(opts.Rows, opts)
if err != nil {
return err
}
@@ -333,13 +441,13 @@ func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp
// addPivotPageFields provides a method to add page fields for pivot table by
// given pivot table options.
-func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
+func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
// page fields
- pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt)
+ pageFieldsIndex, err := f.getPivotFieldsIndex(opts.Filter, opts)
if err != nil {
return err
}
- pageFieldsName := f.getPivotTableFieldsName(opt.Filter)
+ pageFieldsName := f.getPivotTableFieldsName(opts.Filter)
for idx, pageField := range pageFieldsIndex {
if pt.PageFields == nil {
pt.PageFields = &xlsxPageFields{}
@@ -359,14 +467,15 @@ func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opt *PivotTableO
// addPivotDataFields provides a method to add data fields for pivot table by
// given pivot table options.
-func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
+func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
// data fields
- dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt)
+ dataFieldsIndex, err := f.getPivotFieldsIndex(opts.Data, opts)
if err != nil {
return err
}
- dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data)
- dataFieldsName := f.getPivotTableFieldsName(opt.Data)
+ dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data)
+ dataFieldsName := f.getPivotTableFieldsName(opts.Data)
+ dataFieldsNumFmtID := f.getPivotTableFieldsNumFmtID(opts.Data)
for idx, dataField := range dataFieldsIndex {
if pt.DataFields == nil {
pt.DataFields = &xlsxDataFields{}
@@ -375,6 +484,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableO
Name: dataFieldsName[idx],
Fld: dataField,
Subtotal: dataFieldsSubtotals[idx],
+ NumFmtID: dataFieldsNumFmtID[idx],
})
}
@@ -385,17 +495,6 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableO
return err
}
-// inStrSlice provides a method to check if an element is present in an array,
-// and return the index of its location, otherwise return -1.
-func inStrSlice(a []string, x string) int {
- for idx, n := range a {
- if x == n {
- return idx
- }
- }
- return -1
-}
-
// inPivotTableField provides a method to check if an element is present in
// pivot table fields list, and return the index of its location, otherwise
// return -1.
@@ -410,15 +509,24 @@ func inPivotTableField(a []PivotTableField, x string) int {
// addPivotColFields create pivot column fields by given pivot table
// definition and option.
-func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
- if len(opt.Columns) == 0 {
+func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
+ if len(opts.Columns) == 0 {
+ if len(opts.Data) <= 1 {
+ return nil
+ }
+ pt.ColFields = &xlsxColFields{}
+ // in order to create pivot table in case there is no input from Columns
+ pt.ColFields.Count = 1
+ pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{
+ X: -2,
+ })
return nil
}
pt.ColFields = &xlsxColFields{}
// col fields
- colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt)
+ colFieldsIndex, err := f.getPivotFieldsIndex(opts.Columns, opts)
if err != nil {
return err
}
@@ -428,103 +536,154 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp
})
}
+ // in order to create pivot in case there is many Columns and Data
+ if len(opts.Data) > 1 {
+ pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{
+ X: -2,
+ })
+ }
+
// count col fields
pt.ColFields.Count = len(pt.ColFields.Field)
return err
}
+// setClassicLayout provides a method to set classic layout for pivot table by
+// setting Compact and Outline to false.
+func (fld *xlsxPivotField) setClassicLayout(classicLayout bool) {
+ if classicLayout {
+ fld.Compact, fld.Outline = boolPtr(false), boolPtr(false)
+ }
+}
+
// addPivotFields create pivot fields based on the column order of the first
// row in the data region by given pivot table definition and option.
-func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
- order, err := f.getPivotFieldsOrder(opt.DataRange)
+func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
+ order, err := f.getTableFieldsOrder(opts)
if err != nil {
return err
}
+ x := 0
for _, name := range order {
- if inPivotTableField(opt.Rows, name) != -1 {
- pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
- Axis: "axisRow",
- Name: f.getPivotTableFieldName(name, opt.Rows),
+ if inPivotTableField(opts.Rows, name) != -1 {
+ rowOptions, ok := f.getPivotTableFieldOptions(name, opts.Rows)
+ var items []*xlsxItem
+ if !ok || !rowOptions.DefaultSubtotal {
+ items = append(items, &xlsxItem{X: &x})
+ } else {
+ items = append(items, &xlsxItem{T: "default"})
+ }
+ fld := &xlsxPivotField{
+ Name: f.getPivotTableFieldName(name, opts.Rows),
+ Axis: "axisRow",
+ DataField: inPivotTableField(opts.Data, name) != -1,
+ Compact: &rowOptions.Compact,
+ Outline: &rowOptions.Outline,
+ ShowAll: rowOptions.ShowAll,
+ InsertBlankRow: rowOptions.InsertBlankRow,
+ DefaultSubtotal: &rowOptions.DefaultSubtotal,
Items: &xlsxItems{
- Count: 1,
- Item: []*xlsxItem{
- {T: "default"},
- },
+ Count: len(items),
+ Item: items,
},
- })
+ }
+ fld.setClassicLayout(opts.ClassicLayout)
+ pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
- if inPivotTableField(opt.Filter, name) != -1 {
- pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
- Axis: "axisPage",
- Name: f.getPivotTableFieldName(name, opt.Columns),
+ if inPivotTableField(opts.Filter, name) != -1 {
+ fld := &xlsxPivotField{
+ Axis: "axisPage",
+ DataField: inPivotTableField(opts.Data, name) != -1,
+ Name: f.getPivotTableFieldName(name, opts.Columns),
Items: &xlsxItems{
Count: 1,
Item: []*xlsxItem{
{T: "default"},
},
},
- })
+ }
+ fld.setClassicLayout(opts.ClassicLayout)
+ pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
- if inPivotTableField(opt.Columns, name) != -1 {
- pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
- Axis: "axisCol",
- Name: f.getPivotTableFieldName(name, opt.Columns),
+ if inPivotTableField(opts.Columns, name) != -1 {
+ columnOptions, ok := f.getPivotTableFieldOptions(name, opts.Columns)
+ var items []*xlsxItem
+ if !ok || !columnOptions.DefaultSubtotal {
+ items = append(items, &xlsxItem{X: &x})
+ } else {
+ items = append(items, &xlsxItem{T: "default"})
+ }
+ fld := &xlsxPivotField{
+ Name: f.getPivotTableFieldName(name, opts.Columns),
+ Axis: "axisCol",
+ DataField: inPivotTableField(opts.Data, name) != -1,
+ Compact: &columnOptions.Compact,
+ Outline: &columnOptions.Outline,
+ ShowAll: columnOptions.ShowAll,
+ InsertBlankRow: columnOptions.InsertBlankRow,
+ DefaultSubtotal: &columnOptions.DefaultSubtotal,
Items: &xlsxItems{
- Count: 1,
- Item: []*xlsxItem{
- {T: "default"},
- },
+ Count: len(items),
+ Item: items,
},
- })
+ }
+ fld.setClassicLayout(opts.ClassicLayout)
+ pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
- if inPivotTableField(opt.Data, name) != -1 {
- pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
+ if inPivotTableField(opts.Data, name) != -1 {
+ fld := &xlsxPivotField{
DataField: true,
- })
+ }
+ fld.setClassicLayout(opts.ClassicLayout)
+ pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
- pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{})
+ fld := &xlsxPivotField{}
+ fld.setClassicLayout(opts.ClassicLayout)
+ pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
}
return err
}
-// countPivotTables provides a function to get drawing files count storage in
-// the folder xl/pivotTables.
+// countPivotTables provides a function to get pivot table files count storage
+// in the folder xl/pivotTables.
func (f *File) countPivotTables() int {
count := 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/pivotTables/pivotTable") {
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/pivotTables/pivotTable") {
count++
}
- }
+ return true
+ })
return count
}
-// countPivotCache provides a function to get drawing files count storage in
-// the folder xl/pivotCache.
+// countPivotCache provides a function to get pivot table cache definition files
+// count storage in the folder xl/pivotCache.
func (f *File) countPivotCache() int {
count := 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/pivotCache/pivotCacheDefinition") {
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") {
count++
}
- }
+ return true
+ })
return count
}
// getPivotFieldsIndex convert the column of the first row in the data region
// to a sequential index by given fields and pivot option.
-func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) {
- pivotFieldsIndex := []int{}
- orders, err := f.getPivotFieldsOrder(opt.DataRange)
+func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) {
+ var pivotFieldsIndex []int
+ orders, err := f.getTableFieldsOrder(opts)
if err != nil {
return pivotFieldsIndex, err
}
for _, field := range fields {
- if pos := inStrSlice(orders, field.Data); pos != -1 {
+ if pos := inStrSlice(orders, field.Data, true); pos != -1 {
pivotFieldsIndex = append(pivotFieldsIndex, pos)
}
}
@@ -537,7 +696,7 @@ func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string {
enums := []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"}
inEnums := func(enums []string, val string) string {
for _, enum := range enums {
- if strings.ToLower(enum) == strings.ToLower(val) {
+ if strings.EqualFold(enum, val) {
return enum
}
}
@@ -554,8 +713,8 @@ func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string {
func (f *File) getPivotTableFieldsName(fields []PivotTableField) []string {
field := make([]string, len(fields))
for idx, fld := range fields {
- if len(fld.Name) > 255 {
- field[idx] = fld.Name[0:255]
+ if len(fld.Name) > MaxFieldLength {
+ field[idx] = fld.Name[:MaxFieldLength]
continue
}
field[idx] = fld.Name
@@ -574,9 +733,36 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str
return ""
}
-// addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml.
+// getPivotTableFieldsNumFmtID prepare fields number format ID by given pivot
+// table fields.
+func (f *File) getPivotTableFieldsNumFmtID(fields []PivotTableField) []int {
+ field := make([]int, len(fields))
+ for idx, fld := range fields {
+ if _, ok := builtInNumFmt[fld.NumFmt]; ok {
+ field[idx] = fld.NumFmt
+ continue
+ }
+ if (27 <= fld.NumFmt && fld.NumFmt <= 36) || (50 <= fld.NumFmt && fld.NumFmt <= 81) {
+ field[idx] = fld.NumFmt
+ }
+ }
+ return field
+}
+
+// getPivotTableFieldOptions return options for specific field by given field name.
+func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) {
+ for _, field := range fields {
+ if field.Data == name {
+ options, ok = field, true
+ return
+ }
+ }
+ return
+}
+
+// addWorkbookPivotCache add the association ID of the pivot cache in workbook.xml.
func (f *File) addWorkbookPivotCache(RID int) int {
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
if wb.PivotCaches == nil {
wb.PivotCaches = &xlsxPivotCaches{}
}
@@ -593,3 +779,318 @@ func (f *File) addWorkbookPivotCache(RID int) int {
})
return cacheID
}
+
+// GetPivotTables returns all pivot table definitions in a worksheet by given
+// worksheet name.
+func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
+ var pivotTables []PivotTableOptions
+ name, ok := f.getSheetXMLPath(sheet)
+ if !ok {
+ return pivotTables, ErrSheetNotExist{sheet}
+ }
+ rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
+ sheetRels, err := f.relsReader(rels)
+ if err != nil {
+ return pivotTables, err
+ }
+ if sheetRels == nil {
+ sheetRels = &xlsxRelationships{}
+ }
+ for _, v := range sheetRels.Relationships {
+ if v.Type == SourceRelationshipPivotTable {
+ pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
+ pivotCacheRels := "xl/pivotTables/_rels/" + filepath.Base(v.Target) + ".rels"
+ pivotTable, err := f.getPivotTable(sheet, pivotTableXML, pivotCacheRels)
+ if err != nil {
+ return pivotTables, err
+ }
+ pivotTables = append(pivotTables, pivotTable)
+ }
+ }
+ return pivotTables, nil
+}
+
+// getPivotTableDataRange checking given if data range is a cell reference or
+// named reference (defined name or table name), and set pivot table data range.
+func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error {
+ if opts.DataRange == "" {
+ return newPivotTableDataRangeError(ErrParameterRequired.Error())
+ }
+ if opts.pivotDataRange != "" {
+ return nil
+ }
+ if strings.Contains(opts.DataRange, "!") {
+ opts.pivotDataRange = opts.DataRange
+ return nil
+ }
+ tbls, err := f.getTables()
+ if err != nil {
+ return err
+ }
+ for sheetName, tables := range tbls {
+ for _, table := range tables {
+ if table.Name == opts.DataRange {
+ opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true
+ return err
+ }
+ }
+ }
+ if !opts.namedDataRange {
+ opts.pivotDataRange = f.getDefinedNameRefTo(opts.DataRange, opts.pivotSheetName)
+ if opts.pivotDataRange != "" {
+ opts.namedDataRange = true
+ return nil
+ }
+ }
+ return newPivotTableDataRangeError(ErrParameterInvalid.Error())
+}
+
+// getPivotTable provides a function to get a pivot table definition by given
+// worksheet name, pivot table XML path and pivot cache relationship XML path.
+func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
+ var opts PivotTableOptions
+ rels, err := f.relsReader(pivotCacheRels)
+ if err != nil {
+ return opts, err
+ }
+ var pivotCacheXML string
+ for _, v := range rels.Relationships {
+ if v.Type == SourceRelationshipPivotCache {
+ pivotCacheXML = strings.ReplaceAll(v.Target, "..", "xl")
+ break
+ }
+ }
+ pc, err := f.pivotCacheReader(pivotCacheXML)
+ if err != nil {
+ return opts, err
+ }
+ pt, err := f.pivotTableReader(pivotTableXML)
+ if err != nil {
+ return opts, err
+ }
+ opts = PivotTableOptions{
+ pivotTableXML: pivotTableXML,
+ pivotCacheXML: pivotCacheXML,
+ pivotSheetName: sheet,
+ DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref),
+ PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref),
+ Name: pt.Name,
+ ClassicLayout: pt.GridDropZones,
+ FieldPrintTitles: pt.FieldPrintTitles,
+ ItemPrintTitles: pt.ItemPrintTitles,
+ }
+ if pc.CacheSource.WorksheetSource.Name != "" {
+ opts.DataRange = pc.CacheSource.WorksheetSource.Name
+ _ = f.getPivotTableDataRange(&opts)
+ }
+ setPtrFieldsVal([]string{
+ "RowGrandTotals", "ColGrandTotals", "ShowDrill",
+ "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError",
+ }, reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem())
+ if si := pt.PivotTableStyleInfo; si != nil {
+ opts.ShowRowHeaders = si.ShowRowHeaders
+ opts.ShowColHeaders = si.ShowColHeaders
+ opts.ShowRowStripes = si.ShowRowStripes
+ opts.ShowColStripes = si.ShowColStripes
+ opts.ShowLastColumn = si.ShowLastColumn
+ opts.PivotTableStyleName = si.Name
+ }
+ if err = f.getPivotTableDataRange(&opts); err != nil {
+ return opts, err
+ }
+ f.extractPivotTableFields(pc.getPivotCacheFieldsName(), pt, &opts)
+ return opts, err
+}
+
+// getPivotCacheFieldsName returns pivot table fields name list by order from
+// pivot cache fields.
+func (pc *xlsxPivotCacheDefinition) getPivotCacheFieldsName() []string {
+ var order []string
+ if pc.CacheFields != nil {
+ for _, cf := range pc.CacheFields.CacheField {
+ if cf != nil {
+ order = append(order, cf.Name)
+ }
+ }
+ }
+ return order
+}
+
+// pivotTableReader provides a function to get the pointer to the structure
+// after deserialization of xl/pivotTables/pivotTable%d.xml.
+func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) {
+ content, ok := f.Pkg.Load(path)
+ pivotTable := &xlsxPivotTableDefinition{}
+ if ok && content != nil {
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(pivotTable); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ return pivotTable, nil
+}
+
+// pivotCacheReader provides a function to get the pointer to the structure
+// after deserialization of xl/pivotCache/pivotCacheDefinition%d.xml.
+func (f *File) pivotCacheReader(path string) (*xlsxPivotCacheDefinition, error) {
+ content, ok := f.Pkg.Load(path)
+ pivotCache := &xlsxPivotCacheDefinition{}
+ if ok && content != nil {
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(pivotCache); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ return pivotCache, nil
+}
+
+// extractPivotTableFields provides a function to extract all pivot table fields
+// settings by given pivot table fields.
+func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinition, opts *PivotTableOptions) {
+ for fieldIdx, field := range pt.PivotFields.PivotField {
+ if field.Axis == "axisRow" {
+ opts.Rows = append(opts.Rows, extractPivotTableField(order[fieldIdx], field))
+ }
+ if field.Axis == "axisCol" {
+ opts.Columns = append(opts.Columns, extractPivotTableField(order[fieldIdx], field))
+ }
+ if field.Axis == "axisPage" {
+ opts.Filter = append(opts.Filter, extractPivotTableField(order[fieldIdx], field))
+ }
+ }
+ if pt.DataFields != nil {
+ for _, field := range pt.DataFields.DataField {
+ opts.Data = append(opts.Data, PivotTableField{
+ Data: order[field.Fld],
+ Name: field.Name,
+ Subtotal: cases.Title(language.English).String(field.Subtotal),
+ NumFmt: field.NumFmtID,
+ })
+ }
+ }
+}
+
+// extractPivotTableField provides a function to extract pivot table field
+// settings by given pivot table fields.
+func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField {
+ pivotTableField := PivotTableField{
+ Data: data,
+ ShowAll: fld.ShowAll,
+ InsertBlankRow: fld.InsertBlankRow,
+ }
+ setPtrFieldsVal([]string{"Compact", "Name", "Outline", "DefaultSubtotal"},
+ reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem())
+ return pivotTableField
+}
+
+// genPivotCacheDefinitionID generates a unique pivot table cache definition ID.
+func (f *File) genPivotCacheDefinitionID() int {
+ var (
+ ID int
+ decodeExtLst = new(decodeExtLst)
+ decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition)
+ )
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") {
+ pc, err := f.pivotCacheReader(k.(string))
+ if err != nil {
+ return true
+ }
+ if pc.ExtLst != nil {
+ _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst)
+ for _, ext := range decodeExtLst.Ext {
+ if ext.URI == ExtURIPivotCacheDefinition {
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition)
+ if ID < decodeX14PivotCacheDefinition.PivotCacheID {
+ ID = decodeX14PivotCacheDefinition.PivotCacheID
+ }
+ }
+ }
+ }
+ }
+ return true
+ })
+ return ID + 1
+}
+
+// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache
+// relationships.
+func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error {
+ rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/"))
+ if err != nil {
+ return err
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb.PivotCaches != nil {
+ for i, pivotCache := range wb.PivotCaches.PivotCache {
+ if pivotCache.RID == rID {
+ wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...)
+ }
+ }
+ if len(wb.PivotCaches.PivotCache) == 0 {
+ wb.PivotCaches = nil
+ }
+ }
+ return err
+}
+
+// DeletePivotTable delete a pivot table by giving the worksheet name and pivot
+// table name. Note that this function does not clean cell values in the pivot
+// table range.
+func (f *File) DeletePivotTable(sheet, name string) error {
+ sheetXML, ok := f.getSheetXMLPath(sheet)
+ if !ok {
+ return ErrSheetNotExist{sheet}
+ }
+ rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels"
+ sheetRels, err := f.relsReader(rels)
+ if err != nil {
+ return err
+ }
+ if sheetRels == nil {
+ sheetRels = &xlsxRelationships{}
+ }
+ opts, err := f.GetPivotTables(sheet)
+ if err != nil {
+ return err
+ }
+ pivotTableCaches := map[string]int{}
+ pivotTables, _ := f.getPivotTables()
+ for _, sheetPivotTables := range pivotTables {
+ for _, sheetPivotTable := range sheetPivotTables {
+ pivotTableCaches[sheetPivotTable.pivotCacheXML]++
+ }
+ }
+ for _, v := range sheetRels.Relationships {
+ for _, opt := range opts {
+ if v.Type == SourceRelationshipPivotTable {
+ pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
+ if opt.Name == name && opt.pivotTableXML == pivotTableXML {
+ if pivotTableCaches[opt.pivotCacheXML] == 1 {
+ err = f.deleteWorkbookPivotCache(opt)
+ }
+ f.deleteSheetRelationships(sheet, v.ID)
+ return err
+ }
+ }
+ }
+ }
+ return newNoExistTableError(name)
+}
+
+// getPivotTables provides a function to get all pivot tables in a workbook.
+func (f *File) getPivotTables() (map[string][]PivotTableOptions, error) {
+ pivotTables := map[string][]PivotTableOptions{}
+ for _, sheetName := range f.GetSheetList() {
+ pts, err := f.GetPivotTables(sheetName)
+ e := ErrSheetNotExist{sheetName}
+ if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
+ return pivotTables, err
+ }
+ pivotTables[sheetName] = append(pivotTables[sheetName], pts...)
+ }
+ return pivotTables, nil
+}
diff --git a/pivotTable_test.go b/pivotTable_test.go
index cc80835b18..21c2a1d4d0 100644
--- a/pivotTable_test.go
+++ b/pivotTable_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestAddPivotTable(t *testing.T) {
+func TestPivotTable(t *testing.T) {
f := NewFile()
// Create some data in a sheet
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
@@ -18,212 +18,509 @@ func TestAddPivotTable(t *testing.T) {
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
region := []string{"East", "West", "North", "South"}
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
- for i := 0; i < 30; i++ {
- assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]))
- assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]))
- assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]))
- assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)))
- assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]))
+ for row := 2; row < 32; row++ {
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)]))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
}
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$G$2:$M$34",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Filter: []PivotTableField{{Data: "Region"}},
- Columns: []PivotTableField{{Data: "Type"}},
- Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
- }))
+ expected := &PivotTableOptions{
+ pivotTableXML: "xl/pivotTables/pivotTable1.xml",
+ pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml",
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G2:M34",
+ Name: "PivotTable1",
+ Rows: []PivotTableField{{Data: "Month", ShowAll: true, DefaultSubtotal: true}, {Data: "Year"}},
+ Filter: []PivotTableField{{Data: "Region"}},
+ Columns: []PivotTableField{{Data: "Type", ShowAll: true, InsertBlankRow: true, DefaultSubtotal: true}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ClassicLayout: true,
+ ShowError: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ FieldPrintTitles: true,
+ ItemPrintTitles: true,
+ PivotTableStyleName: "PivotStyleLight16",
+ }
+ assert.NoError(t, f.AddPivotTable(expected))
+ // Test get pivot table
+ pivotTables, err := f.GetPivotTables("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, pivotTables, 1)
+ assert.Equal(t, *expected, pivotTables[0])
// Use different order of coordinate tests
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
}))
+ // Test get pivot table with default style name
+ pivotTables, err = f.GetPivotTables("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, pivotTables, 2)
+ assert.Equal(t, "PivotStyleLight16", pivotTables[1].PivotTableStyleName)
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$W$2:$AC$34",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!W2:AC34",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
}))
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$G$37:$W$50",
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G42:W55",
Rows: []PivotTableField{{Data: "Month"}},
- Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ }))
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!AE2:AG33",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
}))
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$AE$2:$AG$33",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}},
+ // Create pivot table with empty subtotal field name and specified style
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!AJ2:AP135",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Filter: []PivotTableField{{Data: "Region"}},
+ Columns: []PivotTableField{},
+ Data: []PivotTableField{{Subtotal: "Sum", Name: "Summarize by Sum"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ PivotTableStyleName: "PivotStyleLight19",
}))
- f.NewSheet("Sheet2")
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet2!$A$1:$AR$15",
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet2!A1:AN17",
Rows: []PivotTableField{{Data: "Month"}},
- Columns: []PivotTableField{{Data: "Region"}, {Data: "Type"}, {Data: "Year"}},
- Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}},
+ Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min", NumFmt: 32}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ }))
+
+ // Test get pivot table with across worksheet data range
+ pivotTables, err = f.GetPivotTables("Sheet2")
+ assert.NoError(t, err)
+ assert.Len(t, pivotTables, 1)
+ assert.Equal(t, "Sheet1!A1:E31", pivotTables[0].DataRange)
+
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet2!A20:AR60",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
+ Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product", NumFmt: 32}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
}))
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet2!$A$18:$AR$54",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Type"}},
- Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}},
- Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}},
+ // Create pivot table with many data, many rows, many cols and defined name
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "dataRange",
+ RefersTo: "Sheet1!A1:E31",
+ Comment: "Pivot Table Data Range",
+ Scope: "Sheet2",
+ }))
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "dataRange",
+ PivotTableRange: "Sheet2!A65:AJ100",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales", NumFmt: -1}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales", NumFmt: 38}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
}))
// Test empty pivot table options
- assert.EqualError(t, f.AddPivotTable(nil), "parameter is required")
+ assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil))
+ // Test add pivot table with custom name which exceeds the max characters limit
+ assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "dataRange",
+ PivotTableRange: "Sheet2!A65:AJ100",
+ Name: strings.Repeat("c", MaxFieldLength+1),
+ }))
// Test invalid data range
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$A$1",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:A1",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), `parameter 'DataRange' parsing error: parameter is invalid`)
+ }))
// Test the data range of the worksheet that is not declared
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
+ DataRange: "A1:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), `parameter 'DataRange' parsing error: parameter is invalid`)
+ }))
// Test the worksheet declared in the data range does not exist
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "SheetN!$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "SheetN!A1:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), "sheet SheetN is not exist")
+ }))
// Test the pivot table range of the worksheet that is not declared
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, newPivotTableRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), `parameter 'PivotTableRange' parsing error: parameter is invalid`)
+ }))
// Test the worksheet declared in the pivot table range does not exist
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "SheetN!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "SheetN!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), "sheet SheetN is not exist")
+ }))
// Test not exists worksheet in data range
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "SheetN!$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "SheetN!A1:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), "sheet SheetN is not exist")
+ }))
// Test invalid row number in data range
- assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$0:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.Equal(t, newPivotTableDataRangeError(newCellNameToCoordinatesError("A0", newInvalidCellNameError("A0")).Error()), f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A0:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
- }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`)
+ }))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx")))
// Test with field names that exceed the length limit and invalid subtotal
- assert.NoError(t, f.AddPivotTable(&PivotTableOption{
- DataRange: "Sheet1!$A$1:$E$31",
- PivotTableRange: "Sheet1!$G$2:$M$34",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
- Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", 256)}},
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G2:M34",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
}))
+ // Test delete pivot table
+ pivotTables, err = f.GetPivotTables("Sheet1")
+ assert.Len(t, pivotTables, 7)
+ assert.NoError(t, err)
+ assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
+ pivotTables, err = f.GetPivotTables("Sheet1")
+ assert.Len(t, pivotTables, 6)
+ assert.NoError(t, err)
+ // Test add pivot table with invalid sheet name
+ assert.Error(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet:1!A1:E31",
+ PivotTableRange: "Sheet:1!G2:M34",
+ Rows: []PivotTableField{{Data: "Year"}},
+ }), ErrSheetNameInvalid)
+ // Test add pivot table with enable ClassicLayout and CompactData in the same time
+ assert.Error(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G2:M34",
+ CompactData: true,
+ ClassicLayout: true,
+ }), ErrPivotTableClassicLayout)
+ // Test delete pivot table with not exists worksheet
+ assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist")
+ // Test delete pivot table with not exists pivot table name
+ assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist")
// Test adjust range with invalid range
- _, _, err := f.adjustRange("")
- assert.EqualError(t, err, "parameter is required")
- // Test get pivot fields order with empty data range
- _, err = f.getPivotFieldsOrder("")
+ _, _, err = f.adjustRange("")
+ assert.Error(t, err, ErrParameterRequired)
+ // Test adjust range with incorrect range
+ _, _, err = f.adjustRange("sheet1!")
+ assert.EqualError(t, err, "parameter is invalid")
+ // Test get table fields order with empty data range
+ _, err = f.getTableFieldsOrder(&PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range
- assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{}, nil), "parameter 'DataRange' parsing error: parameter is required")
- // Test add pivot cache with invalid data range
- assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{
- DataRange: "$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
- Data: []PivotTableField{{Data: "Sales"}},
- }, nil), "parameter 'DataRange' parsing error: parameter is invalid")
+ assert.EqualError(t, f.addPivotCache(&PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
// Test add pivot table with empty options
- assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required")
+ assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot table with invalid data range
- assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required")
+ assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot fields with empty data range
- assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{
- DataRange: "$A$1:$E$31",
- PivotTableRange: "Sheet1!$U$34:$O$2",
- Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}},
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
+ DataRange: "A1:E31",
+ PivotTableRange: "Sheet1!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
// Test get pivot fields index with empty data range
- _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOption{})
+ _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
+ // Test add pivot table with unsupported charset content types.
+ f = NewFile()
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G2:M34",
+ Rows: []PivotTableField{{Data: "Year"}},
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test get pivot table without pivot table
+ f = NewFile()
+ pivotTables, err = f.GetPivotTables("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, pivotTables, 0)
+ // Test get pivot table with not exists worksheet
+ _, err = f.GetPivotTables("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get pivot table with unsupported charset worksheet relationships
+ f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
+ _, err = f.GetPivotTables("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+ // Test get pivot table with unsupported charset pivot cache definition
+ f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetPivotTables("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+ // Test get pivot table with unsupported charset pivot table relationships
+ f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/pivotTables/_rels/pivotTable1.xml.rels", MacintoshCyrillicCharset)
+ _, err = f.GetPivotTables("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+ // Test get pivot table with unsupported charset pivot table
+ f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetPivotTables("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ _, err = f.getPivotTables()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestPivotTableDataRange(t *testing.T) {
+ f := NewFile()
+ // Create table in a worksheet
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ for row := 2; row < 6; row++ {
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10)))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10)))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10)))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10)))
+ }
+ // Test add pivot table with table data range
+ opts := PivotTableOptions{
+ DataRange: "Table1",
+ PivotTableRange: "Sheet1!G2:K7",
+ Rows: []PivotTableField{{Data: "Column1"}},
+ Columns: []PivotTableField{{Data: "Column2"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ ShowError: true,
+ PivotTableStyleName: "PivotStyleLight16",
+ }
+ assert.NoError(t, f.AddPivotTable(&opts))
+ assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx")))
+ assert.NoError(t, f.Close())
+
+ assert.NoError(t, f.AddPivotTable(&opts))
+
+ // Test delete pivot table with unsupported table relationships charset
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test delete pivot table with unsupported worksheet relationships charset
+ f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
+ f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test delete pivot table without worksheet relationships
+ f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
+ f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
+ assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
+
+ t.Run("data_range_with_empty_column", func(t *testing.T) {
+ // Test add pivot table with data range doesn't organized as a list with labeled columns
+ f := NewFile()
+ // Create some data in a sheet
+ month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
+ types := []string{"Meat", "Dairy", "Beverages", "Produce"}
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "", "Type"}))
+ for row := 2; row < 32; row++ {
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
+ assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
+ }
+ assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet1!A1:E31",
+ PivotTableRange: "Sheet1!G2:M34",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}},
+ Data: []PivotTableField{{Data: "Type"}},
+ }))
+ })
+}
+
+func TestParseFormatPivotTableSet(t *testing.T) {
+ f := NewFile()
+ // Create table in a worksheet
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ // Test parse format pivot table options with unsupported table relationships charset
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ _, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{
+ DataRange: "Table1",
+ PivotTableRange: "Sheet1!G2:K7",
+ Rows: []PivotTableField{{Data: "Column1"}},
+ })
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddPivotRowFields(t *testing.T) {
f := NewFile()
// Test invalid data range
- assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
- DataRange: "Sheet1!$A$1:$A$1",
+ assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
+ DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
func TestAddPivotPageFields(t *testing.T) {
f := NewFile()
// Test invalid data range
- assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
- DataRange: "Sheet1!$A$1:$A$1",
+ assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
+ DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
func TestAddPivotDataFields(t *testing.T) {
f := NewFile()
// Test invalid data range
- assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
- DataRange: "Sheet1!$A$1:$A$1",
+ assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
+ DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
func TestAddPivotColFields(t *testing.T) {
f := NewFile()
// Test invalid data range
- assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
- DataRange: "Sheet1!$A$1:$A$1",
- Columns: []PivotTableField{{Data: "Type"}},
+ assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
+ DataRange: "Sheet1!A1:A1",
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile()
- // Test get pivot fields order with not exist worksheet
- _, err := f.getPivotFieldsOrder("SheetN!$A$1:$E$31")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ // Test get table fields order with not exist worksheet
+ _, err := f.getTableFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"})
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Create table in a worksheet
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ // Test get table fields order with unsupported table relationships charset
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ _, err = f.getTableFieldsOrder(&PivotTableOptions{DataRange: "Table"})
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
-func TestInStrSlice(t *testing.T) {
- assert.EqualValues(t, -1, inStrSlice([]string{}, ""))
+func TestGetPivotTableFieldName(t *testing.T) {
+ f := NewFile()
+ assert.Empty(t, f.getPivotTableFieldName("-", []PivotTableField{}))
}
-func TestGetPivotTableFieldName(t *testing.T) {
+func TestGetPivotTableFieldOptions(t *testing.T) {
f := NewFile()
- f.getPivotTableFieldName("-", []PivotTableField{})
+ _, ok := f.getPivotTableFieldOptions("-", []PivotTableField{})
+ assert.False(t, ok)
+}
+
+func TestGenPivotCacheDefinitionID(t *testing.T) {
+ f := NewFile()
+ // Test generate pivot table cache definition ID with unsupported charset
+ f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
+ assert.Equal(t, 1, f.genPivotCacheDefinitionID())
+ assert.NoError(t, f.Close())
+}
+
+func TestDeleteWorkbookPivotCache(t *testing.T) {
+ f := NewFile()
+ // Test delete workbook pivot table cache with unsupported workbook charset
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test delete workbook pivot table cache with unsupported workbook relationships charset
+ f.Relationships.Delete("xl/_rels/workbook.xml.rels")
+ f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/rows.go b/rows.go
index 320ba2fdf2..436a5d6abf 100644
--- a/rows.go
+++ b/rows.go
@@ -1,72 +1,134 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
"encoding/xml"
- "errors"
- "fmt"
"io"
- "log"
"math"
+ "os"
"strconv"
+ "strings"
+
+ "github.com/tiendc/go-deepcopy"
)
-// GetRows return all the rows in a sheet by given worksheet name (case
-// sensitive). For example:
+// duplicateHelperFunc defines functions to duplicate helper.
+var duplicateHelperFunc = [3]func(*File, *xlsxWorksheet, string, int, int) error{
+ func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ return f.duplicateConditionalFormat(ws, sheet, row, row2)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ return f.duplicateDataValidations(ws, sheet, row, row2)
+ },
+ func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ return f.duplicateMergeCells(ws, sheet, row, row2)
+ },
+}
+
+// GetRows return all the rows in a sheet by given worksheet name, returned as
+// a two-dimensional array, where the value of the cell is converted to the
+// string type. If the cell format can be applied to the value of the cell,
+// the applied value will be used, otherwise the original value will be used.
+// GetRows fetched the rows with value or formula cells, the continually blank
+// cells in the tail of each row will be skipped, so the length of each row
+// may be inconsistent.
//
-// rows, err := f.GetRows("Sheet1")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// for _, row := range rows {
-// for _, colCell := range row {
-// fmt.Print(colCell, "\t")
-// }
-// fmt.Println()
-// }
+// For example, get and traverse the value of all cells by rows on a worksheet
+// named 'Sheet1':
//
-func (f *File) GetRows(sheet string) ([][]string, error) {
+// rows, err := f.GetRows("Sheet1")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// for _, row := range rows {
+// for _, colCell := range row {
+// fmt.Print(colCell, "\t")
+// }
+// fmt.Println()
+// }
+func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {
rows, err := f.Rows(sheet)
if err != nil {
return nil, err
}
- results := make([][]string, 0, 64)
+ results, cur, maxVal := make([][]string, 0, 64), 0, 0
for rows.Next() {
- row, err := rows.Columns()
+ cur++
+ row, err := rows.Columns(opts...)
if err != nil {
break
}
- results = append(results, row)
+ if len(row) > 0 {
+ if emptyRows := cur - maxVal - 1; emptyRows > 0 {
+ results = append(results, make([][]string, emptyRows)...)
+ }
+ results = append(results, row)
+ maxVal = cur
+ }
}
- return results, nil
+ return results[:maxVal], rows.Close()
}
// Rows defines an iterator to a sheet.
type Rows struct {
- err error
- curRow, totalRow, stashRow int
- sheet string
- rows []xlsxRow
- f *File
- decoder *xml.Decoder
+ err error
+ curRow, seekRow int
+ needClose, rawCellValue bool
+ sheet string
+ f *File
+ tempFile *os.File
+ sst *xlsxSST
+ decoder *xml.Decoder
+ token xml.Token
+ curRowOpts, seekRowOpts RowOpts
}
-// Next will return true if find the next row element.
+// Next will return true if it finds the next row element.
func (rows *Rows) Next() bool {
- rows.curRow++
- return rows.curRow <= rows.totalRow
+ rows.seekRow++
+ if rows.curRow >= rows.seekRow {
+ rows.curRowOpts = rows.seekRowOpts
+ return true
+ }
+ for {
+ token, _ := rows.decoder.Token()
+ if token == nil {
+ return false
+ }
+ switch xmlElement := token.(type) {
+ case xml.StartElement:
+ if xmlElement.Name.Local == "row" {
+ rows.curRow++
+ if rowNum, _ := attrValToInt("r", xmlElement.Attr); rowNum != 0 {
+ rows.curRow = rowNum
+ }
+ rows.token = token
+ rows.curRowOpts = extractRowOpts(xmlElement.Attr)
+ return true
+ }
+ case xml.EndElement:
+ if xmlElement.Name.Local == "sheetData" {
+ return false
+ }
+ }
+ }
+}
+
+// GetRowOpts will return the RowOpts of the current row.
+func (rows *Rows) GetRowOpts() RowOpts {
+ return rows.curRowOpts
}
// Error will return the error when the error occurs.
@@ -74,64 +136,78 @@ func (rows *Rows) Error() error {
return rows.err
}
-// Columns return the current row's column values.
-func (rows *Rows) Columns() ([]string, error) {
- var (
- err error
- inElement string
- row, cellCol int
- columns []string
- )
-
- if rows.stashRow >= rows.curRow {
- return columns, err
+// Close closes the open worksheet XML file in the system temporary
+// directory.
+func (rows *Rows) Close() error {
+ if rows.tempFile != nil {
+ return rows.tempFile.Close()
}
+ return nil
+}
- d := rows.f.sharedStringsReader()
+// Columns return the current row's column values. This fetches the worksheet
+// data as a stream, returns each cell in a row as is, and will not skip empty
+// rows in the tail of the worksheet.
+func (rows *Rows) Columns(opts ...Options) ([]string, error) {
+ if rows.curRow > rows.seekRow {
+ return nil, nil
+ }
+ var rowIterator rowXMLIterator
+ var token xml.Token
+ rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue
+ if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
+ return rowIterator.cells, rowIterator.err
+ }
for {
- token, _ := rows.decoder.Token()
- if token == nil {
+ if rows.token != nil {
+ token = rows.token
+ } else if token, _ = rows.decoder.Token(); token == nil {
break
}
- switch startElement := token.(type) {
+ switch xmlElement := token.(type) {
case xml.StartElement:
- inElement = startElement.Name.Local
- if inElement == "row" {
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- row, err = strconv.Atoi(attr.Value)
- if err != nil {
- return columns, err
- }
- if row > rows.curRow {
- rows.stashRow = row - 1
- return columns, err
- }
- }
+ rowIterator.inElement = xmlElement.Name.Local
+ if rowIterator.inElement == "row" {
+ rowNum := 0
+ if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 {
+ rows.curRow = rowNum
+ } else if rows.token == nil {
+ rows.curRow++
}
- }
- if inElement == "c" {
- cellCol++
- colCell := xlsxC{}
- _ = rows.decoder.DecodeElement(&colCell, &startElement)
- if colCell.R != "" {
- cellCol, _, err = CellNameToCoordinates(colCell.R)
- if err != nil {
- return columns, err
- }
+ rows.token = token
+ rows.seekRowOpts = extractRowOpts(xmlElement.Attr)
+ if rows.curRow > rows.seekRow {
+ rows.token = nil
+ return rowIterator.cells, rowIterator.err
}
- blank := cellCol - len(columns)
- val, _ := colCell.getValueFrom(rows.f, d)
- columns = append(appendSpace(blank, columns), val)
}
+ if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {
+ rows.token = nil
+ return rowIterator.cells, rowIterator.err
+ }
+ rows.token = nil
case xml.EndElement:
- inElement = startElement.Name.Local
- if inElement == "row" {
- return columns, err
+ if xmlElement.Name.Local == "sheetData" {
+ return rowIterator.cells, rowIterator.err
}
}
}
- return columns, err
+ return rowIterator.cells, rowIterator.err
+}
+
+// extractRowOpts extract row element attributes.
+func extractRowOpts(attrs []xml.Attr) RowOpts {
+ rowOpts := RowOpts{Height: defaultRowHeight}
+ if styleID, err := attrValToInt("s", attrs); err == nil && styleID > 0 && styleID < MaxCellStyles {
+ rowOpts.StyleID = styleID
+ }
+ if hidden, err := attrValToBool("hidden", attrs); err == nil {
+ rowOpts.Hidden = hidden
+ }
+ if height, err := attrValToFloat("ht", attrs); err == nil {
+ rowOpts.Height = height
+ }
+ return rowOpts
}
// appendSpace append blank characters to slice by given length and source slice.
@@ -142,154 +218,238 @@ func appendSpace(l int, s []string) []string {
return s
}
-// ErrSheetNotExist defines an error of sheet is not exist
-type ErrSheetNotExist struct {
- SheetName string
+// rowXMLIterator defined runtime use field for the worksheet row SAX parser.
+type rowXMLIterator struct {
+ err error
+ inElement string
+ cellCol, cellRow int
+ cells []string
}
-func (err ErrSheetNotExist) Error() string {
- return fmt.Sprintf("sheet %s is not exist", string(err.SheetName))
+// rowXMLHandler parse the row XML element of the worksheet.
+func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) {
+ if rowIterator.inElement == "c" {
+ rowIterator.cellCol++
+ colCell := xlsxC{}
+ _ = rows.decoder.DecodeElement(&colCell, xmlElement)
+ if colCell.R != "" {
+ if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil {
+ return
+ }
+ }
+ blank := rowIterator.cellCol - len(rowIterator.cells)
+ if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil {
+ rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val)
+ }
+ }
}
// Rows returns a rows iterator, used for streaming reading data for a
-// worksheet with a large data. For example:
-//
-// rows, err := f.Rows("Sheet1")
-// if err != nil {
-// fmt.Println(err)
-// return
-// }
-// for rows.Next() {
-// row, err := rows.Columns()
-// if err != nil {
-// fmt.Println(err)
-// }
-// for _, colCell := range row {
-// fmt.Print(colCell, "\t")
-// }
-// fmt.Println()
-// }
+// worksheet with a large data. This function is concurrency safe. For
+// example:
//
+// rows, err := f.Rows("Sheet1")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// for rows.Next() {
+// row, err := rows.Columns()
+// if err != nil {
+// fmt.Println(err)
+// }
+// for _, colCell := range row {
+// fmt.Print(colCell, "\t")
+// }
+// fmt.Println()
+// }
+// if err = rows.Close(); err != nil {
+// fmt.Println(err)
+// }
func (f *File) Rows(sheet string) (*Rows, error) {
- name, ok := f.sheetMap[trimSheetName(sheet)]
+ if err := checkSheetName(sheet); err != nil {
+ return nil, err
+ }
+ name, ok := f.getSheetXMLPath(sheet)
if !ok {
return nil, ErrSheetNotExist{sheet}
}
- if f.Sheet[name] != nil {
- // flush data
- output, _ := xml.Marshal(f.Sheet[name])
+ if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
+ ws := worksheet.(*xlsxWorksheet)
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ // Flush data
+ output, _ := xml.Marshal(ws)
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
}
+ var err error
+ rows := Rows{f: f, sheet: name}
+ rows.needClose, rows.decoder, rows.tempFile, err = f.xmlDecoder(name)
+ return &rows, err
+}
+
+// getFromStringItem build shared string item offset list from system temporary
+// file at one time, and return value by given to string index.
+func (f *File) getFromStringItem(index int) string {
+ if f.sharedStringTemp != nil {
+ if len(f.sharedStringItem) <= index {
+ return strconv.Itoa(index)
+ }
+ offsetRange := f.sharedStringItem[index]
+ buf := make([]byte, offsetRange[1]-offsetRange[0])
+ if _, err := f.sharedStringTemp.ReadAt(buf, int64(offsetRange[0])); err != nil {
+ return strconv.Itoa(index)
+ }
+ return string(buf)
+ }
+ needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings)
+ if needClose && err == nil {
+ defer func() {
+ err = tempFile.Close()
+ }()
+ }
+ f.sharedStringItem = [][]uint{}
+ f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-")
+ f.tempFiles.Store(defaultTempFileSST, f.sharedStringTemp.Name())
var (
- err error
inElement string
- row int
- rows Rows
+ i, offset uint
)
- decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
for {
token, _ := decoder.Token()
if token == nil {
break
}
- switch startElement := token.(type) {
+ switch xmlElement := token.(type) {
case xml.StartElement:
- inElement = startElement.Name.Local
- if inElement == "row" {
- row++
- for _, attr := range startElement.Attr {
- if attr.Name.Local == "r" {
- row, err = strconv.Atoi(attr.Value)
- if err != nil {
- return &rows, err
- }
- }
- }
- rows.totalRow = row
+ inElement = xmlElement.Name.Local
+ if inElement == "si" {
+ si := xlsxSI{}
+ _ = decoder.DecodeElement(&si, &xmlElement)
+
+ startIdx := offset
+ n, _ := f.sharedStringTemp.WriteString(si.String())
+ offset += uint(n)
+ f.sharedStringItem = append(f.sharedStringItem, []uint{startIdx, offset})
+ i++
}
- default:
}
}
- rows.f = f
- rows.sheet = name
- rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
- return &rows, nil
+ return f.getFromStringItem(index)
}
-// SetRowHeight provides a function to set the height of a single row. For
-// example, set the height of the first row in Sheet1:
-//
-// err := f.SetRowHeight("Sheet1", 1, 50)
+// xmlDecoder creates XML decoder by given path in the zip from memory data
+// or system temporary file.
+func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
+ var (
+ content []byte
+ err error
+ tempFile *os.File
+ )
+ if content = f.readXML(name); len(content) > 0 {
+ return false, f.xmlNewDecoder(bytes.NewReader(content)), tempFile, err
+ }
+ tempFile, err = f.readTemp(name)
+ return true, f.xmlNewDecoder(tempFile), tempFile, err
+}
+
+// SetRowHeight provides a function to set the height of a single row. If the
+// value of height is 0, will hide the specified row, if the value of height is
+// -1, will unset the custom row height. For example, set the height of the
+// first row in Sheet1:
//
+// err := f.SetRowHeight("Sheet1", 1, 50)
func (f *File) SetRowHeight(sheet string, row int, height float64) error {
if row < 1 {
return newInvalidRowNumberError(row)
}
-
- xlsx, err := f.workSheetReader(sheet)
+ if height > MaxRowHeight {
+ return ErrMaxRowHeight
+ }
+ if height < -1 {
+ return ErrParameterInvalid
+ }
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- prepareSheetXML(xlsx, 0, row)
+ ws.prepareSheetXML(0, row)
rowIdx := row - 1
- xlsx.SheetData.Row[rowIdx].Ht = height
- xlsx.SheetData.Row[rowIdx].CustomHeight = true
- return nil
+ if height == -1 {
+ ws.SheetData.Row[rowIdx].Ht = nil
+ ws.SheetData.Row[rowIdx].CustomHeight = false
+ return err
+ }
+ ws.SheetData.Row[rowIdx].Ht = float64Ptr(height)
+ ws.SheetData.Row[rowIdx].CustomHeight = true
+ return err
}
// getRowHeight provides a function to get row height in pixels by given sheet
-// name and row index.
+// name and row number.
func (f *File) getRowHeight(sheet string, row int) int {
- xlsx, _ := f.workSheetReader(sheet)
- for i := range xlsx.SheetData.Row {
- v := &xlsx.SheetData.Row[i]
- if v.R == row+1 && v.Ht != 0 {
- return int(convertRowHeightToPixels(v.Ht))
+ ws, _ := f.workSheetReader(sheet)
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ for i := range ws.SheetData.Row {
+ v := &ws.SheetData.Row[i]
+ if v.R == row && v.Ht != nil {
+ return int(convertRowHeightToPixels(*v.Ht))
}
}
- // Optimisation for when the row heights haven't changed.
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultRowHeight > 0 {
+ return int(convertRowHeightToPixels(ws.SheetFormatPr.DefaultRowHeight))
+ }
+ // Optimization for when the row heights haven't changed.
return int(defaultRowHeightPixels)
}
// GetRowHeight provides a function to get row height by given worksheet name
-// and row index. For example, get the height of the first row in Sheet1:
-//
-// height, err := f.GetRowHeight("Sheet1", 1)
+// and row number. For example, get the height of the first row in Sheet1:
//
+// height, err := f.GetRowHeight("Sheet1", 1)
func (f *File) GetRowHeight(sheet string, row int) (float64, error) {
if row < 1 {
- return defaultRowHeightPixels, newInvalidRowNumberError(row)
+ return defaultRowHeight, newInvalidRowNumberError(row)
}
-
- xlsx, err := f.workSheetReader(sheet)
+ ht := defaultRowHeight
+ ws, err := f.workSheetReader(sheet)
if err != nil {
- return defaultRowHeightPixels, err
+ return ht, err
}
- if row > len(xlsx.SheetData.Row) {
- return defaultRowHeightPixels, nil // it will be better to use 0, but we take care with BC
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.CustomHeight {
+ ht = ws.SheetFormatPr.DefaultRowHeight
}
- for _, v := range xlsx.SheetData.Row {
- if v.R == row && v.Ht != 0 {
- return v.Ht, nil
+ if row > len(ws.SheetData.Row) {
+ return ht, nil // it will be better to use 0, but we take care with BC
+ }
+ for _, v := range ws.SheetData.Row {
+ if v.R == row && v.Ht != nil {
+ return *v.Ht, nil
}
}
- // Optimisation for when the row heights haven't changed.
- return defaultRowHeightPixels, nil
+ // Optimization for when the row heights haven't changed.
+ return ht, nil
}
// sharedStringsReader provides a function to get the pointer to the structure
// after deserialization of xl/sharedStrings.xml.
-func (f *File) sharedStringsReader() *xlsxSST {
+func (f *File) sharedStringsReader() (*xlsxSST, error) {
var err error
-
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ relPath := f.getWorkbookRelsPath()
if f.SharedStrings == nil {
var sharedStrings xlsxSST
- ss := f.readXML("xl/sharedStrings.xml")
+ ss := f.readXML(defaultXMLPathSharedStrings)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))).
Decode(&sharedStrings); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return f.SharedStrings, err
+ }
+ if sharedStrings.Count == 0 {
+ sharedStrings.Count = len(sharedStrings.SI)
}
if sharedStrings.UniqueCount == 0 {
sharedStrings.UniqueCount = sharedStrings.Count
@@ -300,62 +460,40 @@ func (f *File) sharedStringsReader() *xlsxSST {
f.sharedStringsMap[sharedStrings.SI[i].T.Val] = i
}
}
- f.addContentTypePart(0, "sharedStrings")
- rels := f.relsReader("xl/_rels/workbook.xml.rels")
+ if err = f.addContentTypePart(0, "sharedStrings"); err != nil {
+ return f.SharedStrings, err
+ }
+ rels, err := f.relsReader(relPath)
+ if err != nil {
+ return f.SharedStrings, err
+ }
for _, rel := range rels.Relationships {
- if rel.Target == "sharedStrings.xml" {
- return f.SharedStrings
+ if rel.Target == "/xl/sharedStrings.xml" {
+ return f.SharedStrings, nil
}
}
- // Update xl/_rels/workbook.xml.rels
- f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "")
+ // Update workbook.xml.rels
+ f.addRels(relPath, SourceRelationshipSharedStrings, "/xl/sharedStrings.xml", "")
}
- return f.SharedStrings
-}
-
-// getValueFrom return a value from a column/row cell, this function is
-// inteded to be used with for range on rows an argument with the xlsx opened
-// file.
-func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) {
- switch xlsx.T {
- case "s":
- if xlsx.V != "" {
- xlsxSI := 0
- xlsxSI, _ = strconv.Atoi(xlsx.V)
- if len(d.SI) > xlsxSI {
- return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil
- }
- }
- return f.formattedValue(xlsx.S, xlsx.V), nil
- case "str":
- return f.formattedValue(xlsx.S, xlsx.V), nil
- case "inlineStr":
- if xlsx.IS != nil {
- return f.formattedValue(xlsx.S, xlsx.IS.String()), nil
- }
- return f.formattedValue(xlsx.S, xlsx.V), nil
- default:
- return f.formattedValue(xlsx.S, xlsx.V), nil
- }
+ return f.SharedStrings, nil
}
// SetRowVisible provides a function to set visible of a single row by given
// worksheet name and Excel row number. For example, hide row 2 in Sheet1:
//
-// err := f.SetRowVisible("Sheet1", 2, false)
-//
+// err := f.SetRowVisible("Sheet1", 2, false)
func (f *File) SetRowVisible(sheet string, row int, visible bool) error {
if row < 1 {
return newInvalidRowNumberError(row)
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- prepareSheetXML(xlsx, 0, row)
- xlsx.SheetData.Row[row-1].Hidden = !visible
+ ws.prepareSheetXML(0, row)
+ ws.SheetData.Row[row-1].Hidden = !visible
return nil
}
@@ -363,42 +501,40 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error {
// worksheet name and Excel row number. For example, get visible state of row
// 2 in Sheet1:
//
-// visible, err := f.GetRowVisible("Sheet1", 2)
-//
+// visible, err := f.GetRowVisible("Sheet1", 2)
func (f *File) GetRowVisible(sheet string, row int) (bool, error) {
if row < 1 {
return false, newInvalidRowNumberError(row)
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return false, err
}
- if row > len(xlsx.SheetData.Row) {
+ if row > len(ws.SheetData.Row) {
return false, nil
}
- return !xlsx.SheetData.Row[row-1].Hidden, nil
+ return !ws.SheetData.Row[row-1].Hidden, nil
}
// SetRowOutlineLevel provides a function to set outline level number of a
// single row by given worksheet name and Excel row number. The value of
// parameter 'level' is 1-7. For example, outline row 2 in Sheet1 to level 1:
//
-// err := f.SetRowOutlineLevel("Sheet1", 2, 1)
-//
+// err := f.SetRowOutlineLevel("Sheet1", 2, 1)
func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error {
if row < 1 {
return newInvalidRowNumberError(row)
}
if level > 7 || level < 1 {
- return errors.New("invalid outline level")
+ return ErrOutlineLevel
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- prepareSheetXML(xlsx, 0, row)
- xlsx.SheetData.Row[row-1].OutlineLevel = level
+ ws.prepareSheetXML(0, row)
+ ws.SheetData.Row[row-1].OutlineLevel = level
return nil
}
@@ -406,26 +542,25 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error {
// single row by given worksheet name and Excel row number. For example, get
// outline number of row 2 in Sheet1:
//
-// level, err := f.GetRowOutlineLevel("Sheet1", 2)
-//
+// level, err := f.GetRowOutlineLevel("Sheet1", 2)
func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) {
if row < 1 {
return 0, newInvalidRowNumberError(row)
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return 0, err
}
- if row > len(xlsx.SheetData.Row) {
+ if row > len(ws.SheetData.Row) {
return 0, nil
}
- return xlsx.SheetData.Row[row-1].OutlineLevel, nil
+ return ws.SheetData.Row[row-1].OutlineLevel, nil
}
// RemoveRow provides a function to remove single row by given worksheet name
// and Excel row number. For example, remove row 3 in Sheet1:
//
-// err := f.RemoveRow("Sheet1", 3)
+// err := f.RemoveRow("Sheet1", 3)
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
@@ -436,45 +571,51 @@ func (f *File) RemoveRow(sheet string, row int) error {
return newInvalidRowNumberError(row)
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if row > len(xlsx.SheetData.Row) {
+ if row > len(ws.SheetData.Row) {
return f.adjustHelper(sheet, rows, row, -1)
}
keep := 0
- for rowIdx := 0; rowIdx < len(xlsx.SheetData.Row); rowIdx++ {
- v := &xlsx.SheetData.Row[rowIdx]
+ for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ {
+ v := &ws.SheetData.Row[rowIdx]
if v.R != row {
- xlsx.SheetData.Row[keep] = *v
+ ws.SheetData.Row[keep] = *v
keep++
}
}
- xlsx.SheetData.Row = xlsx.SheetData.Row[:keep]
+ ws.SheetData.Row = ws.SheetData.Row[:keep]
return f.adjustHelper(sheet, rows, row, -1)
}
-// InsertRow provides a function to insert a new row after given Excel row
-// number starting from 1. For example, create a new row before row 3 in
-// Sheet1:
+// InsertRows provides a function to insert new rows after the given Excel row
+// number starting from 1 and number of rows. For example, create two rows
+// before row 3 in Sheet1:
//
-// err := f.InsertRow("Sheet1", 3)
+// err := f.InsertRows("Sheet1", 3, 2)
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
-func (f *File) InsertRow(sheet string, row int) error {
+func (f *File) InsertRows(sheet string, row, n int) error {
if row < 1 {
return newInvalidRowNumberError(row)
}
- return f.adjustHelper(sheet, rows, row, 1)
+ if row >= TotalRows || n >= TotalRows {
+ return ErrMaxRows
+ }
+ if n < 1 {
+ return ErrParameterInvalid
+ }
+ return f.adjustHelper(sheet, rows, row, n)
}
// DuplicateRow inserts a copy of specified row (by its Excel row number) below
//
-// err := f.DuplicateRow("Sheet1", 2)
+// err := f.DuplicateRow("Sheet1", 2)
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
@@ -487,7 +628,7 @@ func (f *File) DuplicateRow(sheet string, row int) error {
// DuplicateRowTo inserts a copy of specified row by it Excel number
// to specified row position moving down exists rows after target position
//
-// err := f.DuplicateRowTo("Sheet1", 2, 7)
+// err := f.DuplicateRowTo("Sheet1", 2, 7)
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
@@ -498,65 +639,149 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
return newInvalidRowNumberError(row)
}
- xlsx, err := f.workSheetReader(sheet)
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 {
- return nil
+
+ if row2 < 1 || row == row2 {
+ return err
}
var ok bool
var rowCopy xlsxRow
- for i, r := range xlsx.SheetData.Row {
+ for i, r := range ws.SheetData.Row {
if r.R == row {
- rowCopy = xlsx.SheetData.Row[i]
+ deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
ok = true
break
}
}
- if !ok {
- return nil
- }
if err := f.adjustHelper(sheet, rows, row2, 1); err != nil {
return err
}
+ if !ok {
+ return err
+ }
+
idx2 := -1
- for i, r := range xlsx.SheetData.Row {
+ for i, r := range ws.SheetData.Row {
if r.R == row2 {
idx2 = i
break
}
}
- if idx2 == -1 && len(xlsx.SheetData.Row) >= row2 {
- return nil
- }
-
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
- f.ajustSingleRowDimensions(&rowCopy, row2)
+ rowCopy.adjustSingleRowDimensions(row2 - row)
+ _ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true)
if idx2 != -1 {
- xlsx.SheetData.Row[idx2] = rowCopy
+ ws.SheetData.Row[idx2] = rowCopy
} else {
- xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy)
+ ws.SheetData.Row = append(ws.SheetData.Row, rowCopy)
+ }
+ for _, fn := range duplicateHelperFunc {
+ if err := fn(f, ws, sheet, row, row2); err != nil {
+ return err
+ }
+ }
+ return err
+}
+
+// duplicateSQRefHelper provides a function to adjust conditional formatting and
+// data validations cell reference when duplicate rows.
+func duplicateSQRefHelper(row, row2 int, ref string) (string, error) {
+ if !strings.Contains(ref, ":") {
+ ref += ":" + ref
+ }
+ abs := strings.Contains(ref, "$")
+ coordinates, err := rangeRefToCoordinates(ref)
+ if err != nil {
+ return "", err
+ }
+ x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
+ if y1 == y2 && y1 == row {
+ if ref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil {
+ return "", err
+ }
+ return ref, err
+ }
+ return "", err
+}
+
+// duplicateConditionalFormat create conditional formatting for the destination
+// row if there are conditional formats in the copied row.
+func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ var cfs []*xlsxConditionalFormatting
+ for _, cf := range ws.ConditionalFormatting {
+ if cf != nil {
+ var SQRef []string
+ for _, ref := range strings.Split(cf.SQRef, " ") {
+ coordinates, err := duplicateSQRefHelper(row, row2, ref)
+ if err != nil {
+ return err
+ }
+ if coordinates != "" {
+ SQRef = append(SQRef, coordinates)
+ }
+ }
+ if len(SQRef) > 0 {
+ var cfCopy xlsxConditionalFormatting
+ deepcopy.Copy(&cfCopy, *cf)
+ cfCopy.SQRef = strings.Join(SQRef, " ")
+ cfs = append(cfs, &cfCopy)
+ }
+ }
}
- return f.duplicateMergeCells(sheet, xlsx, row, row2)
+ ws.ConditionalFormatting = append(ws.ConditionalFormatting, cfs...)
+ return nil
+}
+
+// duplicateDataValidations create data validations for the destination row if
+// there are data validation rules in the copied row.
+func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ if ws.DataValidations == nil {
+ return nil
+ }
+ var dvs []*xlsxDataValidation
+ for _, dv := range ws.DataValidations.DataValidation {
+ if dv != nil {
+ var SQRef []string
+ for _, ref := range strings.Split(dv.Sqref, " ") {
+ coordinates, err := duplicateSQRefHelper(row, row2, ref)
+ if err != nil {
+ return err
+ }
+ if coordinates != "" {
+ SQRef = append(SQRef, coordinates)
+ }
+ }
+ if len(SQRef) > 0 {
+ var dvCopy xlsxDataValidation
+ deepcopy.Copy(&dvCopy, *dv)
+ dvCopy.Sqref = strings.Join(SQRef, " ")
+ dvs = append(dvs, &dvCopy)
+ }
+ }
+ }
+ ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dvs...)
+ return nil
}
// duplicateMergeCells merge cells in the destination row if there are single
// row merged cells in the copied row.
-func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 int) error {
- if xlsx.MergeCells == nil {
+func (f *File) duplicateMergeCells(ws *xlsxWorksheet, sheet string, row, row2 int) error {
+ if ws.MergeCells == nil {
return nil
}
if row > row2 {
row++
}
- for _, rng := range xlsx.MergeCells.Cells {
- coordinates, err := f.areaRefToCoordinates(rng.Ref)
+ for _, rng := range ws.MergeCells.Cells {
+ coordinates, err := rangeRefToCoordinates(rng.Ref)
if err != nil {
return err
}
@@ -564,9 +789,9 @@ func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2
return nil
}
}
- for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
- areaData := xlsx.MergeCells.Cells[i]
- coordinates, _ := f.areaRefToCoordinates(areaData.Ref)
+ for i := 0; i < len(ws.MergeCells.Cells); i++ {
+ mergedCells := ws.MergeCells.Cells[i]
+ coordinates, _ := rangeRefToCoordinates(mergedCells.Ref)
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if y1 == y2 && y1 == row {
from, _ := CoordinatesToCellName(x1, row2)
@@ -574,7 +799,6 @@ func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2
if err := f.MergeCell(sheet, from, to); err != nil {
return err
}
- i++
}
}
return nil
@@ -583,30 +807,30 @@ func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2
// checkRow provides a function to check and fill each column element for all
// rows and make that is continuous in a worksheet of XML. For example:
//
-//
-//
-//
-//
-//
-//
+//
+//
+//
+//
+//
+//
//
// in this case, we should to change it to
//
-//
-//
-//
-//
-//
-//
-//
-//
-//
+//
+//
+//
+//
+//
+//
+//
+//
+//
//
-// Noteice: this method could be very slow for large spreadsheets (more than
+// Notice: this method could be very slow for large spreadsheets (more than
// 3000 rows one sheet).
-func checkRow(xlsx *xlsxWorksheet) error {
- for rowIdx := range xlsx.SheetData.Row {
- rowData := &xlsx.SheetData.Row[rowIdx]
+func (ws *xlsxWorksheet) checkRow() error {
+ for rowIdx := range ws.SheetData.Row {
+ rowData := &ws.SheetData.Row[rowIdx]
colCount := len(rowData.C)
if colCount == 0 {
@@ -634,28 +858,82 @@ func checkRow(xlsx *xlsxWorksheet) error {
}
if colCount < lastCol {
- oldList := rowData.C
- newlist := make([]xlsxC, 0, lastCol)
+ sourceList := rowData.C
+ targetList := make([]xlsxC, 0, lastCol)
- rowData.C = xlsx.SheetData.Row[rowIdx].C[:0]
+ rowData.C = ws.SheetData.Row[rowIdx].C[:0]
for colIdx := 0; colIdx < lastCol; colIdx++ {
cellName, err := CoordinatesToCellName(colIdx+1, rowIdx+1)
if err != nil {
return err
}
- newlist = append(newlist, xlsxC{R: cellName})
+ targetList = append(targetList, xlsxC{R: cellName})
}
- rowData.C = newlist
+ rowData.C = targetList
- for colIdx := range oldList {
- colData := &oldList[colIdx]
+ for colIdx := range sourceList {
+ colData := &sourceList[colIdx]
colNum, _, err := CellNameToCoordinates(colData.R)
if err != nil {
return err
}
- xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData
+ ws.SheetData.Row[rowIdx].C[colNum-1] = *colData
+ }
+ }
+ }
+ return nil
+}
+
+// hasAttr determine if row non-default attributes.
+func (r *xlsxRow) hasAttr() bool {
+ return r.Spans != "" || r.S != 0 || r.CustomFormat || r.Ht != nil ||
+ r.Hidden || r.CustomHeight || r.OutlineLevel != 0 || r.Collapsed ||
+ r.ThickTop || r.ThickBot || r.Ph
+}
+
+// SetRowStyle provides a function to set the style of rows by given worksheet
+// name, row range, and style ID. Note that this will overwrite the existing
+// styles for the rows, it won't append or merge style with existing styles.
+//
+// For example set style of row 1 on Sheet1:
+//
+// err := f.SetRowStyle("Sheet1", 1, 1, styleID)
+//
+// Set style of rows 1 to 10 on Sheet1:
+//
+// err := f.SetRowStyle("Sheet1", 1, 10, styleID)
+func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
+ if end < start {
+ start, end = end, start
+ }
+ if start < 1 {
+ return newInvalidRowNumberError(start)
+ }
+ if end > TotalRows {
+ return ErrMaxRows
+ }
+ s, err := f.stylesReader()
+ if err != nil {
+ return err
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
+ return newInvalidStyleID(styleID)
+ }
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ ws.prepareSheetXML(0, end)
+ for row := start - 1; row < end; row++ {
+ ws.SheetData.Row[row].S = styleID
+ ws.SheetData.Row[row].CustomFormat = true
+ for i := range ws.SheetData.Row[row].C {
+ if _, rowNum, err := CellNameToCoordinates(ws.SheetData.Row[row].C[i].R); err == nil && rowNum-1 == row {
+ ws.SheetData.Row[row].C[i].S = styleID
}
}
}
@@ -666,10 +944,8 @@ func checkRow(xlsx *xlsxWorksheet) error {
// cell from user's units to pixels. If the height hasn't been set by the user
// we use the default value. If the row is hidden it has a value of zero.
func convertRowHeightToPixels(height float64) float64 {
- var pixels float64
if height == 0 {
- return pixels
+ return 0
}
- pixels = math.Ceil(4.0 / 3.0 * height)
- return pixels
+ return math.Ceil(4.0 / 3.4 * height)
}
diff --git a/rows_test.go b/rows_test.go
index 14537eb145..01b20a0fcf 100644
--- a/rows_test.go
+++ b/rows_test.go
@@ -2,6 +2,7 @@ package excelize
import (
"bytes"
+ "encoding/xml"
"fmt"
"path/filepath"
"testing"
@@ -10,19 +11,27 @@ import (
"github.com/stretchr/testify/require"
)
+func TestGetRows(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1"))
+ // Test get rows with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, err := f.GetRows("Sheet1")
+ assert.NoError(t, err)
+}
+
func TestRows(t *testing.T) {
const sheet2 = "Sheet2"
-
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
- rows, err := f.Rows(sheet2)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ // Test get rows with invalid sheet name
+ _, err = f.Rows("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ rows, err := f.Rows(sheet2)
+ assert.NoError(t, err)
var collectedRows [][]string
for rows.Next() {
columns, err := rows.Columns()
@@ -32,6 +41,7 @@ func TestRows(t *testing.T) {
if !assert.NoError(t, rows.Error()) {
t.FailNow()
}
+ assert.NoError(t, rows.Close())
returnedRows, err := f.GetRows(sheet2)
assert.NoError(t, err)
@@ -41,95 +51,167 @@ func TestRows(t *testing.T) {
if !assert.Equal(t, collectedRows, returnedRows) {
t.FailNow()
}
+ assert.NoError(t, f.Close())
- f = NewFile()
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`1
B
`)
+ f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
_, err = f.Rows("Sheet1")
- assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
+ assert.NoError(t, err)
+
+ // Test reload the file to memory from system temporary directory
+ f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
+ assert.NoError(t, err)
+ value, err := f.GetCellValue("Sheet1", "A19")
+ assert.NoError(t, err)
+ assert.Equal(t, "Total:", value)
+ // Test load shared string table to memory
+ err = f.SetCellValue("Sheet1", "A19", "A19")
+ assert.NoError(t, err)
+ value, err = f.GetCellValue("Sheet1", "A19")
+ assert.NoError(t, err)
+ assert.Equal(t, "A19", value)
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRow.xlsx")))
+ assert.NoError(t, f.Close())
+
+ // Test rows iterator with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ rows, err = f.Rows(sheet2)
+ assert.NoError(t, err)
+ _, err = rows.Columns()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestRowsIterator(t *testing.T) {
- const (
- sheet2 = "Sheet2"
- expectedNumRow = 11
- )
+ sheetName, rowCount, expectedNumRow := "Sheet2", 0, 11
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
require.NoError(t, err)
- rows, err := f.Rows(sheet2)
+ rows, err := f.Rows(sheetName)
require.NoError(t, err)
- var rowCount int
+
for rows.Next() {
rowCount++
require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected")
}
assert.Equal(t, expectedNumRow, rowCount)
+ assert.NoError(t, rows.Close())
+ assert.NoError(t, f.Close())
// Valued cell sparse distribution test
- f = NewFile()
+ f, sheetName, rowCount, expectedNumRow = NewFile(), "Sheet1", 0, 3
cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"}
for _, cell := range cells {
- assert.NoError(t, f.SetCellValue("Sheet1", cell, 1))
+ assert.NoError(t, f.SetCellValue(sheetName, cell, 1))
}
- rows, err = f.Rows("Sheet1")
+ rows, err = f.Rows(sheetName)
require.NoError(t, err)
- rowCount = 0
for rows.Next() {
rowCount++
- require.True(t, rowCount <= 3, "rowCount is greater than expected")
+ require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected")
}
- assert.Equal(t, 3, rowCount)
+ assert.Equal(t, expectedNumRow, rowCount)
+}
+
+func TestRowsGetRowOpts(t *testing.T) {
+ sheetName := "Sheet2"
+ expectedRowStyleID1 := RowOpts{Height: 17.0, Hidden: false, StyleID: 1}
+ expectedRowStyleID2 := RowOpts{Height: 17.0, Hidden: false, StyleID: 0}
+ expectedRowStyleID3 := RowOpts{Height: 17.0, Hidden: false, StyleID: 2}
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ require.NoError(t, err)
+
+ rows, err := f.Rows(sheetName)
+ require.NoError(t, err)
+
+ assert.Equal(t, true, rows.Next())
+ _, err = rows.Columns()
+ require.NoError(t, err)
+ rowOpts := rows.GetRowOpts()
+ assert.Equal(t, expectedRowStyleID1, rowOpts)
+ assert.Equal(t, true, rows.Next())
+ rowOpts = rows.GetRowOpts()
+ assert.Equal(t, expectedRowStyleID2, rowOpts)
+ assert.Equal(t, true, rows.Next())
+ _, err = rows.Columns()
+ require.NoError(t, err)
+ rowOpts = rows.GetRowOpts()
+ assert.Equal(t, expectedRowStyleID3, rowOpts)
}
func TestRowsError(t *testing.T) {
- xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
- _, err = xlsx.Rows("SheetN")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ _, err = f.Rows("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
}
func TestRowHeight(t *testing.T) {
- xlsx := NewFile()
- sheet1 := xlsx.GetSheetName(0)
+ f := NewFile()
+ sheet1 := f.GetSheetName(0)
- assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0")
+ assert.EqualError(t, f.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), newInvalidRowNumberError(0).Error())
- _, err := xlsx.GetRowHeight("Sheet1", 0)
- assert.EqualError(t, err, "invalid row number 0")
+ _, err := f.GetRowHeight("Sheet1", 0)
+ assert.EqualError(t, err, newInvalidRowNumberError(0).Error())
- assert.NoError(t, xlsx.SetRowHeight(sheet1, 1, 111.0))
- height, err := xlsx.GetRowHeight(sheet1, 1)
+ assert.NoError(t, f.SetRowHeight(sheet1, 1, 111.0))
+ height, err := f.GetRowHeight(sheet1, 1)
assert.NoError(t, err)
assert.Equal(t, 111.0, height)
- assert.NoError(t, xlsx.SetRowHeight(sheet1, 4, 444.0))
- height, err = xlsx.GetRowHeight(sheet1, 4)
- assert.NoError(t, err)
- assert.Equal(t, 444.0, height)
+ // Test set row height overflow max row height limit
+ assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), ErrMaxRowHeight.Error())
- // Test get row height that rows index over exists rows.
- height, err = xlsx.GetRowHeight(sheet1, 5)
+ // Test get row height that rows index over exists rows
+ height, err = f.GetRowHeight(sheet1, 5)
assert.NoError(t, err)
- assert.Equal(t, defaultRowHeightPixels, height)
+ assert.Equal(t, defaultRowHeight, height)
- // Test get row height that rows heights haven't changed.
- height, err = xlsx.GetRowHeight(sheet1, 3)
+ // Test get row height that rows heights haven't changed
+ height, err = f.GetRowHeight(sheet1, 3)
assert.NoError(t, err)
- assert.Equal(t, defaultRowHeightPixels, height)
+ assert.Equal(t, defaultRowHeight, height)
+
+ // Test set and get row height on not exists worksheet
+ assert.EqualError(t, f.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN does not exist")
+ _, err = f.GetRowHeight("SheetN", 3)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
+ // Test set row height with invalid sheet name
+ assert.EqualError(t, f.SetRowHeight("Sheet:1", 1, 10.0), ErrSheetNameInvalid.Error())
+
+ // Test get row height with invalid sheet name
+ _, err = f.GetRowHeight("Sheet:1", 3)
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+
+ // Test get row height with custom default row height
+ assert.NoError(t, f.SetSheetProps(sheet1, &SheetPropsOptions{
+ DefaultRowHeight: float64Ptr(30.0),
+ CustomHeight: boolPtr(true),
+ }))
+ height, err = f.GetRowHeight(sheet1, 100)
+ assert.NoError(t, err)
+ assert.Equal(t, 30.0, height)
+
+ // Test set row height with custom default row height with prepare XML
+ assert.NoError(t, f.SetCellValue(sheet1, "A10", "A10"))
- // Test set and get row height on not exists worksheet.
- assert.EqualError(t, xlsx.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist")
- _, err = xlsx.GetRowHeight("SheetN", 3)
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet2", "A2", true))
+ height, err = f.GetRowHeight("Sheet2", 1)
+ assert.NoError(t, err)
+ assert.Equal(t, 15.0, height)
- err = xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx"))
+ err = f.SaveAs(filepath.Join("test", "TestRowHeight.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
- convertColWidthToPixels(0)
+ assert.Equal(t, 0.0, convertColWidthToPixels(0))
}
func TestColumns(t *testing.T) {
@@ -145,19 +227,19 @@ func TestColumns(t *testing.T) {
_, err = rows.Columns()
assert.NoError(t, err)
- rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1
B
`)))
- rows.stashRow, rows.curRow = 0, 1
+ rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1
B
`)))
+ assert.True(t, rows.Next())
_, err = rows.Columns()
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
- rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1
B
`)))
+ rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1
B
`)))
_, err = rows.Columns()
assert.NoError(t, err)
- rows.curRow = 3
rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1
`)))
+ assert.True(t, rows.Next())
_, err = rows.Columns()
- assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Test token is nil
rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil))
@@ -167,35 +249,50 @@ func TestColumns(t *testing.T) {
func TestSharedStringsReader(t *testing.T) {
f := NewFile()
- f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset
- f.sharedStringsReader()
- si := xlsxSI{}
- assert.EqualValues(t, "", si.String())
+ // Test read shared string with unsupported charset
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, err := f.sharedStringsReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test read shared strings with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ _, err = f.sharedStringsReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test read shared strings with unsupported charset workbook relationships
+ f = NewFile()
+ f.Relationships.Delete(defaultXMLPathWorkbookRels)
+ f.Pkg.Store(defaultXMLPathWorkbookRels, MacintoshCyrillicCharset)
+ _, err = f.sharedStringsReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestRowVisibility(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
assert.NoError(t, f.SetRowVisible("Sheet3", 2, false))
assert.NoError(t, f.SetRowVisible("Sheet3", 2, true))
- visiable, err := f.GetRowVisible("Sheet3", 2)
- assert.Equal(t, true, visiable)
+ visible, err := f.GetRowVisible("Sheet3", 2)
+ assert.Equal(t, true, visible)
assert.NoError(t, err)
- visiable, err = f.GetRowVisible("Sheet3", 25)
- assert.Equal(t, false, visiable)
+ visible, err = f.GetRowVisible("Sheet3", 25)
+ assert.Equal(t, false, visible)
assert.NoError(t, err)
- assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0")
- assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist")
+ assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), newInvalidRowNumberError(0).Error())
+ assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN does not exist")
+ // Test set row visibility with invalid sheet name
+ assert.EqualError(t, f.SetRowVisible("Sheet:1", 1, false), ErrSheetNameInvalid.Error())
- visible, err := f.GetRowVisible("Sheet3", 0)
+ visible, err = f.GetRowVisible("Sheet3", 0)
assert.Equal(t, false, visible)
- assert.EqualError(t, err, "invalid row number 0")
+ assert.EqualError(t, err, newInvalidRowNumberError(0).Error())
_, err = f.GetRowVisible("SheetN", 1)
- assert.EqualError(t, err, "sheet SheetN is not exist")
-
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get row visibility with invalid sheet name
+ _, err = f.GetRowVisible("Sheet:1", 1)
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx")))
}
@@ -208,13 +305,13 @@ func TestRemoveRow(t *testing.T) {
colCount = 10
rowCount = 10
)
- fillCells(f, sheet1, colCount, rowCount)
+ assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
- assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
+ assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
- assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1")
+ assert.EqualError(t, f.RemoveRow(sheet1, -1), newInvalidRowNumberError(-1).Error())
- assert.EqualError(t, f.RemoveRow(sheet1, 0), "invalid row number 0")
+ assert.EqualError(t, f.RemoveRow(sheet1, 0), newInvalidRowNumberError(0).Error())
assert.NoError(t, f.RemoveRow(sheet1, 4))
if !assert.Len(t, r.SheetData.Row, rowCount-1) {
@@ -233,7 +330,7 @@ func TestRemoveRow(t *testing.T) {
t.FailNow()
}
- err = f.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`)
+ err = f.AutoFilter(sheet1, "A2:A2", []AutoFilterOptions{{Column: "A", Expression: "x != blanks"}})
if !assert.NoError(t, err) {
t.FailNow()
}
@@ -256,54 +353,93 @@ func TestRemoveRow(t *testing.T) {
assert.NoError(t, f.RemoveRow(sheet1, 10))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx")))
+ f = NewFile()
+ assert.NoError(t, f.MergeCell("Sheet1", "A1", "C1"))
+ assert.NoError(t, f.MergeCell("Sheet1", "A2", "C2"))
+ assert.NoError(t, f.RemoveRow("Sheet1", 1))
+ mergedCells, err := f.GetMergeCells("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
+ assert.Equal(t, "C1", mergedCells[0].GetEndAxis())
+
// Test remove row on not exist worksheet
- assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN is not exist`)
+ assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist")
+ // Test remove row with invalid sheet name
+ assert.EqualError(t, f.RemoveRow("Sheet:1", 1), ErrSheetNameInvalid.Error())
}
-func TestInsertRow(t *testing.T) {
- xlsx := NewFile()
- sheet1 := xlsx.GetSheetName(0)
- r, err := xlsx.workSheetReader(sheet1)
+func TestInsertRows(t *testing.T) {
+ f := NewFile()
+ sheet1 := f.GetSheetName(0)
+ r, err := f.workSheetReader(sheet1)
assert.NoError(t, err)
const (
colCount = 10
rowCount = 10
)
- fillCells(xlsx, sheet1, colCount, rowCount)
-
- assert.NoError(t, xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
+ assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
- assert.EqualError(t, xlsx.InsertRow(sheet1, -1), "invalid row number -1")
+ assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
- assert.EqualError(t, xlsx.InsertRow(sheet1, 0), "invalid row number 0")
-
- assert.NoError(t, xlsx.InsertRow(sheet1, 1))
+ assert.NoError(t, f.InsertRows(sheet1, 1, 1))
if !assert.Len(t, r.SheetData.Row, rowCount+1) {
t.FailNow()
}
- assert.NoError(t, xlsx.InsertRow(sheet1, 4))
+ assert.NoError(t, f.InsertRows(sheet1, 4, 1))
if !assert.Len(t, r.SheetData.Row, rowCount+2) {
t.FailNow()
}
- assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx")))
+ assert.NoError(t, f.InsertRows(sheet1, 4, 2))
+ if !assert.Len(t, r.SheetData.Row, rowCount+4) {
+ t.FailNow()
+ }
+ // Test insert rows with invalid sheet name
+ assert.EqualError(t, f.InsertRows("Sheet:1", 1, 1), ErrSheetNameInvalid.Error())
+
+ assert.EqualError(t, f.InsertRows(sheet1, -1, 1), newInvalidRowNumberError(-1).Error())
+ assert.EqualError(t, f.InsertRows(sheet1, 0, 1), newInvalidRowNumberError(0).Error())
+ assert.EqualError(t, f.InsertRows(sheet1, 4, 0), ErrParameterInvalid.Error())
+ assert.EqualError(t, f.InsertRows(sheet1, 4, TotalRows), ErrMaxRows.Error())
+ assert.EqualError(t, f.InsertRows(sheet1, 4, TotalRows-5), ErrMaxRows.Error())
+ assert.EqualError(t, f.InsertRows(sheet1, TotalRows, 1), ErrMaxRows.Error())
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRows.xlsx")))
}
-// Testing internal sructure state after insert operations.
-// It is important for insert workflow to be constant to avoid side effect with functions related to internal structure.
-func TestInsertRowInEmptyFile(t *testing.T) {
- xlsx := NewFile()
- sheet1 := xlsx.GetSheetName(0)
- r, err := xlsx.workSheetReader(sheet1)
+// Test internal structure state after insert operations. It is important
+// for insert workflow to be constant to avoid side effect with functions
+// related to internal structure.
+func TestInsertRowsInEmptyFile(t *testing.T) {
+ f := NewFile()
+ sheet1 := f.GetSheetName(0)
+ r, err := f.workSheetReader(sheet1)
assert.NoError(t, err)
- assert.NoError(t, xlsx.InsertRow(sheet1, 1))
+ assert.NoError(t, f.InsertRows(sheet1, 1, 1))
assert.Len(t, r.SheetData.Row, 0)
- assert.NoError(t, xlsx.InsertRow(sheet1, 2))
+ assert.NoError(t, f.InsertRows(sheet1, 2, 1))
assert.Len(t, r.SheetData.Row, 0)
- assert.NoError(t, xlsx.InsertRow(sheet1, 99))
+ assert.NoError(t, f.InsertRows(sheet1, 99, 1))
assert.Len(t, r.SheetData.Row, 0)
- assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx")))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx")))
+}
+
+func prepareTestBook2() (*File, error) {
+ f := NewFile()
+ for cell, val := range map[string]string{
+ "A1": "A1 Value",
+ "A2": "A2 Value",
+ "A3": "A3 Value",
+ "B1": "B1 Value",
+ "B2": "B2 Value",
+ "B3": "B3 Value",
+ } {
+ if err := f.SetCellStr("Sheet1", cell, val); err != nil {
+ return f, err
+ }
+ }
+ return f, nil
}
func TestDuplicateRowFromSingleRow(t *testing.T) {
@@ -320,12 +456,12 @@ func TestDuplicateRowFromSingleRow(t *testing.T) {
}
t.Run("FromSingleRow", func(t *testing.T) {
- xlsx := NewFile()
- assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"]))
- assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"]))
+ f := NewFile()
+ assert.NoError(t, f.SetCellStr(sheet, "A1", cells["A1"]))
+ assert.NoError(t, f.SetCellStr(sheet, "B1", cells["B1"]))
- assert.NoError(t, xlsx.DuplicateRow(sheet, 1))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) {
+ assert.NoError(t, f.DuplicateRow(sheet, 1))
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FromSingleRow_1"))) {
t.FailNow()
}
expect := map[string]string{
@@ -333,15 +469,15 @@ func TestDuplicateRowFromSingleRow(t *testing.T) {
"A2": cells["A1"], "B2": cells["B1"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
}
}
- assert.NoError(t, xlsx.DuplicateRow(sheet, 2))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) {
+ assert.NoError(t, f.DuplicateRow(sheet, 2))
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FromSingleRow_2"))) {
t.FailNow()
}
expect = map[string]string{
@@ -350,7 +486,7 @@ func TestDuplicateRowFromSingleRow(t *testing.T) {
"A3": cells["A1"], "B3": cells["B1"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -373,16 +509,16 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) {
}
t.Run("UpdateDuplicatedRows", func(t *testing.T) {
- xlsx := NewFile()
- assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"]))
- assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"]))
+ f := NewFile()
+ assert.NoError(t, f.SetCellStr(sheet, "A1", cells["A1"]))
+ assert.NoError(t, f.SetCellStr(sheet, "B1", cells["B1"]))
- assert.NoError(t, xlsx.DuplicateRow(sheet, 1))
+ assert.NoError(t, f.DuplicateRow(sheet, 1))
- assert.NoError(t, xlsx.SetCellStr(sheet, "A2", cells["A2"]))
- assert.NoError(t, xlsx.SetCellStr(sheet, "B2", cells["B2"]))
+ assert.NoError(t, f.SetCellStr(sheet, "A2", cells["A2"]))
+ assert.NoError(t, f.SetCellStr(sheet, "B2", cells["B2"]))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "UpdateDuplicatedRows"))) {
t.FailNow()
}
expect := map[string]string{
@@ -390,7 +526,7 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) {
"A2": cells["A2"], "B2": cells["B2"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -402,7 +538,6 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) {
func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
-
cells := map[string]string{
"A1": "A1 Value",
"A2": "A2 Value",
@@ -411,21 +546,12 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
"B2": "B2 Value",
"B3": "B3 Value",
}
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
- return f
- }
-
t.Run("FirstOfMultipleRows", func(t *testing.T) {
- xlsx := newFileWithDefaults()
-
- assert.NoError(t, xlsx.DuplicateRow(sheet, 1))
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
+ assert.NoError(t, f.DuplicateRow(sheet, 1))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) {
t.FailNow()
}
expect := map[string]string{
@@ -435,7 +561,7 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
"A4": cells["A3"], "B4": cells["B3"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -449,24 +575,24 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) {
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
t.Run("ZeroWithNoRows", func(t *testing.T) {
- xlsx := NewFile()
+ f := NewFile()
- assert.EqualError(t, xlsx.DuplicateRow(sheet, 0), "invalid row number 0")
+ assert.EqualError(t, f.DuplicateRow(sheet, 0), newInvalidRowNumberError(0).Error())
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "ZeroWithNoRows"))) {
t.FailNow()
}
- val, err := xlsx.GetCellValue(sheet, "A1")
+ val, err := f.GetCellValue(sheet, "A1")
assert.NoError(t, err)
assert.Equal(t, "", val)
- val, err = xlsx.GetCellValue(sheet, "B1")
+ val, err = f.GetCellValue(sheet, "B1")
assert.NoError(t, err)
assert.Equal(t, "", val)
- val, err = xlsx.GetCellValue(sheet, "A2")
+ val, err = f.GetCellValue(sheet, "A2")
assert.NoError(t, err)
assert.Equal(t, "", val)
- val, err = xlsx.GetCellValue(sheet, "B2")
+ val, err = f.GetCellValue(sheet, "B2")
assert.NoError(t, err)
assert.Equal(t, "", val)
@@ -477,7 +603,7 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) {
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -491,11 +617,11 @@ func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) {
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
t.Run("MiddleRowOfEmptyFile", func(t *testing.T) {
- xlsx := NewFile()
+ f := NewFile()
- assert.NoError(t, xlsx.DuplicateRow(sheet, 99))
+ assert.NoError(t, f.DuplicateRow(sheet, 99))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "MiddleRowOfEmptyFile"))) {
t.FailNow()
}
expect := map[string]string{
@@ -504,7 +630,7 @@ func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) {
"A100": "",
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -525,21 +651,12 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
"B2": "B2 Value",
"B3": "B3 Value",
}
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
- return f
- }
-
t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) {
- xlsx := newFileWithDefaults()
-
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 3))
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
+ assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) {
t.FailNow()
}
expect := map[string]string{
@@ -549,7 +666,7 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
"A4": cells["A3"], "B4": cells["B3"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -561,7 +678,6 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
-
cells := map[string]string{
"A1": "A1 Value",
"A2": "A2 Value",
@@ -570,21 +686,12 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
"B2": "B2 Value",
"B3": "B3 Value",
}
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
- return f
- }
-
t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) {
- xlsx := newFileWithDefaults()
-
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 7))
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
+ assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) {
t.FailNow()
}
expect := map[string]string{
@@ -594,7 +701,7 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
"A7": cells["A1"], "B7": cells["B1"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -606,7 +713,6 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
func TestDuplicateRowInsertBefore(t *testing.T) {
const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
-
cells := map[string]string{
"A1": "A1 Value",
"A2": "A2 Value",
@@ -615,21 +721,13 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
"B2": "B2 Value",
"B3": "B3 Value",
}
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
- return f
- }
-
t.Run("InsertBefore", func(t *testing.T) {
- xlsx := newFileWithDefaults()
-
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1))
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
+ assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
+ assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBefore"))) {
t.FailNow()
}
@@ -637,10 +735,10 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
"A1": cells["A2"], "B1": cells["B2"],
"A2": cells["A1"], "B2": cells["B1"],
"A3": cells["A2"], "B3": cells["B2"],
- "A4": cells["A3"], "B4": cells["B3"],
+ "A5": cells["A3"], "B5": cells["B3"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v, cell) {
t.FailNow()
@@ -652,7 +750,6 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
-
cells := map[string]string{
"A1": "A1 Value",
"A2": "A2 Value",
@@ -661,21 +758,12 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
"B2": "B2 Value",
"B3": "B3 Value",
}
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
- return f
- }
-
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
- xlsx := newFileWithDefaults()
-
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 3, 1))
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
+ assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) {
t.FailNow()
}
@@ -686,7 +774,7 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
"A4": cells["A3"], "B4": cells["B3"],
}
for cell, val := range expect {
- v, err := xlsx.GetCellValue(sheet, cell)
+ v, err := f.GetCellValue(sheet, cell)
assert.NoError(t, err)
if !assert.Equal(t, val, v) {
t.FailNow()
@@ -698,33 +786,16 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) {
const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
-
- cells := map[string]string{
- "A1": "A1 Value",
- "A2": "A2 Value",
- "A3": "A3 Value",
- "B1": "B1 Value",
- "B2": "B2 Value",
- "B3": "B3 Value",
- }
-
- newFileWithDefaults := func() *File {
- f := NewFile()
- for cell, val := range cells {
- assert.NoError(t, f.SetCellStr(sheet, cell, val))
- }
+ t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
+ f, err := prepareTestBook2()
+ assert.NoError(t, err)
assert.NoError(t, f.MergeCell(sheet, "B2", "C2"))
assert.NoError(t, f.MergeCell(sheet, "C6", "C8"))
- return f
- }
-
- t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
- xlsx := newFileWithDefaults()
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1))
- assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 8))
+ assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
+ assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8))
- if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) {
+ if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithMergeCells"))) {
t.FailNow()
}
@@ -734,7 +805,7 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) {
{"B1:C1", "B2 Value"},
}
- mergeCells, err := xlsx.GetMergeCells(sheet)
+ mergeCells, err := f.GetMergeCells(sheet)
assert.NoError(t, err)
for idx, val := range expect {
if !assert.Equal(t, val, mergeCells[idx]) {
@@ -744,9 +815,9 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) {
})
}
-func TestDuplicateRowInvalidRownum(t *testing.T) {
+func TestDuplicateRowInvalidRowNum(t *testing.T) {
const sheet = "Sheet1"
- outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx")
+ outFile := filepath.Join("test", "TestDuplicateRow.InvalidRowNum.%s.xlsx")
cells := map[string]string{
"A1": "A1 Value",
@@ -762,21 +833,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) {
for _, row := range invalidIndexes {
name := fmt.Sprintf("%d", row)
t.Run(name, func(t *testing.T) {
- xlsx := NewFile()
+ f := NewFile()
for col, val := range cells {
- assert.NoError(t, xlsx.SetCellStr(sheet, col, val))
+ assert.NoError(t, f.SetCellStr(sheet, col, val))
}
- assert.EqualError(t, xlsx.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row))
+ assert.EqualError(t, f.DuplicateRow(sheet, row), newInvalidRowNumberError(row).Error())
for col, val := range cells {
- v, err := xlsx.GetCellValue(sheet, col)
+ v, err := f.GetCellValue(sheet, col)
assert.NoError(t, err)
if !assert.Equal(t, val, v) {
t.FailNow()
}
}
- assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name)))
+ assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, name)))
})
}
@@ -784,64 +855,306 @@ func TestDuplicateRowInvalidRownum(t *testing.T) {
for _, row2 := range invalidIndexes {
name := fmt.Sprintf("[%d,%d]", row1, row2)
t.Run(name, func(t *testing.T) {
- xlsx := NewFile()
+ f := NewFile()
for col, val := range cells {
- assert.NoError(t, xlsx.SetCellStr(sheet, col, val))
+ assert.NoError(t, f.SetCellStr(sheet, col, val))
}
- assert.EqualError(t, xlsx.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1))
+ assert.EqualError(t, f.DuplicateRowTo(sheet, row1, row2), newInvalidRowNumberError(row1).Error())
for col, val := range cells {
- v, err := xlsx.GetCellValue(sheet, col)
+ v, err := f.GetCellValue(sheet, col)
assert.NoError(t, err)
if !assert.Equal(t, val, v) {
t.FailNow()
}
}
- assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name)))
+ assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, name)))
})
}
}
}
+func TestDuplicateRow(t *testing.T) {
+ f := NewFile()
+ // Test duplicate row with invalid sheet name
+ assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())
+
+ f = NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Amount",
+ RefersTo: "Sheet1!$B$1",
+ }))
+ assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1"))
+ assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10"))
+
+ format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
+ assert.NoError(t, err)
+
+ expected := []ConditionalFormatOptions{
+ {Type: "cell", Criteria: "greater than", Format: &format, Value: "0"},
+ }
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1", expected))
+
+ dv := NewDataValidation(true)
+ dv.Sqref = "A1"
+ assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
+ assert.NoError(t, f.AddDataValidation("Sheet1", dv))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1"
+
+ assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
+ formula, err := f.GetCellFormula("Sheet1", "A10")
+ assert.NoError(t, err)
+ assert.Equal(t, "Amount+C10", formula)
+ value, err := f.GetCellValue("Sheet1", "A11")
+ assert.NoError(t, err)
+ assert.Equal(t, "A10", value)
+
+ cfs, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, cfs, 2)
+ assert.Equal(t, expected, cfs["A10:A10"])
+
+ dvs, err := f.GetDataValidations("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, dvs, 2)
+ assert.Equal(t, "A10:A10", dvs[1].Sqref)
+
+ // Test duplicate data validation with row number exceeds maximum limit
+ assert.Equal(t, ErrMaxRows, f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
+ // Test duplicate data validation with invalid range reference
+ ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A"
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
+
+ // Test duplicate conditional formatting with row number exceeds maximum limit
+ assert.Equal(t, ErrMaxRows, f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
+ // Test duplicate conditional formatting with invalid range reference
+ ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "A"
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
+}
+
func TestDuplicateRowTo(t *testing.T) {
- f := File{}
- assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN is not exist")
+ f, sheetName := NewFile(), "Sheet1"
+ // Test duplicate row with invalid target row number
+ assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 0))
+ // Test duplicate row with equal source and target row number
+ assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 1))
+ // Test duplicate row on the blank worksheet
+ assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 2))
+ // Test duplicate row on the worksheet with illegal cell reference
+ f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
+ MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}},
+ })
+ assert.EqualError(t, f.DuplicateRowTo(sheetName, 1, 2), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ // Test duplicate row on not exists worksheet
+ assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN does not exist")
+ // Test duplicate row with invalid sheet name
+ assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())
}
func TestDuplicateMergeCells(t *testing.T) {
f := File{}
- xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{
+ ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{{Ref: "A1:-"}},
}}
- assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
- xlsx.MergeCells.Cells[0].Ref = "A1:B1"
- assert.EqualError(t, f.duplicateMergeCells("SheetN", xlsx, 1, 2), "sheet SheetN is not exist")
+ assert.EqualError(t, f.duplicateMergeCells(ws, "Sheet1", 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
+ ws.MergeCells.Cells[0].Ref = "A1:B1"
+ assert.EqualError(t, f.duplicateMergeCells(ws, "SheetN", 1, 2), "sheet SheetN does not exist")
}
-func TestGetValueFrom(t *testing.T) {
+func TestGetValueFromInlineStr(t *testing.T) {
c := &xlsxC{T: "inlineStr"}
f := NewFile()
d := &xlsxSST{}
- val, err := c.getValueFrom(f, d)
+ val, err := c.getValueFrom(f, d, false)
assert.NoError(t, err)
assert.Equal(t, "", val)
}
+func TestGetValueFromNumber(t *testing.T) {
+ c := &xlsxC{T: "n"}
+ f := NewFile()
+ d := &xlsxSST{}
+ for input, expected := range map[string]string{
+ "2.2.": "2.2.",
+ "1.1000000000000001": "1.1",
+ "2.2200000000000002": "2.22",
+ "28.552": "28.552",
+ "27.399000000000001": "27.399",
+ "26.245999999999999": "26.246",
+ "2422.3000000000002": "2422.3",
+ "2.220000ddsf0000000002-r": "2.220000ddsf0000000002-r",
+ } {
+ c.V = input
+ val, err := c.getValueFrom(f, d, false)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, val)
+ }
+}
+
func TestErrSheetNotExistError(t *testing.T) {
- err := ErrSheetNotExist{SheetName: "Sheet1"}
- assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist")
+ assert.Equal(t, "sheet Sheet1 does not exist", ErrSheetNotExist{"Sheet1"}.Error())
}
func TestCheckRow(t *testing.T) {
f := NewFile()
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345
`)
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345
`))
_, err := f.GetRows("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", false))
f = NewFile()
- f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345
`)
- assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345
`))
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.checked.Delete("xl/worksheets/sheet1.xml")
+ assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error())
+}
+
+func TestSetRowStyle(t *testing.T) {
+ f := NewFile()
+ style1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"63BE7B"}, Pattern: 1}})
+ assert.NoError(t, err)
+ style2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1}})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellStyle("Sheet1", "B2", "B2", style1))
+ assert.EqualError(t, f.SetRowStyle("Sheet1", 5, -1, style2), newInvalidRowNumberError(-1).Error())
+ assert.EqualError(t, f.SetRowStyle("Sheet1", 1, TotalRows+1, style2), ErrMaxRows.Error())
+ // Test set row style with invalid style ID
+ assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, -1), newInvalidStyleID(-1).Error())
+ // Test set row style with not exists style ID
+ assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, 10), newInvalidStyleID(10).Error())
+ assert.EqualError(t, f.SetRowStyle("SheetN", 1, 1, style2), "sheet SheetN does not exist")
+ // Test set row style with invalid sheet name
+ assert.EqualError(t, f.SetRowStyle("Sheet:1", 1, 1, 0), ErrSheetNameInvalid.Error())
+ assert.NoError(t, f.SetRowStyle("Sheet1", 5, 1, style2))
+ cellStyleID, err := f.GetCellStyle("Sheet1", "B2")
+ assert.NoError(t, err)
+ assert.Equal(t, style2, cellStyleID)
+ // Test cell inheritance rows style
+ assert.NoError(t, f.SetCellValue("Sheet1", "C1", nil))
+ cellStyleID, err = f.GetCellStyle("Sheet1", "C1")
+ assert.NoError(t, err)
+ assert.Equal(t, style2, cellStyleID)
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx")))
+ // Test set row style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestSetRowHeight(t *testing.T) {
+ f := NewFile()
+ // Test hidden row by set row height to 0
+ assert.NoError(t, f.SetRowHeight("Sheet1", 2, 0))
+ ht, err := f.GetRowHeight("Sheet1", 2)
+ assert.NoError(t, err)
+ assert.Empty(t, ht)
+ // Test unset custom row height
+ assert.NoError(t, f.SetRowHeight("Sheet1", 2, -1))
+ ht, err = f.GetRowHeight("Sheet1", 2)
+ assert.NoError(t, err)
+ assert.Equal(t, defaultRowHeight, ht)
+ // Test set row height with invalid height value
+ assert.Equal(t, ErrParameterInvalid, f.SetRowHeight("Sheet1", 2, -2))
+}
+
+func TestNumberFormats(t *testing.T) {
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ cells := make([][]string, 0)
+ cols, err := f.Cols("Sheet2")
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+ for cols.Next() {
+ col, err := cols.Rows()
+ assert.NoError(t, err)
+ if err != nil {
+ break
+ }
+ cells = append(cells, col)
+ }
+ assert.Equal(t, []string{"", "200", "450", "200", "510", "315", "127", "89", "348", "53", "37"}, cells[3])
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ numFmt1, err := f.NewStyle(&Style{NumFmt: 1})
+ assert.NoError(t, err)
+ numFmt2, err := f.NewStyle(&Style{NumFmt: 2})
+ assert.NoError(t, err)
+ numFmt3, err := f.NewStyle(&Style{NumFmt: 3})
+ assert.NoError(t, err)
+ numFmt9, err := f.NewStyle(&Style{NumFmt: 9})
+ assert.NoError(t, err)
+ numFmt10, err := f.NewStyle(&Style{NumFmt: 10})
+ assert.NoError(t, err)
+ numFmt21, err := f.NewStyle(&Style{NumFmt: 21})
+ assert.NoError(t, err)
+ numFmt37, err := f.NewStyle(&Style{NumFmt: 37})
+ assert.NoError(t, err)
+ numFmt38, err := f.NewStyle(&Style{NumFmt: 38})
+ assert.NoError(t, err)
+ numFmt39, err := f.NewStyle(&Style{NumFmt: 39})
+ assert.NoError(t, err)
+ numFmt40, err := f.NewStyle(&Style{NumFmt: 40})
+ assert.NoError(t, err)
+ for _, cases := range [][]interface{}{
+ {"A1", numFmt1, 8.8888666665555493e+19, "88888666665555500000"},
+ {"A2", numFmt1, 8.8888666665555487, "9"},
+ {"A3", numFmt2, 8.8888666665555493e+19, "88888666665555500000.00"},
+ {"A4", numFmt2, 8.8888666665555487, "8.89"},
+ {"A5", numFmt3, 8.8888666665555493e+19, "88,888,666,665,555,500,000"},
+ {"A6", numFmt3, 8.8888666665555487, "9"},
+ {"A7", numFmt3, 123, "123"},
+ {"A8", numFmt3, -1234, "-1,234"},
+ {"A9", numFmt9, 8.8888666665555493e+19, "8888866666555550000000%"},
+ {"A10", numFmt9, -8.8888666665555493e+19, "-8888866666555550000000%"},
+ {"A11", numFmt9, 8.8888666665555487, "889%"},
+ {"A12", numFmt9, -8.8888666665555487, "-889%"},
+ {"A13", numFmt10, 8.8888666665555493e+19, "8888866666555550000000.00%"},
+ {"A14", numFmt10, -8.8888666665555493e+19, "-8888866666555550000000.00%"},
+ {"A15", numFmt10, 8.8888666665555487, "888.89%"},
+ {"A16", numFmt10, -8.8888666665555487, "-888.89%"},
+ {"A17", numFmt37, 8.8888666665555493e+19, "88,888,666,665,555,500,000 "},
+ {"A18", numFmt37, -8.8888666665555493e+19, "(88,888,666,665,555,500,000)"},
+ {"A19", numFmt37, 8.8888666665555487, "9 "},
+ {"A20", numFmt37, -8.8888666665555487, "(9)"},
+ {"A21", numFmt38, 8.8888666665555493e+19, "88,888,666,665,555,500,000 "},
+ {"A22", numFmt38, -8.8888666665555493e+19, "(88,888,666,665,555,500,000)"},
+ {"A23", numFmt38, 8.8888666665555487, "9 "},
+ {"A24", numFmt38, -8.8888666665555487, "(9)"},
+ {"A25", numFmt39, 8.8888666665555493e+19, "88,888,666,665,555,500,000.00 "},
+ {"A26", numFmt39, -8.8888666665555493e+19, "(88,888,666,665,555,500,000.00)"},
+ {"A27", numFmt39, 8.8888666665555487, "8.89 "},
+ {"A28", numFmt39, -8.8888666665555487, "(8.89)"},
+ {"A29", numFmt40, 8.8888666665555493e+19, "88,888,666,665,555,500,000.00 "},
+ {"A30", numFmt40, -8.8888666665555493e+19, "(88,888,666,665,555,500,000.00)"},
+ {"A31", numFmt40, 8.8888666665555487, "8.89 "},
+ {"A32", numFmt40, -8.8888666665555487, "(8.89)"},
+ {"A33", numFmt21, 44729.999988368058, "23:59:59"},
+ {"A34", numFmt21, 44944.375005787035, "09:00:00"},
+ {"A35", numFmt21, 44944.375005798611, "09:00:01"},
+ } {
+ cell, styleID, value, expected := cases[0].(string), cases[1].(int), cases[2], cases[3].(string)
+ assert.NoError(t, f.SetCellStyle("Sheet1", cell, cell, styleID))
+ assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
+ result, err := f.GetCellValue("Sheet1", cell)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result, cell)
+ }
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx")))
+
+ f = NewFile(Options{ShortDatePattern: "yyyy/m/d"})
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519))
+ numFmt14, err := f.NewStyle(&Style{NumFmt: 14})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", numFmt14))
+ result, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "2019/3/19", result, "A1")
}
func BenchmarkRows(b *testing.B) {
@@ -856,9 +1169,16 @@ func BenchmarkRows(b *testing.B) {
}
}
}
+ if err := rows.Close(); err != nil {
+ b.Error(err)
+ }
+ }
+ if err := f.Close(); err != nil {
+ b.Error(err)
}
}
+// trimSliceSpace trim continually blank element in the tail of slice.
func trimSliceSpace(s []string) []string {
for {
if len(s) > 0 && s[len(s)-1] == "" {
diff --git a/shape.go b/shape.go
index 0a5164b4a1..1bbf6964d6 100644
--- a/shape.go
+++ b/shape.go
@@ -1,267 +1,300 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
- "encoding/json"
"strconv"
"strings"
)
-// parseFormatShapeSet provides a function to parse the format settings of the
+// parseShapeOptions provides a function to parse the format settings of the
// shape with default value.
-func parseFormatShapeSet(formatSet string) (*formatShape, error) {
- format := formatShape{
- Width: 160,
- Height: 160,
- Format: formatPicture{
- FPrintsWithSheet: true,
- FLocksWithSheet: false,
- NoChangeAspect: false,
- OffsetX: 0,
- OffsetY: 0,
- XScale: 1.0,
- YScale: 1.0,
- },
+func parseShapeOptions(opts *Shape) (*Shape, error) {
+ if opts == nil {
+ return nil, ErrParameterInvalid
+ }
+ if opts.Type == "" {
+ return nil, ErrParameterInvalid
+ }
+ if opts.Width == 0 {
+ opts.Width = defaultShapeSize
+ }
+ if opts.Height == 0 {
+ opts.Height = defaultShapeSize
+ }
+ if opts.Format.PrintObject == nil {
+ opts.Format.PrintObject = boolPtr(true)
+ }
+ if opts.Format.Locked == nil {
+ opts.Format.Locked = boolPtr(false)
+ }
+ if opts.Format.ScaleX == 0 {
+ opts.Format.ScaleX = defaultDrawingScale
+ }
+ if opts.Format.ScaleY == 0 {
+ opts.Format.ScaleY = defaultDrawingScale
}
- err := json.Unmarshal([]byte(formatSet), &format)
- return &format, err
+ if opts.Line.Width == nil {
+ opts.Line.Width = float64Ptr(defaultShapeLineWidth)
+ }
+ return opts, nil
}
// AddShape provides the method to add shape in a sheet by given worksheet
-// index, shape format set (such as offset, scale, aspect ratio setting and
-// print settings) and properties set. For example, add text box (rect shape)
-// in Sheet1:
+// name and shape format set (such as offset, scale, aspect ratio setting and
+// print settings). For example, add text box (rect shape) in Sheet1:
//
-// err := f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`)
+// lineWidth := 1.2
+// err := f.AddShape("Sheet1",
+// &excelize.Shape{
+// Cell: "G6",
+// Type: "rect",
+// Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth},
+// Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1},
+// Paragraph: []excelize.RichTextRun{
+// {
+// Text: "Rectangle Shape",
+// Font: &excelize.Font{
+// Bold: true,
+// Italic: true,
+// Family: "Times New Roman",
+// Size: 18,
+// Color: "777777",
+// Underline: "sng",
+// },
+// },
+// },
+// Width: 180,
+// Height: 40,
+// },
+// )
//
// The following shows the type of shape supported by excelize:
//
-// accentBorderCallout1 (Callout 1 with Border and Accent Shape)
-// accentBorderCallout2 (Callout 2 with Border and Accent Shape)
-// accentBorderCallout3 (Callout 3 with Border and Accent Shape)
-// accentCallout1 (Callout 1 Shape)
-// accentCallout2 (Callout 2 Shape)
-// accentCallout3 (Callout 3 Shape)
-// actionButtonBackPrevious (Back or Previous Button Shape)
-// actionButtonBeginning (Beginning Button Shape)
-// actionButtonBlank (Blank Button Shape)
-// actionButtonDocument (Document Button Shape)
-// actionButtonEnd (End Button Shape)
-// actionButtonForwardNext (Forward or Next Button Shape)
-// actionButtonHelp (Help Button Shape)
-// actionButtonHome (Home Button Shape)
-// actionButtonInformation (Information Button Shape)
-// actionButtonMovie (Movie Button Shape)
-// actionButtonReturn (Return Button Shape)
-// actionButtonSound (Sound Button Shape)
-// arc (Curved Arc Shape)
-// bentArrow (Bent Arrow Shape)
-// bentConnector2 (Bent Connector 2 Shape)
-// bentConnector3 (Bent Connector 3 Shape)
-// bentConnector4 (Bent Connector 4 Shape)
-// bentConnector5 (Bent Connector 5 Shape)
-// bentUpArrow (Bent Up Arrow Shape)
-// bevel (Bevel Shape)
-// blockArc (Block Arc Shape)
-// borderCallout1 (Callout 1 with Border Shape)
-// borderCallout2 (Callout 2 with Border Shape)
-// borderCallout3 (Callout 3 with Border Shape)
-// bracePair (Brace Pair Shape)
-// bracketPair (Bracket Pair Shape)
-// callout1 (Callout 1 Shape)
-// callout2 (Callout 2 Shape)
-// callout3 (Callout 3 Shape)
-// can (Can Shape)
-// chartPlus (Chart Plus Shape)
-// chartStar (Chart Star Shape)
-// chartX (Chart X Shape)
-// chevron (Chevron Shape)
-// chord (Chord Shape)
-// circularArrow (Circular Arrow Shape)
-// cloud (Cloud Shape)
-// cloudCallout (Callout Cloud Shape)
-// corner (Corner Shape)
-// cornerTabs (Corner Tabs Shape)
-// cube (Cube Shape)
-// curvedConnector2 (Curved Connector 2 Shape)
-// curvedConnector3 (Curved Connector 3 Shape)
-// curvedConnector4 (Curved Connector 4 Shape)
-// curvedConnector5 (Curved Connector 5 Shape)
-// curvedDownArrow (Curved Down Arrow Shape)
-// curvedLeftArrow (Curved Left Arrow Shape)
-// curvedRightArrow (Curved Right Arrow Shape)
-// curvedUpArrow (Curved Up Arrow Shape)
-// decagon (Decagon Shape)
-// diagStripe (Diagonal Stripe Shape)
-// diamond (Diamond Shape)
-// dodecagon (Dodecagon Shape)
-// donut (Donut Shape)
-// doubleWave (Double Wave Shape)
-// downArrow (Down Arrow Shape)
-// downArrowCallout (Callout Down Arrow Shape)
-// ellipse (Ellipse Shape)
-// ellipseRibbon (Ellipse Ribbon Shape)
-// ellipseRibbon2 (Ellipse Ribbon 2 Shape)
-// flowChartAlternateProcess (Alternate Process Flow Shape)
-// flowChartCollate (Collate Flow Shape)
-// flowChartConnector (Connector Flow Shape)
-// flowChartDecision (Decision Flow Shape)
-// flowChartDelay (Delay Flow Shape)
-// flowChartDisplay (Display Flow Shape)
-// flowChartDocument (Document Flow Shape)
-// flowChartExtract (Extract Flow Shape)
-// flowChartInputOutput (Input Output Flow Shape)
-// flowChartInternalStorage (Internal Storage Flow Shape)
-// flowChartMagneticDisk (Magnetic Disk Flow Shape)
-// flowChartMagneticDrum (Magnetic Drum Flow Shape)
-// flowChartMagneticTape (Magnetic Tape Flow Shape)
-// flowChartManualInput (Manual Input Flow Shape)
-// flowChartManualOperation (Manual Operation Flow Shape)
-// flowChartMerge (Merge Flow Shape)
-// flowChartMultidocument (Multi-Document Flow Shape)
-// flowChartOfflineStorage (Offline Storage Flow Shape)
-// flowChartOffpageConnector (Off-Page Connector Flow Shape)
-// flowChartOnlineStorage (Online Storage Flow Shape)
-// flowChartOr (Or Flow Shape)
-// flowChartPredefinedProcess (Predefined Process Flow Shape)
-// flowChartPreparation (Preparation Flow Shape)
-// flowChartProcess (Process Flow Shape)
-// flowChartPunchedCard (Punched Card Flow Shape)
-// flowChartPunchedTape (Punched Tape Flow Shape)
-// flowChartSort (Sort Flow Shape)
-// flowChartSummingJunction (Summing Junction Flow Shape)
-// flowChartTerminator (Terminator Flow Shape)
-// foldedCorner (Folded Corner Shape)
-// frame (Frame Shape)
-// funnel (Funnel Shape)
-// gear6 (Gear 6 Shape)
-// gear9 (Gear 9 Shape)
-// halfFrame (Half Frame Shape)
-// heart (Heart Shape)
-// heptagon (Heptagon Shape)
-// hexagon (Hexagon Shape)
-// homePlate (Home Plate Shape)
-// horizontalScroll (Horizontal Scroll Shape)
-// irregularSeal1 (Irregular Seal 1 Shape)
-// irregularSeal2 (Irregular Seal 2 Shape)
-// leftArrow (Left Arrow Shape)
-// leftArrowCallout (Callout Left Arrow Shape)
-// leftBrace (Left Brace Shape)
-// leftBracket (Left Bracket Shape)
-// leftCircularArrow (Left Circular Arrow Shape)
-// leftRightArrow (Left Right Arrow Shape)
-// leftRightArrowCallout (Callout Left Right Arrow Shape)
-// leftRightCircularArrow (Left Right Circular Arrow Shape)
-// leftRightRibbon (Left Right Ribbon Shape)
-// leftRightUpArrow (Left Right Up Arrow Shape)
-// leftUpArrow (Left Up Arrow Shape)
-// lightningBolt (Lightning Bolt Shape)
-// line (Line Shape)
-// lineInv (Line Inverse Shape)
-// mathDivide (Divide Math Shape)
-// mathEqual (Equal Math Shape)
-// mathMinus (Minus Math Shape)
-// mathMultiply (Multiply Math Shape)
-// mathNotEqual (Not Equal Math Shape)
-// mathPlus (Plus Math Shape)
-// moon (Moon Shape)
-// nonIsoscelesTrapezoid (Non-Isosceles Trapezoid Shape)
-// noSmoking (No Smoking Shape)
-// notchedRightArrow (Notched Right Arrow Shape)
-// octagon (Octagon Shape)
-// parallelogram (Parallelogram Shape)
-// pentagon (Pentagon Shape)
-// pie (Pie Shape)
-// pieWedge (Pie Wedge Shape)
-// plaque (Plaque Shape)
-// plaqueTabs (Plaque Tabs Shape)
-// plus (Plus Shape)
-// quadArrow (Quad-Arrow Shape)
-// quadArrowCallout (Callout Quad-Arrow Shape)
-// rect (Rectangle Shape)
-// ribbon (Ribbon Shape)
-// ribbon2 (Ribbon 2 Shape)
-// rightArrow (Right Arrow Shape)
-// rightArrowCallout (Callout Right Arrow Shape)
-// rightBrace (Right Brace Shape)
-// rightBracket (Right Bracket Shape)
-// round1Rect (One Round Corner Rectangle Shape)
-// round2DiagRect (Two Diagonal Round Corner Rectangle Shape)
-// round2SameRect (Two Same-side Round Corner Rectangle Shape)
-// roundRect (Round Corner Rectangle Shape)
-// rtTriangle (Right Triangle Shape)
-// smileyFace (Smiley Face Shape)
-// snip1Rect (One Snip Corner Rectangle Shape)
-// snip2DiagRect (Two Diagonal Snip Corner Rectangle Shape)
-// snip2SameRect (Two Same-side Snip Corner Rectangle Shape)
-// snipRoundRect (One Snip One Round Corner Rectangle Shape)
-// squareTabs (Square Tabs Shape)
-// star10 (Ten Pointed Star Shape)
-// star12 (Twelve Pointed Star Shape)
-// star16 (Sixteen Pointed Star Shape)
-// star24 (Twenty Four Pointed Star Shape)
-// star32 (Thirty Two Pointed Star Shape)
-// star4 (Four Pointed Star Shape)
-// star5 (Five Pointed Star Shape)
-// star6 (Six Pointed Star Shape)
-// star7 (Seven Pointed Star Shape)
-// star8 (Eight Pointed Star Shape)
-// straightConnector1 (Straight Connector 1 Shape)
-// stripedRightArrow (Striped Right Arrow Shape)
-// sun (Sun Shape)
-// swooshArrow (Swoosh Arrow Shape)
-// teardrop (Teardrop Shape)
-// trapezoid (Trapezoid Shape)
-// triangle (Triangle Shape)
-// upArrow (Up Arrow Shape)
-// upArrowCallout (Callout Up Arrow Shape)
-// upDownArrow (Up Down Arrow Shape)
-// upDownArrowCallout (Callout Up Down Arrow Shape)
-// uturnArrow (U-Turn Arrow Shape)
-// verticalScroll (Vertical Scroll Shape)
-// wave (Wave Shape)
-// wedgeEllipseCallout (Callout Wedge Ellipse Shape)
-// wedgeRectCallout (Callout Wedge Rectangle Shape)
-// wedgeRoundRectCallout (Callout Wedge Round Rectangle Shape)
+// accentBorderCallout1 (Callout 1 with Border and Accent Shape)
+// accentBorderCallout2 (Callout 2 with Border and Accent Shape)
+// accentBorderCallout3 (Callout 3 with Border and Accent Shape)
+// accentCallout1 (Callout 1 Shape)
+// accentCallout2 (Callout 2 Shape)
+// accentCallout3 (Callout 3 Shape)
+// actionButtonBackPrevious (Back or Previous Button Shape)
+// actionButtonBeginning (Beginning Button Shape)
+// actionButtonBlank (Blank Button Shape)
+// actionButtonDocument (Document Button Shape)
+// actionButtonEnd (End Button Shape)
+// actionButtonForwardNext (Forward or Next Button Shape)
+// actionButtonHelp (Help Button Shape)
+// actionButtonHome (Home Button Shape)
+// actionButtonInformation (Information Button Shape)
+// actionButtonMovie (Movie Button Shape)
+// actionButtonReturn (Return Button Shape)
+// actionButtonSound (Sound Button Shape)
+// arc (Curved Arc Shape)
+// bentArrow (Bent Arrow Shape)
+// bentConnector2 (Bent Connector 2 Shape)
+// bentConnector3 (Bent Connector 3 Shape)
+// bentConnector4 (Bent Connector 4 Shape)
+// bentConnector5 (Bent Connector 5 Shape)
+// bentUpArrow (Bent Up Arrow Shape)
+// bevel (Bevel Shape)
+// blockArc (Block Arc Shape)
+// borderCallout1 (Callout 1 with Border Shape)
+// borderCallout2 (Callout 2 with Border Shape)
+// borderCallout3 (Callout 3 with Border Shape)
+// bracePair (Brace Pair Shape)
+// bracketPair (Bracket Pair Shape)
+// callout1 (Callout 1 Shape)
+// callout2 (Callout 2 Shape)
+// callout3 (Callout 3 Shape)
+// can (Can Shape)
+// chartPlus (Chart Plus Shape)
+// chartStar (Chart Star Shape)
+// chartX (Chart X Shape)
+// chevron (Chevron Shape)
+// chord (Chord Shape)
+// circularArrow (Circular Arrow Shape)
+// cloud (Cloud Shape)
+// cloudCallout (Callout Cloud Shape)
+// corner (Corner Shape)
+// cornerTabs (Corner Tabs Shape)
+// cube (Cube Shape)
+// curvedConnector2 (Curved Connector 2 Shape)
+// curvedConnector3 (Curved Connector 3 Shape)
+// curvedConnector4 (Curved Connector 4 Shape)
+// curvedConnector5 (Curved Connector 5 Shape)
+// curvedDownArrow (Curved Down Arrow Shape)
+// curvedLeftArrow (Curved Left Arrow Shape)
+// curvedRightArrow (Curved Right Arrow Shape)
+// curvedUpArrow (Curved Up Arrow Shape)
+// decagon (Decagon Shape)
+// diagStripe (Diagonal Stripe Shape)
+// diamond (Diamond Shape)
+// dodecagon (Dodecagon Shape)
+// donut (Donut Shape)
+// doubleWave (Double Wave Shape)
+// downArrow (Down Arrow Shape)
+// downArrowCallout (Callout Down Arrow Shape)
+// ellipse (Ellipse Shape)
+// ellipseRibbon (Ellipse Ribbon Shape)
+// ellipseRibbon2 (Ellipse Ribbon 2 Shape)
+// flowChartAlternateProcess (Alternate Process Flow Shape)
+// flowChartCollate (Collate Flow Shape)
+// flowChartConnector (Connector Flow Shape)
+// flowChartDecision (Decision Flow Shape)
+// flowChartDelay (Delay Flow Shape)
+// flowChartDisplay (Display Flow Shape)
+// flowChartDocument (Document Flow Shape)
+// flowChartExtract (Extract Flow Shape)
+// flowChartInputOutput (Input Output Flow Shape)
+// flowChartInternalStorage (Internal Storage Flow Shape)
+// flowChartMagneticDisk (Magnetic Disk Flow Shape)
+// flowChartMagneticDrum (Magnetic Drum Flow Shape)
+// flowChartMagneticTape (Magnetic Tape Flow Shape)
+// flowChartManualInput (Manual Input Flow Shape)
+// flowChartManualOperation (Manual Operation Flow Shape)
+// flowChartMerge (Merge Flow Shape)
+// flowChartMultidocument (Multi-Document Flow Shape)
+// flowChartOfflineStorage (Offline Storage Flow Shape)
+// flowChartOffpageConnector (Off-Page Connector Flow Shape)
+// flowChartOnlineStorage (Online Storage Flow Shape)
+// flowChartOr (Or Flow Shape)
+// flowChartPredefinedProcess (Predefined Process Flow Shape)
+// flowChartPreparation (Preparation Flow Shape)
+// flowChartProcess (Process Flow Shape)
+// flowChartPunchedCard (Punched Card Flow Shape)
+// flowChartPunchedTape (Punched Tape Flow Shape)
+// flowChartSort (Sort Flow Shape)
+// flowChartSummingJunction (Summing Junction Flow Shape)
+// flowChartTerminator (Terminator Flow Shape)
+// foldedCorner (Folded Corner Shape)
+// frame (Frame Shape)
+// funnel (Funnel Shape)
+// gear6 (Gear 6 Shape)
+// gear9 (Gear 9 Shape)
+// halfFrame (Half Frame Shape)
+// heart (Heart Shape)
+// heptagon (Heptagon Shape)
+// hexagon (Hexagon Shape)
+// homePlate (Home Plate Shape)
+// horizontalScroll (Horizontal Scroll Shape)
+// irregularSeal1 (Irregular Seal 1 Shape)
+// irregularSeal2 (Irregular Seal 2 Shape)
+// leftArrow (Left Arrow Shape)
+// leftArrowCallout (Callout Left Arrow Shape)
+// leftBrace (Left Brace Shape)
+// leftBracket (Left Bracket Shape)
+// leftCircularArrow (Left Circular Arrow Shape)
+// leftRightArrow (Left Right Arrow Shape)
+// leftRightArrowCallout (Callout Left Right Arrow Shape)
+// leftRightCircularArrow (Left Right Circular Arrow Shape)
+// leftRightRibbon (Left Right Ribbon Shape)
+// leftRightUpArrow (Left Right Up Arrow Shape)
+// leftUpArrow (Left Up Arrow Shape)
+// lightningBolt (Lightning Bolt Shape)
+// line (Line Shape)
+// lineInv (Line Inverse Shape)
+// mathDivide (Divide Math Shape)
+// mathEqual (Equal Math Shape)
+// mathMinus (Minus Math Shape)
+// mathMultiply (Multiply Math Shape)
+// mathNotEqual (Not Equal Math Shape)
+// mathPlus (Plus Math Shape)
+// moon (Moon Shape)
+// nonIsoscelesTrapezoid (Non-Isosceles Trapezoid Shape)
+// noSmoking (No Smoking Shape)
+// notchedRightArrow (Notched Right Arrow Shape)
+// octagon (Octagon Shape)
+// parallelogram (Parallelogram Shape)
+// pentagon (Pentagon Shape)
+// pie (Pie Shape)
+// pieWedge (Pie Wedge Shape)
+// plaque (Plaque Shape)
+// plaqueTabs (Plaque Tabs Shape)
+// plus (Plus Shape)
+// quadArrow (Quad-Arrow Shape)
+// quadArrowCallout (Callout Quad-Arrow Shape)
+// rect (Rectangle Shape)
+// ribbon (Ribbon Shape)
+// ribbon2 (Ribbon 2 Shape)
+// rightArrow (Right Arrow Shape)
+// rightArrowCallout (Callout Right Arrow Shape)
+// rightBrace (Right Brace Shape)
+// rightBracket (Right Bracket Shape)
+// round1Rect (One Round Corner Rectangle Shape)
+// round2DiagRect (Two Diagonal Round Corner Rectangle Shape)
+// round2SameRect (Two Same-side Round Corner Rectangle Shape)
+// roundRect (Round Corner Rectangle Shape)
+// rtTriangle (Right Triangle Shape)
+// smileyFace (Smiley Face Shape)
+// snip1Rect (One Snip Corner Rectangle Shape)
+// snip2DiagRect (Two Diagonal Snip Corner Rectangle Shape)
+// snip2SameRect (Two Same-side Snip Corner Rectangle Shape)
+// snipRoundRect (One Snip One Round Corner Rectangle Shape)
+// squareTabs (Square Tabs Shape)
+// star10 (Ten Pointed Star Shape)
+// star12 (Twelve Pointed Star Shape)
+// star16 (Sixteen Pointed Star Shape)
+// star24 (Twenty Four Pointed Star Shape)
+// star32 (Thirty Two Pointed Star Shape)
+// star4 (Four Pointed Star Shape)
+// star5 (Five Pointed Star Shape)
+// star6 (Six Pointed Star Shape)
+// star7 (Seven Pointed Star Shape)
+// star8 (Eight Pointed Star Shape)
+// straightConnector1 (Straight Connector 1 Shape)
+// stripedRightArrow (Striped Right Arrow Shape)
+// sun (Sun Shape)
+// swooshArrow (Swoosh Arrow Shape)
+// teardrop (Teardrop Shape)
+// trapezoid (Trapezoid Shape)
+// triangle (Triangle Shape)
+// upArrow (Up Arrow Shape)
+// upArrowCallout (Callout Up Arrow Shape)
+// upDownArrow (Up Down Arrow Shape)
+// upDownArrowCallout (Callout Up Down Arrow Shape)
+// uturnArrow (U-Turn Arrow Shape)
+// verticalScroll (Vertical Scroll Shape)
+// wave (Wave Shape)
+// wedgeEllipseCallout (Callout Wedge Ellipse Shape)
+// wedgeRectCallout (Callout Wedge Rectangle Shape)
+// wedgeRoundRectCallout (Callout Wedge Round Rectangle Shape)
//
// The following shows the type of text underline supported by excelize:
//
-// none
-// words
-// sng
-// dbl
-// heavy
-// dotted
-// dottedHeavy
-// dash
-// dashHeavy
-// dashLong
-// dashLongHeavy
-// dotDash
-// dotDashHeavy
-// dotDotDash
-// dotDotDashHeavy
-// wavy
-// wavyHeavy
-// wavyDbl
-//
-func (f *File) AddShape(sheet, cell, format string) error {
- formatSet, err := parseFormatShapeSet(format)
+// none
+// words
+// sng
+// dbl
+// heavy
+// dotted
+// dottedHeavy
+// dash
+// dashHeavy
+// dashLong
+// dashLongHeavy
+// dotDash
+// dotDashHeavy
+// dotDotDash
+// dotDotDashHeavy
+// wavy
+// wavyHeavy
+// wavyDbl
+func (f *File) AddShape(sheet string, opts *Shape) error {
+ options, err := parseShapeOptions(opts)
if err != nil {
return err
}
- // Read sheet data.
- xlsx, err := f.workSheetReader(sheet)
+ // Read sheet data
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
@@ -270,71 +303,46 @@ func (f *File) AddShape(sheet, cell, format string) error {
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
- if xlsx.Drawing != nil {
+ if ws.Drawing != nil {
// The worksheet already has a shape or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
- sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
+ sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
- drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1)
+ drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl")
} else {
// Add first shape for given sheet.
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetDrawing(sheet, rID)
f.addSheetNameSpace(sheet, SourceRelationship)
}
- err = f.addDrawingShape(sheet, drawingXML, cell, formatSet)
- if err != nil {
+ if err = f.addDrawingShape(sheet, drawingXML, opts.Cell, options); err != nil {
return err
}
- f.addContentTypePart(drawingID, "drawings")
- return err
+ return f.addContentTypePart(drawingID, "drawings")
}
-// addDrawingShape provides a function to add preset geometry by given sheet,
-// drawingXMLand format sets.
-func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) error {
+// twoCellAnchorShape create a two cell anchor shape size placeholder for a
+// group, a shape, or a drawing element.
+func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height uint, format GraphicOptions) (*xlsxWsDr, *xdrCellAnchor, int, error) {
fromCol, fromRow, err := CellNameToCoordinates(cell)
if err != nil {
- return err
+ return nil, nil, 0, err
}
- colIdx := fromCol - 1
- rowIdx := fromRow - 1
-
- textUnderlineType := map[string]bool{
- "none": true,
- "words": true,
- "sng": true,
- "dbl": true,
- "heavy": true,
- "dotted": true,
- "dottedHeavy": true,
- "dash": true,
- "dashHeavy": true,
- "dashLong": true,
- "dashLongHeavy": true,
- "dotDash": true,
- "dotDashHeavy": true,
- "dotDotDash": true,
- "dotDotDashHeavy": true,
- "wavy": true,
- "wavyHeavy": true,
- "wavyDbl": true,
+ w := int(float64(width) * format.ScaleX)
+ h := int(float64(height) * format.ScaleY)
+ colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, format.OffsetX, format.OffsetY, w, h)
+ content, cNvPrID, err := f.drawingParser(drawingXML)
+ if err != nil {
+ return content, nil, cNvPrID, err
}
-
- width := int(float64(formatSet.Width) * formatSet.Format.XScale)
- height := int(float64(formatSet.Height) * formatSet.Format.YScale)
-
- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 :=
- f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY,
- width, height)
- content, cNvPrID := f.drawingParser(drawingXML)
twoCellAnchor := xdrCellAnchor{}
- twoCellAnchor.EditAs = formatSet.Format.Positioning
+ twoCellAnchor.EditAs = format.Positioning
from := xlsxFrom{}
from.Col = colStart
- from.ColOff = formatSet.Format.OffsetX * EMU
+ from.ColOff = format.OffsetX * EMU
from.Row = rowStart
- from.RowOff = formatSet.Format.OffsetY * EMU
+ from.RowOff = format.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
@@ -342,7 +350,23 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
to.RowOff = y2 * EMU
twoCellAnchor.From = &from
twoCellAnchor.To = &to
+ return content, &twoCellAnchor, cNvPrID, err
+}
+
+// addDrawingShape provides a function to add preset geometry by given sheet,
+// drawingXML and format sets.
+func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
+ content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(
+ sheet, drawingXML, cell, opts.Width, opts.Height, opts.Format)
+ if err != nil {
+ return err
+ }
+ var solidColor string
+ if len(opts.Fill.Color) == 1 {
+ solidColor = opts.Fill.Color[0]
+ }
shape := xdrSp{
+ Macro: opts.Macro,
NvSpPr: &xdrNvSpPr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
@@ -354,13 +378,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
},
SpPr: &xlsxSpPr{
PrstGeom: xlsxPrstGeom{
- Prst: formatSet.Type,
+ Prst: opts.Type,
},
},
Style: &xdrStyle{
- LnRef: setShapeRef(formatSet.Color.Line, 2),
- FillRef: setShapeRef(formatSet.Color.Fill, 1),
- EffectRef: setShapeRef(formatSet.Color.Effect, 0),
+ LnRef: setShapeRef(opts.Line.Color, 2),
+ FillRef: setShapeRef(solidColor, 1),
+ EffectRef: setShapeRef("", 0),
FontRef: &aFontRef{
Idx: "minor",
SchemeClr: &attrValString{
@@ -378,26 +402,38 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
},
},
}
- if len(formatSet.Paragraph) < 1 {
- formatSet.Paragraph = []formatShapeParagraph{
+ if *opts.Line.Width != 1 {
+ shape.SpPr.Ln = xlsxLineProperties{
+ W: f.ptToEMUs(*opts.Line.Width),
+ }
+ }
+ defaultFont, err := f.GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ if len(opts.Paragraph) < 1 {
+ opts.Paragraph = []RichTextRun{
{
- Font: Font{
+ Font: &Font{
Bold: false,
Italic: false,
Underline: "none",
- Family: f.GetDefaultFont(),
+ Family: defaultFont,
Size: 11,
- Color: "#000000",
+ Color: "000000",
},
Text: " ",
},
}
}
- for _, p := range formatSet.Paragraph {
- u := p.Font.Underline
- _, ok := textUnderlineType[u]
- if !ok {
- u = "none"
+ for _, p := range opts.Paragraph {
+ u := "none"
+ font := &Font{}
+ if p.Font != nil {
+ font = p.Font
+ }
+ if idx := inStrSlice(supportedDrawingUnderlineTypes, font.Underline, true); idx != -1 {
+ u = supportedDrawingUnderlineTypes[idx]
}
text := p.Text
if text == "" {
@@ -406,13 +442,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
paragraph := &aP{
R: &aR{
RPr: aRPr{
- I: p.Font.Italic,
- B: p.Font.Bold,
+ I: font.Italic,
+ B: font.Bold,
Lang: "en-US",
AltLang: "en-US",
U: u,
- Sz: p.Font.Size * 100,
- Latin: &aLatin{Typeface: p.Font.Family},
+ Sz: font.Size * 100,
+ Latin: &xlsxCTTextFont{Typeface: font.Family},
},
T: text,
},
@@ -420,7 +456,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
Lang: "en-US",
},
}
- srgbClr := strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1)
+ srgbClr := strings.ReplaceAll(strings.ToUpper(font.Color), "#", "")
if len(srgbClr) == 6 {
paragraph.R.RPr.SolidFill = &aSolidFill{
SrgbClr: &attrValString{
@@ -432,11 +468,11 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
}
twoCellAnchor.Sp = &shape
twoCellAnchor.ClientData = &xdrClientData{
- FLocksWithSheet: formatSet.Format.FLocksWithSheet,
- FPrintsWithSheet: formatSet.Format.FPrintsWithSheet,
+ FLocksWithSheet: *opts.Format.Locked,
+ FPrintsWithSheet: *opts.Format.PrintObject,
}
- content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
- f.Drawings[drawingXML] = content
+ content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
+ f.Drawings.Store(drawingXML, content)
return err
}
@@ -456,7 +492,7 @@ func setShapeRef(color string, i int) *aRef {
return &aRef{
Idx: i,
SrgbClr: &attrValString{
- Val: stringPtr(strings.Replace(strings.ToUpper(color), "#", "", -1)),
+ Val: stringPtr(strings.ReplaceAll(strings.ToUpper(color), "#", "")),
},
}
}
diff --git a/shape_test.go b/shape_test.go
index 61fb443d7d..57c7501a3f 100644
--- a/shape_test.go
+++ b/shape_test.go
@@ -12,17 +12,104 @@ func TestAddShape(t *testing.T) {
if !assert.NoError(t, err) {
t.FailNow()
}
-
- assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`))
- assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`))
- assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`))
- assert.EqualError(t, f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is not exist")
- assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input")
- assert.EqualError(t, f.AddShape("Sheet1", "A", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.NoError(t, f.AddShape("Sheet1", &Shape{
+ Cell: "A30",
+ Type: "rect",
+ Paragraph: []RichTextRun{
+ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
+ {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
+ },
+ }))
+ assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
+ assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"}))
+ assert.EqualError(t, f.AddShape("Sheet3",
+ &Shape{
+ Cell: "H1",
+ Type: "ellipseRibbon",
+ Line: ShapeLine{Color: "4286F4"},
+ Fill: Fill{Color: []string{"8EB9FF"}},
+ Paragraph: []RichTextRun{
+ {
+ Font: &Font{
+ Bold: true,
+ Italic: true,
+ Family: "Times New Roman",
+ Size: 36,
+ Color: "777777",
+ Underline: "single",
+ },
+ },
+ },
+ },
+ ), "sheet Sheet3 does not exist")
+ assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil))
+ assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet1", &Shape{Cell: "A1"}))
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddShape("Sheet1", &Shape{
+ Cell: "A",
+ Type: "rect",
+ Paragraph: []RichTextRun{
+ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
+ {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
+ },
+ }))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
- // Test add first shape for given sheet.
+ // Test add first shape for given sheet
f = NewFile()
- assert.NoError(t, f.AddShape("Sheet1", "A1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`))
+ lineWidth := 1.2
+ assert.NoError(t, f.AddShape("Sheet1",
+ &Shape{
+ Cell: "A1",
+ Type: "ellipseRibbon",
+ Line: ShapeLine{Color: "4286F4", Width: &lineWidth},
+ Fill: Fill{Color: []string{"8EB9FF"}},
+ Paragraph: []RichTextRun{
+ {
+ Font: &Font{
+ Bold: true,
+ Italic: true,
+ Family: "Times New Roman",
+ Size: 36,
+ Color: "777777",
+ Underline: "single",
+ },
+ },
+ },
+ Height: 90,
+ }))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
+ // Test add shape with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.AddShape("Sheet:1", &Shape{
+ Cell: "A30",
+ Type: "rect",
+ Paragraph: []RichTextRun{
+ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
+ {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
+ },
+ }))
+ // Test add shape with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
+ // Test add shape with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestAddDrawingShape(t *testing.T) {
+ f := NewFile()
+ path := "xl/drawings/drawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1",
+ &Shape{
+ Width: defaultShapeSize,
+ Height: defaultShapeSize,
+ Format: GraphicOptions{
+ PrintObject: boolPtr(true),
+ Locked: boolPtr(false),
+ },
+ },
+ ), "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/sheet.go b/sheet.go
index a92221d4fb..65f2a4b008 100644
--- a/sheet.go
+++ b/sheet.go
@@ -1,48 +1,68 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
- "encoding/json"
"encoding/xml"
- "errors"
"fmt"
"io"
- "io/ioutil"
- "log"
"os"
"path"
+ "path/filepath"
"reflect"
"regexp"
+ "sort"
"strconv"
"strings"
+ "unicode/utf16"
"unicode/utf8"
- "github.com/mohae/deepcopy"
+ "github.com/tiendc/go-deepcopy"
)
-// NewSheet provides function to create a new sheet by given worksheet name.
-// When creating a new spreadsheet file, the default worksheet will be
-// created. Returns the number of sheets in the workbook (file) after
-// appending the new sheet.
-func (f *File) NewSheet(name string) int {
+// IgnoredErrorsType is the type of ignored errors.
+type IgnoredErrorsType byte
+
+// Ignored errors types enumeration.
+const (
+ IgnoredErrorsEvalError = iota
+ IgnoredErrorsTwoDigitTextYear
+ IgnoredErrorsNumberStoredAsText
+ IgnoredErrorsFormula
+ IgnoredErrorsFormulaRange
+ IgnoredErrorsUnlockedFormula
+ IgnoredErrorsEmptyCellReference
+ IgnoredErrorsListDataValidation
+ IgnoredErrorsCalculatedColumn
+)
+
+// NewSheet provides the function to create a new sheet by given a worksheet
+// name and returns the index of the sheets in the workbook after it appended.
+// Note that when creating a new workbook, the default worksheet named
+// `Sheet1` will be created.
+func (f *File) NewSheet(sheet string) (int, error) {
+ var err error
+ if err = checkSheetName(sheet); err != nil {
+ return -1, err
+ }
// Check if the worksheet already exists
- if f.GetSheetIndex(name) != -1 {
- return f.SheetCount
+ index, err := f.GetSheetIndex(sheet)
+ if index != -1 {
+ return index, err
}
- f.DeleteSheet(name)
+ _ = f.DeleteSheet(sheet)
f.SheetCount++
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
sheetID := 0
for _, v := range wb.Sheets.Sheet {
if v.SheetID > sheetID {
@@ -50,33 +70,30 @@ func (f *File) NewSheet(name string) int {
}
}
sheetID++
- // Update docProps/app.xml
- f.setAppXML()
// Update [Content_Types].xml
- f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet)
+ _ = f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet)
// Create new sheet /xl/worksheets/sheet%d.xml
- f.setSheet(sheetID, name)
- // Update xl/_rels/workbook.xml.rels
- rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipWorkSheet, fmt.Sprintf("worksheets/sheet%d.xml", sheetID), "")
- // Update xl/workbook.xml
- f.setWorkbook(name, sheetID, rID)
- return f.GetSheetIndex(name)
+ f.setSheet(sheetID, sheet)
+ // Update workbook.xml.rels
+ rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipWorkSheet, fmt.Sprintf("/xl/worksheets/sheet%d.xml", sheetID), "")
+ // Update workbook.xml
+ f.setWorkbook(sheet, sheetID, rID)
+ return f.GetSheetIndex(sheet)
}
// contentTypesReader provides a function to get the pointer to the
// [Content_Types].xml structure after deserialization.
-func (f *File) contentTypesReader() *xlsxTypes {
- var err error
-
+func (f *File) contentTypesReader() (*xlsxTypes, error) {
if f.ContentTypes == nil {
f.ContentTypes = new(xlsxTypes)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))).
+ f.ContentTypes.mu.Lock()
+ defer f.ContentTypes.mu.Unlock()
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))).
Decode(f.ContentTypes); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return f.ContentTypes, err
}
}
-
- return f.ContentTypes
+ return f.ContentTypes, nil
}
// contentTypesWriter provides a function to save [Content_Types].xml after
@@ -84,149 +101,198 @@ func (f *File) contentTypesReader() *xlsxTypes {
func (f *File) contentTypesWriter() {
if f.ContentTypes != nil {
output, _ := xml.Marshal(f.ContentTypes)
- f.saveFileList("[Content_Types].xml", output)
+ f.saveFileList(defaultXMLPathContentTypes, output)
}
}
-// workbookReader provides a function to get the pointer to the xl/workbook.xml
-// structure after deserialization.
-func (f *File) workbookReader() *xlsxWorkbook {
- var err error
- if f.WorkBook == nil {
- f.WorkBook = new(xlsxWorkbook)
- if _, ok := f.xmlAttr["xl/workbook.xml"]; !ok {
- d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml"))))
- f.xmlAttr["xl/workbook.xml"] = append(f.xmlAttr["xl/workbook.xml"], getRootElement(d)...)
- f.addNameSpaces("xl/workbook.xml", SourceRelationship)
- }
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))).
- Decode(f.WorkBook); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
- }
+// getWorksheetPath construct a target XML as xl/worksheets/sheet%d by split
+// path, compatible with different types of relative paths in
+// workbook.xml.rels, for example: worksheets/sheet%d.xml
+// and /xl/worksheets/sheet%d.xml
+func (f *File) getWorksheetPath(relTarget string) (path string) {
+ path = filepath.ToSlash(strings.TrimPrefix(
+ strings.ReplaceAll(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), relTarget)), "\\", "/"), "/"))
+ if strings.HasPrefix(relTarget, "/") {
+ path = filepath.ToSlash(strings.TrimPrefix(strings.ReplaceAll(filepath.Clean(relTarget), "\\", "/"), "/"))
}
- return f.WorkBook
+ return path
}
-// workBookWriter provides a function to save xl/workbook.xml after serialize
-// structure.
-func (f *File) workBookWriter() {
- if f.WorkBook != nil {
- output, _ := xml.Marshal(f.WorkBook)
- f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(f.replaceNameSpaceBytes("xl/workbook.xml", output)))
+// mergeExpandedCols merge expanded columns.
+func (f *File) mergeExpandedCols(ws *xlsxWorksheet) {
+ sort.Slice(ws.Cols.Col, func(i, j int) bool {
+ return ws.Cols.Col[i].Min < ws.Cols.Col[j].Min
+ })
+ var columns []xlsxCol
+ for i, n := 0, len(ws.Cols.Col); i < n; {
+ left := i
+ for i++; i < n && reflect.DeepEqual(
+ xlsxCol{
+ BestFit: ws.Cols.Col[i-1].BestFit,
+ Collapsed: ws.Cols.Col[i-1].Collapsed,
+ CustomWidth: ws.Cols.Col[i-1].CustomWidth,
+ Hidden: ws.Cols.Col[i-1].Hidden,
+ Max: ws.Cols.Col[i-1].Max + 1,
+ Min: ws.Cols.Col[i-1].Min + 1,
+ OutlineLevel: ws.Cols.Col[i-1].OutlineLevel,
+ Phonetic: ws.Cols.Col[i-1].Phonetic,
+ Style: ws.Cols.Col[i-1].Style,
+ Width: ws.Cols.Col[i-1].Width,
+ }, ws.Cols.Col[i]); i++ {
+ }
+ var column xlsxCol
+ deepcopy.Copy(&column, ws.Cols.Col[left])
+ if left < i-1 {
+ column.Max = ws.Cols.Col[i-1].Min
+ }
+ columns = append(columns, column)
}
+ ws.Cols.Col = columns
}
// workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after
// serialize structure.
func (f *File) workSheetWriter() {
- for p, sheet := range f.Sheet {
- if sheet != nil {
- for k, v := range sheet.SheetData.Row {
- f.Sheet[p].SheetData.Row[k].C = trimCell(v.C)
+ var (
+ arr []byte
+ buffer = bytes.NewBuffer(arr)
+ encoder = xml.NewEncoder(buffer)
+ )
+ f.Sheet.Range(func(p, ws interface{}) bool {
+ if ws != nil {
+ sheet := ws.(*xlsxWorksheet)
+ if sheet.MergeCells != nil && len(sheet.MergeCells.Cells) > 0 {
+ _ = f.mergeOverlapCells(sheet)
+ }
+ if sheet.Cols != nil && len(sheet.Cols.Col) > 0 {
+ f.mergeExpandedCols(sheet)
}
- output, _ := xml.Marshal(sheet)
- f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, output)))
- ok := f.checked[p]
+ sheet.SheetData.Row = trimRow(&sheet.SheetData)
+ if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil {
+ f.addNameSpaces(p.(string), SourceRelationship)
+ }
+ if sheet.DecodeAlternateContent != nil {
+ sheet.AlternateContent = &xlsxAlternateContent{
+ Content: sheet.DecodeAlternateContent.Content,
+ XMLNSMC: SourceRelationshipCompatibility.Value,
+ }
+ }
+ sheet.DecodeAlternateContent = nil
+ // reusing buffer
+ _ = encoder.Encode(sheet)
+ f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes())))
+ _, ok := f.checked.Load(p.(string))
if ok {
- delete(f.Sheet, p)
- f.checked[p] = false
+ f.Sheet.Delete(p.(string))
+ f.checked.Delete(p.(string))
}
+ buffer.Reset()
}
+ return true
+ })
+}
+
+// trimRow provides a function to trim empty rows.
+func trimRow(sheetData *xlsxSheetData) []xlsxRow {
+ var (
+ row xlsxRow
+ i int
+ )
+
+ for k := range sheetData.Row {
+ row = sheetData.Row[k]
+ if row = trimCell(row); len(row.C) != 0 || row.hasAttr() {
+ sheetData.Row[i] = row
+ }
+ i++
}
+ return sheetData.Row[:i]
}
// trimCell provides a function to trim blank cells which created by fillColumns.
-func trimCell(column []xlsxC) []xlsxC {
+func trimCell(row xlsxRow) xlsxRow {
+ column := row.C
rowFull := true
for i := range column {
rowFull = column[i].hasValue() && rowFull
}
if rowFull {
- return column
+ return row
}
- col := make([]xlsxC, len(column))
i := 0
for _, c := range column {
if c.hasValue() {
- col[i] = c
+ row.C[i] = c
i++
}
}
- return col[0:i]
+ row.C = row.C[:i]
+ return row
}
// setContentTypes provides a function to read and update property of contents
// type of the spreadsheet.
-func (f *File) setContentTypes(partName, contentType string) {
- content := f.contentTypesReader()
+func (f *File) setContentTypes(partName, contentType string) error {
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
content.Overrides = append(content.Overrides, xlsxOverride{
PartName: partName,
ContentType: contentType,
})
+ return err
}
// setSheet provides a function to update sheet property by given index.
func (f *File) setSheet(index int, name string) {
- xlsx := xlsxWorksheet{
+ ws := xlsxWorksheet{
Dimension: &xlsxDimension{Ref: "A1"},
SheetViews: &xlsxSheetViews{
SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
},
}
- path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
- f.sheetMap[trimSheetName(name)] = path
- f.Sheet[path] = &xlsx
- f.xmlAttr[path] = append(f.xmlAttr[path], NameSpaceSpreadSheet)
-}
-
-// setWorkbook update workbook property of the spreadsheet. Maximum 31
-// characters are allowed in sheet title.
-func (f *File) setWorkbook(name string, sheetID, rid int) {
- content := f.workbookReader()
- content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{
- Name: trimSheetName(name),
- SheetID: sheetID,
- ID: "rId" + strconv.Itoa(rid),
- })
+ sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
+ f.sheetMap[name] = sheetXMLPath
+ f.Sheet.Store(sheetXMLPath, &ws)
+ f.xmlAttr.Store(sheetXMLPath, []xml.Attr{NameSpaceSpreadSheet})
}
// relsWriter provides a function to save relationships after
// serialize structure.
func (f *File) relsWriter() {
- for path, rel := range f.Relationships {
+ f.Relationships.Range(func(path, rel interface{}) bool {
if rel != nil {
- output, _ := xml.Marshal(rel)
- if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") {
- output = f.replaceNameSpaceBytes(path, output)
+ output, _ := xml.Marshal(rel.(*xlsxRelationships))
+ if strings.HasPrefix(path.(string), "xl/worksheets/sheet/rels/sheet") {
+ output = f.replaceNameSpaceBytes(path.(string), output)
}
- f.saveFileList(path, replaceRelationshipsBytes(output))
+ f.saveFileList(path.(string), replaceRelationshipsBytes(output))
}
- }
-}
-
-// setAppXML update docProps/app.xml file of XML.
-func (f *File) setAppXML() {
- f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp))
+ return true
+ })
}
// replaceRelationshipsBytes; Some tools that read spreadsheet files have very
// strict requirements about the structure of the input XML. This function is
// a horrible hack to fix that after the XML marshalling is completed.
func replaceRelationshipsBytes(content []byte) []byte {
- oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`)
- newXmlns := []byte("r")
- return bytesReplace(content, oldXmlns, newXmlns, -1)
+ sourceXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`)
+ targetXmlns := []byte("r")
+ return bytesReplace(content, sourceXmlns, targetXmlns, -1)
}
// SetActiveSheet provides a function to set the default active sheet of the
// workbook by a given index. Note that the active index is different from the
-// ID returned by function GetSheetMap(). It should be greater or equal to 0
+// ID returned by function GetSheetMap(). It should be greater than or equal to 0
// and less than the total worksheet numbers.
func (f *File) SetActiveSheet(index int) {
if index < 0 {
index = 0
}
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
for activeTab := range wb.Sheets.Sheet {
if activeTab == index {
if wb.BookViews == nil {
@@ -242,24 +308,24 @@ func (f *File) SetActiveSheet(index int) {
}
}
for idx, name := range f.GetSheetList() {
- xlsx, err := f.workSheetReader(name)
+ ws, err := f.workSheetReader(name)
if err != nil {
- // Chartsheet or dialogsheet
+ // Chartsheet, macrosheet or dialogsheet
return
}
- if xlsx.SheetViews == nil {
- xlsx.SheetViews = &xlsxSheetViews{
+ if ws.SheetViews == nil {
+ ws.SheetViews = &xlsxSheetViews{
SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
}
}
- if len(xlsx.SheetViews.SheetView) > 0 {
- xlsx.SheetViews.SheetView[0].TabSelected = false
+ if len(ws.SheetViews.SheetView) > 0 {
+ ws.SheetViews.SheetView[0].TabSelected = false
}
if index == idx {
- if len(xlsx.SheetViews.SheetView) > 0 {
- xlsx.SheetViews.SheetView[0].TabSelected = true
+ if len(ws.SheetViews.SheetView) > 0 {
+ ws.SheetViews.SheetView[0].TabSelected = true
} else {
- xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
+ ws.SheetViews.SheetView = append(ws.SheetViews.SheetView, xlsxSheetView{
TabSelected: true,
})
}
@@ -270,22 +336,23 @@ func (f *File) SetActiveSheet(index int) {
// GetActiveSheetIndex provides a function to get active sheet index of the
// spreadsheet. If not found the active sheet will be return integer 0.
func (f *File) GetActiveSheetIndex() (index int) {
- var sheetID = f.getActiveSheetID()
- wb := f.workbookReader()
+ sheetID := f.getActiveSheetID()
+ wb, _ := f.workbookReader()
if wb != nil {
for idx, sheet := range wb.Sheets.Sheet {
if sheet.SheetID == sheetID {
index = idx
+ return
}
}
}
return
}
-// getActiveSheetID provides a function to get active sheet index of the
+// getActiveSheetID provides a function to get active sheet ID of the
// spreadsheet. If not found the active sheet will be return integer 0.
func (f *File) getActiveSheetID() int {
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
if wb != nil {
if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 {
activeTab := wb.BookViews.WorkBookView[0].ActiveTab
@@ -300,38 +367,37 @@ func (f *File) getActiveSheetID() int {
return 0
}
-// SetSheetName provides a function to set the worksheet name by given old and
-// new worksheet names. Maximum 31 characters are allowed in sheet title and
+// SetSheetName provides a function to set the worksheet name by given source and
+// target worksheet names. Maximum 31 characters are allowed in sheet title and
// this function only changes the name of the sheet and will not update the
// sheet name in the formula or reference associated with the cell. So there
// may be problem formula error or reference missing.
-func (f *File) SetSheetName(oldName, newName string) {
- oldName = trimSheetName(oldName)
- newName = trimSheetName(newName)
- content := f.workbookReader()
- for k, v := range content.Sheets.Sheet {
- if v.Name == oldName {
- content.Sheets.Sheet[k].Name = newName
- f.sheetMap[newName] = f.sheetMap[oldName]
- delete(f.sheetMap, oldName)
- }
+func (f *File) SetSheetName(source, target string) error {
+ var err error
+ if err = checkSheetName(source); err != nil {
+ return err
}
-}
-
-// getSheetNameByID provides a function to get worksheet name of the
-// spreadsheet by given worksheet ID. If given sheet ID is invalid, will
-// return an empty string.
-func (f *File) getSheetNameByID(ID int) string {
- wb := f.workbookReader()
- if wb == nil || ID < 1 {
- return ""
+ if err = checkSheetName(target); err != nil {
+ return err
}
- for _, sheet := range wb.Sheets.Sheet {
- if ID == sheet.SheetID {
- return sheet.Name
+ if target == source {
+ return err
+ }
+ wb, _ := f.workbookReader()
+ for k, v := range wb.Sheets.Sheet {
+ if v.Name == source {
+ wb.Sheets.Sheet[k].Name = target
+ f.sheetMap[target] = f.sheetMap[source]
+ delete(f.sheetMap, source)
}
}
- return ""
+ if wb.DefinedNames == nil {
+ return err
+ }
+ for i, dn := range wb.DefinedNames.DefinedName {
+ wb.DefinedNames.DefinedName[i].Data = adjustRangeSheetName(dn.Data, source, target)
+ }
+ return err
}
// GetSheetName provides a function to get the sheet name of the workbook by
@@ -341,6 +407,7 @@ func (f *File) GetSheetName(index int) (name string) {
for idx, sheet := range f.GetSheetList() {
if idx == index {
name = sheet
+ return
}
}
return
@@ -349,42 +416,47 @@ func (f *File) GetSheetName(index int) (name string) {
// getSheetID provides a function to get worksheet ID of the spreadsheet by
// given sheet name. If given worksheet name is invalid, will return an
// integer type value -1.
-func (f *File) getSheetID(name string) int {
- var ID = -1
- for sheetID, sheet := range f.GetSheetMap() {
- if sheet == trimSheetName(name) {
- ID = sheetID
+func (f *File) getSheetID(sheet string) int {
+ for sheetID, name := range f.GetSheetMap() {
+ if strings.EqualFold(name, sheet) {
+ return sheetID
}
}
- return ID
+ return -1
}
// GetSheetIndex provides a function to get a sheet index of the workbook by
-// the given sheet name. If the given sheet name is invalid, it will return an
-// integer type value 0.
-func (f *File) GetSheetIndex(name string) int {
- var idx = -1
- for index, sheet := range f.GetSheetList() {
- if sheet == trimSheetName(name) {
- idx = index
+// the given sheet name. If the given sheet name is invalid or sheet doesn't
+// exist, it will return an integer type value -1.
+func (f *File) GetSheetIndex(sheet string) (int, error) {
+ if err := checkSheetName(sheet); err != nil {
+ return -1, err
+ }
+ for index, name := range f.GetSheetList() {
+ if strings.EqualFold(name, sheet) {
+ return index, nil
}
}
- return idx
+ return -1, nil
}
// GetSheetMap provides a function to get worksheets, chart sheets, dialog
// sheets ID and name map of the workbook. For example:
//
-// f, err := excelize.OpenFile("Book1.xlsx")
-// if err != nil {
-// return
-// }
-// for index, name := range f.GetSheetMap() {
-// fmt.Println(index, name)
-// }
-//
+// f, err := excelize.OpenFile("Book1.xlsx")
+// if err != nil {
+// return
+// }
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// for index, name := range f.GetSheetMap() {
+// fmt.Println(index, name)
+// }
func (f *File) GetSheetMap() map[int]string {
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
sheetMap := map[int]string{}
if wb != nil {
for _, sheet := range wb.Sheets.Sheet {
@@ -397,7 +469,7 @@ func (f *File) GetSheetMap() map[int]string {
// GetSheetList provides a function to get worksheets, chart sheets, and
// dialog sheets name list of the workbook.
func (f *File) GetSheetList() (list []string) {
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
if wb != nil {
for _, sheet := range wb.Sheets.Sheet {
list = append(list, sheet.Name)
@@ -407,134 +479,282 @@ func (f *File) GetSheetList() (list []string) {
}
// getSheetMap provides a function to get worksheet name and XML file path map
-// of XLSX.
-func (f *File) getSheetMap() map[string]string {
- content := f.workbookReader()
- rels := f.relsReader("xl/_rels/workbook.xml.rels")
+// of the spreadsheet.
+func (f *File) getSheetMap() (map[string]string, error) {
maps := map[string]string{}
- for _, v := range content.Sheets.Sheet {
+ wb, err := f.workbookReader()
+ if err != nil {
+ return nil, err
+ }
+ rels, err := f.relsReader(f.getWorkbookRelsPath())
+ if err != nil {
+ return nil, err
+ }
+ if rels == nil {
+ return maps, nil
+ }
+ for _, v := range wb.Sheets.Sheet {
for _, rel := range rels.Relationships {
if rel.ID == v.ID {
- // Construct a target XML as xl/worksheets/sheet%d by split path, compatible with different types of relative paths in workbook.xml.rels, for example: worksheets/sheet%d.xml and /xl/worksheets/sheet%d.xml
- pathInfo := strings.Split(rel.Target, "/")
- pathInfoLen := len(pathInfo)
- if pathInfoLen > 1 {
- maps[v.Name] = fmt.Sprintf("xl/%s", strings.Join(pathInfo[pathInfoLen-2:], "/"))
+ sheetXMLPath := f.getWorksheetPath(rel.Target)
+ if _, ok := f.Pkg.Load(sheetXMLPath); ok {
+ maps[v.Name] = sheetXMLPath
+ }
+ if _, ok := f.tempFiles.Load(sheetXMLPath); ok {
+ maps[v.Name] = sheetXMLPath
}
}
}
}
- return maps
+ return maps, nil
+}
+
+// getSheetXMLPath provides a function to get XML file path by given sheet
+// name.
+func (f *File) getSheetXMLPath(sheet string) (string, bool) {
+ var (
+ name string
+ ok bool
+ )
+ for sheetName, filePath := range f.sheetMap {
+ if strings.EqualFold(sheetName, sheet) {
+ name, ok = filePath, true
+ break
+ }
+ }
+ return name, ok
}
// SetSheetBackground provides a function to set background picture by given
-// worksheet name and file path.
+// worksheet name and file path. Supported image types: BMP, EMF, EMZ, GIF,
+// JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ.
func (f *File) SetSheetBackground(sheet, picture string) error {
var err error
// Check picture exists first.
if _, err = os.Stat(picture); os.IsNotExist(err) {
return err
}
- ext, ok := supportImageTypes[path.Ext(picture)]
+ file, _ := os.ReadFile(filepath.Clean(picture))
+ return f.setSheetBackground(sheet, path.Ext(picture), file)
+}
+
+// SetSheetBackgroundFromBytes provides a function to set background picture by
+// given worksheet name, extension name and image data. Supported image types:
+// BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ.
+func (f *File) SetSheetBackgroundFromBytes(sheet, extension string, picture []byte) error {
+ if len(picture) == 0 {
+ return ErrParameterInvalid
+ }
+ return f.setSheetBackground(sheet, extension, picture)
+}
+
+// setSheetBackground provides a function to set background picture by given
+// worksheet name, file name extension and image data.
+func (f *File) setSheetBackground(sheet, extension string, file []byte) error {
+ imageType, ok := supportedImageTypes[strings.ToLower(extension)]
if !ok {
- return errors.New("unsupported image extension")
+ return ErrImgExt
}
- file, _ := ioutil.ReadFile(picture)
- name := f.addMedia(file, ext)
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
+ name := f.addMedia(file, imageType)
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "")
- f.addSheetPicture(sheet, rID)
+ if err := f.addSheetPicture(sheet, rID); err != nil {
+ return err
+ }
f.addSheetNameSpace(sheet, SourceRelationship)
- f.setContentTypePartImageExtensions()
- return err
+ return f.setContentTypePartImageExtensions()
}
// DeleteSheet provides a function to delete worksheet in a workbook by given
// worksheet name. Use this method with caution, which will affect changes in
// references such as formulas, charts, and so on. If there is any referenced
-// value of the deleted worksheet, it will cause a file error when you open it.
-// This function will be invalid when only the one worksheet is left.
-func (f *File) DeleteSheet(name string) {
- if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 {
- return
+// value of the deleted worksheet, it will cause a file error when you open
+// it. This function will be invalid when only one worksheet is left.
+func (f *File) DeleteSheet(sheet string) error {
+ if err := checkSheetName(sheet); err != nil {
+ return err
}
- sheetName := trimSheetName(name)
- wb := f.workbookReader()
- wbRels := f.relsReader("xl/_rels/workbook.xml.rels")
- for idx, sheet := range wb.Sheets.Sheet {
- if sheet.Name == sheetName {
- wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...)
- var sheetXML, rels string
- if wbRels != nil {
- for _, rel := range wbRels.Relationships {
- if rel.ID == sheet.ID {
- sheetXML = fmt.Sprintf("xl/%s", rel.Target)
- pathInfo := strings.Split(rel.Target, "/")
- if len(pathInfo) == 2 {
- rels = fmt.Sprintf("xl/%s/_rels/%s.rels", pathInfo[0], pathInfo[1])
- }
- }
+ if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 {
+ return nil
+ }
+
+ wb, _ := f.workbookReader()
+ wbRels, _ := f.relsReader(f.getWorkbookRelsPath())
+ activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
+ deleteLocalSheetID, _ := f.GetSheetIndex(sheet)
+ deleteAndAdjustDefinedNames(wb, deleteLocalSheetID)
+
+ for idx, v := range wb.Sheets.Sheet {
+ if !strings.EqualFold(v.Name, sheet) {
+ continue
+ }
+
+ wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...)
+ var sheetXML, rels string
+ if wbRels != nil {
+ for _, rel := range wbRels.Relationships {
+ if rel.ID == v.ID {
+ sheetXML = f.getWorksheetPath(rel.Target)
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ rels = "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
}
}
- target := f.deleteSheetFromWorkbookRels(sheet.ID)
- f.deleteSheetFromContentTypes(target)
- f.deleteCalcChain(sheet.SheetID, "") // Delete CalcChain
- delete(f.sheetMap, sheetName)
- delete(f.XLSX, sheetXML)
- delete(f.XLSX, rels)
- delete(f.Relationships, rels)
- delete(f.Sheet, sheetXML)
- delete(f.xmlAttr, sheetXML)
- f.SheetCount--
- }
- }
- if wb.BookViews != nil {
- for idx, bookView := range wb.BookViews.WorkBookView {
- if bookView.ActiveTab >= f.SheetCount {
- wb.BookViews.WorkBookView[idx].ActiveTab--
+ }
+ target := f.deleteSheetFromWorkbookRels(v.ID)
+ _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, target)
+ _ = f.deleteCalcChain(f.getSheetID(sheet), "")
+ delete(f.sheetMap, v.Name)
+ f.Pkg.Delete(sheetXML)
+ f.Pkg.Delete(rels)
+ f.Relationships.Delete(rels)
+ f.Sheet.Delete(sheetXML)
+ f.xmlAttr.Delete(sheetXML)
+ f.SheetCount--
+ }
+ index, err := f.GetSheetIndex(activeSheetName)
+ f.SetActiveSheet(index)
+ return err
+}
+
+// MoveSheet moves a sheet to a specified position in the workbook. The function
+// moves the source sheet before the target sheet. After moving, other sheets
+// will be shifted to the left or right. If the sheet is already at the target
+// position, the function will not perform any action. Not that this function
+// will be ungroup all sheets after moving. For example, move Sheet2 before
+// Sheet1:
+//
+// err := f.MoveSheet("Sheet2", "Sheet1")
+func (f *File) MoveSheet(source, target string) error {
+ if strings.EqualFold(source, target) {
+ return nil
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ sourceIdx, err := f.GetSheetIndex(source)
+ if err != nil {
+ return err
+ }
+ targetIdx, err := f.GetSheetIndex(target)
+ if err != nil {
+ return err
+ }
+ if sourceIdx < 0 {
+ return ErrSheetNotExist{source}
+ }
+ if targetIdx < 0 {
+ return ErrSheetNotExist{target}
+ }
+ _ = f.UngroupSheets()
+ activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
+ sourceSheet := wb.Sheets.Sheet[sourceIdx]
+ wb.Sheets.Sheet = append(wb.Sheets.Sheet[:sourceIdx], wb.Sheets.Sheet[sourceIdx+1:]...)
+ if targetIdx > sourceIdx {
+ targetIdx--
+ }
+ wb.Sheets.Sheet = append(wb.Sheets.Sheet[:targetIdx], append([]xlsxSheet{sourceSheet}, wb.Sheets.Sheet[targetIdx:]...)...)
+ activeSheetIdx, _ := f.GetSheetIndex(activeSheetName)
+ f.SetActiveSheet(activeSheetIdx)
+ return err
+}
+
+// deleteAndAdjustDefinedNames delete and adjust defined name in the workbook
+// by given worksheet ID.
+func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
+ if wb == nil || wb.DefinedNames == nil {
+ return
+ }
+ for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ {
+ dn := wb.DefinedNames.DefinedName[idx]
+ if dn.LocalSheetID != nil {
+ localSheetID := *dn.LocalSheetID
+ if localSheetID == deleteLocalSheetID {
+ wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...)
+ idx--
+ } else if localSheetID > deleteLocalSheetID {
+ wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1)
}
}
}
- f.SetActiveSheet(len(f.GetSheetMap()))
}
// deleteSheetFromWorkbookRels provides a function to remove worksheet
-// relationships by given relationships ID in the file
-// xl/_rels/workbook.xml.rels.
+// relationships by given relationships ID in the file workbook.xml.rels.
func (f *File) deleteSheetFromWorkbookRels(rID string) string {
- content := f.relsReader("xl/_rels/workbook.xml.rels")
- for k, v := range content.Relationships {
+ rels, _ := f.relsReader(f.getWorkbookRelsPath())
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
+ for k, v := range rels.Relationships {
if v.ID == rID {
- content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...)
+ rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
return v.Target
}
}
return ""
}
-// deleteSheetFromContentTypes provides a function to remove worksheet
-// relationships by given target name in the file [Content_Types].xml.
-func (f *File) deleteSheetFromContentTypes(target string) {
- content := f.contentTypesReader()
- for k, v := range content.Overrides {
- if v.PartName == "/xl/"+target {
- content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
+// deleteSheetRelationships provides a function to delete relationships in
+// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
+// relationship index.
+func (f *File) deleteSheetRelationships(sheet, rID string) {
+ name, ok := f.getSheetXMLPath(sheet)
+ if !ok {
+ name = strings.ToLower(sheet) + ".xml"
+ }
+ rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
+ sheetRels, _ := f.relsReader(rels)
+ if sheetRels == nil {
+ sheetRels = &xlsxRelationships{}
+ }
+ sheetRels.mu.Lock()
+ defer sheetRels.mu.Unlock()
+ for k, v := range sheetRels.Relationships {
+ if v.ID == rID {
+ sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
+ }
+ }
+ f.Relationships.Store(rels, sheetRels)
+}
+
+// getSheetRelationshipsTargetByID provides a function to get Target attribute
+// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
+// relationship index.
+func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
+ name, ok := f.getSheetXMLPath(sheet)
+ if !ok {
+ name = strings.ToLower(sheet) + ".xml"
+ }
+ rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
+ sheetRels, _ := f.relsReader(rels)
+ if sheetRels == nil {
+ sheetRels = &xlsxRelationships{}
+ }
+ sheetRels.mu.Lock()
+ defer sheetRels.mu.Unlock()
+ for _, v := range sheetRels.Relationships {
+ if v.ID == rID {
+ return v.Target
}
}
+ return ""
}
// CopySheet provides a function to duplicate a worksheet by gave source and
// target worksheet index. Note that currently doesn't support duplicate
// workbooks that contain tables, charts or pictures. For Example:
//
-// // Sheet1 already exists...
-// index := f.NewSheet("Sheet2")
-// err := f.CopySheet(1, index)
-// return err
-//
+// // Sheet1 already exists...
+// index, err := f.NewSheet("Sheet2")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// err := f.CopySheet(1, index)
func (f *File) CopySheet(from, to int) error {
if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" {
- return errors.New("invalid worksheet index")
+ return ErrSheetIdx
}
return f.copySheet(from, to)
}
@@ -547,263 +767,363 @@ func (f *File) copySheet(from, to int) error {
if err != nil {
return err
}
- worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet)
+ worksheet := &xlsxWorksheet{}
+ deepcopy.Copy(worksheet, sheet)
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
- path := "xl/worksheets/sheet" + toSheetID + ".xml"
+ sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml"
if len(worksheet.SheetViews.SheetView) > 0 {
worksheet.SheetViews.SheetView[0].TabSelected = false
}
worksheet.Drawing = nil
worksheet.TableParts = nil
worksheet.PageSetUp = nil
- f.Sheet[path] = worksheet
+ f.Sheet.Store(sheetXMLPath, worksheet)
toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels"
fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels"
- _, ok := f.XLSX[fromRels]
- if ok {
- f.XLSX[toRels] = f.XLSX[fromRels]
+ if rels, ok := f.Pkg.Load(fromRels); ok && rels != nil {
+ f.Pkg.Store(toRels, rels.([]byte))
}
- fromSheetXMLPath, _ := f.sheetMap[trimSheetName(fromSheet)]
- fromSheetAttr, _ := f.xmlAttr[fromSheetXMLPath]
- f.xmlAttr[path] = fromSheetAttr
+ fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet)
+ fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath)
+ f.xmlAttr.Store(sheetXMLPath, fromSheetAttr)
return err
}
-// SetSheetVisible provides a function to set worksheet visible by given worksheet
-// name. A workbook must contain at least one visible worksheet. If the given
-// worksheet has been activated, this setting will be invalidated. Sheet state
-// values as defined by https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues
-//
-// visible
-// hidden
-// veryHidden
+// getSheetState returns sheet visible enumeration by given hidden status.
+func getSheetState(visible bool, veryHidden []bool) string {
+ state := "hidden"
+ if !visible && len(veryHidden) > 0 && veryHidden[0] {
+ state = "veryHidden"
+ }
+ return state
+}
+
+// SetSheetVisible provides a function to set worksheet visible by given
+// worksheet name. A workbook must contain at least one visible worksheet. If
+// the given worksheet has been activated, this setting will be invalidated.
+// The third optional veryHidden parameter only works when visible was false.
//
// For example, hide Sheet1:
//
-// err := f.SetSheetVisible("Sheet1", false)
-//
-func (f *File) SetSheetVisible(name string, visible bool) error {
- name = trimSheetName(name)
- content := f.workbookReader()
+// err := f.SetSheetVisible("Sheet1", false)
+func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) error {
+ if err := checkSheetName(sheet); err != nil {
+ return err
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
if visible {
- for k, v := range content.Sheets.Sheet {
- if v.Name == name {
- content.Sheets.Sheet[k].State = ""
+ for k, v := range wb.Sheets.Sheet {
+ if strings.EqualFold(v.Name, sheet) {
+ wb.Sheets.Sheet[k].State = ""
}
}
- return nil
+ return err
}
- count := 0
- for _, v := range content.Sheets.Sheet {
- if v.State != "hidden" {
+ count, state := 0, getSheetState(visible, veryHidden)
+ for _, v := range wb.Sheets.Sheet {
+ if v.State != state {
count++
}
}
- for k, v := range content.Sheets.Sheet {
- xlsx, err := f.workSheetReader(v.Name)
+ for k, v := range wb.Sheets.Sheet {
+ ws, err := f.workSheetReader(v.Name)
if err != nil {
return err
}
tabSelected := false
- if len(xlsx.SheetViews.SheetView) > 0 {
- tabSelected = xlsx.SheetViews.SheetView[0].TabSelected
+ if ws.SheetViews == nil {
+ ws.SheetViews = &xlsxSheetViews{
+ SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
+ }
+ }
+ if len(ws.SheetViews.SheetView) > 0 {
+ tabSelected = ws.SheetViews.SheetView[0].TabSelected
}
- if v.Name == name && count > 1 && !tabSelected {
- content.Sheets.Sheet[k].State = "hidden"
+ if strings.EqualFold(v.Name, sheet) && count > 1 && !tabSelected {
+ wb.Sheets.Sheet[k].State = state
}
}
- return nil
+ return err
}
-// parseFormatPanesSet provides a function to parse the panes settings.
-func parseFormatPanesSet(formatSet string) (*formatPanes, error) {
- format := formatPanes{}
- err := json.Unmarshal([]byte(formatSet), &format)
- return &format, err
+// setPanes set create freeze panes and split panes by given options.
+func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
+ if panes == nil {
+ return ErrParameterInvalid
+ }
+ p := &xlsxPane{
+ ActivePane: panes.ActivePane,
+ TopLeftCell: panes.TopLeftCell,
+ XSplit: float64(panes.XSplit),
+ YSplit: float64(panes.YSplit),
+ }
+ if panes.Freeze {
+ p.State = "frozen"
+ }
+ if ws.SheetViews == nil {
+ ws.SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}}
+ }
+ ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p
+ if !(panes.Freeze) && !(panes.Split) {
+ if len(ws.SheetViews.SheetView) > 0 {
+ ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil
+ }
+ }
+ var s []*xlsxSelection
+ for _, p := range panes.Selection {
+ s = append(s, &xlsxSelection{
+ ActiveCell: p.ActiveCell,
+ Pane: p.Pane,
+ SQRef: p.SQRef,
+ })
+ }
+ ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Selection = s
+ return nil
}
// SetPanes provides a function to create and remove freeze panes and split panes
-// by given worksheet name and panes format set.
+// by given worksheet name and panes options.
//
-// activePane defines the pane that is active. The possible values for this
+// ActivePane defines the pane that is active. The possible values for this
// attribute are defined in the following table:
//
-// Enumeration Value | Description
-// --------------------------------+-------------------------------------------------------------
-// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal
-// | splits are applied.
-// |
-// | This value is also used when only a horizontal split has
-// | been applied, dividing the pane into upper and lower
-// | regions. In that case, this value specifies the bottom
-// | pane.
-// |
-// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal
-// | splits are applied.
-// |
-// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits
-// | are applied.
-// |
-// | This value is also used when only a horizontal split has
-// | been applied, dividing the pane into upper and lower
-// | regions. In that case, this value specifies the top pane.
-// |
-// | This value is also used when only a vertical split has
-// | been applied, dividing the pane into right and left
-// | regions. In that case, this value specifies the left pane
-// |
-// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal
-// | splits are applied.
-// |
-// | This value is also used when only a vertical split has
-// | been applied, dividing the pane into right and left
-// | regions. In that case, this value specifies the right
-// | pane.
+// Enumeration Value | Description
+// ---------------------------------+-------------------------------------------------------------
+// bottomLeft (Bottom Left Pane) | Bottom left pane, when both vertical and horizontal
+// | splits are applied.
+// |
+// | This value is also used when only a horizontal split has
+// | been applied, dividing the pane into upper and lower
+// | regions. In that case, this value specifies the bottom
+// | pane.
+// |
+// bottomRight (Bottom Right Pane) | Bottom right pane, when both vertical and horizontal
+// | splits are applied.
+// |
+// topLeft (Top Left Pane) | Top left pane, when both vertical and horizontal splits
+// | are applied.
+// |
+// | This value is also used when only a horizontal split has
+// | been applied, dividing the pane into upper and lower
+// | regions. In that case, this value specifies the top pane.
+// |
+// | This value is also used when only a vertical split has
+// | been applied, dividing the pane into right and left
+// | regions. In that case, this value specifies the left pane
+// |
+// topRight (Top Right Pane) | Top right pane, when both vertical and horizontal
+// | splits are applied.
+// |
+// | This value is also used when only a vertical split has
+// | been applied, dividing the pane into right and left
+// | regions. In that case, this value specifies the right
+// | pane.
//
// Pane state type is restricted to the values supported currently listed in the following table:
//
-// Enumeration Value | Description
-// --------------------------------+-------------------------------------------------------------
-// frozen (Frozen) | Panes are frozen, but were not split being frozen. In
-// | this state, when the panes are unfrozen again, a single
-// | pane results, with no split.
-// |
-// | In this state, the split bars are not adjustable.
-// |
-// split (Split) | Panes are split, but not frozen. In this state, the split
-// | bars are adjustable by the user.
+// Enumeration Value | Description
+// ---------------------------------+-------------------------------------------------------------
+// frozen (Frozen) | Panes are frozen, but were not split being frozen. In
+// | this state, when the panes are unfrozen again, a single
+// | pane results, with no split.
+// |
+// | In this state, the split bars are not adjustable.
+// |
+// split (Split) | Panes are split, but not frozen. In this state, the split
+// | bars are adjustable by the user.
//
-// x_split (Horizontal Split Position): Horizontal position of the split, in
+// XSplit (Horizontal Split Position): Horizontal position of the split, in
// 1/20th of a point; 0 (zero) if none. If the pane is frozen, this value
// indicates the number of columns visible in the top pane.
//
-// y_split (Vertical Split Position): Vertical position of the split, in 1/20th
+// YSplit (Vertical Split Position): Vertical position of the split, in 1/20th
// of a point; 0 (zero) if none. If the pane is frozen, this value indicates the
// number of rows visible in the left pane. The possible values for this
// attribute are defined by the W3C XML Schema double datatype.
//
-// top_left_cell: Location of the top left visible cell in the bottom right pane
+// TopLeftCell: Location of the top left visible cell in the bottom right pane
// (when in Left-To-Right mode).
//
-// sqref (Sequence of References): Range of the selection. Can be non-contiguous
+// SQRef (Sequence of References): Range of the selection. Can be non-contiguous
// set of ranges.
//
// An example of how to freeze column A in the Sheet1 and set the active cell on
// Sheet1!K16:
//
-// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)
+// err := f.SetPanes("Sheet1", &excelize.Panes{
+// Freeze: true,
+// Split: false,
+// XSplit: 1,
+// YSplit: 0,
+// TopLeftCell: "B1",
+// ActivePane: "topRight",
+// Selection: []excelize.Selection{
+// {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
+// },
+// })
//
// An example of how to freeze rows 1 to 9 in the Sheet1 and set the active cell
// ranges on Sheet1!A11:XFD11:
//
-// f.SetPanes("Sheet1", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)
+// err := f.SetPanes("Sheet1", &excelize.Panes{
+// Freeze: true,
+// Split: false,
+// XSplit: 0,
+// YSplit: 9,
+// TopLeftCell: "A34",
+// ActivePane: "bottomLeft",
+// Selection: []excelize.Selection{
+// {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
+// },
+// })
//
// An example of how to create split panes in the Sheet1 and set the active cell
// on Sheet1!J60:
//
-// f.SetPanes("Sheet1", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`)
+// err := f.SetPanes("Sheet1", &excelize.Panes{
+// Freeze: false,
+// Split: true,
+// XSplit: 3270,
+// YSplit: 1800,
+// TopLeftCell: "N57",
+// ActivePane: "bottomLeft",
+// Selection: []excelize.Selection{
+// {SQRef: "I36", ActiveCell: "I36"},
+// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
+// {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
+// {SQRef: "O60", ActiveCell: "O60", Pane: "bottomRight"},
+// },
+// })
//
// An example of how to unfreeze and remove all panes on Sheet1:
//
-// f.SetPanes("Sheet1", `{"freeze":false,"split":false}`)
-//
-func (f *File) SetPanes(sheet, panes string) error {
- fs, _ := parseFormatPanesSet(panes)
- xlsx, err := f.workSheetReader(sheet)
+// err := f.SetPanes("Sheet1", &excelize.Panes{Freeze: false, Split: false})
+func (f *File) SetPanes(sheet string, panes *Panes) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- p := &xlsxPane{
- ActivePane: fs.ActivePane,
- TopLeftCell: fs.TopLeftCell,
- XSplit: float64(fs.XSplit),
- YSplit: float64(fs.YSplit),
+ return ws.setPanes(panes)
+}
+
+// getPanes returns freeze panes, split panes, and views of the worksheet.
+func (ws *xlsxWorksheet) getPanes() Panes {
+ var (
+ panes Panes
+ section []Selection
+ )
+ if ws.SheetViews == nil || len(ws.SheetViews.SheetView) < 1 {
+ return panes
+ }
+ sw := ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1]
+ for _, s := range sw.Selection {
+ if s != nil {
+ section = append(section, Selection{
+ SQRef: s.SQRef,
+ ActiveCell: s.ActiveCell,
+ Pane: s.Pane,
+ })
+ }
}
- if fs.Freeze {
- p.State = "frozen"
+ panes.Selection = section
+ if sw.Pane == nil {
+ return panes
}
- xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = p
- if !(fs.Freeze) && !(fs.Split) {
- if len(xlsx.SheetViews.SheetView) > 0 {
- xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = nil
- }
+ panes.ActivePane = sw.Pane.ActivePane
+ if sw.Pane.State == "frozen" {
+ panes.Freeze = true
}
- s := []*xlsxSelection{}
- for _, p := range fs.Panes {
- s = append(s, &xlsxSelection{
- ActiveCell: p.ActiveCell,
- Pane: p.Pane,
- SQRef: p.SQRef,
- })
+ panes.TopLeftCell = sw.Pane.TopLeftCell
+ panes.XSplit = int(sw.Pane.XSplit)
+ panes.YSplit = int(sw.Pane.YSplit)
+ return panes
+}
+
+// GetPanes provides a function to get freeze panes, split panes, and worksheet
+// views by given worksheet name.
+func (f *File) GetPanes(sheet string) (Panes, error) {
+ var panes Panes
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return panes, err
}
- xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Selection = s
- return err
+ return ws.getPanes(), err
}
// GetSheetVisible provides a function to get worksheet visible by given worksheet
// name. For example, get visible state of Sheet1:
//
-// f.GetSheetVisible("Sheet1")
-//
-func (f *File) GetSheetVisible(name string) bool {
- content := f.workbookReader()
- visible := false
- for k, v := range content.Sheets.Sheet {
- if v.Name == trimSheetName(name) {
- if content.Sheets.Sheet[k].State == "" || content.Sheets.Sheet[k].State == "visible" {
+// visible, err := f.GetSheetVisible("Sheet1")
+func (f *File) GetSheetVisible(sheet string) (bool, error) {
+ var visible bool
+ if err := checkSheetName(sheet); err != nil {
+ return visible, err
+ }
+ wb, _ := f.workbookReader()
+ for k, v := range wb.Sheets.Sheet {
+ if strings.EqualFold(v.Name, sheet) {
+ if wb.Sheets.Sheet[k].State == "" || wb.Sheets.Sheet[k].State == "visible" {
visible = true
}
}
}
- return visible
+ return visible, nil
}
-// SearchSheet provides a function to get coordinates by given worksheet name,
+// SearchSheet provides a function to get cell reference by given worksheet name,
// cell value, and regular expression. The function doesn't support searching
// on the calculated result, formatted numbers and conditional lookup
-// currently. If it is a merged cell, it will return the coordinates of the
-// upper left corner of the merged area.
+// currently. If it is a merged cell, it will return the cell reference of the
+// upper left cell of the merged range reference.
//
-// An example of search the coordinates of the value of "100" on Sheet1:
+// An example of search the cell reference of the value of "100" on Sheet1:
//
-// result, err := f.SearchSheet("Sheet1", "100")
+// result, err := f.SearchSheet("Sheet1", "100")
//
-// An example of search the coordinates where the numerical value in the range
+// An example of search the cell reference where the numerical value in the range
// of "0-9" of Sheet1 is described:
//
-// result, err := f.SearchSheet("Sheet1", "[0-9]", true)
-//
+// result, err := f.SearchSheet("Sheet1", "[0-9]", true)
func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) {
var (
regSearch bool
result []string
)
+ if err := checkSheetName(sheet); err != nil {
+ return result, err
+ }
for _, r := range reg {
regSearch = r
}
- name, ok := f.sheetMap[trimSheetName(sheet)]
+ name, ok := f.getSheetXMLPath(sheet)
if !ok {
return result, ErrSheetNotExist{sheet}
}
- if f.Sheet[name] != nil {
- // flush data
- output, _ := xml.Marshal(f.Sheet[name])
+ if ws, ok := f.Sheet.Load(name); ok && ws != nil {
+ // Flush data
+ output, _ := xml.Marshal(ws.(*xlsxWorksheet))
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
}
return f.searchSheet(name, value, regSearch)
}
-// searchSheet provides a function to get coordinates by given worksheet name,
-// cell value, and regular expression.
+// searchSheet provides a function to get cell reference by given worksheet
+// name, cell value, and regular expression.
func (f *File) searchSheet(name, value string, regSearch bool) (result []string, err error) {
var (
cellName, inElement string
cellCol, row int
- d *xlsxSST
+ sst *xlsxSST
)
- d = f.sharedStringsReader()
- decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name)))
+ if sst, err = f.sharedStringsReader(); err != nil {
+ return
+ }
+ regex := regexp.MustCompile(value)
+ decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name)))
for {
var token xml.Token
token, err = decoder.Token()
@@ -813,21 +1133,20 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
}
break
}
- switch startElement := token.(type) {
+ switch xmlElement := token.(type) {
case xml.StartElement:
- inElement = startElement.Name.Local
+ inElement = xmlElement.Name.Local
if inElement == "row" {
- row, err = attrValToInt("r", startElement.Attr)
+ row, err = attrValToInt("r", xmlElement.Attr)
if err != nil {
return
}
}
if inElement == "c" {
colCell := xlsxC{}
- _ = decoder.DecodeElement(&colCell, &startElement)
- val, _ := colCell.getValueFrom(f, d)
+ _ = decoder.DecodeElement(&colCell, &xmlElement)
+ val, _ := colCell.getValueFrom(f, sst, false)
if regSearch {
- regex := regexp.MustCompile(value)
if !regex.MatchString(val) {
continue
}
@@ -866,100 +1185,128 @@ func attrValToInt(name string, attrs []xml.Attr) (val int, err error) {
return
}
+// attrValToFloat provides a function to convert the local names to a float64
+// by given XML attributes and specified names.
+func attrValToFloat(name string, attrs []xml.Attr) (val float64, err error) {
+ for _, attr := range attrs {
+ if attr.Name.Local == name {
+ val, err = strconv.ParseFloat(attr.Value, 64)
+ if err != nil {
+ return
+ }
+ }
+ }
+ return
+}
+
+// attrValToBool provides a function to convert the local names to a boolean
+// by given XML attributes and specified names.
+func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
+ for _, attr := range attrs {
+ if attr.Name.Local == name {
+ val, err = strconv.ParseBool(attr.Value)
+ if err != nil {
+ return
+ }
+ }
+ }
+ return
+}
+
// SetHeaderFooter provides a function to set headers and footers by given
// worksheet name and the control characters.
//
// Headers and footers are specified using the following settings fields:
//
-// Fields | Description
-// ------------------+-----------------------------------------------------------
-// AlignWithMargins | Align header footer margins with page margins
-// DifferentFirst | Different first-page header and footer indicator
-// DifferentOddEven | Different odd and even page headers and footers indicator
-// ScaleWithDoc | Scale header and footer with document scaling
-// OddFooter | Odd Page Footer
-// OddHeader | Odd Header
-// EvenFooter | Even Page Footer
-// EvenHeader | Even Page Header
-// FirstFooter | First Page Footer
-// FirstHeader | First Page Header
+// Fields | Description
+// ------------------+-----------------------------------------------------------
+// AlignWithMargins | Align header footer margins with page margins
+// DifferentFirst | Different first-page header and footer indicator
+// DifferentOddEven | Different odd and even page headers and footers indicator
+// ScaleWithDoc | Scale header and footer with document scaling
+// OddFooter | Odd Page Footer, or primary Page Footer if 'DifferentOddEven' is 'false'
+// OddHeader | Odd Header, or primary Page Header if 'DifferentOddEven' is 'false'
+// EvenFooter | Even Page Footer
+// EvenHeader | Even Page Header
+// FirstFooter | First Page Footer
+// FirstHeader | First Page Header
//
// The following formatting codes can be used in 6 string type fields:
// OddHeader, OddFooter, EvenHeader, EvenFooter, FirstFooter, FirstHeader
//
-// Formatting Code | Description
-// ------------------------+-------------------------------------------------------------------------
-// && | The character "&"
-// |
-// &font-size | Size of the text font, where font-size is a decimal font size in points
-// |
-// &"font name,font type" | A text font-name string, font name, and a text font-type string,
-// | font type
-// |
-// &"-,Regular" | Regular text format. Toggles bold and italic modes to off
-// |
-// &A | Current worksheet's tab name
-// |
-// &B or &"-,Bold" | Bold text format, from off to on, or vice versa. The default mode is off
-// |
-// &D | Current date
-// |
-// &C | Center section
-// |
-// &E | Double-underline text format
-// |
-// &F | Current workbook's file name
-// |
-// &G | Drawing object as background
-// |
-// &H | Shadow text format
-// |
-// &I or &"-,Italic" | Italic text format
-// |
-// &K | Text font color
-// |
-// | An RGB Color is specified as RRGGBB
-// |
-// | A Theme Color is specified as TTSNNN where TT is the theme color Id,
-// | S is either "+" or "-" of the tint/shade value, and NNN is the
-// | tint/shade value
-// |
-// &L | Left section
-// |
-// &N | Total number of pages
-// |
-// &O | Outline text format
-// |
-// &P[[+|-]n] | Without the optional suffix, the current page number in decimal
-// |
-// &R | Right section
-// |
-// &S | Strikethrough text format
-// |
-// &T | Current time
-// |
-// &U | Single-underline text format. If double-underline mode is on, the next
-// | occurrence in a section specifier toggles double-underline mode to off;
-// | otherwise, it toggles single-underline mode, from off to on, or vice
-// | versa. The default mode is off
-// |
-// &X | Superscript text format
-// |
-// &Y | Subscript text format
-// |
-// &Z | Current workbook's file path
+// Formatting Code | Description
+// ------------------------+-------------------------------------------------------------------------
+// && | The character "&"
+// |
+// &font-size | Size of the text font, where font-size is a decimal font size in points
+// |
+// &"font name,font type" | A text font-name string, font name, and a text font-type string,
+// | font type
+// |
+// &"-,Regular" | Regular text format. Toggles bold and italic modes to off
+// |
+// &A | Current worksheet's tab name
+// |
+// &B or &"-,Bold" | Bold text format, from off to on, or vice versa. The default mode is off
+// |
+// &D | Current date
+// |
+// &C | Center section
+// |
+// &E | Double-underline text format
+// |
+// &F | Current workbook's file name
+// |
+// &G | Drawing object as background (Use AddHeaderFooterImage)
+// |
+// &H | Shadow text format
+// |
+// &I or &"-,Italic" | Italic text format
+// |
+// &K | Text font color
+// |
+// | An RGB Color is specified as RRGGBB
+// |
+// | A Theme Color is specified as TTSNNN where TT is the theme color Id,
+// | S is either "+" or "-" of the tint/shade value, and NNN is the
+// | tint/shade value
+// |
+// &L | Left section
+// |
+// &N | Total number of pages
+// |
+// &O | Outline text format
+// |
+// &P[[+|-]n] | Without the optional suffix, the current page number in decimal
+// |
+// &R | Right section
+// |
+// &S | Strike through text format
+// |
+// &T | Current time
+// |
+// &U | Single-underline text format. If double-underline mode is on, the next
+// | occurrence in a section specifier toggles double-underline mode to off;
+// | otherwise, it toggles single-underline mode, from off to on, or vice
+// | versa. The default mode is off
+// |
+// &X | Superscript text format
+// |
+// &Y | Subscript text format
+// |
+// &Z | Current workbook's file path
//
// For example:
//
-// err := f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{
-// DifferentFirst: true,
-// DifferentOddEven: true,
-// OddHeader: "&R&P",
-// OddFooter: "&C&F",
-// EvenHeader: "&L&P",
-// EvenFooter: "&L&D&R&T",
-// FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`,
-// })
+// err := f.SetHeaderFooter("Sheet1", &excelize.HeaderFooterOptions{
+// DifferentFirst: true,
+// DifferentOddEven: true,
+// OddHeader: "&R&P",
+// OddFooter: "&C&F",
+// EvenHeader: "&L&P",
+// EvenFooter: "&L&D&R&T",
+// FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`,
+// })
//
// This example shows:
//
@@ -981,405 +1328,459 @@ func attrValToInt(name string, attrs []xml.Attr) (val int, err error) {
// that same page
//
// - No footer on the first page
-//
-func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error {
- xlsx, err := f.workSheetReader(sheet)
+func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if settings == nil {
- xlsx.HeaderFooter = nil
+ if opts == nil {
+ ws.HeaderFooter = nil
return err
}
- v := reflect.ValueOf(*settings)
+ v := reflect.ValueOf(*opts)
// Check 6 string type fields: OddHeader, OddFooter, EvenHeader, EvenFooter,
// FirstFooter, FirstHeader
for i := 4; i < v.NumField()-1; i++ {
- if v.Field(i).Len() >= 255 {
- return fmt.Errorf("field %s must be less than 255 characters", v.Type().Field(i).Name)
- }
- }
- xlsx.HeaderFooter = &xlsxHeaderFooter{
- AlignWithMargins: settings.AlignWithMargins,
- DifferentFirst: settings.DifferentFirst,
- DifferentOddEven: settings.DifferentOddEven,
- ScaleWithDoc: settings.ScaleWithDoc,
- OddHeader: settings.OddHeader,
- OddFooter: settings.OddFooter,
- EvenHeader: settings.EvenHeader,
- EvenFooter: settings.EvenFooter,
- FirstFooter: settings.FirstFooter,
- FirstHeader: settings.FirstHeader,
+ if len(utf16.Encode([]rune(v.Field(i).String()))) > MaxFieldLength {
+ return newFieldLengthError(v.Type().Field(i).Name)
+ }
+ }
+ ws.HeaderFooter = &xlsxHeaderFooter{
+ AlignWithMargins: opts.AlignWithMargins,
+ DifferentFirst: opts.DifferentFirst,
+ DifferentOddEven: opts.DifferentOddEven,
+ ScaleWithDoc: opts.ScaleWithDoc,
+ OddHeader: opts.OddHeader,
+ OddFooter: opts.OddFooter,
+ EvenHeader: opts.EvenHeader,
+ EvenFooter: opts.EvenFooter,
+ FirstFooter: opts.FirstFooter,
+ FirstHeader: opts.FirstHeader,
}
return err
}
-// ProtectSheet provides a function to prevent other users from accidentally
-// or deliberately changing, moving, or deleting data in a worksheet. For
-// example, protect Sheet1 with protection settings:
-//
-// err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{
-// Password: "password",
-// EditScenarios: false,
-// })
+// GetHeaderFooter provides a function to get worksheet header and footer by
+// given worksheet name.
+func (f *File) GetHeaderFooter(sheet string) (*HeaderFooterOptions, error) {
+ var opts *HeaderFooterOptions
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return opts, err
+ }
+ if ws.HeaderFooter == nil {
+ return opts, err
+ }
+ opts = &HeaderFooterOptions{
+ AlignWithMargins: ws.HeaderFooter.AlignWithMargins,
+ DifferentFirst: ws.HeaderFooter.DifferentFirst,
+ DifferentOddEven: ws.HeaderFooter.DifferentOddEven,
+ ScaleWithDoc: ws.HeaderFooter.ScaleWithDoc,
+ OddHeader: ws.HeaderFooter.OddHeader,
+ OddFooter: ws.HeaderFooter.OddFooter,
+ EvenHeader: ws.HeaderFooter.EvenHeader,
+ EvenFooter: ws.HeaderFooter.EvenFooter,
+ FirstHeader: ws.HeaderFooter.FirstHeader,
+ FirstFooter: ws.HeaderFooter.FirstFooter,
+ }
+ return opts, err
+}
+
+// ProtectSheet provides a function to prevent other users from accidentally or
+// deliberately changing, moving, or deleting data in a worksheet. The
+// optional field AlgorithmName specified hash algorithm, support XOR, MD4,
+// MD5, SHA-1, SHA2-56, SHA-384, and SHA-512 currently, if no hash algorithm
+// specified, will be using the XOR algorithm as default. For example, protect
+// Sheet1 with protection settings:
//
-func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error {
- xlsx, err := f.workSheetReader(sheet)
+// err := f.ProtectSheet("Sheet1", &excelize.SheetProtectionOptions{
+// AlgorithmName: "SHA-512",
+// Password: "password",
+// SelectLockedCells: true,
+// SelectUnlockedCells: true,
+// EditScenarios: true,
+// })
+func (f *File) ProtectSheet(sheet string, opts *SheetProtectionOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if settings == nil {
- settings = &FormatSheetProtection{
- EditObjects: true,
- EditScenarios: true,
- SelectLockedCells: true,
- }
- }
- xlsx.SheetProtection = &xlsxSheetProtection{
- AutoFilter: settings.AutoFilter,
- DeleteColumns: settings.DeleteColumns,
- DeleteRows: settings.DeleteRows,
- FormatCells: settings.FormatCells,
- FormatColumns: settings.FormatColumns,
- FormatRows: settings.FormatRows,
- InsertColumns: settings.InsertColumns,
- InsertHyperlinks: settings.InsertHyperlinks,
- InsertRows: settings.InsertRows,
- Objects: settings.EditObjects,
- PivotTables: settings.PivotTables,
- Scenarios: settings.EditScenarios,
- SelectLockedCells: settings.SelectLockedCells,
- SelectUnlockedCells: settings.SelectUnlockedCells,
+ if opts == nil {
+ return ErrParameterInvalid
+ }
+ ws.SheetProtection = &xlsxSheetProtection{
+ AutoFilter: !opts.AutoFilter,
+ DeleteColumns: !opts.DeleteColumns,
+ DeleteRows: !opts.DeleteRows,
+ FormatCells: !opts.FormatCells,
+ FormatColumns: !opts.FormatColumns,
+ FormatRows: !opts.FormatRows,
+ InsertColumns: !opts.InsertColumns,
+ InsertHyperlinks: !opts.InsertHyperlinks,
+ InsertRows: !opts.InsertRows,
+ Objects: !opts.EditObjects,
+ PivotTables: !opts.PivotTables,
+ Scenarios: !opts.EditScenarios,
+ SelectLockedCells: !opts.SelectLockedCells,
+ SelectUnlockedCells: !opts.SelectUnlockedCells,
Sheet: true,
- Sort: settings.Sort,
+ Sort: !opts.Sort,
}
- if settings.Password != "" {
- xlsx.SheetProtection.Password = genSheetPasswd(settings.Password)
+ if opts.Password != "" {
+ if opts.AlgorithmName == "" {
+ ws.SheetProtection.Password = genSheetPasswd(opts.Password)
+ return err
+ }
+ hashValue, saltValue, err := genISOPasswdHash(opts.Password, opts.AlgorithmName, "", int(sheetProtectionSpinCount))
+ if err != nil {
+ return err
+ }
+ ws.SheetProtection.Password = ""
+ ws.SheetProtection.AlgorithmName = opts.AlgorithmName
+ ws.SheetProtection.SaltValue = saltValue
+ ws.SheetProtection.HashValue = hashValue
+ ws.SheetProtection.SpinCount = int(sheetProtectionSpinCount)
}
return err
}
-// UnprotectSheet provides a function to unprotect an Excel worksheet.
-func (f *File) UnprotectSheet(sheet string) error {
- xlsx, err := f.workSheetReader(sheet)
+// UnprotectSheet provides a function to remove protection for a sheet,
+// specified the second optional password parameter to remove sheet
+// protection with password verification.
+func (f *File) UnprotectSheet(sheet string, password ...string) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- xlsx.SheetProtection = nil
- return err
-}
-
-// trimSheetName provides a function to trim invaild characters by given worksheet
-// name.
-func trimSheetName(name string) string {
- if strings.ContainsAny(name, ":\\/?*[]") || utf8.RuneCountInString(name) > 31 {
- r := make([]rune, 0, 31)
- for _, v := range name {
- switch v {
- case 58, 92, 47, 63, 42, 91, 93: // replace :\/?*[]
- continue
- default:
- r = append(r, v)
+ // password verification
+ if len(password) > 0 {
+ if ws.SheetProtection == nil {
+ return ErrUnprotectSheet
+ }
+ if ws.SheetProtection.AlgorithmName == "" && ws.SheetProtection.Password != genSheetPasswd(password[0]) {
+ return ErrUnprotectSheetPassword
+ }
+ if ws.SheetProtection.AlgorithmName != "" {
+ // check with given salt value
+ hashValue, _, err := genISOPasswdHash(password[0], ws.SheetProtection.AlgorithmName, ws.SheetProtection.SaltValue, ws.SheetProtection.SpinCount)
+ if err != nil {
+ return err
}
- if len(r) == 31 {
- break
+ if ws.SheetProtection.HashValue != hashValue {
+ return ErrUnprotectSheetPassword
}
}
- name = string(r)
- }
- return name
-}
-
-// PageLayoutOption is an option of a page layout of a worksheet. See
-// SetPageLayout().
-type PageLayoutOption interface {
- setPageLayout(layout *xlsxPageSetUp)
-}
-
-// PageLayoutOptionPtr is a writable PageLayoutOption. See GetPageLayout().
-type PageLayoutOptionPtr interface {
- PageLayoutOption
- getPageLayout(layout *xlsxPageSetUp)
-}
-
-type (
- // PageLayoutOrientation defines the orientation of page layout for a
- // worksheet.
- PageLayoutOrientation string
- // PageLayoutPaperSize defines the paper size of the worksheet
- PageLayoutPaperSize int
- // FitToHeight specified number of vertical pages to fit on
- FitToHeight int
- // FitToWidth specified number of horizontal pages to fit on
- FitToWidth int
-)
-
-const (
- // OrientationPortrait indicates page layout orientation id portrait.
- OrientationPortrait = "portrait"
- // OrientationLandscape indicates page layout orientation id landscape.
- OrientationLandscape = "landscape"
-)
-
-// setPageLayout provides a method to set the orientation for the worksheet.
-func (o PageLayoutOrientation) setPageLayout(ps *xlsxPageSetUp) {
- ps.Orientation = string(o)
-}
-
-// getPageLayout provides a method to get the orientation for the worksheet.
-func (o *PageLayoutOrientation) getPageLayout(ps *xlsxPageSetUp) {
- // Excel default: portrait
- if ps == nil || ps.Orientation == "" {
- *o = OrientationPortrait
- return
}
- *o = PageLayoutOrientation(ps.Orientation)
-}
-
-// setPageLayout provides a method to set the paper size for the worksheet.
-func (p PageLayoutPaperSize) setPageLayout(ps *xlsxPageSetUp) {
- ps.PaperSize = int(p)
-}
-
-// getPageLayout provides a method to get the paper size for the worksheet.
-func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) {
- // Excel default: 1
- if ps == nil || ps.PaperSize == 0 {
- *p = 1
- return
- }
- *p = PageLayoutPaperSize(ps.PaperSize)
+ ws.SheetProtection = nil
+ return err
}
-// setPageLayout provides a method to set the fit to height for the worksheet.
-func (p FitToHeight) setPageLayout(ps *xlsxPageSetUp) {
- if int(p) > 0 {
- ps.FitToHeight = int(p)
+// checkSheetName check whether there are illegal characters in the sheet name.
+// 1. Confirm that the sheet name is not empty
+// 2. Make sure to enter a name with no more than 31 characters
+// 3. Make sure the first or last character of the name cannot be a single quote
+// 4. Verify that the following characters are not included in the name :\/?*[]
+func checkSheetName(name string) error {
+ if name == "" {
+ return ErrSheetNameBlank
}
-}
-
-// getPageLayout provides a method to get the fit to height for the worksheet.
-func (p *FitToHeight) getPageLayout(ps *xlsxPageSetUp) {
- if ps == nil || ps.FitToHeight == 0 {
- *p = 1
- return
+ if utf8.RuneCountInString(name) > MaxSheetNameLength {
+ return ErrSheetNameLength
}
- *p = FitToHeight(ps.FitToHeight)
-}
-
-// setPageLayout provides a method to set the fit to width for the worksheet.
-func (p FitToWidth) setPageLayout(ps *xlsxPageSetUp) {
- if int(p) > 0 {
- ps.FitToWidth = int(p)
+ if strings.HasPrefix(name, "'") || strings.HasSuffix(name, "'") {
+ return ErrSheetNameSingleQuote
}
-}
-
-// getPageLayout provides a method to get the fit to width for the worksheet.
-func (p *FitToWidth) getPageLayout(ps *xlsxPageSetUp) {
- if ps == nil || ps.FitToWidth == 0 {
- *p = 1
- return
+ if strings.ContainsAny(name, ":\\/?*[]") {
+ return ErrSheetNameInvalid
}
- *p = FitToWidth(ps.FitToWidth)
+ return nil
}
// SetPageLayout provides a function to sets worksheet page layout.
//
-// Available options:
-// PageLayoutOrientation(string)
-// PageLayoutPaperSize(int)
-//
// The following shows the paper size sorted by excelize index number:
//
-// Index | Paper Size
-// -------+-----------------------------------------------
-// 1 | Letter paper (8.5 in. by 11 in.)
-// 2 | Letter small paper (8.5 in. by 11 in.)
-// 3 | Tabloid paper (11 in. by 17 in.)
-// 4 | Ledger paper (17 in. by 11 in.)
-// 5 | Legal paper (8.5 in. by 14 in.)
-// 6 | Statement paper (5.5 in. by 8.5 in.)
-// 7 | Executive paper (7.25 in. by 10.5 in.)
-// 8 | A3 paper (297 mm by 420 mm)
-// 9 | A4 paper (210 mm by 297 mm)
-// 10 | A4 small paper (210 mm by 297 mm)
-// 11 | A5 paper (148 mm by 210 mm)
-// 12 | B4 paper (250 mm by 353 mm)
-// 13 | B5 paper (176 mm by 250 mm)
-// 14 | Folio paper (8.5 in. by 13 in.)
-// 15 | Quarto paper (215 mm by 275 mm)
-// 16 | Standard paper (10 in. by 14 in.)
-// 17 | Standard paper (11 in. by 17 in.)
-// 18 | Note paper (8.5 in. by 11 in.)
-// 19 | #9 envelope (3.875 in. by 8.875 in.)
-// 20 | #10 envelope (4.125 in. by 9.5 in.)
-// 21 | #11 envelope (4.5 in. by 10.375 in.)
-// 22 | #12 envelope (4.75 in. by 11 in.)
-// 23 | #14 envelope (5 in. by 11.5 in.)
-// 24 | C paper (17 in. by 22 in.)
-// 25 | D paper (22 in. by 34 in.)
-// 26 | E paper (34 in. by 44 in.)
-// 27 | DL envelope (110 mm by 220 mm)
-// 28 | C5 envelope (162 mm by 229 mm)
-// 29 | C3 envelope (324 mm by 458 mm)
-// 30 | C4 envelope (229 mm by 324 mm)
-// 31 | C6 envelope (114 mm by 162 mm)
-// 32 | C65 envelope (114 mm by 229 mm)
-// 33 | B4 envelope (250 mm by 353 mm)
-// 34 | B5 envelope (176 mm by 250 mm)
-// 35 | B6 envelope (176 mm by 125 mm)
-// 36 | Italy envelope (110 mm by 230 mm)
-// 37 | Monarch envelope (3.875 in. by 7.5 in.).
-// 38 | 6 3/4 envelope (3.625 in. by 6.5 in.)
-// 39 | US standard fanfold (14.875 in. by 11 in.)
-// 40 | German standard fanfold (8.5 in. by 12 in.)
-// 41 | German legal fanfold (8.5 in. by 13 in.)
-// 42 | ISO B4 (250 mm by 353 mm)
-// 43 | Japanese postcard (100 mm by 148 mm)
-// 44 | Standard paper (9 in. by 11 in.)
-// 45 | Standard paper (10 in. by 11 in.)
-// 46 | Standard paper (15 in. by 11 in.)
-// 47 | Invite envelope (220 mm by 220 mm)
-// 50 | Letter extra paper (9.275 in. by 12 in.)
-// 51 | Legal extra paper (9.275 in. by 15 in.)
-// 52 | Tabloid extra paper (11.69 in. by 18 in.)
-// 53 | A4 extra paper (236 mm by 322 mm)
-// 54 | Letter transverse paper (8.275 in. by 11 in.)
-// 55 | A4 transverse paper (210 mm by 297 mm)
-// 56 | Letter extra transverse paper (9.275 in. by 12 in.)
-// 57 | SuperA/SuperA/A4 paper (227 mm by 356 mm)
-// 58 | SuperB/SuperB/A3 paper (305 mm by 487 mm)
-// 59 | Letter plus paper (8.5 in. by 12.69 in.)
-// 60 | A4 plus paper (210 mm by 330 mm)
-// 61 | A5 transverse paper (148 mm by 210 mm)
-// 62 | JIS B5 transverse paper (182 mm by 257 mm)
-// 63 | A3 extra paper (322 mm by 445 mm)
-// 64 | A5 extra paper (174 mm by 235 mm)
-// 65 | ISO B5 extra paper (201 mm by 276 mm)
-// 66 | A2 paper (420 mm by 594 mm)
-// 67 | A3 transverse paper (297 mm by 420 mm)
-// 68 | A3 extra transverse paper (322 mm by 445 mm)
-// 69 | Japanese Double Postcard (200 mm x 148 mm)
-// 70 | A6 (105 mm x 148 mm)
-// 71 | Japanese Envelope Kaku #2
-// 72 | Japanese Envelope Kaku #3
-// 73 | Japanese Envelope Chou #3
-// 74 | Japanese Envelope Chou #4
-// 75 | Letter Rotated (11in x 8 1/2 11 in)
-// 76 | A3 Rotated (420 mm x 297 mm)
-// 77 | A4 Rotated (297 mm x 210 mm)
-// 78 | A5 Rotated (210 mm x 148 mm)
-// 79 | B4 (JIS) Rotated (364 mm x 257 mm)
-// 80 | B5 (JIS) Rotated (257 mm x 182 mm)
-// 81 | Japanese Postcard Rotated (148 mm x 100 mm)
-// 82 | Double Japanese Postcard Rotated (148 mm x 200 mm)
-// 83 | A6 Rotated (148 mm x 105 mm)
-// 84 | Japanese Envelope Kaku #2 Rotated
-// 85 | Japanese Envelope Kaku #3 Rotated
-// 86 | Japanese Envelope Chou #3 Rotated
-// 87 | Japanese Envelope Chou #4 Rotated
-// 88 | B6 (JIS) (128 mm x 182 mm)
-// 89 | B6 (JIS) Rotated (182 mm x 128 mm)
-// 90 | (12 in x 11 in)
-// 91 | Japanese Envelope You #4
-// 92 | Japanese Envelope You #4 Rotated
-// 93 | PRC 16K (146 mm x 215 mm)
-// 94 | PRC 32K (97 mm x 151 mm)
-// 95 | PRC 32K(Big) (97 mm x 151 mm)
-// 96 | PRC Envelope #1 (102 mm x 165 mm)
-// 97 | PRC Envelope #2 (102 mm x 176 mm)
-// 98 | PRC Envelope #3 (125 mm x 176 mm)
-// 99 | PRC Envelope #4 (110 mm x 208 mm)
-// 100 | PRC Envelope #5 (110 mm x 220 mm)
-// 101 | PRC Envelope #6 (120 mm x 230 mm)
-// 102 | PRC Envelope #7 (160 mm x 230 mm)
-// 103 | PRC Envelope #8 (120 mm x 309 mm)
-// 104 | PRC Envelope #9 (229 mm x 324 mm)
-// 105 | PRC Envelope #10 (324 mm x 458 mm)
-// 106 | PRC 16K Rotated
-// 107 | PRC 32K Rotated
-// 108 | PRC 32K(Big) Rotated
-// 109 | PRC Envelope #1 Rotated (165 mm x 102 mm)
-// 110 | PRC Envelope #2 Rotated (176 mm x 102 mm)
-// 111 | PRC Envelope #3 Rotated (176 mm x 125 mm)
-// 112 | PRC Envelope #4 Rotated (208 mm x 110 mm)
-// 113 | PRC Envelope #5 Rotated (220 mm x 110 mm)
-// 114 | PRC Envelope #6 Rotated (230 mm x 120 mm)
-// 115 | PRC Envelope #7 Rotated (230 mm x 160 mm)
-// 116 | PRC Envelope #8 Rotated (309 mm x 120 mm)
-// 117 | PRC Envelope #9 Rotated (324 mm x 229 mm)
-// 118 | PRC Envelope #10 Rotated (458 mm x 324 mm)
-//
-func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error {
- s, err := f.workSheetReader(sheet)
+// Index | Paper Size
+// -------+-----------------------------------------------
+// 1 | Letter paper (8.5 in. by 11 in.)
+// 2 | Letter small paper (8.5 in. by 11 in.)
+// 3 | Tabloid paper (11 in. by 17 in.)
+// 4 | Ledger paper (17 in. by 11 in.)
+// 5 | Legal paper (8.5 in. by 14 in.)
+// 6 | Statement paper (5.5 in. by 8.5 in.)
+// 7 | Executive paper (7.25 in. by 10.5 in.)
+// 8 | A3 paper (297 mm by 420 mm)
+// 9 | A4 paper (210 mm by 297 mm)
+// 10 | A4 small paper (210 mm by 297 mm)
+// 11 | A5 paper (148 mm by 210 mm)
+// 12 | B4 paper (250 mm by 353 mm)
+// 13 | B5 paper (176 mm by 250 mm)
+// 14 | Folio paper (8.5 in. by 13 in.)
+// 15 | Quarto paper (215 mm by 275 mm)
+// 16 | Standard paper (10 in. by 14 in.)
+// 17 | Standard paper (11 in. by 17 in.)
+// 18 | Note paper (8.5 in. by 11 in.)
+// 19 | #9 envelope (3.875 in. by 8.875 in.)
+// 20 | #10 envelope (4.125 in. by 9.5 in.)
+// 21 | #11 envelope (4.5 in. by 10.375 in.)
+// 22 | #12 envelope (4.75 in. by 11 in.)
+// 23 | #14 envelope (5 in. by 11.5 in.)
+// 24 | C paper (17 in. by 22 in.)
+// 25 | D paper (22 in. by 34 in.)
+// 26 | E paper (34 in. by 44 in.)
+// 27 | DL envelope (110 mm by 220 mm)
+// 28 | C5 envelope (162 mm by 229 mm)
+// 29 | C3 envelope (324 mm by 458 mm)
+// 30 | C4 envelope (229 mm by 324 mm)
+// 31 | C6 envelope (114 mm by 162 mm)
+// 32 | C65 envelope (114 mm by 229 mm)
+// 33 | B4 envelope (250 mm by 353 mm)
+// 34 | B5 envelope (176 mm by 250 mm)
+// 35 | B6 envelope (176 mm by 125 mm)
+// 36 | Italy envelope (110 mm by 230 mm)
+// 37 | Monarch envelope (3.875 in. by 7.5 in.).
+// 38 | 6 3/4 envelope (3.625 in. by 6.5 in.)
+// 39 | US standard fanfold (14.875 in. by 11 in.)
+// 40 | German standard fanfold (8.5 in. by 12 in.)
+// 41 | German legal fanfold (8.5 in. by 13 in.)
+// 42 | ISO B4 (250 mm by 353 mm)
+// 43 | Japanese postcard (100 mm by 148 mm)
+// 44 | Standard paper (9 in. by 11 in.)
+// 45 | Standard paper (10 in. by 11 in.)
+// 46 | Standard paper (15 in. by 11 in.)
+// 47 | Invite envelope (220 mm by 220 mm)
+// 50 | Letter extra paper (9.275 in. by 12 in.)
+// 51 | Legal extra paper (9.275 in. by 15 in.)
+// 52 | Tabloid extra paper (11.69 in. by 18 in.)
+// 53 | A4 extra paper (236 mm by 322 mm)
+// 54 | Letter transverse paper (8.275 in. by 11 in.)
+// 55 | A4 transverse paper (210 mm by 297 mm)
+// 56 | Letter extra transverse paper (9.275 in. by 12 in.)
+// 57 | SuperA/SuperA/A4 paper (227 mm by 356 mm)
+// 58 | SuperB/SuperB/A3 paper (305 mm by 487 mm)
+// 59 | Letter plus paper (8.5 in. by 12.69 in.)
+// 60 | A4 plus paper (210 mm by 330 mm)
+// 61 | A5 transverse paper (148 mm by 210 mm)
+// 62 | JIS B5 transverse paper (182 mm by 257 mm)
+// 63 | A3 extra paper (322 mm by 445 mm)
+// 64 | A5 extra paper (174 mm by 235 mm)
+// 65 | ISO B5 extra paper (201 mm by 276 mm)
+// 66 | A2 paper (420 mm by 594 mm)
+// 67 | A3 transverse paper (297 mm by 420 mm)
+// 68 | A3 extra transverse paper (322 mm by 445 mm)
+// 69 | Japanese Double Postcard (200 mm x 148 mm)
+// 70 | A6 (105 mm x 148 mm)
+// 71 | Japanese Envelope Kaku #2
+// 72 | Japanese Envelope Kaku #3
+// 73 | Japanese Envelope Chou #3
+// 74 | Japanese Envelope Chou #4
+// 75 | Letter Rotated (11in x 8 1/2 11 in)
+// 76 | A3 Rotated (420 mm x 297 mm)
+// 77 | A4 Rotated (297 mm x 210 mm)
+// 78 | A5 Rotated (210 mm x 148 mm)
+// 79 | B4 (JIS) Rotated (364 mm x 257 mm)
+// 80 | B5 (JIS) Rotated (257 mm x 182 mm)
+// 81 | Japanese Postcard Rotated (148 mm x 100 mm)
+// 82 | Double Japanese Postcard Rotated (148 mm x 200 mm)
+// 83 | A6 Rotated (148 mm x 105 mm)
+// 84 | Japanese Envelope Kaku #2 Rotated
+// 85 | Japanese Envelope Kaku #3 Rotated
+// 86 | Japanese Envelope Chou #3 Rotated
+// 87 | Japanese Envelope Chou #4 Rotated
+// 88 | B6 (JIS) (128 mm x 182 mm)
+// 89 | B6 (JIS) Rotated (182 mm x 128 mm)
+// 90 | (12 in x 11 in)
+// 91 | Japanese Envelope You #4
+// 92 | Japanese Envelope You #4 Rotated
+// 93 | PRC 16K (146 mm x 215 mm)
+// 94 | PRC 32K (97 mm x 151 mm)
+// 95 | PRC 32K(Big) (97 mm x 151 mm)
+// 96 | PRC Envelope #1 (102 mm x 165 mm)
+// 97 | PRC Envelope #2 (102 mm x 176 mm)
+// 98 | PRC Envelope #3 (125 mm x 176 mm)
+// 99 | PRC Envelope #4 (110 mm x 208 mm)
+// 100 | PRC Envelope #5 (110 mm x 220 mm)
+// 101 | PRC Envelope #6 (120 mm x 230 mm)
+// 102 | PRC Envelope #7 (160 mm x 230 mm)
+// 103 | PRC Envelope #8 (120 mm x 309 mm)
+// 104 | PRC Envelope #9 (229 mm x 324 mm)
+// 105 | PRC Envelope #10 (324 mm x 458 mm)
+// 106 | PRC 16K Rotated
+// 107 | PRC 32K Rotated
+// 108 | PRC 32K(Big) Rotated
+// 109 | PRC Envelope #1 Rotated (165 mm x 102 mm)
+// 110 | PRC Envelope #2 Rotated (176 mm x 102 mm)
+// 111 | PRC Envelope #3 Rotated (176 mm x 125 mm)
+// 112 | PRC Envelope #4 Rotated (208 mm x 110 mm)
+// 113 | PRC Envelope #5 Rotated (220 mm x 110 mm)
+// 114 | PRC Envelope #6 Rotated (230 mm x 120 mm)
+// 115 | PRC Envelope #7 Rotated (230 mm x 160 mm)
+// 116 | PRC Envelope #8 Rotated (309 mm x 120 mm)
+// 117 | PRC Envelope #9 Rotated (324 mm x 229 mm)
+// 118 | PRC Envelope #10 Rotated (458 mm x 324 mm)
+func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- ps := s.PageSetUp
- if ps == nil {
- ps = new(xlsxPageSetUp)
- s.PageSetUp = ps
+ if opts == nil {
+ return err
}
+ return ws.setPageSetUp(opts)
+}
- for _, opt := range opts {
- opt.setPageLayout(ps)
+// newPageSetUp initialize page setup settings for the worksheet if which not
+// exist.
+func (ws *xlsxWorksheet) newPageSetUp() {
+ if ws.PageSetUp == nil {
+ ws.PageSetUp = new(xlsxPageSetUp)
}
- return err
+}
+
+// setPageSetUp set page setup settings for the worksheet by given options.
+func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error {
+ if opts.Size != nil {
+ ws.newPageSetUp()
+ ws.PageSetUp.PaperSize = opts.Size
+ }
+ if opts.Orientation != nil {
+ if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 {
+ return newInvalidOptionalValue("Orientation", *opts.Orientation, supportedPageOrientation)
+ }
+ ws.newPageSetUp()
+ ws.PageSetUp.Orientation = *opts.Orientation
+ }
+ if opts.FirstPageNumber != nil && *opts.FirstPageNumber > 0 {
+ ws.newPageSetUp()
+ ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber))
+ ws.PageSetUp.UseFirstPageNumber = true
+ }
+ if opts.AdjustTo != nil {
+ if *opts.AdjustTo < 10 || 400 < *opts.AdjustTo {
+ return ErrPageSetupAdjustTo
+ }
+ ws.newPageSetUp()
+ ws.PageSetUp.Scale = int(*opts.AdjustTo)
+ }
+ if opts.FitToHeight != nil {
+ ws.newPageSetUp()
+ ws.PageSetUp.FitToHeight = opts.FitToHeight
+ }
+ if opts.FitToWidth != nil {
+ ws.newPageSetUp()
+ ws.PageSetUp.FitToWidth = opts.FitToWidth
+ }
+ if opts.BlackAndWhite != nil {
+ ws.newPageSetUp()
+ ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite
+ }
+ if opts.PageOrder != nil {
+ if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 {
+ return newInvalidOptionalValue("PageOrder", *opts.PageOrder, supportedPageOrder)
+ }
+ ws.newPageSetUp()
+ ws.PageSetUp.PageOrder = *opts.PageOrder
+ }
+ return nil
}
// GetPageLayout provides a function to gets worksheet page layout.
-//
-// Available options:
-// PageLayoutOrientation(string)
-// PageLayoutPaperSize(int)
-// FitToHeight(int)
-// FitToWidth(int)
-func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error {
- s, err := f.workSheetReader(sheet)
+func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
+ opts := PageLayoutOptions{
+ Size: intPtr(0),
+ Orientation: stringPtr(supportedPageOrientation[0]),
+ FirstPageNumber: uintPtr(1),
+ AdjustTo: uintPtr(100),
+ }
+ ws, err := f.workSheetReader(sheet)
if err != nil {
- return err
+ return opts, err
}
- ps := s.PageSetUp
-
- for _, opt := range opts {
- opt.getPageLayout(ps)
+ if ws.PageSetUp != nil {
+ if ws.PageSetUp.PaperSize != nil {
+ opts.Size = ws.PageSetUp.PaperSize
+ }
+ if ws.PageSetUp.Orientation != "" {
+ opts.Orientation = stringPtr(ws.PageSetUp.Orientation)
+ }
+ if num, _ := strconv.Atoi(ws.PageSetUp.FirstPageNumber); num != 0 {
+ opts.FirstPageNumber = uintPtr(uint(num))
+ }
+ if ws.PageSetUp.Scale >= 10 && ws.PageSetUp.Scale <= 400 {
+ opts.AdjustTo = uintPtr(uint(ws.PageSetUp.Scale))
+ }
+ if ws.PageSetUp.FitToHeight != nil {
+ opts.FitToHeight = ws.PageSetUp.FitToHeight
+ }
+ if ws.PageSetUp.FitToWidth != nil {
+ opts.FitToWidth = ws.PageSetUp.FitToWidth
+ }
+ opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite)
+ if ws.PageSetUp.PageOrder != "" {
+ opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder)
+ }
}
- return err
+ return opts, err
}
// SetDefinedName provides a function to set the defined names of the workbook
// or worksheet. If not specified scope, the default scope is workbook.
// For example:
//
-// f.SetDefinedName(&excelize.DefinedName{
-// Name: "Amount",
-// RefersTo: "Sheet1!$A$2:$D$5",
-// Comment: "defined name comment",
-// Scope: "Sheet2",
-// })
+// err := f.SetDefinedName(&excelize.DefinedName{
+// Name: "Amount",
+// RefersTo: "Sheet1!$A$2:$D$5",
+// Comment: "defined name comment",
+// Scope: "Sheet2",
+// })
+//
+// If you fill the RefersTo property with only one columns range without a
+// comma, it will work as "Columns to repeat at left" only. For example:
//
+// err := f.SetDefinedName(&excelize.DefinedName{
+// Name: "_xlnm.Print_Titles",
+// RefersTo: "Sheet1!$A:$A",
+// Scope: "Sheet1",
+// })
+//
+// If you fill the RefersTo property with only one rows range without a comma,
+// it will work as "Rows to repeat at top" only. For example:
+//
+// err := f.SetDefinedName(&excelize.DefinedName{
+// Name: "_xlnm.Print_Titles",
+// RefersTo: "Sheet1!$1:$1",
+// Scope: "Sheet1",
+// })
func (f *File) SetDefinedName(definedName *DefinedName) error {
- wb := f.workbookReader()
+ if definedName.Name == "" || definedName.RefersTo == "" {
+ return ErrParameterInvalid
+ }
+ if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
+ return err
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
d := xlsxDefinedName{
Name: definedName.Name,
Comment: definedName.Comment,
Data: definedName.RefersTo,
}
if definedName.Scope != "" {
- if sheetID := f.getSheetID(definedName.Scope); sheetID != 0 {
- sheetID--
- d.LocalSheetID = &sheetID
+ if sheetIndex, _ := f.GetSheetIndex(definedName.Scope); sheetIndex >= 0 {
+ d.LocalSheetID = &sheetIndex
}
}
if wb.DefinedNames != nil {
for _, dn := range wb.DefinedNames.DefinedName {
var scope string
if dn.LocalSheetID != nil {
- scope = f.getSheetNameByID(*dn.LocalSheetID + 1)
+ scope = f.GetSheetName(*dn.LocalSheetID)
}
if scope == definedName.Scope && dn.Name == definedName.Name {
- return errors.New("the same name already exists on the scope")
+ return ErrDefinedNameDuplicate
}
}
wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d)
@@ -1395,33 +1796,39 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
// workbook or worksheet. If not specified scope, the default scope is
// workbook. For example:
//
-// f.DeleteDefinedName(&excelize.DefinedName{
-// Name: "Amount",
-// Scope: "Sheet2",
-// })
-//
+// err := f.DeleteDefinedName(&excelize.DefinedName{
+// Name: "Amount",
+// Scope: "Sheet2",
+// })
func (f *File) DeleteDefinedName(definedName *DefinedName) error {
- wb := f.workbookReader()
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
if wb.DefinedNames != nil {
for idx, dn := range wb.DefinedNames.DefinedName {
- var scope string
+ scope := "Workbook"
+ deleteScope := definedName.Scope
+ if deleteScope == "" {
+ deleteScope = "Workbook"
+ }
if dn.LocalSheetID != nil {
- scope = f.getSheetNameByID(*dn.LocalSheetID + 1)
+ scope = f.GetSheetName(*dn.LocalSheetID)
}
- if scope == definedName.Scope && dn.Name == definedName.Name {
+ if scope == deleteScope && dn.Name == definedName.Name {
wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...)
- return nil
+ return err
}
}
}
- return errors.New("no defined name on the scope")
+ return ErrDefinedNameScope
}
// GetDefinedName provides a function to get the defined names of the workbook
// or worksheet.
func (f *File) GetDefinedName() []DefinedName {
var definedNames []DefinedName
- wb := f.workbookReader()
+ wb, _ := f.workbookReader()
if wb.DefinedNames != nil {
for _, dn := range wb.DefinedNames.DefinedName {
definedName := DefinedName{
@@ -1431,7 +1838,7 @@ func (f *File) GetDefinedName() []DefinedName {
Scope: "Workbook",
}
if dn.LocalSheetID != nil && *dn.LocalSheetID >= 0 {
- definedName.Scope = f.getSheetNameByID(*dn.LocalSheetID + 1)
+ definedName.Scope = f.GetSheetName(*dn.LocalSheetID)
}
definedNames = append(definedNames, definedName)
}
@@ -1442,36 +1849,33 @@ func (f *File) GetDefinedName() []DefinedName {
// GroupSheets provides a function to group worksheets by given worksheets
// name. Group worksheets must contain an active worksheet.
func (f *File) GroupSheets(sheets []string) error {
- // check an active worksheet in group worksheets
+ // Check an active worksheet in group worksheets
var inActiveSheet bool
activeSheet := f.GetActiveSheetIndex()
sheetMap := f.GetSheetList()
for idx, sheetName := range sheetMap {
for _, s := range sheets {
- if s == sheetName && idx == activeSheet {
+ if strings.EqualFold(s, sheetName) && idx == activeSheet {
inActiveSheet = true
}
}
}
if !inActiveSheet {
- return errors.New("group worksheet must contain an active worksheet")
+ return ErrGroupSheets
}
// check worksheet exists
- ws := []*xlsxWorksheet{}
+ var wss []*xlsxWorksheet
for _, sheet := range sheets {
- xlsx, err := f.workSheetReader(sheet)
+ worksheet, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- ws = append(ws, xlsx)
+ wss = append(wss, worksheet)
}
- for _, s := range ws {
- sheetViews := s.SheetViews.SheetView
- if len(sheetViews) > 0 {
- for idx := range sheetViews {
- s.SheetViews.SheetView[idx].TabSelected = true
- }
- continue
+ for _, ws := range wss {
+ sheetViews := ws.SheetViews.SheetView
+ for idx := range sheetViews {
+ ws.SheetViews.SheetView[idx].TabSelected = true
}
}
return nil
@@ -1486,39 +1890,46 @@ func (f *File) UngroupSheets() error {
}
ws, _ := f.workSheetReader(sheet)
sheetViews := ws.SheetViews.SheetView
- if len(sheetViews) > 0 {
- for idx := range sheetViews {
- ws.SheetViews.SheetView[idx].TabSelected = false
- }
+ for idx := range sheetViews {
+ ws.SheetViews.SheetView[idx].TabSelected = false
}
}
return nil
}
// InsertPageBreak create a page break to determine where the printed page
-// ends and where begins the next one by given worksheet name and axis, so the
-// content before the page break will be printed on one page and after the
-// page break on another.
-func (f *File) InsertPageBreak(sheet, cell string) (err error) {
- var ws *xlsxWorksheet
- var row, col int
- var rowBrk, colBrk = -1, -1
- if ws, err = f.workSheetReader(sheet); err != nil {
- return
+// ends and where begins the next one by given worksheet name and cell
+// reference, so the content before the page break will be printed on one page
+// and after the page break on another.
+func (f *File) InsertPageBreak(sheet, cell string) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
}
+ return ws.insertPageBreak(cell)
+}
+
+// insertPageBreak create a page break in the worksheet by specific cell
+// reference.
+func (ws *xlsxWorksheet) insertPageBreak(cell string) error {
+ var (
+ row, col int
+ err error
+ rowBrk, colBrk = -1, -1
+ )
if col, row, err = CellNameToCoordinates(cell); err != nil {
- return
+ return err
}
col--
row--
if col == row && col == 0 {
- return
+ return err
}
if ws.RowBreaks == nil {
- ws.RowBreaks = &xlsxBreaks{}
+ ws.RowBreaks = &xlsxRowBreaks{}
}
if ws.ColBreaks == nil {
- ws.ColBreaks = &xlsxBreaks{}
+ ws.ColBreaks = &xlsxColBreaks{}
}
for idx, brk := range ws.RowBreaks.Brk {
@@ -1535,7 +1946,7 @@ func (f *File) InsertPageBreak(sheet, cell string) (err error) {
if row != 0 && rowBrk == -1 {
ws.RowBreaks.Brk = append(ws.RowBreaks.Brk, &xlsxBrk{
ID: row,
- Max: 16383,
+ Max: MaxColumns - 1,
Man: true,
})
ws.RowBreaks.ManualBreakCount++
@@ -1543,30 +1954,34 @@ func (f *File) InsertPageBreak(sheet, cell string) (err error) {
if col != 0 && colBrk == -1 {
ws.ColBreaks.Brk = append(ws.ColBreaks.Brk, &xlsxBrk{
ID: col,
- Max: 1048575,
+ Max: TotalRows - 1,
Man: true,
})
ws.ColBreaks.ManualBreakCount++
}
ws.RowBreaks.Count = len(ws.RowBreaks.Brk)
ws.ColBreaks.Count = len(ws.ColBreaks.Brk)
- return
+ return err
}
-// RemovePageBreak remove a page break by given worksheet name and axis.
-func (f *File) RemovePageBreak(sheet, cell string) (err error) {
- var ws *xlsxWorksheet
- var row, col int
+// RemovePageBreak remove a page break by given worksheet name and cell
+// reference.
+func (f *File) RemovePageBreak(sheet, cell string) error {
+ var (
+ ws *xlsxWorksheet
+ row, col int
+ err error
+ )
if ws, err = f.workSheetReader(sheet); err != nil {
- return
+ return err
}
if col, row, err = CellNameToCoordinates(cell); err != nil {
- return
+ return err
}
col--
row--
if col == row && col == 0 {
- return
+ return err
}
removeBrk := func(ID int, brks []*xlsxBrk) []*xlsxBrk {
for i, brk := range brks {
@@ -1577,7 +1992,7 @@ func (f *File) RemovePageBreak(sheet, cell string) (err error) {
return brks
}
if ws.RowBreaks == nil || ws.ColBreaks == nil {
- return
+ return err
}
rowBrks := len(ws.RowBreaks.Brk)
colBrks := len(ws.ColBreaks.Brk)
@@ -1588,61 +2003,68 @@ func (f *File) RemovePageBreak(sheet, cell string) (err error) {
ws.ColBreaks.Count = len(ws.ColBreaks.Brk)
ws.RowBreaks.ManualBreakCount--
ws.ColBreaks.ManualBreakCount--
- return
+ return err
}
if rowBrks > 0 && rowBrks > colBrks {
ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk)
ws.RowBreaks.Count = len(ws.RowBreaks.Brk)
ws.RowBreaks.ManualBreakCount--
- return
+ return err
}
if colBrks > 0 && colBrks > rowBrks {
ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk)
ws.ColBreaks.Count = len(ws.ColBreaks.Brk)
ws.ColBreaks.ManualBreakCount--
}
- return
+ return err
}
// relsReader provides a function to get the pointer to the structure
-// after deserialization of xl/worksheets/_rels/sheet%d.xml.rels.
-func (f *File) relsReader(path string) *xlsxRelationships {
- var err error
-
- if f.Relationships[path] == nil {
- _, ok := f.XLSX[path]
- if ok {
+// after deserialization of relationships parts.
+func (f *File) relsReader(path string) (*xlsxRelationships, error) {
+ rels, _ := f.Relationships.Load(path)
+ if rels == nil {
+ if _, ok := f.Pkg.Load(path); ok {
c := xlsxRelationships{}
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
Decode(&c); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return nil, err
}
- f.Relationships[path] = &c
+ f.Relationships.Store(path, &c)
}
}
-
- return f.Relationships[path]
+ if rels, _ = f.Relationships.Load(path); rels != nil {
+ return rels.(*xlsxRelationships), nil
+ }
+ return nil, nil
}
// fillSheetData ensures there are enough rows, and columns in the chosen
// row to accept data. Missing rows are backfilled and given their row number
// Uses the last populated row as a hint for the size of the next row to add
-func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) {
- rowCount := len(xlsx.SheetData.Row)
+func (ws *xlsxWorksheet) prepareSheetXML(col, row int) {
+ rowCount := len(ws.SheetData.Row)
sizeHint := 0
+ var ht *float64
+ var customHeight bool
+ if ws.SheetFormatPr != nil && ws.SheetFormatPr.CustomHeight {
+ ht = float64Ptr(ws.SheetFormatPr.DefaultRowHeight)
+ customHeight = true
+ }
if rowCount > 0 {
- sizeHint = len(xlsx.SheetData.Row[rowCount-1].C)
+ sizeHint = len(ws.SheetData.Row[rowCount-1].C)
}
if rowCount < row {
// append missing rows
for rowIdx := rowCount; rowIdx < row; rowIdx++ {
- xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, C: make([]xlsxC, 0, sizeHint)})
+ ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)})
}
}
- rowData := &xlsx.SheetData.Row[row-1]
+ rowData := &ws.SheetData.Row[row-1]
fillColumns(rowData, col, row)
}
+// fillColumns fill cells in the column of the row as contiguous.
func fillColumns(rowData *xlsxRow, col, row int) {
cellCount := len(rowData.C)
if cellCount < col {
@@ -1653,9 +2075,91 @@ func fillColumns(rowData *xlsxRow, col, row int) {
}
}
-func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) {
+// makeContiguousColumns make columns in specific rows as contiguous.
+func (ws *xlsxWorksheet) makeContiguousColumns(fromRow, toRow, colCount int) {
for ; fromRow < toRow; fromRow++ {
- rowData := &xlsx.SheetData.Row[fromRow-1]
+ rowData := &ws.SheetData.Row[fromRow-1]
fillColumns(rowData, colCount, fromRow)
}
}
+
+// SetSheetDimension provides the method to set or remove the used range of the
+// worksheet by a given range reference. It specifies the row and column bounds
+// of used cells in the worksheet. The range reference is set using the A1
+// reference style(e.g., "A1:D5"). Passing an empty range reference will remove
+// the used range of the worksheet.
+func (f *File) SetSheetDimension(sheet, rangeRef string) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ // Remove the dimension element if an empty string is provided
+ if rangeRef == "" {
+ ws.Dimension = nil
+ return nil
+ }
+ parts := len(strings.Split(rangeRef, ":"))
+ if parts == 1 {
+ _, _, err = CellNameToCoordinates(rangeRef)
+ if err == nil {
+ ws.Dimension = &xlsxDimension{Ref: strings.ToUpper(rangeRef)}
+ }
+ return err
+ }
+ if parts != 2 {
+ return ErrParameterInvalid
+ }
+ coordinates, err := rangeRefToCoordinates(rangeRef)
+ if err != nil {
+ return err
+ }
+ _ = sortCoordinates(coordinates)
+ ref, err := coordinatesToRangeRef(coordinates)
+ ws.Dimension = &xlsxDimension{Ref: ref}
+ return err
+}
+
+// GetSheetDimension provides the method to get the used range of the worksheet.
+func (f *File) GetSheetDimension(sheet string) (string, error) {
+ var ref string
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return ref, err
+ }
+ if ws.Dimension != nil {
+ ref = ws.Dimension.Ref
+ }
+ return ref, err
+}
+
+// AddIgnoredErrors provides the method to ignored error for a range of cells.
+func (f *File) AddIgnoredErrors(sheet, rangeRef string, ignoredErrorsType IgnoredErrorsType) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ if rangeRef == "" {
+ return ErrParameterInvalid
+ }
+ if ws.IgnoredErrors == nil {
+ ws.IgnoredErrors = &xlsxIgnoredErrors{}
+ }
+ ie := map[IgnoredErrorsType]xlsxIgnoredError{
+ IgnoredErrorsEvalError: {Sqref: rangeRef, EvalError: true},
+ IgnoredErrorsTwoDigitTextYear: {Sqref: rangeRef, TwoDigitTextYear: true},
+ IgnoredErrorsNumberStoredAsText: {Sqref: rangeRef, NumberStoredAsText: true},
+ IgnoredErrorsFormula: {Sqref: rangeRef, Formula: true},
+ IgnoredErrorsFormulaRange: {Sqref: rangeRef, FormulaRange: true},
+ IgnoredErrorsUnlockedFormula: {Sqref: rangeRef, UnlockedFormula: true},
+ IgnoredErrorsEmptyCellReference: {Sqref: rangeRef, EmptyCellReference: true},
+ IgnoredErrorsListDataValidation: {Sqref: rangeRef, ListDataValidation: true},
+ IgnoredErrorsCalculatedColumn: {Sqref: rangeRef, CalculatedColumn: true},
+ }[ignoredErrorsType]
+ for _, val := range ws.IgnoredErrors.IgnoredError {
+ if reflect.DeepEqual(val, ie) {
+ return err
+ }
+ }
+ ws.IgnoredErrors.IgnoredError = append(ws.IgnoredErrors.IgnoredError, ie)
+ return err
+}
diff --git a/sheet_test.go b/sheet_test.go
index 0014220a26..48bb423447 100644
--- a/sheet_test.go
+++ b/sheet_test.go
@@ -1,164 +1,157 @@
-package excelize_test
+package excelize
import (
+ "encoding/xml"
"fmt"
+ "io"
+ "os"
"path/filepath"
+ "strconv"
"strings"
+ "sync"
"testing"
- "github.com/360EntSecGroup-Skylar/excelize/v2"
-
- "github.com/mohae/deepcopy"
"github.com/stretchr/testify/assert"
)
-func ExampleFile_SetPageLayout() {
- f := excelize.NewFile()
-
- if err := f.SetPageLayout(
- "Sheet1",
- excelize.PageLayoutOrientation(excelize.OrientationLandscape),
- ); err != nil {
- fmt.Println(err)
- }
- if err := f.SetPageLayout(
- "Sheet1",
- excelize.PageLayoutPaperSize(10),
- excelize.FitToHeight(2),
- excelize.FitToWidth(2),
- ); err != nil {
- fmt.Println(err)
- }
- // Output:
-}
-
-func ExampleFile_GetPageLayout() {
- f := excelize.NewFile()
- var (
- orientation excelize.PageLayoutOrientation
- paperSize excelize.PageLayoutPaperSize
- fitToHeight excelize.FitToHeight
- fitToWidth excelize.FitToWidth
- )
- if err := f.GetPageLayout("Sheet1", &orientation); err != nil {
- fmt.Println(err)
- }
- if err := f.GetPageLayout("Sheet1", &paperSize); err != nil {
- fmt.Println(err)
- }
- if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil {
- fmt.Println(err)
- }
- fmt.Println("Defaults:")
- fmt.Printf("- orientation: %q\n", orientation)
- fmt.Printf("- paper size: %d\n", paperSize)
- fmt.Printf("- fit to height: %d\n", fitToHeight)
- fmt.Printf("- fit to width: %d\n", fitToWidth)
- // Output:
- // Defaults:
- // - orientation: "portrait"
- // - paper size: 1
- // - fit to height: 1
- // - fit to width: 1
-}
-
func TestNewSheet(t *testing.T) {
- f := excelize.NewFile()
- sheetID := f.NewSheet("Sheet2")
+ f := NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ sheetID, err := f.NewSheet("sheet2")
+ assert.NoError(t, err)
f.SetActiveSheet(sheetID)
- // delete original sheet
- f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1")))
+ // Test delete original sheet
+ idx, err := f.GetSheetIndex("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, f.DeleteSheet(f.GetSheetName(idx)))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx")))
+ // Test create new worksheet with already exists name
+ sheetID, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ idx, err = f.GetSheetIndex("Sheet2")
+ assert.NoError(t, err)
+ assert.Equal(t, idx, sheetID)
+ // Test create new worksheet with empty sheet name
+ sheetID, err = f.NewSheet(":\\/?*[]")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+ assert.Equal(t, -1, sheetID)
}
-func TestSetPane(t *testing.T) {
- f := excelize.NewFile()
- assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":false,"split":false}`))
- f.NewSheet("Panes 2")
- assert.NoError(t, f.SetPanes("Panes 2", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`))
- f.NewSheet("Panes 3")
- assert.NoError(t, f.SetPanes("Panes 3", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`))
- f.NewSheet("Panes 4")
- assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`))
- assert.NoError(t, f.SetPanes("Panes 4", ""))
- assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN is not exist")
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx")))
-}
+func TestPanes(t *testing.T) {
+ f := NewFile()
-func TestPageLayoutOption(t *testing.T) {
- const sheet = "Sheet1"
+ assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false}))
+ _, err := f.NewSheet("Panes 2")
+ assert.NoError(t, err)
- testData := []struct {
- container excelize.PageLayoutOptionPtr
- nonDefault excelize.PageLayoutOption
- }{
- {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)},
- {new(excelize.PageLayoutPaperSize), excelize.PageLayoutPaperSize(10)},
- {new(excelize.FitToHeight), excelize.FitToHeight(2)},
- {new(excelize.FitToWidth), excelize.FitToWidth(2)},
+ expected := Panes{
+ Freeze: true,
+ Split: false,
+ XSplit: 1,
+ YSplit: 0,
+ TopLeftCell: "B1",
+ ActivePane: "topRight",
+ Selection: []Selection{
+ {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
+ },
}
+ assert.NoError(t, f.SetPanes("Panes 2", &expected))
+ panes, err := f.GetPanes("Panes 2")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, panes)
- for i, test := range testData {
- t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
-
- opt := test.nonDefault
- t.Logf("option %T", opt)
-
- def := deepcopy.Copy(test.container).(excelize.PageLayoutOptionPtr)
- val1 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr)
- val2 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr)
-
- f := excelize.NewFile()
- // Get the default value
- assert.NoError(t, f.GetPageLayout(sheet, def), opt)
- // Get again and check
- assert.NoError(t, f.GetPageLayout(sheet, val1), opt)
- if !assert.Equal(t, val1, def, opt) {
- t.FailNow()
- }
- // Set the same value
- assert.NoError(t, f.SetPageLayout(sheet, val1), opt)
- // Get again and check
- assert.NoError(t, f.GetPageLayout(sheet, val1), opt)
- if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Set a different value
- assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opt)
- assert.NoError(t, f.GetPageLayout(sheet, val1), opt)
- // Get again and compare
- assert.NoError(t, f.GetPageLayout(sheet, val2), opt)
- if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Value should not be the same as the default
- if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) {
- t.FailNow()
- }
- // Restore the default value
- assert.NoError(t, f.SetPageLayout(sheet, def), opt)
- assert.NoError(t, f.GetPageLayout(sheet, val1), opt)
- if !assert.Equal(t, def, val1) {
- t.FailNow()
- }
- })
- }
+ _, err = f.NewSheet("Panes 3")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetPanes("Panes 3",
+ &Panes{
+ Freeze: false,
+ Split: true,
+ XSplit: 3270,
+ YSplit: 1800,
+ TopLeftCell: "N57",
+ ActivePane: "bottomLeft",
+ Selection: []Selection{
+ {SQRef: "I36", ActiveCell: "I36"},
+ {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
+ {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
+ {SQRef: "O60", ActiveCell: "O60", Pane: "bottomRight"},
+ },
+ },
+ ))
+ _, err = f.NewSheet("Panes 4")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetPanes("Panes 4",
+ &Panes{
+ Freeze: true,
+ Split: false,
+ XSplit: 0,
+ YSplit: 9,
+ TopLeftCell: "A34",
+ ActivePane: "bottomLeft",
+ Selection: []Selection{
+ {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
+ },
+ },
+ ))
+ assert.EqualError(t, f.SetPanes("Panes 4", nil), ErrParameterInvalid.Error())
+ assert.EqualError(t, f.SetPanes("SheetN", nil), "sheet SheetN does not exist")
+ // Test set panes with invalid sheet name
+ assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error())
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx")))
+
+ // Test get panes with empty sheet views
+ f = NewFile()
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{}
+ _, err = f.GetPanes("Sheet1")
+ assert.NoError(t, err)
+ // Test get panes without panes
+ ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}}
+ _, err = f.GetPanes("Sheet1")
+ assert.NoError(t, err)
+ // Test get panes without sheet views
+ ws.(*xlsxWorksheet).SheetViews = nil
+ _, err = f.GetPanes("Sheet1")
+ assert.NoError(t, err)
+ // Test get panes on not exists worksheet
+ _, err = f.GetPanes("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
+ // Test add pane on empty sheet views worksheet
+ f = NewFile()
+ f.checked = sync.Map{}
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``))
+ assert.NoError(t, f.SetPanes("Sheet1",
+ &Panes{
+ Freeze: true,
+ Split: false,
+ XSplit: 1,
+ YSplit: 0,
+ TopLeftCell: "B1",
+ ActivePane: "topRight",
+ Selection: []Selection{
+ {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
+ },
+ },
+ ))
}
func TestSearchSheet(t *testing.T) {
- f, err := excelize.OpenFile(filepath.Join("test", "SharedStrings.xlsx"))
+ f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
- // Test search in a not exists worksheet.
+ // Test search in a not exists worksheet
_, err = f.SearchSheet("Sheet4", "")
- assert.EqualError(t, err, "sheet Sheet4 is not exist")
+ assert.EqualError(t, err, "sheet Sheet4 does not exist")
+ // Test search sheet with invalid sheet name
+ _, err = f.SearchSheet("Sheet:1", "")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
var expected []string
- // Test search a not exists value.
+ // Test search a not exists value
result, err := f.SearchSheet("Sheet1", "X")
assert.NoError(t, err)
assert.EqualValues(t, expected, result)
@@ -170,38 +163,115 @@ func TestSearchSheet(t *testing.T) {
result, err = f.SearchSheet("Sheet1", "[0-9]", true)
assert.NoError(t, err)
assert.EqualValues(t, expected, result)
+ assert.NoError(t, f.Close())
// Test search worksheet data after set cell value
- f = excelize.NewFile()
+ f = NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
_, err = f.SearchSheet("Sheet1", "")
assert.NoError(t, err)
+
+ f = NewFile()
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A
`))
+ f.checked = sync.Map{}
+ result, err = f.SearchSheet("Sheet1", "A")
+ assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
+ assert.Equal(t, []string(nil), result)
+
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A
`))
+ result, err = f.SearchSheet("Sheet1", "A")
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
+ assert.Equal(t, []string(nil), result)
+
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A
`))
+ result, err = f.SearchSheet("Sheet1", "A")
+ assert.Equal(t, newCoordinatesToCellNameError(1, 0), err)
+ assert.Equal(t, []string(nil), result)
+
+ // Test search sheet with unsupported charset shared strings table
+ f.SharedStrings = nil
+ f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
+ _, err = f.SearchSheet("Sheet1", "A")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestSetPageLayout(t *testing.T) {
- f := excelize.NewFile()
- // Test set page layout on not exists worksheet.
- assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN is not exist")
+ f := NewFile()
+ assert.NoError(t, f.SetPageLayout("Sheet1", nil))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).PageSetUp = nil
+ expected := PageLayoutOptions{
+ Size: intPtr(1),
+ Orientation: stringPtr("landscape"),
+ FirstPageNumber: uintPtr(1),
+ AdjustTo: uintPtr(120),
+ FitToHeight: intPtr(2),
+ FitToWidth: intPtr(2),
+ BlackAndWhite: boolPtr(true),
+ PageOrder: stringPtr("overThenDown"),
+ }
+ assert.NoError(t, f.SetPageLayout("Sheet1", &expected))
+ opts, err := f.GetPageLayout("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+ // Test set page layout on not exists worksheet
+ assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist")
+ // Test set page layout with invalid sheet name
+ assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error())
+ // Test set page layout with invalid parameters
+ assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
+ AdjustTo: uintPtr(5),
+ }), "adjust to value must be between 10 and 400")
+ assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
+ Orientation: stringPtr("x"),
+ }), "invalid Orientation value \"x\", acceptable value should be one of portrait, landscape")
+ assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
+ PageOrder: stringPtr("x"),
+ }), "invalid PageOrder value \"x\", acceptable value should be one of overThenDown, downThenOver")
}
func TestGetPageLayout(t *testing.T) {
- f := excelize.NewFile()
- // Test get page layout on not exists worksheet.
- assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist")
+ f := NewFile()
+ // Test get page layout on not exists worksheet
+ _, err := f.GetPageLayout("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get page layout with invalid sheet name
+ _, err = f.GetPageLayout("Sheet:1")
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
}
-func TestSetHeaderFooter(t *testing.T) {
- f := excelize.NewFile()
+func TestHeaderFooter(t *testing.T) {
+ f := NewFile()
+ // Test get header and footer with default header and footer settings
+ opts, err := f.GetHeaderFooter("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, (*HeaderFooterOptions)(nil), opts)
+ // Test get header and footer on not exists worksheet
+ _, err = f.GetHeaderFooter("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+
assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter"))
- // Test set header and footer on not exists worksheet.
- assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN is not exist")
- // Test set header and footer with illegal setting.
- assert.EqualError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{
- OddHeader: strings.Repeat("c", 256),
- }), "field OddHeader must be less than 255 characters")
+ // Test set header and footer on not exists worksheet
+ assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist")
+ // Test Sheet:1 with invalid sheet name
+ assert.EqualError(t, f.SetHeaderFooter("Sheet:1", nil), ErrSheetNameInvalid.Error())
+ // Test set header and footer with illegal setting
+ assert.EqualError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{
+ OddHeader: strings.Repeat("c", MaxFieldLength+1),
+ }), newFieldLengthError("OddHeader").Error())
assert.NoError(t, f.SetHeaderFooter("Sheet1", nil))
- assert.NoError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{
+ text := strings.Repeat("一", MaxFieldLength)
+ assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{
+ OddHeader: text,
+ OddFooter: text,
+ EvenHeader: text,
+ EvenFooter: text,
+ FirstHeader: text,
+ }))
+ expected := &HeaderFooterOptions{
DifferentFirst: true,
DifferentOddEven: true,
OddHeader: "&R&P",
@@ -209,74 +279,113 @@ func TestSetHeaderFooter(t *testing.T) {
EvenHeader: "&L&P",
EvenFooter: "&L&D&R&T",
FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`,
- }))
+ }
+ assert.NoError(t, f.SetHeaderFooter("Sheet1", expected))
+ opts, err = f.GetHeaderFooter("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx")))
}
func TestDefinedName(t *testing.T) {
- f := excelize.NewFile()
- assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{
- Name: "Amount",
+ f := NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Amount.",
RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment",
Scope: "Sheet1",
}))
- assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment",
}))
- assert.EqualError(t, f.SetDefinedName(&excelize.DefinedName{
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: builtInDefinedNames[0],
+ RefersTo: "Sheet1!$A$1:$Z$100",
+ Scope: "Sheet1",
+ }))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: builtInDefinedNames[1],
+ RefersTo: "Sheet1!$A:$A,Sheet1!$1:$1",
+ Scope: "Sheet1",
+ }))
+ assert.EqualError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment",
- }), "the same name already exists on the scope")
- assert.EqualError(t, f.DeleteDefinedName(&excelize.DefinedName{
+ }), ErrDefinedNameDuplicate.Error())
+ assert.EqualError(t, f.DeleteDefinedName(&DefinedName{
Name: "No Exist Defined Name",
- }), "no defined name on the scope")
+ }), ErrDefinedNameScope.Error())
+ // Test set defined name without name
+ assert.EqualError(t, f.SetDefinedName(&DefinedName{
+ RefersTo: "Sheet1!$A$2:$D$5",
+ }), ErrParameterInvalid.Error())
+ // Test set defined name without reference
+ assert.EqualError(t, f.SetDefinedName(&DefinedName{
+ Name: "Amount",
+ }), ErrParameterInvalid.Error())
assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo)
- assert.NoError(t, f.DeleteDefinedName(&excelize.DefinedName{
+ assert.NoError(t, f.DeleteDefinedName(&DefinedName{
Name: "Amount",
}))
assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo)
- assert.Exactly(t, 1, len(f.GetDefinedName()))
+ assert.Len(t, f.GetDefinedName(), 3)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx")))
+ // Test set defined name with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetDefinedName(&DefinedName{
+ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ // Test delete defined name with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeleteDefinedName(&DefinedName{Name: "Amount"}),
+ "XML syntax error on line 1: invalid UTF-8")
}
func TestGroupSheets(t *testing.T) {
- f := excelize.NewFile()
+ f := NewFile()
sheets := []string{"Sheet2", "Sheet3"}
for _, sheet := range sheets {
- f.NewSheet(sheet)
+ _, err := f.NewSheet(sheet)
+ assert.NoError(t, err)
}
- assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN is not exist")
+ assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN does not exist")
assert.EqualError(t, f.GroupSheets([]string{"Sheet2", "Sheet3"}), "group worksheet must contain an active worksheet")
+ // Test group sheets with invalid sheet name
+ assert.EqualError(t, f.GroupSheets([]string{"Sheet:1", "Sheet1"}), ErrSheetNameInvalid.Error())
assert.NoError(t, f.GroupSheets([]string{"Sheet1", "Sheet2"}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGroupSheets.xlsx")))
}
func TestUngroupSheets(t *testing.T) {
- f := excelize.NewFile()
+ f := NewFile()
sheets := []string{"Sheet2", "Sheet3", "Sheet4", "Sheet5"}
for _, sheet := range sheets {
- f.NewSheet(sheet)
+ _, err := f.NewSheet(sheet)
+ assert.NoError(t, err)
}
assert.NoError(t, f.UngroupSheets())
}
func TestInsertPageBreak(t *testing.T) {
- f := excelize.NewFile()
+ f := NewFile()
assert.NoError(t, f.InsertPageBreak("Sheet1", "A1"))
assert.NoError(t, f.InsertPageBreak("Sheet1", "B2"))
assert.NoError(t, f.InsertPageBreak("Sheet1", "C3"))
assert.NoError(t, f.InsertPageBreak("Sheet1", "C3"))
- assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN is not exist")
+ assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN does not exist")
+ // Test insert page break with invalid sheet name
+ assert.EqualError(t, f.InsertPageBreak("Sheet:1", "C3"), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx")))
}
func TestRemovePageBreak(t *testing.T) {
- f := excelize.NewFile()
+ f := NewFile()
assert.NoError(t, f.RemovePageBreak("Sheet1", "A2"))
assert.NoError(t, f.InsertPageBreak("Sheet1", "A2"))
@@ -291,22 +400,27 @@ func TestRemovePageBreak(t *testing.T) {
assert.NoError(t, f.RemovePageBreak("Sheet1", "B3"))
assert.NoError(t, f.RemovePageBreak("Sheet1", "A3"))
- f.NewSheet("Sheet2")
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
assert.NoError(t, f.InsertPageBreak("Sheet2", "B2"))
assert.NoError(t, f.InsertPageBreak("Sheet2", "C2"))
assert.NoError(t, f.RemovePageBreak("Sheet2", "B2"))
- assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN is not exist")
+ assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
+ assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN does not exist")
+ // Test remove page break with invalid sheet name
+ assert.EqualError(t, f.RemovePageBreak("Sheet:1", "A3"), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx")))
}
func TestGetSheetName(t *testing.T) {
- f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx"))
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
assert.Equal(t, "Sheet1", f.GetSheetName(0))
assert.Equal(t, "Sheet2", f.GetSheetName(1))
assert.Equal(t, "", f.GetSheetName(-1))
assert.Equal(t, "", f.GetSheetName(2))
+ assert.NoError(t, f.Close())
}
func TestGetSheetMap(t *testing.T) {
@@ -314,10 +428,416 @@ func TestGetSheetMap(t *testing.T) {
1: "Sheet1",
2: "Sheet2",
}
- f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx"))
+ f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
+ assert.NoError(t, err)
sheetMap := f.GetSheetMap()
for idx, name := range sheetMap {
assert.Equal(t, expectedMap[idx], name)
}
- assert.Equal(t, len(sheetMap), 2)
+ assert.Len(t, sheetMap, 2)
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ _, err = f.getSheetMap()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestSetActiveSheet(t *testing.T) {
+ f := NewFile()
+ f.WorkBook.BookViews = nil
+ f.SetActiveSheet(1)
+ f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}}
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}}
+ f.SetActiveSheet(1)
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = nil
+ f.SetActiveSheet(1)
+ f = NewFile()
+ f.SetActiveSheet(-1)
+ assert.Equal(t, f.GetActiveSheetIndex(), 0)
+
+ f = NewFile()
+ f.WorkBook.BookViews = nil
+ idx, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet2.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}}
+ f.SetActiveSheet(idx)
+}
+
+func TestSetSheetName(t *testing.T) {
+ f := NewFile()
+ // Test set worksheet with the same name
+ assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1"))
+ assert.Equal(t, "Sheet1", f.GetSheetName(0))
+ // Test set worksheet with the different name
+ assert.NoError(t, f.SetSheetName("Sheet1", "sheet1"))
+ assert.Equal(t, "sheet1", f.GetSheetName(0))
+ // Test set sheet name with invalid sheet name
+ assert.Equal(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid)
+
+ // Test set worksheet name with existing defined name and auto filter
+ assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name1",
+ RefersTo: "$B$2",
+ }))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name2",
+ RefersTo: "$A1$2:A2",
+ }))
+ assert.NoError(t, f.SetDefinedName(&DefinedName{
+ Name: "Name3",
+ RefersTo: "Sheet1!$A$1:'Sheet1'!A1:Sheet1!$A$1,Sheet1!A1:Sheet3!A1,Sheet3!A1",
+ }))
+ assert.NoError(t, f.SetSheetName("Sheet1", "Sheet2"))
+ for i, expected := range []string{"'Sheet2'!$A$1:$A$2", "$B$2", "$A1$2:A2", "Sheet2!$A$1:'Sheet2'!A1:Sheet2!$A$1,Sheet2!A1:Sheet3!A1,Sheet3!A1"} {
+ assert.Equal(t, expected, f.WorkBook.DefinedNames.DefinedName[i].Data)
+ }
+}
+
+func TestWorksheetWriter(t *testing.T) {
+ f := NewFile()
+ // Test set cell value with alternate content
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ worksheet := xml.Header + `%d
`
+ f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1)))
+ f.checked = sync.Map{}
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2))
+ f.workSheetWriter()
+ value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ assert.Equal(t, fmt.Sprintf(worksheet, 2), string(value.([]byte)))
+}
+
+func TestGetWorkbookPath(t *testing.T) {
+ f := NewFile()
+ f.Pkg.Delete("_rels/.rels")
+ assert.Equal(t, "", f.getWorkbookPath())
+}
+
+func TestGetWorkbookRelsPath(t *testing.T) {
+ f := NewFile()
+ f.Pkg.Delete("xl/_rels/.rels")
+ f.Pkg.Store("_rels/.rels", []byte(xml.Header+``))
+ assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath())
+}
+
+func TestDeleteSheet(t *testing.T) {
+ f := NewFile()
+ idx, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ f.SetActiveSheet(idx)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ assert.NoError(t, f.DeleteSheet("Sheet1"))
+ assert.Equal(t, "Sheet2", f.GetSheetName(f.GetActiveSheetIndex()))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet.xlsx")))
+ // Test with auto filter defined names
+ f = NewFile()
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ _, err = f.NewSheet("Sheet3")
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A"))
+ assert.NoError(t, f.SetCellValue("Sheet2", "A1", "A"))
+ assert.NoError(t, f.SetCellValue("Sheet3", "A1", "A"))
+ assert.NoError(t, f.AutoFilter("Sheet1", "A1:A1", nil))
+ assert.NoError(t, f.AutoFilter("Sheet2", "A1:A1", nil))
+ assert.NoError(t, f.AutoFilter("Sheet3", "A1:A1", nil))
+ assert.NoError(t, f.DeleteSheet("Sheet2"))
+ assert.NoError(t, f.DeleteSheet("Sheet1"))
+ // Test delete sheet with invalid sheet name
+ assert.EqualError(t, f.DeleteSheet("Sheet:1"), ErrSheetNameInvalid.Error())
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx")))
+}
+
+func TestMoveSheet(t *testing.T) {
+ f := NewFile()
+ defer f.Close()
+ for i := 2; i < 6; i++ {
+ _, err := f.NewSheet("Sheet" + strconv.Itoa(i))
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, []string{"Sheet1", "Sheet2", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
+
+ // Move target to first position
+ assert.NoError(t, f.MoveSheet("Sheet2", "Sheet1"))
+ assert.Equal(t, []string{"Sheet2", "Sheet1", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
+ assert.Equal(t, "Sheet1", f.GetSheetName(f.GetActiveSheetIndex()))
+
+ // Move target to last position
+ assert.NoError(t, f.MoveSheet("Sheet2", "Sheet5"))
+ assert.NoError(t, f.MoveSheet("Sheet5", "Sheet2"))
+ assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
+
+ // Move target to same position
+ assert.NoError(t, f.MoveSheet("Sheet1", "Sheet1"))
+ assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
+
+ // Test move sheet with invalid sheet name
+ assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("", "Sheet2"))
+ assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("Sheet1", ""))
+
+ // Test move sheet on not exists worksheet
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("SheetN", "Sheet2"))
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("Sheet1", "SheetN"))
+
+ // Test move sheet with unsupported workbook charset
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestDeleteAndAdjustDefinedNames(t *testing.T) {
+ deleteAndAdjustDefinedNames(nil, 0)
+ deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0)
+}
+
+func TestGetSheetID(t *testing.T) {
+ f := NewFile()
+ _, err := f.NewSheet("Sheet1")
+ assert.NoError(t, err)
+ id := f.getSheetID("sheet1")
+ assert.NotEqual(t, -1, id)
+}
+
+func TestSetSheetVisible(t *testing.T) {
+ f := NewFile()
+ // Test set sheet visible with invalid sheet name
+ assert.EqualError(t, f.SetSheetVisible("Sheet:1", false), ErrSheetNameInvalid.Error())
+ f.WorkBook.Sheets.Sheet[0].Name = "SheetN"
+ assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN does not exist")
+ // Test set sheet visible with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test set sheet visible with empty sheet views
+ f = NewFile()
+ _, err := f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet2.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = nil
+ assert.NoError(t, f.SetSheetVisible("Sheet2", false))
+ visible, err := f.GetSheetVisible("Sheet2")
+ assert.NoError(t, err)
+ assert.False(t, visible)
+}
+
+func TestGetSheetVisible(t *testing.T) {
+ f := NewFile()
+ // Test get sheet visible with invalid sheet name
+ visible, err := f.GetSheetVisible("Sheet:1")
+ assert.Equal(t, false, visible)
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+}
+
+func TestGetSheetIndex(t *testing.T) {
+ f := NewFile()
+ // Test get sheet index with invalid sheet name
+ idx, err := f.GetSheetIndex("Sheet:1")
+ assert.Equal(t, -1, idx)
+ assert.EqualError(t, err, ErrSheetNameInvalid.Error())
+}
+
+func TestSetContentTypes(t *testing.T) {
+ f := NewFile()
+ // Test set content type with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestRemoveContentTypesPart(t *testing.T) {
+ f := NewFile()
+ // Test delete sheet from content types with unsupported charset content types
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, "/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func BenchmarkNewSheet(b *testing.B) {
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ newSheetWithSet()
+ }
+ })
+}
+
+func newSheetWithSet() {
+ file := NewFile()
+ for i := 0; i < 1000; i++ {
+ _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), int64(i))
+ }
+ file = nil
+}
+
+func BenchmarkFile_SaveAs(b *testing.B) {
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ newSheetWithSave()
+ }
+ })
+}
+
+func newSheetWithSave() {
+ file := NewFile()
+ for i := 0; i < 1000; i++ {
+ _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), int64(i))
+ }
+ _ = file.Save()
+}
+
+func TestAttrValToBool(t *testing.T) {
+ _, err := attrValToBool("hidden", []xml.Attr{
+ {Name: xml.Name{Local: "hidden"}},
+ })
+ assert.EqualError(t, err, `strconv.ParseBool: parsing "": invalid syntax`)
+
+ got, err := attrValToBool("hidden", []xml.Attr{
+ {Name: xml.Name{Local: "hidden"}, Value: "1"},
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, true, got)
+}
+
+func TestAttrValToFloat(t *testing.T) {
+ _, err := attrValToFloat("ht", []xml.Attr{
+ {Name: xml.Name{Local: "ht"}},
+ })
+ assert.EqualError(t, err, `strconv.ParseFloat: parsing "": invalid syntax`)
+
+ got, err := attrValToFloat("ht", []xml.Attr{
+ {Name: xml.Name{Local: "ht"}, Value: "42.1"},
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, 42.1, got)
+}
+
+func TestSetSheetBackgroundFromBytes(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetName("Sheet1", ".svg"))
+ for i, imageTypes := range []string{
+ ".svg", ".bmp", ".emf", ".emz", ".gif",
+ ".jpg", ".png", ".tif", ".wmf", ".wmz",
+ } {
+ file := fmt.Sprintf("excelize%s", imageTypes)
+ if i > 0 {
+ file = filepath.Join("test", "images", fmt.Sprintf("excel%s", imageTypes))
+ _, err := f.NewSheet(imageTypes)
+ assert.NoError(t, err)
+ }
+ img, err := os.Open(file)
+ assert.NoError(t, err)
+ content, err := io.ReadAll(img)
+ assert.NoError(t, err)
+ assert.NoError(t, img.Close())
+ assert.NoError(t, f.SetSheetBackgroundFromBytes(imageTypes, imageTypes, content))
+ }
+ // Test set worksheet background with invalid sheet name
+ img, err := os.Open(filepath.Join("test", "images", "excel.png"))
+ assert.NoError(t, err)
+ content, err := io.ReadAll(img)
+ assert.NoError(t, err)
+ assert.EqualError(t, f.SetSheetBackgroundFromBytes("Sheet:1", ".png", content), ErrSheetNameInvalid.Error())
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackgroundFromBytes.xlsx")))
+ assert.NoError(t, f.Close())
+
+ assert.EqualError(t, f.SetSheetBackgroundFromBytes("Sheet1", ".svg", nil), ErrParameterInvalid.Error())
+}
+
+func TestCheckSheetName(t *testing.T) {
+ // Test valid sheet name
+ assert.NoError(t, checkSheetName("Sheet1"))
+ assert.NoError(t, checkSheetName("She'et1"))
+ // Test invalid sheet name, empty name
+ assert.EqualError(t, checkSheetName(""), ErrSheetNameBlank.Error())
+ // Test invalid sheet name, include :\/?*[]
+ assert.EqualError(t, checkSheetName("Sheet:"), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName(`Sheet\`), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName("Sheet/"), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName("Sheet?"), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName("Sheet*"), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName("Sheet["), ErrSheetNameInvalid.Error())
+ assert.EqualError(t, checkSheetName("Sheet]"), ErrSheetNameInvalid.Error())
+ // Test invalid sheet name, single quotes at the front or at the end
+ assert.EqualError(t, checkSheetName("'Sheet"), ErrSheetNameSingleQuote.Error())
+ assert.EqualError(t, checkSheetName("Sheet'"), ErrSheetNameSingleQuote.Error())
+}
+
+func TestSheetDimension(t *testing.T) {
+ f := NewFile()
+ const sheetName = "Sheet1"
+ // Test get a new worksheet dimension
+ dimension, err := f.GetSheetDimension(sheetName)
+ assert.NoError(t, err)
+ assert.Equal(t, "A1", dimension)
+ // Test remove the worksheet dimension
+ assert.NoError(t, f.SetSheetDimension(sheetName, ""))
+ assert.NoError(t, err)
+ dimension, err = f.GetSheetDimension(sheetName)
+ assert.NoError(t, err)
+ assert.Equal(t, "", dimension)
+ // Test set the worksheet dimension
+ for _, excepted := range []string{"A1", "A1:D5", "A1:XFD1048576", "a1", "A1:d5"} {
+ err = f.SetSheetDimension(sheetName, excepted)
+ assert.NoError(t, err)
+ dimension, err := f.GetSheetDimension(sheetName)
+ assert.NoError(t, err)
+ assert.Equal(t, strings.ToUpper(excepted), dimension)
+ }
+ // Test set the worksheet dimension with invalid range reference or no exists worksheet
+ for _, c := range []struct {
+ sheetName string
+ rangeRef string
+ err string
+ }{
+ {"Sheet1", "A-1", "cannot convert cell \"A-1\" to coordinates: invalid cell name \"A-1\""},
+ {"Sheet1", "A1:B-1", "cannot convert cell \"B-1\" to coordinates: invalid cell name \"B-1\""},
+ {"Sheet1", "A1:XFD1048577", "row number exceeds maximum limit"},
+ {"Sheet1", "123", "cannot convert cell \"123\" to coordinates: invalid cell name \"123\""},
+ {"Sheet1", "A:B", "cannot convert cell \"A\" to coordinates: invalid cell name \"A\""},
+ {"Sheet1", ":B10", "cannot convert cell \"\" to coordinates: invalid cell name \"\""},
+ {"Sheet1", "XFE1", "the column number must be greater than or equal to 1 and less than or equal to 16384"},
+ {"Sheet1", "A1048577", "row number exceeds maximum limit"},
+ {"Sheet1", "ZZZ", "cannot convert cell \"ZZZ\" to coordinates: invalid cell name \"ZZZ\""},
+ {"SheetN", "A1", "sheet SheetN does not exist"},
+ {"Sheet1", "A1:B3:D5", ErrParameterInvalid.Error()},
+ } {
+ err = f.SetSheetDimension(c.sheetName, c.rangeRef)
+ assert.EqualError(t, err, c.err)
+ }
+ // Test get the worksheet dimension no exists worksheet
+ dimension, err = f.GetSheetDimension("SheetN")
+ assert.Empty(t, dimension)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+}
+
+func TestAddIgnoredErrors(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEvalError))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEvalError))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsTwoDigitTextYear))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsNumberStoredAsText))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsFormula))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsFormulaRange))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsUnlockedFormula))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEmptyCellReference))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsListDataValidation))
+ assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsCalculatedColumn))
+
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddIgnoredErrors("SheetN", "A1", IgnoredErrorsEvalError))
+ assert.Equal(t, ErrParameterInvalid, f.AddIgnoredErrors("Sheet1", "", IgnoredErrorsEvalError))
+
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddIgnoredErrors.xlsx")))
+ assert.NoError(t, f.Close())
}
diff --git a/sheetpr.go b/sheetpr.go
index ee3b23c504..523e7b9375 100644
--- a/sheetpr.go
+++ b/sheetpr.go
@@ -1,554 +1,228 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-// SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions().
-type SheetPrOption interface {
- setSheetPrOption(view *xlsxSheetPr)
-}
-
-// SheetPrOptionPtr is a writable SheetPrOption. See GetSheetPrOptions().
-type SheetPrOptionPtr interface {
- SheetPrOption
- getSheetPrOption(view *xlsxSheetPr)
-}
-
-type (
- // CodeName is a SheetPrOption
- CodeName string
- // EnableFormatConditionsCalculation is a SheetPrOption
- EnableFormatConditionsCalculation bool
- // Published is a SheetPrOption
- Published bool
- // FitToPage is a SheetPrOption
- FitToPage bool
- // AutoPageBreaks is a SheetPrOption
- AutoPageBreaks bool
- // OutlineSummaryBelow is an outlinePr, within SheetPr option
- OutlineSummaryBelow bool
-)
-
-// setSheetPrOption implements the SheetPrOption interface.
-func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) {
- if pr.OutlinePr == nil {
- pr.OutlinePr = new(xlsxOutlinePr)
- }
- pr.OutlinePr.SummaryBelow = bool(o)
-}
-
-// getSheetPrOption implements the SheetPrOptionPtr interface.
-func (o *OutlineSummaryBelow) getSheetPrOption(pr *xlsxSheetPr) {
- // Excel default: true
- if pr == nil || pr.OutlinePr == nil {
- *o = true
- return
- }
- *o = OutlineSummaryBelow(defaultTrue(&pr.OutlinePr.SummaryBelow))
-}
-
-// setSheetPrOption implements the SheetPrOption interface and specifies a
-// stable name of the sheet.
-func (o CodeName) setSheetPrOption(pr *xlsxSheetPr) {
- pr.CodeName = string(o)
-}
+import "reflect"
-// getSheetPrOption implements the SheetPrOptionPtr interface and get the
-// stable name of the sheet.
-func (o *CodeName) getSheetPrOption(pr *xlsxSheetPr) {
- if pr == nil {
- *o = ""
- return
- }
- *o = CodeName(pr.CodeName)
-}
-
-// setSheetPrOption implements the SheetPrOption interface and flag indicating
-// whether the conditional formatting calculations shall be evaluated.
-func (o EnableFormatConditionsCalculation) setSheetPrOption(pr *xlsxSheetPr) {
- pr.EnableFormatConditionsCalculation = boolPtr(bool(o))
-}
-
-// getSheetPrOption implements the SheetPrOptionPtr interface and get the
-// settings of whether the conditional formatting calculations shall be
-// evaluated.
-func (o *EnableFormatConditionsCalculation) getSheetPrOption(pr *xlsxSheetPr) {
- if pr == nil {
- *o = true
- return
+// SetPageMargins provides a function to set worksheet page margins.
+func (f *File) SetPageMargins(sheet string, opts *PageLayoutMarginsOptions) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
}
- *o = EnableFormatConditionsCalculation(defaultTrue(pr.EnableFormatConditionsCalculation))
-}
-
-// setSheetPrOption implements the SheetPrOption interface and flag indicating
-// whether the worksheet is published.
-func (o Published) setSheetPrOption(pr *xlsxSheetPr) {
- pr.Published = boolPtr(bool(o))
-}
-
-// getSheetPrOption implements the SheetPrOptionPtr interface and get the
-// settings of whether the worksheet is published.
-func (o *Published) getSheetPrOption(pr *xlsxSheetPr) {
- if pr == nil {
- *o = true
- return
+ if opts == nil {
+ return err
}
- *o = Published(defaultTrue(pr.Published))
-}
-
-// setSheetPrOption implements the SheetPrOption interface.
-func (o FitToPage) setSheetPrOption(pr *xlsxSheetPr) {
- if pr.PageSetUpPr == nil {
- if !o {
- return
+ preparePageMargins := func(ws *xlsxWorksheet) {
+ if ws.PageMargins == nil {
+ ws.PageMargins = new(xlsxPageMargins)
}
- pr.PageSetUpPr = new(xlsxPageSetUpPr)
}
- pr.PageSetUpPr.FitToPage = bool(o)
-}
-
-// getSheetPrOption implements the SheetPrOptionPtr interface.
-func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) {
- // Excel default: false
- if pr == nil || pr.PageSetUpPr == nil {
- *o = false
- return
- }
- *o = FitToPage(pr.PageSetUpPr.FitToPage)
-}
-
-// setSheetPrOption implements the SheetPrOption interface.
-func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) {
- if pr.PageSetUpPr == nil {
- if !o {
- return
+ preparePrintOptions := func(ws *xlsxWorksheet) {
+ if ws.PrintOptions == nil {
+ ws.PrintOptions = new(xlsxPrintOptions)
}
- pr.PageSetUpPr = new(xlsxPageSetUpPr)
}
- pr.PageSetUpPr.AutoPageBreaks = bool(o)
-}
-
-// getSheetPrOption implements the SheetPrOptionPtr interface.
-func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) {
- // Excel default: false
- if pr == nil || pr.PageSetUpPr == nil {
- *o = false
- return
- }
- *o = AutoPageBreaks(pr.PageSetUpPr.AutoPageBreaks)
-}
-
-// SetSheetPrOptions provides a function to sets worksheet properties.
-//
-// Available options:
-// CodeName(string)
-// EnableFormatConditionsCalculation(bool)
-// Published(bool)
-// FitToPage(bool)
-// AutoPageBreaks(bool)
-// OutlineSummaryBelow(bool)
-func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error {
- sheet, err := f.workSheetReader(name)
- if err != nil {
- return err
+ s := reflect.ValueOf(opts).Elem()
+ for i := 0; i < 6; i++ {
+ if !s.Field(i).IsNil() {
+ preparePageMargins(ws)
+ name := s.Type().Field(i).Name
+ reflect.ValueOf(ws.PageMargins).Elem().FieldByName(name).Set(s.Field(i).Elem())
+ }
}
- pr := sheet.SheetPr
- if pr == nil {
- pr = new(xlsxSheetPr)
- sheet.SheetPr = pr
+ if opts.Horizontally != nil {
+ preparePrintOptions(ws)
+ ws.PrintOptions.HorizontalCentered = *opts.Horizontally
}
-
- for _, opt := range opts {
- opt.setSheetPrOption(pr)
+ if opts.Vertically != nil {
+ preparePrintOptions(ws)
+ ws.PrintOptions.VerticalCentered = *opts.Vertically
}
return err
}
-// GetSheetPrOptions provides a function to gets worksheet properties.
-//
-// Available options:
-// CodeName(string)
-// EnableFormatConditionsCalculation(bool)
-// Published(bool)
-// FitToPage(bool)
-// AutoPageBreaks(bool)
-// OutlineSummaryBelow(bool)
-func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error {
- sheet, err := f.workSheetReader(name)
+// GetPageMargins provides a function to get worksheet page margins.
+func (f *File) GetPageMargins(sheet string) (PageLayoutMarginsOptions, error) {
+ opts := PageLayoutMarginsOptions{
+ Bottom: float64Ptr(0.75),
+ Footer: float64Ptr(0.3),
+ Header: float64Ptr(0.3),
+ Left: float64Ptr(0.7),
+ Right: float64Ptr(0.7),
+ Top: float64Ptr(0.75),
+ }
+ ws, err := f.workSheetReader(sheet)
if err != nil {
- return err
+ return opts, err
}
- pr := sheet.SheetPr
-
- for _, opt := range opts {
- opt.getSheetPrOption(pr)
+ if ws.PageMargins != nil {
+ opts.Bottom = float64Ptr(ws.PageMargins.Bottom)
+ opts.Footer = float64Ptr(ws.PageMargins.Footer)
+ opts.Header = float64Ptr(ws.PageMargins.Header)
+ opts.Left = float64Ptr(ws.PageMargins.Left)
+ opts.Right = float64Ptr(ws.PageMargins.Right)
+ opts.Top = float64Ptr(ws.PageMargins.Top)
}
- return err
-}
-
-type (
- // PageMarginBottom specifies the bottom margin for the page.
- PageMarginBottom float64
- // PageMarginFooter specifies the footer margin for the page.
- PageMarginFooter float64
- // PageMarginHeader specifies the header margin for the page.
- PageMarginHeader float64
- // PageMarginLeft specifies the left margin for the page.
- PageMarginLeft float64
- // PageMarginRight specifies the right margin for the page.
- PageMarginRight float64
- // PageMarginTop specifies the top margin for the page.
- PageMarginTop float64
-)
-
-// setPageMargins provides a method to set the bottom margin for the worksheet.
-func (p PageMarginBottom) setPageMargins(pm *xlsxPageMargins) {
- pm.Bottom = float64(p)
-}
-
-// setPageMargins provides a method to get the bottom margin for the worksheet.
-func (p *PageMarginBottom) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.75
- if pm == nil || pm.Bottom == 0 {
- *p = 0.75
- return
- }
- *p = PageMarginBottom(pm.Bottom)
-}
-
-// setPageMargins provides a method to set the footer margin for the worksheet.
-func (p PageMarginFooter) setPageMargins(pm *xlsxPageMargins) {
- pm.Footer = float64(p)
-}
-
-// setPageMargins provides a method to get the footer margin for the worksheet.
-func (p *PageMarginFooter) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.3
- if pm == nil || pm.Footer == 0 {
- *p = 0.3
- return
+ if ws.PrintOptions != nil {
+ opts.Horizontally = boolPtr(ws.PrintOptions.HorizontalCentered)
+ opts.Vertically = boolPtr(ws.PrintOptions.VerticalCentered)
}
- *p = PageMarginFooter(pm.Footer)
-}
-
-// setPageMargins provides a method to set the header margin for the worksheet.
-func (p PageMarginHeader) setPageMargins(pm *xlsxPageMargins) {
- pm.Header = float64(p)
+ return opts, err
}
-// setPageMargins provides a method to get the header margin for the worksheet.
-func (p *PageMarginHeader) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.3
- if pm == nil || pm.Header == 0 {
- *p = 0.3
- return
+// prepareSheetPr create sheetPr element which not exist.
+func (ws *xlsxWorksheet) prepareSheetPr() {
+ if ws.SheetPr == nil {
+ ws.SheetPr = new(xlsxSheetPr)
}
- *p = PageMarginHeader(pm.Header)
}
-// setPageMargins provides a method to set the left margin for the worksheet.
-func (p PageMarginLeft) setPageMargins(pm *xlsxPageMargins) {
- pm.Left = float64(p)
-}
-
-// setPageMargins provides a method to get the left margin for the worksheet.
-func (p *PageMarginLeft) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.7
- if pm == nil || pm.Left == 0 {
- *p = 0.7
- return
+// setSheetOutlineProps set worksheet outline properties by given options.
+func (ws *xlsxWorksheet) setSheetOutlineProps(opts *SheetPropsOptions) {
+ prepareOutlinePr := func(ws *xlsxWorksheet) {
+ ws.prepareSheetPr()
+ if ws.SheetPr.OutlinePr == nil {
+ ws.SheetPr.OutlinePr = new(xlsxOutlinePr)
+ }
}
- *p = PageMarginLeft(pm.Left)
-}
-
-// setPageMargins provides a method to set the right margin for the worksheet.
-func (p PageMarginRight) setPageMargins(pm *xlsxPageMargins) {
- pm.Right = float64(p)
-}
-
-// setPageMargins provides a method to get the right margin for the worksheet.
-func (p *PageMarginRight) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.7
- if pm == nil || pm.Right == 0 {
- *p = 0.7
- return
+ if opts.OutlineSummaryBelow != nil {
+ prepareOutlinePr(ws)
+ ws.SheetPr.OutlinePr.SummaryBelow = opts.OutlineSummaryBelow
}
- *p = PageMarginRight(pm.Right)
-}
-
-// setPageMargins provides a method to set the top margin for the worksheet.
-func (p PageMarginTop) setPageMargins(pm *xlsxPageMargins) {
- pm.Top = float64(p)
-}
-
-// setPageMargins provides a method to get the top margin for the worksheet.
-func (p *PageMarginTop) getPageMargins(pm *xlsxPageMargins) {
- // Excel default: 0.75
- if pm == nil || pm.Top == 0 {
- *p = 0.75
- return
+ if opts.OutlineSummaryRight != nil {
+ prepareOutlinePr(ws)
+ ws.SheetPr.OutlinePr.SummaryRight = opts.OutlineSummaryRight
}
- *p = PageMarginTop(pm.Top)
-}
-
-// PageMarginsOptions is an option of a page margin of a worksheet. See
-// SetPageMargins().
-type PageMarginsOptions interface {
- setPageMargins(layout *xlsxPageMargins)
}
-// PageMarginsOptionsPtr is a writable PageMarginsOptions. See
-// GetPageMargins().
-type PageMarginsOptionsPtr interface {
- PageMarginsOptions
- getPageMargins(layout *xlsxPageMargins)
-}
-
-// SetPageMargins provides a function to set worksheet page margins.
-//
-// Available options:
-// PageMarginBottom(float64)
-// PageMarginFooter(float64)
-// PageMarginHeader(float64)
-// PageMarginLeft(float64)
-// PageMarginRight(float64)
-// PageMarginTop(float64)
-func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error {
- s, err := f.workSheetReader(sheet)
- if err != nil {
- return err
+// setSheetProps set worksheet format properties by given options.
+func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) {
+ preparePageSetUpPr := func(ws *xlsxWorksheet) {
+ ws.prepareSheetPr()
+ if ws.SheetPr.PageSetUpPr == nil {
+ ws.SheetPr.PageSetUpPr = new(xlsxPageSetUpPr)
+ }
}
- pm := s.PageMargins
- if pm == nil {
- pm = new(xlsxPageMargins)
- s.PageMargins = pm
+ prepareTabColor := func(ws *xlsxWorksheet) {
+ ws.prepareSheetPr()
+ if ws.SheetPr.TabColor == nil {
+ ws.SheetPr.TabColor = new(xlsxColor)
+ }
}
-
- for _, opt := range opts {
- opt.setPageMargins(pm)
+ if opts.CodeName != nil {
+ ws.prepareSheetPr()
+ ws.SheetPr.CodeName = *opts.CodeName
+ }
+ if opts.EnableFormatConditionsCalculation != nil {
+ ws.prepareSheetPr()
+ ws.SheetPr.EnableFormatConditionsCalculation = opts.EnableFormatConditionsCalculation
+ }
+ if opts.Published != nil {
+ ws.prepareSheetPr()
+ ws.SheetPr.Published = opts.Published
+ }
+ if opts.AutoPageBreaks != nil {
+ preparePageSetUpPr(ws)
+ ws.SheetPr.PageSetUpPr.AutoPageBreaks = *opts.AutoPageBreaks
+ }
+ if opts.FitToPage != nil {
+ preparePageSetUpPr(ws)
+ ws.SheetPr.PageSetUpPr.FitToPage = *opts.FitToPage
+ }
+ ws.setSheetOutlineProps(opts)
+ s := reflect.ValueOf(opts).Elem()
+ for i := 5; i < 9; i++ {
+ if !s.Field(i).IsNil() {
+ prepareTabColor(ws)
+ name := s.Type().Field(i).Name
+ fld := reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:])
+ if s.Field(i).Kind() == reflect.Ptr && fld.Kind() == reflect.Ptr {
+ fld.Set(s.Field(i))
+ continue
+ }
+ fld.Set(s.Field(i).Elem())
+ }
}
- return err
}
-// GetPageMargins provides a function to get worksheet page margins.
-//
-// Available options:
-// PageMarginBottom(float64)
-// PageMarginFooter(float64)
-// PageMarginHeader(float64)
-// PageMarginLeft(float64)
-// PageMarginRight(float64)
-// PageMarginTop(float64)
-func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error {
- s, err := f.workSheetReader(sheet)
+// SetSheetProps provides a function to set worksheet properties.
+func (f *File) SetSheetProps(sheet string, opts *SheetPropsOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- pm := s.PageMargins
-
- for _, opt := range opts {
- opt.getPageMargins(pm)
- }
- return err
-}
-
-// SheetFormatPrOptions is an option of the formatting properties of a
-// worksheet. See SetSheetFormatPr().
-type SheetFormatPrOptions interface {
- setSheetFormatPr(formatPr *xlsxSheetFormatPr)
-}
-
-// SheetFormatPrOptionsPtr is a writable SheetFormatPrOptions. See
-// GetSheetFormatPr().
-type SheetFormatPrOptionsPtr interface {
- SheetFormatPrOptions
- getSheetFormatPr(formatPr *xlsxSheetFormatPr)
-}
-
-type (
- // BaseColWidth specifies the number of characters of the maximum digit width
- // of the normal style's font. This value does not include margin padding or
- // extra padding for gridlines. It is only the number of characters.
- BaseColWidth uint8
- // DefaultColWidth specifies the default column width measured as the number
- // of characters of the maximum digit width of the normal style's font.
- DefaultColWidth float64
- // DefaultRowHeight specifies the default row height measured in point size.
- // Optimization so we don't have to write the height on all rows. This can be
- // written out if most rows have custom height, to achieve the optimization.
- DefaultRowHeight float64
- // CustomHeight specifies the custom height.
- CustomHeight bool
- // ZeroHeight specifies if rows are hidden.
- ZeroHeight bool
- // ThickTop specifies if rows have a thick top border by default.
- ThickTop bool
- // ThickBottom specifies if rows have a thick bottom border by default.
- ThickBottom bool
-)
-
-// setSheetFormatPr provides a method to set the number of characters of the
-// maximum digit width of the normal style's font.
-func (p BaseColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.BaseColWidth = uint8(p)
-}
-
-// setSheetFormatPr provides a method to set the number of characters of the
-// maximum digit width of the normal style's font.
-func (p *BaseColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = 0
- return
- }
- *p = BaseColWidth(fp.BaseColWidth)
-}
-
-// setSheetFormatPr provides a method to set the default column width measured
-// as the number of characters of the maximum digit width of the normal
-// style's font.
-func (p DefaultColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.DefaultColWidth = float64(p)
-}
-
-// getSheetFormatPr provides a method to get the default column width measured
-// as the number of characters of the maximum digit width of the normal
-// style's font.
-func (p *DefaultColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = 0
- return
- }
- *p = DefaultColWidth(fp.DefaultColWidth)
-}
-
-// setSheetFormatPr provides a method to set the default row height measured
-// in point size.
-func (p DefaultRowHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.DefaultRowHeight = float64(p)
-}
-
-// getSheetFormatPr provides a method to get the default row height measured
-// in point size.
-func (p *DefaultRowHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = 15
- return
- }
- *p = DefaultRowHeight(fp.DefaultRowHeight)
-}
-
-// setSheetFormatPr provides a method to set the custom height.
-func (p CustomHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.CustomHeight = bool(p)
-}
-
-// getSheetFormatPr provides a method to get the custom height.
-func (p *CustomHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = false
- return
- }
- *p = CustomHeight(fp.CustomHeight)
-}
-
-// setSheetFormatPr provides a method to set if rows are hidden.
-func (p ZeroHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.ZeroHeight = bool(p)
-}
-
-// getSheetFormatPr provides a method to get if rows are hidden.
-func (p *ZeroHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = false
- return
- }
- *p = ZeroHeight(fp.ZeroHeight)
-}
-
-// setSheetFormatPr provides a method to set if rows have a thick top border
-// by default.
-func (p ThickTop) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.ThickTop = bool(p)
-}
-
-// getSheetFormatPr provides a method to get if rows have a thick top border
-// by default.
-func (p *ThickTop) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = false
- return
- }
- *p = ThickTop(fp.ThickTop)
-}
-
-// setSheetFormatPr provides a method to set if rows have a thick bottom
-// border by default.
-func (p ThickBottom) setSheetFormatPr(fp *xlsxSheetFormatPr) {
- fp.ThickBottom = bool(p)
-}
-
-// setSheetFormatPr provides a method to set if rows have a thick bottom
-// border by default.
-func (p *ThickBottom) getSheetFormatPr(fp *xlsxSheetFormatPr) {
- if fp == nil {
- *p = false
- return
- }
- *p = ThickBottom(fp.ThickBottom)
-}
-
-// SetSheetFormatPr provides a function to set worksheet formatting properties.
-//
-// Available options:
-// BaseColWidth(uint8)
-// DefaultColWidth(float64)
-// DefaultRowHeight(float64)
-// CustomHeight(bool)
-// ZeroHeight(bool)
-// ThickTop(bool)
-// ThickBottom(bool)
-func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOptions) error {
- s, err := f.workSheetReader(sheet)
- if err != nil {
+ if opts == nil {
return err
}
- fp := s.SheetFormatPr
- if fp == nil {
- fp = new(xlsxSheetFormatPr)
- s.SheetFormatPr = fp
+ ws.setSheetProps(opts)
+ if ws.SheetFormatPr == nil {
+ ws.SheetFormatPr = &xlsxSheetFormatPr{DefaultRowHeight: defaultRowHeight}
}
- for _, opt := range opts {
- opt.setSheetFormatPr(fp)
+ s := reflect.ValueOf(opts).Elem()
+ for i := 11; i < 18; i++ {
+ if !s.Field(i).IsNil() {
+ name := s.Type().Field(i).Name
+ reflect.ValueOf(ws.SheetFormatPr).Elem().FieldByName(name).Set(s.Field(i).Elem())
+ }
}
return err
}
-// GetSheetFormatPr provides a function to get worksheet formatting properties.
-//
-// Available options:
-// BaseColWidth(uint8)
-// DefaultColWidth(float64)
-// DefaultRowHeight(float64)
-// CustomHeight(bool)
-// ZeroHeight(bool)
-// ThickTop(bool)
-// ThickBottom(bool)
-func (f *File) GetSheetFormatPr(sheet string, opts ...SheetFormatPrOptionsPtr) error {
- s, err := f.workSheetReader(sheet)
+// GetSheetProps provides a function to get worksheet properties.
+func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) {
+ baseColWidth := uint8(8)
+ opts := SheetPropsOptions{
+ EnableFormatConditionsCalculation: boolPtr(true),
+ Published: boolPtr(true),
+ AutoPageBreaks: boolPtr(true),
+ OutlineSummaryBelow: boolPtr(true),
+ BaseColWidth: &baseColWidth,
+ }
+ ws, err := f.workSheetReader(sheet)
if err != nil {
- return err
+ return opts, err
+ }
+ if ws.SheetPr != nil {
+ opts.CodeName = stringPtr(ws.SheetPr.CodeName)
+ if ws.SheetPr.EnableFormatConditionsCalculation != nil {
+ opts.EnableFormatConditionsCalculation = ws.SheetPr.EnableFormatConditionsCalculation
+ }
+ if ws.SheetPr.Published != nil {
+ opts.Published = ws.SheetPr.Published
+ }
+ if ws.SheetPr.PageSetUpPr != nil {
+ opts.AutoPageBreaks = boolPtr(ws.SheetPr.PageSetUpPr.AutoPageBreaks)
+ opts.FitToPage = boolPtr(ws.SheetPr.PageSetUpPr.FitToPage)
+ }
+ if ws.SheetPr.OutlinePr != nil {
+ opts.OutlineSummaryBelow = ws.SheetPr.OutlinePr.SummaryBelow
+ opts.OutlineSummaryRight = ws.SheetPr.OutlinePr.SummaryRight
+ }
+ if ws.SheetPr.TabColor != nil {
+ opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed)
+ opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB)
+ opts.TabColorTheme = ws.SheetPr.TabColor.Theme
+ opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint)
+ }
}
- fp := s.SheetFormatPr
- for _, opt := range opts {
- opt.getSheetFormatPr(fp)
+ if ws.SheetFormatPr != nil {
+ opts.BaseColWidth = &ws.SheetFormatPr.BaseColWidth
+ opts.DefaultColWidth = float64Ptr(ws.SheetFormatPr.DefaultColWidth)
+ opts.DefaultRowHeight = float64Ptr(ws.SheetFormatPr.DefaultRowHeight)
+ opts.CustomHeight = boolPtr(ws.SheetFormatPr.CustomHeight)
+ opts.ZeroHeight = boolPtr(ws.SheetFormatPr.ZeroHeight)
+ opts.ThickTop = boolPtr(ws.SheetFormatPr.ThickTop)
+ opts.ThickBottom = boolPtr(ws.SheetFormatPr.ThickBottom)
}
- return err
+ return opts, err
}
diff --git a/sheetpr_test.go b/sheetpr_test.go
index 6e031518e1..63b732326d 100644
--- a/sheetpr_test.go
+++ b/sheetpr_test.go
@@ -1,469 +1,102 @@
-package excelize_test
+package excelize
import (
- "fmt"
"testing"
- "github.com/mohae/deepcopy"
"github.com/stretchr/testify/assert"
-
- "github.com/360EntSecGroup-Skylar/excelize/v2"
)
-var _ = []excelize.SheetPrOption{
- excelize.CodeName("hello"),
- excelize.EnableFormatConditionsCalculation(false),
- excelize.Published(false),
- excelize.FitToPage(true),
- excelize.AutoPageBreaks(true),
- excelize.OutlineSummaryBelow(true),
-}
-
-var _ = []excelize.SheetPrOptionPtr{
- (*excelize.CodeName)(nil),
- (*excelize.EnableFormatConditionsCalculation)(nil),
- (*excelize.Published)(nil),
- (*excelize.FitToPage)(nil),
- (*excelize.AutoPageBreaks)(nil),
- (*excelize.OutlineSummaryBelow)(nil),
-}
-
-func ExampleFile_SetSheetPrOptions() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- if err := f.SetSheetPrOptions(sheet,
- excelize.CodeName("code"),
- excelize.EnableFormatConditionsCalculation(false),
- excelize.Published(false),
- excelize.FitToPage(true),
- excelize.AutoPageBreaks(true),
- excelize.OutlineSummaryBelow(false),
- ); err != nil {
- fmt.Println(err)
- }
- // Output:
-}
-
-func ExampleFile_GetSheetPrOptions() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- var (
- codeName excelize.CodeName
- enableFormatConditionsCalculation excelize.EnableFormatConditionsCalculation
- published excelize.Published
- fitToPage excelize.FitToPage
- autoPageBreaks excelize.AutoPageBreaks
- outlineSummaryBelow excelize.OutlineSummaryBelow
- )
-
- if err := f.GetSheetPrOptions(sheet,
- &codeName,
- &enableFormatConditionsCalculation,
- &published,
- &fitToPage,
- &autoPageBreaks,
- &outlineSummaryBelow,
- ); err != nil {
- fmt.Println(err)
- }
- fmt.Println("Defaults:")
- fmt.Printf("- codeName: %q\n", codeName)
- fmt.Println("- enableFormatConditionsCalculation:", enableFormatConditionsCalculation)
- fmt.Println("- published:", published)
- fmt.Println("- fitToPage:", fitToPage)
- fmt.Println("- autoPageBreaks:", autoPageBreaks)
- fmt.Println("- outlineSummaryBelow:", outlineSummaryBelow)
- // Output:
- // Defaults:
- // - codeName: ""
- // - enableFormatConditionsCalculation: true
- // - published: true
- // - fitToPage: false
- // - autoPageBreaks: false
- // - outlineSummaryBelow: true
-}
-
-func TestSheetPrOptions(t *testing.T) {
- const sheet = "Sheet1"
-
- testData := []struct {
- container excelize.SheetPrOptionPtr
- nonDefault excelize.SheetPrOption
- }{
- {new(excelize.CodeName), excelize.CodeName("xx")},
- {new(excelize.EnableFormatConditionsCalculation), excelize.EnableFormatConditionsCalculation(false)},
- {new(excelize.Published), excelize.Published(false)},
- {new(excelize.FitToPage), excelize.FitToPage(true)},
- {new(excelize.AutoPageBreaks), excelize.AutoPageBreaks(true)},
- {new(excelize.OutlineSummaryBelow), excelize.OutlineSummaryBelow(false)},
- }
-
- for i, test := range testData {
- t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
-
- opt := test.nonDefault
- t.Logf("option %T", opt)
-
- def := deepcopy.Copy(test.container).(excelize.SheetPrOptionPtr)
- val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr)
- val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr)
-
- f := excelize.NewFile()
- // Get the default value
- assert.NoError(t, f.GetSheetPrOptions(sheet, def), opt)
- // Get again and check
- assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt)
- if !assert.Equal(t, val1, def, opt) {
- t.FailNow()
- }
- // Set the same value
- assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opt)
- // Get again and check
- assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt)
- if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Set a different value
- assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opt)
- assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt)
- // Get again and compare
- assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opt)
- if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Value should not be the same as the default
- if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) {
- t.FailNow()
- }
- // Restore the default value
- assert.NoError(t, f.SetSheetPrOptions(sheet, def), opt)
- assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt)
- if !assert.Equal(t, def, val1) {
- t.FailNow()
- }
- })
- }
-}
-
-func TestSetSheetrOptions(t *testing.T) {
- f := excelize.NewFile()
- // Test SetSheetrOptions on not exists worksheet.
- assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist")
-}
-
-func TestGetSheetPrOptions(t *testing.T) {
- f := excelize.NewFile()
- // Test GetSheetPrOptions on not exists worksheet.
- assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN is not exist")
-}
-
-var _ = []excelize.PageMarginsOptions{
- excelize.PageMarginBottom(1.0),
- excelize.PageMarginFooter(1.0),
- excelize.PageMarginHeader(1.0),
- excelize.PageMarginLeft(1.0),
- excelize.PageMarginRight(1.0),
- excelize.PageMarginTop(1.0),
-}
-
-var _ = []excelize.PageMarginsOptionsPtr{
- (*excelize.PageMarginBottom)(nil),
- (*excelize.PageMarginFooter)(nil),
- (*excelize.PageMarginHeader)(nil),
- (*excelize.PageMarginLeft)(nil),
- (*excelize.PageMarginRight)(nil),
- (*excelize.PageMarginTop)(nil),
-}
-
-func ExampleFile_SetPageMargins() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- if err := f.SetPageMargins(sheet,
- excelize.PageMarginBottom(1.0),
- excelize.PageMarginFooter(1.0),
- excelize.PageMarginHeader(1.0),
- excelize.PageMarginLeft(1.0),
- excelize.PageMarginRight(1.0),
- excelize.PageMarginTop(1.0),
- ); err != nil {
- fmt.Println(err)
- }
- // Output:
-}
-
-func ExampleFile_GetPageMargins() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- var (
- marginBottom excelize.PageMarginBottom
- marginFooter excelize.PageMarginFooter
- marginHeader excelize.PageMarginHeader
- marginLeft excelize.PageMarginLeft
- marginRight excelize.PageMarginRight
- marginTop excelize.PageMarginTop
- )
-
- if err := f.GetPageMargins(sheet,
- &marginBottom,
- &marginFooter,
- &marginHeader,
- &marginLeft,
- &marginRight,
- &marginTop,
- ); err != nil {
- fmt.Println(err)
- }
- fmt.Println("Defaults:")
- fmt.Println("- marginBottom:", marginBottom)
- fmt.Println("- marginFooter:", marginFooter)
- fmt.Println("- marginHeader:", marginHeader)
- fmt.Println("- marginLeft:", marginLeft)
- fmt.Println("- marginRight:", marginRight)
- fmt.Println("- marginTop:", marginTop)
- // Output:
- // Defaults:
- // - marginBottom: 0.75
- // - marginFooter: 0.3
- // - marginHeader: 0.3
- // - marginLeft: 0.7
- // - marginRight: 0.7
- // - marginTop: 0.75
-}
-
-func TestPageMarginsOption(t *testing.T) {
- const sheet = "Sheet1"
-
- testData := []struct {
- container excelize.PageMarginsOptionsPtr
- nonDefault excelize.PageMarginsOptions
- }{
- {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)},
- {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)},
- {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)},
- {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)},
- {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)},
- {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)},
- }
-
- for i, test := range testData {
- t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
-
- opt := test.nonDefault
- t.Logf("option %T", opt)
-
- def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr)
- val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr)
- val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr)
-
- f := excelize.NewFile()
- // Get the default value
- assert.NoError(t, f.GetPageMargins(sheet, def), opt)
- // Get again and check
- assert.NoError(t, f.GetPageMargins(sheet, val1), opt)
- if !assert.Equal(t, val1, def, opt) {
- t.FailNow()
- }
- // Set the same value
- assert.NoError(t, f.SetPageMargins(sheet, val1), opt)
- // Get again and check
- assert.NoError(t, f.GetPageMargins(sheet, val1), opt)
- if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Set a different value
- assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt)
- assert.NoError(t, f.GetPageMargins(sheet, val1), opt)
- // Get again and compare
- assert.NoError(t, f.GetPageMargins(sheet, val2), opt)
- if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Value should not be the same as the default
- if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) {
- t.FailNow()
- }
- // Restore the default value
- assert.NoError(t, f.SetPageMargins(sheet, def), opt)
- assert.NoError(t, f.GetPageMargins(sheet, val1), opt)
- if !assert.Equal(t, def, val1) {
- t.FailNow()
- }
- })
- }
-}
-
func TestSetPageMargins(t *testing.T) {
- f := excelize.NewFile()
- // Test set page margins on not exists worksheet.
- assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN is not exist")
-}
-
-func TestGetPageMargins(t *testing.T) {
- f := excelize.NewFile()
- // Test get page margins on not exists worksheet.
- assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist")
-}
-
-func ExampleFile_SetSheetFormatPr() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- if err := f.SetSheetFormatPr(sheet,
- excelize.BaseColWidth(1.0),
- excelize.DefaultColWidth(1.0),
- excelize.DefaultRowHeight(1.0),
- excelize.CustomHeight(true),
- excelize.ZeroHeight(true),
- excelize.ThickTop(true),
- excelize.ThickBottom(true),
- ); err != nil {
- fmt.Println(err)
+ f := NewFile()
+ assert.NoError(t, f.SetPageMargins("Sheet1", nil))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).PageMargins = nil
+ ws.(*xlsxWorksheet).PrintOptions = nil
+ expected := PageLayoutMarginsOptions{
+ Bottom: float64Ptr(1.0),
+ Footer: float64Ptr(1.0),
+ Header: float64Ptr(1.0),
+ Left: float64Ptr(1.0),
+ Right: float64Ptr(1.0),
+ Top: float64Ptr(1.0),
+ Horizontally: boolPtr(true),
+ Vertically: boolPtr(true),
}
- // Output:
+ assert.NoError(t, f.SetPageMargins("Sheet1", &expected))
+ opts, err := f.GetPageMargins("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+ // Test set page margins on not exists worksheet
+ assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist")
+ // Test set page margins with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetPageMargins("Sheet:1", nil))
}
-func ExampleFile_GetSheetFormatPr() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- var (
- baseColWidth excelize.BaseColWidth
- defaultColWidth excelize.DefaultColWidth
- defaultRowHeight excelize.DefaultRowHeight
- customHeight excelize.CustomHeight
- zeroHeight excelize.ZeroHeight
- thickTop excelize.ThickTop
- thickBottom excelize.ThickBottom
- )
-
- if err := f.GetSheetFormatPr(sheet,
- &baseColWidth,
- &defaultColWidth,
- &defaultRowHeight,
- &customHeight,
- &zeroHeight,
- &thickTop,
- &thickBottom,
- ); err != nil {
- fmt.Println(err)
- }
- fmt.Println("Defaults:")
- fmt.Println("- baseColWidth:", baseColWidth)
- fmt.Println("- defaultColWidth:", defaultColWidth)
- fmt.Println("- defaultRowHeight:", defaultRowHeight)
- fmt.Println("- customHeight:", customHeight)
- fmt.Println("- zeroHeight:", zeroHeight)
- fmt.Println("- thickTop:", thickTop)
- fmt.Println("- thickBottom:", thickBottom)
- // Output:
- // Defaults:
- // - baseColWidth: 0
- // - defaultColWidth: 0
- // - defaultRowHeight: 15
- // - customHeight: false
- // - zeroHeight: false
- // - thickTop: false
- // - thickBottom: false
-}
-
-func TestSheetFormatPrOptions(t *testing.T) {
- const sheet = "Sheet1"
-
- testData := []struct {
- container excelize.SheetFormatPrOptionsPtr
- nonDefault excelize.SheetFormatPrOptions
- }{
- {new(excelize.BaseColWidth), excelize.BaseColWidth(1.0)},
- {new(excelize.DefaultColWidth), excelize.DefaultColWidth(1.0)},
- {new(excelize.DefaultRowHeight), excelize.DefaultRowHeight(1.0)},
- {new(excelize.CustomHeight), excelize.CustomHeight(true)},
- {new(excelize.ZeroHeight), excelize.ZeroHeight(true)},
- {new(excelize.ThickTop), excelize.ThickTop(true)},
- {new(excelize.ThickBottom), excelize.ThickBottom(true)},
- }
-
- for i, test := range testData {
- t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
-
- opt := test.nonDefault
- t.Logf("option %T", opt)
-
- def := deepcopy.Copy(test.container).(excelize.SheetFormatPrOptionsPtr)
- val1 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr)
- val2 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr)
-
- f := excelize.NewFile()
- // Get the default value
- assert.NoError(t, f.GetSheetFormatPr(sheet, def), opt)
- // Get again and check
- assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt)
- if !assert.Equal(t, val1, def, opt) {
- t.FailNow()
- }
- // Set the same value
- assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opt)
- // Get again and check
- assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt)
- if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Set a different value
- assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opt)
- assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt)
- // Get again and compare
- assert.NoError(t, f.GetSheetFormatPr(sheet, val2), opt)
- if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) {
- t.FailNow()
- }
- // Value should not be the same as the default
- if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) {
- t.FailNow()
- }
- // Restore the default value
- assert.NoError(t, f.SetSheetFormatPr(sheet, def), opt)
- assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt)
- if !assert.Equal(t, def, val1) {
- t.FailNow()
- }
- })
+func TestGetPageMargins(t *testing.T) {
+ f := NewFile()
+ // Test get page margins on not exists worksheet
+ _, err := f.GetPageMargins("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get page margins with invalid sheet name
+ _, err = f.GetPageMargins("Sheet:1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+}
+
+func TestSetSheetProps(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetProps("Sheet1", nil))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetPr = nil
+ ws.(*xlsxWorksheet).SheetFormatPr = nil
+ baseColWidth, enable := uint8(8), boolPtr(true)
+ expected := SheetPropsOptions{
+ CodeName: stringPtr("code"),
+ EnableFormatConditionsCalculation: enable,
+ Published: enable,
+ AutoPageBreaks: enable,
+ FitToPage: enable,
+ TabColorIndexed: intPtr(1),
+ TabColorRGB: stringPtr("FFFF00"),
+ TabColorTheme: intPtr(1),
+ TabColorTint: float64Ptr(1),
+ OutlineSummaryBelow: enable,
+ OutlineSummaryRight: enable,
+ BaseColWidth: &baseColWidth,
+ DefaultColWidth: float64Ptr(10),
+ DefaultRowHeight: float64Ptr(10),
+ CustomHeight: enable,
+ ZeroHeight: enable,
+ ThickTop: enable,
+ ThickBottom: enable,
}
-}
-
-func TestSetSheetFormatPr(t *testing.T) {
- f := excelize.NewFile()
- assert.NoError(t, f.GetSheetFormatPr("Sheet1"))
- f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil
- assert.NoError(t, f.SetSheetFormatPr("Sheet1", excelize.BaseColWidth(1.0)))
- // Test set formatting properties on not exists worksheet.
- assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN is not exist")
-}
-
-func TestGetSheetFormatPr(t *testing.T) {
- f := excelize.NewFile()
- assert.NoError(t, f.GetSheetFormatPr("Sheet1"))
- f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil
- var (
- baseColWidth excelize.BaseColWidth
- defaultColWidth excelize.DefaultColWidth
- defaultRowHeight excelize.DefaultRowHeight
- customHeight excelize.CustomHeight
- zeroHeight excelize.ZeroHeight
- thickTop excelize.ThickTop
- thickBottom excelize.ThickBottom
- )
- assert.NoError(t, f.GetSheetFormatPr("Sheet1",
- &baseColWidth,
- &defaultColWidth,
- &defaultRowHeight,
- &customHeight,
- &zeroHeight,
- &thickTop,
- &thickBottom,
- ))
- // Test get formatting properties on not exists worksheet.
- assert.EqualError(t, f.GetSheetFormatPr("SheetN"), "sheet SheetN is not exist")
+ assert.NoError(t, f.SetSheetProps("Sheet1", &expected))
+ opts, err := f.GetSheetProps("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+
+ ws.(*xlsxWorksheet).SheetPr = nil
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{FitToPage: enable}))
+ ws.(*xlsxWorksheet).SheetPr = nil
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorRGB: stringPtr("FFFF00")}))
+ ws.(*xlsxWorksheet).SheetPr = nil
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTheme: intPtr(1)}))
+ ws.(*xlsxWorksheet).SheetPr = nil
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{TabColorTint: float64Ptr(1)}))
+
+ // Test set worksheet properties on not exists worksheet
+ assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist")
+ // Test set worksheet properties with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.SetSheetProps("Sheet:1", nil))
+}
+
+func TestGetSheetProps(t *testing.T) {
+ f := NewFile()
+ // Test get worksheet properties on not exists worksheet
+ _, err := f.GetSheetProps("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get worksheet properties with invalid sheet name
+ _, err = f.GetSheetProps("Sheet:1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
}
diff --git a/sheetview.go b/sheetview.go
index 23a0377d9e..fdc645b17a 100644
--- a/sheetview.go
+++ b/sheetview.go
@@ -1,217 +1,129 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "fmt"
-
-// SheetViewOption is an option of a view of a worksheet. See
-// SetSheetViewOptions().
-type SheetViewOption interface {
- setSheetViewOption(view *xlsxSheetView)
-}
-
-// SheetViewOptionPtr is a writable SheetViewOption. See
-// GetSheetViewOptions().
-type SheetViewOptionPtr interface {
- SheetViewOption
- getSheetViewOption(view *xlsxSheetView)
-}
-
-type (
- // DefaultGridColor is a SheetViewOption. It specifies a flag indicating that
- // the consuming application should use the default grid lines color (system
- // dependent). Overrides any color specified in colorId.
- DefaultGridColor bool
- // RightToLeft is a SheetViewOption. It specifies a flag indicating whether
- // the sheet is in 'right to left' display mode. When in this mode, Column A
- // is on the far right, Column B ;is one column left of Column A, and so on.
- // Also, information in cells is displayed in the Right to Left format.
- RightToLeft bool
- // ShowFormulas is a SheetViewOption. It specifies a flag indicating whether
- // this sheet should display formulas.
- ShowFormulas bool
- // ShowGridLines is a SheetViewOption. It specifies a flag indicating whether
- // this sheet should display gridlines.
- ShowGridLines bool
- // ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating
- // whether the sheet should display row and column headings.
- ShowRowColHeaders bool
- // ZoomScale is a SheetViewOption. It specifies a window zoom magnification
- // for current view representing percent values. This attribute is restricted
- // to values ranging from 10 to 400. Horizontal & Vertical scale together.
- ZoomScale float64
- // TopLeftCell is a SheetViewOption. It specifies a location of the top left
- // visible cell Location of the top left visible cell in the bottom right
- // pane (when in Left-to-Right mode).
- TopLeftCell string
- // ShowZeros is a SheetViewOption. It specifies a flag indicating
- // whether to "show a zero in cells that have zero value".
- // When using a formula to reference another cell which is empty, the referenced value becomes 0
- // when the flag is true. (Default setting is true.)
- ShowZeros bool
-
- /* TODO
- // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating
- // whether page layout view shall display margins. False means do not display
- // left, right, top (header), and bottom (footer) margins (even when there is
- // data in the header or footer).
- ShowWhiteSpace bool
- // WindowProtection is a SheetViewOption.
- WindowProtection bool
- */
-)
-
-// Defaults for each option are described in XML schema for CT_SheetView
-
-func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) {
- view.TopLeftCell = string(o)
-}
-
-func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) {
- *o = TopLeftCell(string(view.TopLeftCell))
-}
-
-func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) {
- view.DefaultGridColor = boolPtr(bool(o))
-}
-
-func (o *DefaultGridColor) getSheetViewOption(view *xlsxSheetView) {
- *o = DefaultGridColor(defaultTrue(view.DefaultGridColor)) // Excel default: true
-}
-
-func (o RightToLeft) setSheetViewOption(view *xlsxSheetView) {
- view.RightToLeft = bool(o) // Excel default: false
-}
-
-func (o *RightToLeft) getSheetViewOption(view *xlsxSheetView) {
- *o = RightToLeft(view.RightToLeft)
-}
-
-func (o ShowFormulas) setSheetViewOption(view *xlsxSheetView) {
- view.ShowFormulas = bool(o) // Excel default: false
-}
-
-func (o *ShowFormulas) getSheetViewOption(view *xlsxSheetView) {
- *o = ShowFormulas(view.ShowFormulas) // Excel default: false
-}
-
-func (o ShowGridLines) setSheetViewOption(view *xlsxSheetView) {
- view.ShowGridLines = boolPtr(bool(o))
-}
-
-func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) {
- *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true
-}
-
-func (o ShowZeros) setSheetViewOption(view *xlsxSheetView) {
- view.ShowZeros = boolPtr(bool(o))
-}
-
-func (o *ShowZeros) getSheetViewOption(view *xlsxSheetView) {
- *o = ShowZeros(defaultTrue(view.ShowZeros)) // Excel default: true
-}
-
-func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) {
- view.ShowRowColHeaders = boolPtr(bool(o))
-}
-
-func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) {
- *o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true
-}
-
-func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) {
- // This attribute is restricted to values ranging from 10 to 400.
- if float64(o) >= 10 && float64(o) <= 400 {
- view.ZoomScale = float64(o)
- }
-}
-
-func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) {
- *o = ZoomScale(view.ZoomScale)
-}
-
// getSheetView returns the SheetView object
-func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, error) {
- xlsx, err := f.workSheetReader(sheetName)
+func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return nil, err
}
+ if ws.SheetViews == nil {
+ ws.SheetViews = &xlsxSheetViews{
+ SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
+ }
+ }
if viewIndex < 0 {
- if viewIndex < -len(xlsx.SheetViews.SheetView) {
- return nil, fmt.Errorf("view index %d out of range", viewIndex)
+ if viewIndex < -len(ws.SheetViews.SheetView) {
+ return nil, newViewIdxError(viewIndex)
}
- viewIndex = len(xlsx.SheetViews.SheetView) + viewIndex
- } else if viewIndex >= len(xlsx.SheetViews.SheetView) {
- return nil, fmt.Errorf("view index %d out of range", viewIndex)
+ viewIndex = len(ws.SheetViews.SheetView) + viewIndex
+ } else if viewIndex >= len(ws.SheetViews.SheetView) {
+ return nil, newViewIdxError(viewIndex)
}
- return &(xlsx.SheetViews.SheetView[viewIndex]), err
+ return &(ws.SheetViews.SheetView[viewIndex]), err
}
-// SetSheetViewOptions sets sheet view options. The viewIndex may be negative
-// and if so is counted backward (-1 is the last view).
-//
-// Available options:
-//
-// DefaultGridColor(bool)
-// RightToLeft(bool)
-// ShowFormulas(bool)
-// ShowGridLines(bool)
-// ShowRowColHeaders(bool)
-// ZoomScale(float64)
-// TopLeftCell(string)
-//
-// Example:
-//
-// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false))
-//
-func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error {
- view, err := f.getSheetView(name, viewIndex)
+// setSheetView set sheet view by given options.
+func (view *xlsxSheetView) setSheetView(opts *ViewOptions) {
+ if opts.DefaultGridColor != nil {
+ view.DefaultGridColor = opts.DefaultGridColor
+ }
+ if opts.RightToLeft != nil {
+ view.RightToLeft = *opts.RightToLeft
+ }
+ if opts.ShowFormulas != nil {
+ view.ShowFormulas = *opts.ShowFormulas
+ }
+ if opts.ShowGridLines != nil {
+ view.ShowGridLines = opts.ShowGridLines
+ }
+ if opts.ShowRowColHeaders != nil {
+ view.ShowRowColHeaders = opts.ShowRowColHeaders
+ }
+ if opts.ShowRuler != nil {
+ view.ShowRuler = opts.ShowRuler
+ }
+ if opts.ShowZeros != nil {
+ view.ShowZeros = opts.ShowZeros
+ }
+ if opts.TopLeftCell != nil {
+ view.TopLeftCell = *opts.TopLeftCell
+ }
+ if opts.View != nil {
+ if inStrSlice([]string{"normal", "pageLayout", "pageBreakPreview"}, *opts.View, true) != -1 {
+ view.View = *opts.View
+ }
+ }
+ if opts.ZoomScale != nil && *opts.ZoomScale >= 10 && *opts.ZoomScale <= 400 {
+ view.ZoomScale = *opts.ZoomScale
+ }
+}
+
+// SetSheetView sets sheet view options. The viewIndex may be negative and if
+// so is counted backward (-1 is the last view).
+func (f *File) SetSheetView(sheet string, viewIndex int, opts *ViewOptions) error {
+ view, err := f.getSheetView(sheet, viewIndex)
if err != nil {
return err
}
-
- for _, opt := range opts {
- opt.setSheetViewOption(view)
+ if opts == nil {
+ return err
}
+ view.setSheetView(opts)
return nil
}
-// GetSheetViewOptions gets the value of sheet view options. The viewIndex may
-// be negative and if so is counted backward (-1 is the last view).
-//
-// Available options:
-//
-// DefaultGridColor(bool)
-// RightToLeft(bool)
-// ShowFormulas(bool)
-// ShowGridLines(bool)
-// ShowRowColHeaders(bool)
-// ZoomScale(float64)
-// TopLeftCell(string)
-//
-// Example:
-//
-// var showGridLines excelize.ShowGridLines
-// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines)
-//
-func (f *File) GetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOptionPtr) error {
- view, err := f.getSheetView(name, viewIndex)
+// GetSheetView gets the value of sheet view options. The viewIndex may be
+// negative and if so is counted backward (-1 is the last view).
+func (f *File) GetSheetView(sheet string, viewIndex int) (ViewOptions, error) {
+ opts := ViewOptions{
+ DefaultGridColor: boolPtr(true),
+ ShowFormulas: boolPtr(true),
+ ShowGridLines: boolPtr(true),
+ ShowRowColHeaders: boolPtr(true),
+ ShowRuler: boolPtr(true),
+ ShowZeros: boolPtr(true),
+ View: stringPtr("normal"),
+ ZoomScale: float64Ptr(100),
+ }
+ view, err := f.getSheetView(sheet, viewIndex)
if err != nil {
- return err
+ return opts, err
}
-
- for _, opt := range opts {
- opt.getSheetViewOption(view)
+ if view.DefaultGridColor != nil {
+ opts.DefaultGridColor = view.DefaultGridColor
}
- return nil
+ opts.RightToLeft = boolPtr(view.RightToLeft)
+ opts.ShowFormulas = boolPtr(view.ShowFormulas)
+ if view.ShowGridLines != nil {
+ opts.ShowGridLines = view.ShowGridLines
+ }
+ if view.ShowRowColHeaders != nil {
+ opts.ShowRowColHeaders = view.ShowRowColHeaders
+ }
+ if view.ShowRuler != nil {
+ opts.ShowRuler = view.ShowRuler
+ }
+ if view.ShowZeros != nil {
+ opts.ShowZeros = view.ShowZeros
+ }
+ opts.TopLeftCell = stringPtr(view.TopLeftCell)
+ if view.View != "" {
+ opts.View = stringPtr(view.View)
+ }
+ if view.ZoomScale >= 10 && view.ZoomScale <= 400 {
+ opts.ZoomScale = float64Ptr(view.ZoomScale)
+ }
+ return opts, err
}
diff --git a/sheetview_test.go b/sheetview_test.go
index d999875030..b7347775d7 100644
--- a/sheetview_test.go
+++ b/sheetview_test.go
@@ -1,184 +1,50 @@
-package excelize_test
+package excelize
import (
- "fmt"
"testing"
"github.com/stretchr/testify/assert"
-
- "github.com/360EntSecGroup-Skylar/excelize/v2"
)
-var _ = []excelize.SheetViewOption{
- excelize.DefaultGridColor(true),
- excelize.RightToLeft(false),
- excelize.ShowFormulas(false),
- excelize.ShowGridLines(true),
- excelize.ShowRowColHeaders(true),
- excelize.TopLeftCell("B2"),
- // SheetViewOptionPtr are also SheetViewOption
- new(excelize.DefaultGridColor),
- new(excelize.RightToLeft),
- new(excelize.ShowFormulas),
- new(excelize.ShowGridLines),
- new(excelize.ShowRowColHeaders),
- new(excelize.TopLeftCell),
-}
-
-var _ = []excelize.SheetViewOptionPtr{
- (*excelize.DefaultGridColor)(nil),
- (*excelize.RightToLeft)(nil),
- (*excelize.ShowFormulas)(nil),
- (*excelize.ShowGridLines)(nil),
- (*excelize.ShowRowColHeaders)(nil),
- (*excelize.TopLeftCell)(nil),
+func TestSetView(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetSheetView("Sheet1", -1, nil))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).SheetViews = nil
+ expected := ViewOptions{
+ DefaultGridColor: boolPtr(false),
+ RightToLeft: boolPtr(false),
+ ShowFormulas: boolPtr(false),
+ ShowGridLines: boolPtr(false),
+ ShowRowColHeaders: boolPtr(false),
+ ShowRuler: boolPtr(false),
+ ShowZeros: boolPtr(false),
+ TopLeftCell: stringPtr("A1"),
+ View: stringPtr("normal"),
+ ZoomScale: float64Ptr(120),
+ }
+ assert.NoError(t, f.SetSheetView("Sheet1", 0, &expected))
+ opts, err := f.GetSheetView("Sheet1", 0)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+ // Test set sheet view options with invalid view index
+ assert.EqualError(t, f.SetSheetView("Sheet1", 1, nil), "view index 1 out of range")
+ assert.EqualError(t, f.SetSheetView("Sheet1", -2, nil), "view index -2 out of range")
+ // Test set sheet view options on not exists worksheet
+ assert.EqualError(t, f.SetSheetView("SheetN", 0, nil), "sheet SheetN does not exist")
}
-func ExampleFile_SetSheetViewOptions() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- if err := f.SetSheetViewOptions(sheet, 0,
- excelize.DefaultGridColor(false),
- excelize.RightToLeft(false),
- excelize.ShowFormulas(true),
- excelize.ShowGridLines(true),
- excelize.ShowRowColHeaders(true),
- excelize.ZoomScale(80),
- excelize.TopLeftCell("C3"),
- ); err != nil {
- fmt.Println(err)
- }
-
- var zoomScale excelize.ZoomScale
- fmt.Println("Default:")
- fmt.Println("- zoomScale: 80")
-
- if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil {
- fmt.Println(err)
- }
-
- fmt.Println("Used out of range value:")
- fmt.Println("- zoomScale:", zoomScale)
-
- if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil {
- fmt.Println(err)
- }
-
- fmt.Println("Used correct value:")
- fmt.Println("- zoomScale:", zoomScale)
-
- // Output:
- // Default:
- // - zoomScale: 80
- // Used out of range value:
- // - zoomScale: 80
- // Used correct value:
- // - zoomScale: 123
-
-}
-
-func ExampleFile_GetSheetViewOptions() {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- var (
- defaultGridColor excelize.DefaultGridColor
- rightToLeft excelize.RightToLeft
- showFormulas excelize.ShowFormulas
- showGridLines excelize.ShowGridLines
- showZeros excelize.ShowZeros
- showRowColHeaders excelize.ShowRowColHeaders
- zoomScale excelize.ZoomScale
- topLeftCell excelize.TopLeftCell
- )
-
- if err := f.GetSheetViewOptions(sheet, 0,
- &defaultGridColor,
- &rightToLeft,
- &showFormulas,
- &showGridLines,
- &showZeros,
- &showRowColHeaders,
- &zoomScale,
- &topLeftCell,
- ); err != nil {
- fmt.Println(err)
- }
-
- fmt.Println("Default:")
- fmt.Println("- defaultGridColor:", defaultGridColor)
- fmt.Println("- rightToLeft:", rightToLeft)
- fmt.Println("- showFormulas:", showFormulas)
- fmt.Println("- showGridLines:", showGridLines)
- fmt.Println("- showZeros:", showZeros)
- fmt.Println("- showRowColHeaders:", showRowColHeaders)
- fmt.Println("- zoomScale:", zoomScale)
- fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`)
-
- if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil {
- fmt.Println(err)
- }
-
- if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil {
- fmt.Println(err)
- }
-
- if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil {
- fmt.Println(err)
- }
-
- if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil {
- fmt.Println(err)
- }
-
- fmt.Println("After change:")
- fmt.Println("- showGridLines:", showGridLines)
- fmt.Println("- showZeros:", showZeros)
- fmt.Println("- topLeftCell:", topLeftCell)
-
- // Output:
- // Default:
- // - defaultGridColor: true
- // - rightToLeft: false
- // - showFormulas: false
- // - showGridLines: true
- // - showZeros: true
- // - showRowColHeaders: true
- // - zoomScale: 0
- // - topLeftCell: ""
- // After change:
- // - showGridLines: false
- // - showZeros: false
- // - topLeftCell: B2
-}
-
-func TestSheetViewOptionsErrors(t *testing.T) {
- f := excelize.NewFile()
- const sheet = "Sheet1"
-
- assert.NoError(t, f.GetSheetViewOptions(sheet, 0))
- assert.NoError(t, f.GetSheetViewOptions(sheet, -1))
- assert.Error(t, f.GetSheetViewOptions(sheet, 1))
- assert.Error(t, f.GetSheetViewOptions(sheet, -2))
- assert.NoError(t, f.SetSheetViewOptions(sheet, 0))
- assert.NoError(t, f.SetSheetViewOptions(sheet, -1))
- assert.Error(t, f.SetSheetViewOptions(sheet, 1))
- assert.Error(t, f.SetSheetViewOptions(sheet, -2))
+func TestGetView(t *testing.T) {
+ f := NewFile()
+ _, err := f.getSheetView("SheetN", 0)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get sheet view options with invalid view index
+ _, err = f.GetSheetView("Sheet1", 1)
+ assert.EqualError(t, err, "view index 1 out of range")
+ _, err = f.GetSheetView("Sheet1", -2)
+ assert.EqualError(t, err, "view index -2 out of range")
+ // Test get sheet view options on not exists worksheet
+ _, err = f.GetSheetView("SheetN", 0)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
}
diff --git a/slicer.go b/slicer.go
new file mode 100644
index 0000000000..8073cf72ff
--- /dev/null
+++ b/slicer.go
@@ -0,0 +1,1050 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+// SlicerOptions represents the settings of the slicer.
+//
+// Name specifies the slicer name, should be an existing field name of the given
+// table or pivot table, this setting is required.
+//
+// Cell specifies the left top cell coordinates the position for inserting the
+// slicer, this setting is required.
+//
+// TableSheet specifies the worksheet name of the table or pivot table, this
+// setting is required.
+//
+// TableName specifies the name of the table or pivot table, this setting is
+// required.
+//
+// Caption specifies the caption of the slicer, this setting is optional.
+//
+// Macro used for set macro for the slicer, the workbook extension should be
+// XLSM or XLTM.
+//
+// Width specifies the width of the slicer, this setting is optional.
+//
+// Height specifies the height of the slicer, this setting is optional.
+//
+// DisplayHeader specifies if display header of the slicer, this setting is
+// optional, the default setting is display.
+//
+// ItemDesc specifies descending (Z-A) item sorting, this setting is optional,
+// and the default setting is false (represents ascending).
+//
+// Format specifies the format of the slicer, this setting is optional.
+type SlicerOptions struct {
+ slicerXML string
+ slicerCacheXML string
+ slicerCacheName string
+ slicerSheetName string
+ slicerSheetRID string
+ drawingXML string
+ Name string
+ Cell string
+ TableSheet string
+ TableName string
+ Caption string
+ Macro string
+ Width uint
+ Height uint
+ DisplayHeader *bool
+ ItemDesc bool
+ Format GraphicOptions
+}
+
+// AddSlicer function inserts a slicer by giving the worksheet name and slicer
+// settings.
+//
+// For example, insert a slicer on the Sheet1!E1 with field Column1 for the
+// table named Table1:
+//
+// err := f.AddSlicer("Sheet1", &excelize.SlicerOptions{
+// Name: "Column1",
+// Cell: "E1",
+// TableSheet: "Sheet1",
+// TableName: "Table1",
+// Caption: "Column1",
+// Width: 200,
+// Height: 200,
+// })
+func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error {
+ opts, err := parseSlicerOptions(opts)
+ if err != nil {
+ return err
+ }
+ table, pivotTable, colIdx, err := f.getSlicerSource(opts)
+ if err != nil {
+ return err
+ }
+ extURI, ns := ExtURISlicerListX14, NameSpaceDrawingMLA14
+ if table != nil {
+ extURI = ExtURISlicerListX15
+ ns = NameSpaceDrawingMLSlicerX15
+ }
+ slicerID, err := f.addSheetSlicer(sheet, extURI)
+ if err != nil {
+ return err
+ }
+ slicerCacheName, err := f.setSlicerCache(colIdx, opts, table, pivotTable)
+ if err != nil {
+ return err
+ }
+ slicerName := f.genSlicerName(opts.Name)
+ if err := f.addDrawingSlicer(sheet, slicerName, ns, opts); err != nil {
+ return err
+ }
+ return f.addSlicer(slicerID, xlsxSlicer{
+ Name: slicerName,
+ Cache: slicerCacheName,
+ Caption: opts.Caption,
+ ShowCaption: opts.DisplayHeader,
+ RowHeight: 251883,
+ })
+}
+
+// parseSlicerOptions provides a function to parse the format settings of the
+// slicer with default value.
+func parseSlicerOptions(opts *SlicerOptions) (*SlicerOptions, error) {
+ if opts == nil {
+ return nil, ErrParameterRequired
+ }
+ if opts.Name == "" || opts.Cell == "" || opts.TableSheet == "" || opts.TableName == "" {
+ return nil, ErrParameterInvalid
+ }
+ if opts.Width == 0 {
+ opts.Width = defaultSlicerWidth
+ }
+ if opts.Height == 0 {
+ opts.Height = defaultSlicerHeight
+ }
+ if opts.Format.PrintObject == nil {
+ opts.Format.PrintObject = boolPtr(true)
+ }
+ if opts.Format.Locked == nil {
+ opts.Format.Locked = boolPtr(false)
+ }
+ if opts.Format.ScaleX == 0 {
+ opts.Format.ScaleX = defaultDrawingScale
+ }
+ if opts.Format.ScaleY == 0 {
+ opts.Format.ScaleY = defaultDrawingScale
+ }
+ return opts, nil
+}
+
+// countSlicers provides a function to get slicer files count storage in the
+// folder xl/slicers.
+func (f *File) countSlicers() int {
+ count := 0
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicers/slicer") {
+ count++
+ }
+ return true
+ })
+ return count
+}
+
+// countSlicerCache provides a function to get slicer cache files count storage
+// in the folder xl/SlicerCaches.
+func (f *File) countSlicerCache() int {
+ count := 0
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
+ count++
+ }
+ return true
+ })
+ return count
+}
+
+// getSlicerSource returns the slicer data source table or pivot table settings
+// and the index of the given slicer fields in the table or pivot table
+// column.
+func (f *File) getSlicerSource(opts *SlicerOptions) (*Table, *PivotTableOptions, int, error) {
+ var (
+ table *Table
+ pivotTable *PivotTableOptions
+ colIdx int
+ err error
+ dataRange string
+ tables []Table
+ pivotTables []PivotTableOptions
+ )
+ if tables, err = f.GetTables(opts.TableSheet); err != nil {
+ return table, pivotTable, colIdx, err
+ }
+ for _, tbl := range tables {
+ if tbl.Name == opts.TableName {
+ table = &tbl
+ dataRange = fmt.Sprintf("%s!%s", opts.TableSheet, tbl.Range)
+ break
+ }
+ }
+ if table == nil {
+ if pivotTables, err = f.GetPivotTables(opts.TableSheet); err != nil {
+ return table, pivotTable, colIdx, err
+ }
+ for _, tbl := range pivotTables {
+ if tbl.Name == opts.TableName {
+ pivotTable = &tbl
+ dataRange = tbl.DataRange
+ break
+ }
+ }
+ if pivotTable == nil {
+ return table, pivotTable, colIdx, newNoExistTableError(opts.TableName)
+ }
+ }
+ order, _ := f.getTableFieldsOrder(&PivotTableOptions{DataRange: dataRange})
+ if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 {
+ return table, pivotTable, colIdx, newInvalidSlicerNameError(opts.Name)
+ }
+ return table, pivotTable, colIdx, err
+}
+
+// addSheetSlicer adds a new slicer and updates the namespace and relationships
+// parts of the worksheet by giving the worksheet name.
+func (f *File) addSheetSlicer(sheet, extURI string) (int, error) {
+ var (
+ slicerID = f.countSlicers() + 1
+ ws, err = f.workSheetReader(sheet)
+ decodeExtLst = new(decodeExtLst)
+ )
+ if err != nil {
+ return slicerID, err
+ }
+ if ws.ExtLst != nil {
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return slicerID, err
+ }
+ for _, ext := range decodeExtLst.Ext {
+ if ext.URI == extURI {
+ slicerList := new(decodeSlicerList)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList)
+ for _, slicer := range slicerList.Slicer {
+ if slicer.RID != "" {
+ sheetRelationshipsDrawingXML := f.getSheetRelationshipsTargetByID(sheet, slicer.RID)
+ slicerID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../slicers/slicer"), ".xml"))
+ return slicerID, err
+ }
+ }
+ }
+ }
+ }
+ sheetRelationshipsSlicerXML := "../slicers/slicer" + strconv.Itoa(slicerID) + ".xml"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
+ rID := f.addRels(sheetRels, SourceRelationshipSlicer, sheetRelationshipsSlicerXML, "")
+ f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14)
+ return slicerID, f.addSheetTableSlicer(ws, rID, extURI)
+}
+
+// addSheetTableSlicer adds a new table slicer for the worksheet by giving the
+// worksheet relationships ID and extension URI.
+func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int, extURI string) error {
+ var (
+ decodeExtLst = new(decodeExtLst)
+ err error
+ slicerListBytes, extLstBytes []byte
+ )
+ if ws.ExtLst != nil {
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ }
+ slicerListBytes, _ = xml.Marshal(&xlsxX14SlicerList{
+ Slicer: []*xlsxX14Slicer{{RID: "rId" + strconv.Itoa(rID)}},
+ })
+ ext := &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
+ URI: extURI, Content: string(slicerListBytes),
+ }
+ if extURI == ExtURISlicerListX15 {
+ ext.xmlns = []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}
+ }
+ decodeExtLst.Ext = append(decodeExtLst.Ext, ext)
+ sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
+ return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
+ inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
+ })
+ extLstBytes, err = xml.Marshal(decodeExtLst)
+ ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ return err
+}
+
+// addSlicer adds a new slicer to the workbook by giving the slicer ID and
+// settings.
+func (f *File) addSlicer(slicerID int, slicer xlsxSlicer) error {
+ slicerXML := "xl/slicers/slicer" + strconv.Itoa(slicerID) + ".xml"
+ slicers, err := f.slicerReader(slicerXML)
+ if err != nil {
+ return err
+ }
+ if err := f.addContentTypePart(slicerID, "slicer"); err != nil {
+ return err
+ }
+ slicers.Slicer = append(slicers.Slicer, slicer)
+ output, err := xml.Marshal(slicers)
+ f.saveFileList(slicerXML, output)
+ return err
+}
+
+// genSlicerName generates a unique slicer cache name by giving the slicer name.
+func (f *File) genSlicerName(name string) string {
+ var (
+ cnt int
+ slicerName string
+ names []string
+ )
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicers/slicer") {
+ slicers, err := f.slicerReader(k.(string))
+ if err != nil {
+ return true
+ }
+ for _, slicer := range slicers.Slicer {
+ names = append(names, slicer.Name)
+ }
+ }
+ if strings.Contains(k.(string), "xl/timelines/timeline") {
+ timelines, err := f.timelineReader(k.(string))
+ if err != nil {
+ return true
+ }
+ for _, timeline := range timelines.Timeline {
+ names = append(names, timeline.Name)
+ }
+ }
+ return true
+ })
+ slicerName = name
+ for {
+ tmp := slicerName
+ if cnt > 0 {
+ tmp = fmt.Sprintf("%s %d", slicerName, cnt)
+ }
+ if inStrSlice(names, tmp, true) == -1 {
+ slicerName = tmp
+ break
+ }
+ cnt++
+ }
+ return slicerName
+}
+
+// genSlicerCacheName generates a unique slicer cache name by giving the slicer name.
+func (f *File) genSlicerCacheName(name string) string {
+ var (
+ cnt int
+ definedNames []string
+ slicerCacheName string
+ )
+ for _, dn := range f.GetDefinedName() {
+ if dn.Scope == "Workbook" {
+ definedNames = append(definedNames, dn.Name)
+ }
+ }
+ for i, c := range name {
+ if unicode.IsLetter(c) {
+ slicerCacheName += string(c)
+ continue
+ }
+ if i > 0 && (unicode.IsDigit(c) || c == '.') {
+ slicerCacheName += string(c)
+ continue
+ }
+ slicerCacheName += "_"
+ }
+ slicerCacheName = fmt.Sprintf("Slicer_%s", slicerCacheName)
+ for {
+ tmp := slicerCacheName
+ if cnt > 0 {
+ tmp = fmt.Sprintf("%s%d", slicerCacheName, cnt)
+ }
+ if inStrSlice(definedNames, tmp, true) == -1 {
+ slicerCacheName = tmp
+ break
+ }
+ cnt++
+ }
+ return slicerCacheName
+}
+
+// setSlicerCache check if a slicer cache already exists or add a new slicer
+// cache by giving the column index, slicer, table options, and returns the
+// slicer cache name.
+func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) {
+ var ok bool
+ var slicerCacheName string
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
+ slicerCache, err := f.slicerCacheReader(k.(string))
+ if err != nil {
+ return true
+ }
+ if pivotTable != nil && slicerCache.PivotTables != nil {
+ for _, tbl := range slicerCache.PivotTables.PivotTable {
+ if tbl.Name == pivotTable.Name {
+ ok, slicerCacheName = true, slicerCache.Name
+ return false
+ }
+ }
+ }
+ if table == nil || slicerCache.ExtLst == nil {
+ return true
+ }
+ ext := new(xlsxExt)
+ _ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext)
+ if ext.URI == ExtURISlicerCacheDefinition {
+ tableSlicerCache := new(decodeTableSlicerCache)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache)
+ if tableSlicerCache.TableID == table.tID && tableSlicerCache.Column == colIdx+1 {
+ ok, slicerCacheName = true, slicerCache.Name
+ return false
+ }
+ }
+ }
+ return true
+ })
+ if ok {
+ return slicerCacheName, nil
+ }
+ slicerCacheName = f.genSlicerCacheName(opts.Name)
+ return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table, pivotTable)
+}
+
+// slicerReader provides a function to get the pointer to the structure
+// after deserialization of xl/slicers/slicer%d.xml.
+func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) {
+ content, ok := f.Pkg.Load(slicerXML)
+ slicer := &xlsxSlicers{
+ XMLNSXMC: SourceRelationshipCompatibility.Value,
+ XMLNSX: NameSpaceSpreadSheet.Value,
+ XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
+ }
+ if ok && content != nil {
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(slicer); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ return slicer, nil
+}
+
+// slicerCacheReader provides a function to get the pointer to the structure
+// after deserialization of xl/slicerCaches/slicerCache%d.xml.
+func (f *File) slicerCacheReader(slicerCacheXML string) (*xlsxSlicerCacheDefinition, error) {
+ content, ok := f.Pkg.Load(slicerCacheXML)
+ slicerCache := &xlsxSlicerCacheDefinition{}
+ if ok && content != nil {
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(slicerCache); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ return slicerCache, nil
+}
+
+// timelineReader provides a function to get the pointer to the structure
+// after deserialization of xl/timelines/timeline%d.xml.
+func (f *File) timelineReader(timelineXML string) (*xlsxTimelines, error) {
+ content, ok := f.Pkg.Load(timelineXML)
+ timeline := &xlsxTimelines{
+ XMLNSXMC: SourceRelationshipCompatibility.Value,
+ XMLNSX: NameSpaceSpreadSheet.Value,
+ XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
+ }
+ if ok && content != nil {
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(timeline); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ return timeline, nil
+}
+
+// addSlicerCache adds a new slicer cache by giving the slicer cache name,
+// column index, slicer, and table or pivot table options.
+func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) error {
+ var (
+ sortOrder string
+ slicerCacheBytes, tableSlicerBytes, extLstBytes []byte
+ extURI = ExtURISlicerCachesX14
+ slicerCacheID = f.countSlicerCache() + 1
+ decodeExtLst = new(decodeExtLst)
+ slicerCache = xlsxSlicerCacheDefinition{
+ XMLNSXMC: SourceRelationshipCompatibility.Value,
+ XMLNSX: NameSpaceSpreadSheet.Value,
+ XMLNSX15: NameSpaceSpreadSheetX15.Value,
+ XMLNSXR10: NameSpaceSpreadSheetXR10.Value,
+ Name: slicerCacheName,
+ SourceName: opts.Name,
+ }
+ )
+ if opts.ItemDesc {
+ sortOrder = "descending"
+ }
+ if pivotTable != nil {
+ pivotCacheID, err := f.addPivotCacheSlicer(pivotTable)
+ if err != nil {
+ return err
+ }
+ slicerCache.PivotTables = &xlsxSlicerCachePivotTables{
+ PivotTable: []xlsxSlicerCachePivotTable{
+ {TabID: f.getSheetID(opts.TableSheet), Name: pivotTable.Name},
+ },
+ }
+ slicerCache.Data = &xlsxSlicerCacheData{
+ Tabular: &xlsxTabularSlicerCache{
+ PivotCacheID: pivotCacheID,
+ SortOrder: sortOrder,
+ ShowMissing: boolPtr(false),
+ Items: &xlsxTabularSlicerCacheItems{
+ Count: 1, I: []xlsxTabularSlicerCacheItem{{S: true}},
+ },
+ },
+ }
+ }
+ if table != nil {
+ tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{
+ TableID: table.tID,
+ Column: colIdx + 1,
+ SortOrder: sortOrder,
+ })
+ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}},
+ URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes),
+ })
+ extLstBytes, _ = xml.Marshal(decodeExtLst)
+ slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ extURI = ExtURISlicerCachesX15
+ }
+ slicerCacheXML := "xl/slicerCaches/slicerCache" + strconv.Itoa(slicerCacheID) + ".xml"
+ slicerCacheBytes, _ = xml.Marshal(slicerCache)
+ f.saveFileList(slicerCacheXML, slicerCacheBytes)
+ if err := f.addContentTypePart(slicerCacheID, "slicerCache"); err != nil {
+ return err
+ }
+ if err := f.addWorkbookSlicerCache(slicerCacheID, extURI); err != nil {
+ return err
+ }
+ return f.SetDefinedName(&DefinedName{Name: slicerCacheName, RefersTo: formulaErrorNA})
+}
+
+// addPivotCacheSlicer adds a new slicer cache by giving the pivot table options
+// and returns pivot table cache ID.
+func (f *File) addPivotCacheSlicer(opts *PivotTableOptions) (int, error) {
+ var (
+ pivotCacheID int
+ pivotCacheBytes, extLstBytes []byte
+ decodeExtLst = new(decodeExtLst)
+ decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition)
+ )
+ pc, err := f.pivotCacheReader(opts.pivotCacheXML)
+ if err != nil {
+ return pivotCacheID, err
+ }
+ if pc.ExtLst != nil {
+ _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst)
+ for _, ext := range decodeExtLst.Ext {
+ if ext.URI == ExtURIPivotCacheDefinition {
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition)
+ return decodeX14PivotCacheDefinition.PivotCacheID, err
+ }
+ }
+ }
+ pivotCacheID = f.genPivotCacheDefinitionID()
+ pivotCacheBytes, _ = xml.Marshal(&xlsxX14PivotCacheDefinition{PivotCacheID: pivotCacheID})
+ ext := &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
+ URI: ExtURIPivotCacheDefinition, Content: string(pivotCacheBytes),
+ }
+ decodeExtLst.Ext = append(decodeExtLst.Ext, ext)
+ extLstBytes, _ = xml.Marshal(decodeExtLst)
+ pc.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ pivotCache, err := xml.Marshal(pc)
+ f.saveFileList(opts.pivotCacheXML, pivotCache)
+ return pivotCacheID, err
+}
+
+// addDrawingSlicer adds a slicer shape and fallback shape by giving the
+// worksheet name, slicer name, and slicer options.
+func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *SlicerOptions) error {
+ drawingID := f.countDrawings() + 1
+ drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
+ content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(sheet, drawingXML, opts.Cell, opts.Width, opts.Height, opts.Format)
+ if err != nil {
+ return err
+ }
+ graphicFrame := xlsxGraphicFrame{
+ Macro: opts.Macro,
+ NvGraphicFramePr: xlsxNvGraphicFramePr{
+ CNvPr: &xlsxCNvPr{
+ ID: cNvPrID,
+ Name: slicerName,
+ },
+ },
+ Xfrm: xlsxXfrm{Off: xlsxOff{}, Ext: aExt{}},
+ Graphic: &xlsxGraphic{
+ GraphicData: &xlsxGraphicData{
+ URI: NameSpaceDrawingMLSlicer.Value,
+ Sle: &xlsxSle{XMLNS: NameSpaceDrawingMLSlicer.Value, Name: slicerName},
+ },
+ },
+ }
+ graphic, _ := xml.Marshal(graphicFrame)
+ sp := xdrSp{
+ Macro: opts.Macro,
+ NvSpPr: &xdrNvSpPr{
+ CNvPr: &xlsxCNvPr{
+ ID: cNvPrID,
+ },
+ CNvSpPr: &xdrCNvSpPr{
+ TxBox: true,
+ },
+ },
+ SpPr: &xlsxSpPr{
+ Xfrm: xlsxXfrm{Off: xlsxOff{X: 2914650, Y: 152400}, Ext: aExt{Cx: 1828800, Cy: 2238375}},
+ SolidFill: &xlsxInnerXML{Content: ""},
+ PrstGeom: xlsxPrstGeom{
+ Prst: "rect",
+ },
+ Ln: xlsxLineProperties{W: 1, SolidFill: &xlsxInnerXML{Content: ""}},
+ },
+ TxBody: &xdrTxBody{
+ BodyPr: &aBodyPr{VertOverflow: "clip", HorzOverflow: "clip"},
+ P: []*aP{
+ {R: &aR{T: "This shape represents a table slicer. Table slicers are not supported in this version of Excel."}},
+ {R: &aR{T: "If the shape was modified in an earlier version of Excel, or if the workbook was saved in Excel 2007 or earlier, the slicer can't be used."}},
+ },
+ },
+ }
+ shape, _ := xml.Marshal(sp)
+ twoCellAnchor.ClientData = &xdrClientData{
+ FLocksWithSheet: *opts.Format.Locked,
+ FPrintsWithSheet: *opts.Format.PrintObject,
+ }
+ choice := xlsxChoice{Requires: ns.Name.Local, Content: string(graphic)}
+ if ns.Value == NameSpaceDrawingMLA14.Value { // pivot table slicer
+ choice.XMLNSA14 = ns.Value
+ }
+ if ns.Value == NameSpaceDrawingMLSlicerX15.Value { // table slicer
+ choice.XMLNSSle15 = ns.Value
+ }
+ fallback := xlsxFallback{Content: string(shape)}
+ choiceBytes, _ := xml.Marshal(choice)
+ shapeBytes, _ := xml.Marshal(fallback)
+ twoCellAnchor.AlternateContent = append(twoCellAnchor.AlternateContent, &xlsxAlternateContent{
+ XMLNSMC: SourceRelationshipCompatibility.Value,
+ Content: string(choiceBytes) + string(shapeBytes),
+ })
+ content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
+ f.Drawings.Store(drawingXML, content)
+ return f.addContentTypePart(drawingID, "drawings")
+}
+
+// addWorkbookSlicerCache add the association ID of the slicer cache in
+// workbook.xml.
+func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error {
+ var (
+ wb *xlsxWorkbook
+ err error
+ idx int
+ appendMode bool
+ decodeExtLst = new(decodeExtLst)
+ decodeSlicerCaches = new(decodeSlicerCaches)
+ x14SlicerCaches = new(xlsxX14SlicerCaches)
+ x15SlicerCaches = new(xlsxX15SlicerCaches)
+ ext *xlsxExt
+ slicerCacheBytes, slicerCachesBytes, extLstBytes []byte
+ )
+ if wb, err = f.workbookReader(); err != nil {
+ return err
+ }
+ rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipSlicerCache, fmt.Sprintf("/xl/slicerCaches/slicerCache%d.xml", slicerCacheID), "")
+ if wb.ExtLst != nil { // append mode ext
+ if err = f.xmlNewDecoder(strings.NewReader("" + wb.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ for idx, ext = range decodeExtLst.Ext {
+ if ext.URI == URI {
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches)
+ slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)}
+ slicerCacheBytes, _ = xml.Marshal(slicerCache)
+ if URI == ExtURISlicerCachesX14 { // pivot table slicer
+ x14SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes)
+ x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
+ slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches)
+ }
+ if URI == ExtURISlicerCachesX15 { // table slicer
+ x15SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes)
+ x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
+ slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches)
+ }
+ decodeExtLst.Ext[idx].Content = string(slicerCachesBytes)
+ appendMode = true
+ }
+ }
+ }
+ if !appendMode {
+ slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)}
+ slicerCacheBytes, _ = xml.Marshal(slicerCache)
+ if URI == ExtURISlicerCachesX14 {
+ x14SlicerCaches.Content = string(slicerCacheBytes)
+ x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
+ slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches)
+ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}},
+ URI: ExtURISlicerCachesX14, Content: string(slicerCachesBytes),
+ })
+ }
+ if URI == ExtURISlicerCachesX15 {
+ x15SlicerCaches.Content = string(slicerCacheBytes)
+ x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value
+ slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches)
+ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
+ xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}},
+ URI: ExtURISlicerCachesX15, Content: string(slicerCachesBytes),
+ })
+ }
+ }
+ sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
+ return inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[i].URI, false) <
+ inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[j].URI, false)
+ })
+ extLstBytes, err = xml.Marshal(decodeExtLst)
+ wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ return err
+}
+
+// GetSlicers provides the method to get all slicers in a worksheet by a given
+// worksheet name. Note that, this function does not support getting the height,
+// width, and graphic options of the slicer shape currently.
+func (f *File) GetSlicers(sheet string) ([]SlicerOptions, error) {
+ var (
+ slicers []SlicerOptions
+ ws, err = f.workSheetReader(sheet)
+ decodeExtLst = new(decodeExtLst)
+ )
+ if err != nil {
+ return slicers, err
+ }
+ if ws.ExtLst == nil {
+ return slicers, err
+ }
+ target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
+ drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return slicers, err
+ }
+ for _, ext := range decodeExtLst.Ext {
+ if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 {
+ slicerList := new(decodeSlicerList)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(&slicerList)
+ for _, slicer := range slicerList.Slicer {
+ if slicer.RID != "" {
+ opts, err := f.getSlicers(sheet, slicer.RID, drawingXML)
+ if err != nil {
+ return slicers, err
+ }
+ slicers = append(slicers, opts...)
+ }
+ }
+ }
+ }
+ return slicers, err
+}
+
+// getSlicerCache provides a function to get a slicer cache by given slicer
+// cache name and slicer options.
+func (f *File) getSlicerCache(slicerCacheName string, opt *SlicerOptions) *xlsxSlicerCacheDefinition {
+ var (
+ err error
+ slicerCache *xlsxSlicerCacheDefinition
+ )
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
+ slicerCache, err = f.slicerCacheReader(k.(string))
+ if err != nil {
+ return true
+ }
+ if slicerCache.Name == slicerCacheName {
+ opt.slicerCacheXML = k.(string)
+ return false
+ }
+ }
+ return true
+ })
+ return slicerCache
+}
+
+// getSlicers provides a function to get slicers options by given worksheet
+// name, slicer part relationship ID and drawing part path.
+func (f *File) getSlicers(sheet, rID, drawingXML string) ([]SlicerOptions, error) {
+ var (
+ opts []SlicerOptions
+ sheetRelationshipsSlicerXML = f.getSheetRelationshipsTargetByID(sheet, rID)
+ slicerXML = strings.ReplaceAll(sheetRelationshipsSlicerXML, "..", "xl")
+ slicers, err = f.slicerReader(slicerXML)
+ )
+ if err != nil {
+ return opts, err
+ }
+ for _, slicer := range slicers.Slicer {
+ opt := SlicerOptions{
+ slicerXML: slicerXML,
+ slicerCacheName: slicer.Cache,
+ slicerSheetName: sheet,
+ slicerSheetRID: rID,
+ drawingXML: drawingXML,
+ Name: slicer.Name,
+ Caption: slicer.Caption,
+ DisplayHeader: slicer.ShowCaption,
+ }
+ slicerCache := f.getSlicerCache(slicer.Cache, &opt)
+ if slicerCache == nil {
+ return opts, err
+ }
+ if err := f.extractTableSlicer(slicerCache, &opt); err != nil {
+ return opts, err
+ }
+ if err := f.extractPivotTableSlicer(slicerCache, &opt); err != nil {
+ return opts, err
+ }
+ if err = f.extractSlicerCellAnchor(drawingXML, &opt); err != nil {
+ return opts, err
+ }
+ opts = append(opts, opt)
+ }
+ return opts, err
+}
+
+// extractTableSlicer extract table slicer options from slicer cache.
+func (f *File) extractTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error {
+ if slicerCache.ExtLst != nil {
+ tables, err := f.getTables()
+ if err != nil {
+ return err
+ }
+ ext := new(xlsxExt)
+ _ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext)
+ if ext.URI == ExtURISlicerCacheDefinition {
+ tableSlicerCache := new(decodeTableSlicerCache)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache)
+ opt.ItemDesc = tableSlicerCache.SortOrder == "descending"
+ for sheetName, sheetTables := range tables {
+ for _, table := range sheetTables {
+ if tableSlicerCache.TableID == table.tID {
+ opt.TableName = table.Name
+ opt.TableSheet = sheetName
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// extractPivotTableSlicer extract pivot table slicer options from slicer cache.
+func (f *File) extractPivotTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error {
+ pivotTables, err := f.getPivotTables()
+ if err != nil {
+ return err
+ }
+ if slicerCache.PivotTables != nil {
+ for _, pt := range slicerCache.PivotTables.PivotTable {
+ opt.TableName = pt.Name
+ for sheetName, sheetPivotTables := range pivotTables {
+ for _, pivotTable := range sheetPivotTables {
+ if opt.TableName == pivotTable.Name {
+ opt.TableSheet = sheetName
+ }
+ }
+ }
+ }
+ if slicerCache.Data != nil && slicerCache.Data.Tabular != nil {
+ opt.ItemDesc = slicerCache.Data.Tabular.SortOrder == "descending"
+ }
+ }
+ return nil
+}
+
+// extractSlicerCellAnchor extract slicer drawing object from two cell anchor by
+// giving drawing part path and slicer options.
+func (f *File) extractSlicerCellAnchor(drawingXML string, opt *SlicerOptions) error {
+ var (
+ wsDr *xlsxWsDr
+ deCellAnchor = new(decodeCellAnchor)
+ deChoice = new(decodeChoice)
+ err error
+ )
+ if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
+ return err
+ }
+ wsDr.mu.Lock()
+ defer wsDr.mu.Unlock()
+ cond := func(ac *xlsxAlternateContent) bool {
+ if ac != nil {
+ _ = f.xmlNewDecoder(strings.NewReader(ac.Content)).Decode(&deChoice)
+ if deChoice.XMLNSSle15 == NameSpaceDrawingMLSlicerX15.Value || deChoice.XMLNSA14 == NameSpaceDrawingMLA14.Value {
+ if deChoice.GraphicFrame.NvGraphicFramePr.CNvPr.Name == opt.Name {
+ return true
+ }
+ }
+ }
+ return false
+ }
+ for _, anchor := range wsDr.TwoCellAnchor {
+ for _, ac := range anchor.AlternateContent {
+ if cond(ac) {
+ if anchor.From != nil {
+ opt.Macro = deChoice.GraphicFrame.Macro
+ if opt.Cell, err = CoordinatesToCellName(anchor.From.Col+1, anchor.From.Row+1); err != nil {
+ return err
+ }
+ }
+ return err
+ }
+ }
+ _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor)
+ for _, ac := range deCellAnchor.AlternateContent {
+ if cond(ac) {
+ if deCellAnchor.From != nil {
+ opt.Macro = deChoice.GraphicFrame.Macro
+ if opt.Cell, err = CoordinatesToCellName(deCellAnchor.From.Col+1, deCellAnchor.From.Row+1); err != nil {
+ return err
+ }
+ }
+ return err
+ }
+ }
+ }
+ return err
+}
+
+// getAllSlicers provides a function to get all slicers in a workbook.
+func (f *File) getAllSlicers() (map[string][]SlicerOptions, error) {
+ slicers := map[string][]SlicerOptions{}
+ for _, sheetName := range f.GetSheetList() {
+ sles, err := f.GetSlicers(sheetName)
+ e := ErrSheetNotExist{sheetName}
+ if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
+ return slicers, err
+ }
+ slicers[sheetName] = append(slicers[sheetName], sles...)
+ }
+ return slicers, nil
+}
+
+// DeleteSlicer provides the method to delete a slicer by a given slicer name.
+func (f *File) DeleteSlicer(name string) error {
+ sles, err := f.getAllSlicers()
+ if err != nil {
+ return err
+ }
+ for _, slicers := range sles {
+ for _, slicer := range slicers {
+ if slicer.Name != name {
+ continue
+ }
+ _ = f.deleteSlicer(slicer)
+ return f.deleteSlicerCache(sles, slicer)
+ }
+ }
+ return newNoExistSlicerError(name)
+}
+
+// getSlicers provides a function to delete slicer by given slicer options.
+func (f *File) deleteSlicer(opts SlicerOptions) error {
+ slicers, err := f.slicerReader(opts.slicerXML)
+ if err != nil {
+ return err
+ }
+ for i := 0; i < len(slicers.Slicer); i++ {
+ if slicers.Slicer[i].Name == opts.Name {
+ slicers.Slicer = append(slicers.Slicer[:i], slicers.Slicer[i+1:]...)
+ i--
+ }
+ }
+ if len(slicers.Slicer) == 0 {
+ var (
+ extLstBytes []byte
+ ws, err = f.workSheetReader(opts.slicerSheetName)
+ decodeExtLst = new(decodeExtLst)
+ )
+ if err != nil {
+ return err
+ }
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ for i, ext := range decodeExtLst.Ext {
+ if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 {
+ slicerList := new(decodeSlicerList)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList)
+ for _, slicer := range slicerList.Slicer {
+ if slicer.RID == opts.slicerSheetRID {
+ decodeExtLst.Ext = append(decodeExtLst.Ext[:i], decodeExtLst.Ext[i+1:]...)
+ extLstBytes, err = xml.Marshal(decodeExtLst)
+ ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ f.Pkg.Delete(opts.slicerXML)
+ _ = f.removeContentTypesPart(ContentTypeSlicer, "/"+opts.slicerXML)
+ f.deleteSheetRelationships(opts.slicerSheetName, opts.slicerSheetRID)
+ return err
+ }
+ }
+ }
+ }
+ }
+ output, err := xml.Marshal(slicers)
+ f.saveFileList(opts.slicerXML, output)
+ return err
+}
+
+// deleteSlicerCache provides a function to delete the slicer cache by giving
+// slicer options if the slicer cache is no longer used.
+func (f *File) deleteSlicerCache(sles map[string][]SlicerOptions, opts SlicerOptions) error {
+ for _, slicers := range sles {
+ for _, slicer := range slicers {
+ if slicer.Name != opts.Name && slicer.slicerCacheName == opts.slicerCacheName {
+ return nil
+ }
+ }
+ }
+ if err := f.DeleteDefinedName(&DefinedName{Name: opts.slicerCacheName}); err != nil {
+ return err
+ }
+ f.Pkg.Delete(opts.slicerCacheXML)
+ return f.removeContentTypesPart(ContentTypeSlicerCache, "/"+opts.slicerCacheXML)
+}
diff --git a/slicer_test.go b/slicer_test.go
new file mode 100644
index 0000000000..df9e6678a9
--- /dev/null
+++ b/slicer_test.go
@@ -0,0 +1,621 @@
+package excelize
+
+import (
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSlicer(t *testing.T) {
+ f := NewFile()
+ disable, colName := false, "_!@#$%^&*()-+=|\\/<>"
+ assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName))
+ // Create table in a worksheet
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: "Column1",
+ }))
+ assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "I1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: "Column1",
+ }))
+ assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: colName,
+ Cell: "M1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: colName,
+ Macro: "Button1_Click",
+ Width: 200,
+ Height: 200,
+ DisplayHeader: &disable,
+ ItemDesc: true,
+ }))
+ // Test get table slicers
+ slicers, err := f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Column1", slicers[0].Name)
+ assert.Equal(t, "E1", slicers[0].Cell)
+ assert.Equal(t, "Sheet1", slicers[0].TableSheet)
+ assert.Equal(t, "Table1", slicers[0].TableName)
+ assert.Equal(t, "Column1", slicers[0].Caption)
+ assert.Equal(t, "Column1 1", slicers[1].Name)
+ assert.Equal(t, "I1", slicers[1].Cell)
+ assert.Equal(t, "Sheet1", slicers[1].TableSheet)
+ assert.Equal(t, "Table1", slicers[1].TableName)
+ assert.Equal(t, "Column1", slicers[1].Caption)
+ assert.Equal(t, colName, slicers[2].Name)
+ assert.Equal(t, "M1", slicers[2].Cell)
+ assert.Equal(t, "Sheet1", slicers[2].TableSheet)
+ assert.Equal(t, "Table1", slicers[2].TableName)
+ assert.Equal(t, colName, slicers[2].Caption)
+ assert.Equal(t, "Button1_Click", slicers[2].Macro)
+ assert.False(t, *slicers[2].DisplayHeader)
+ assert.True(t, slicers[2].ItemDesc)
+ // Test create two pivot tables in a new worksheet
+ _, err = f.NewSheet("Sheet2")
+ assert.NoError(t, err)
+ // Create some data in a sheet
+ month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
+ year := []int{2017, 2018, 2019}
+ types := []string{"Meat", "Dairy", "Beverages", "Produce"}
+ region := []string{"East", "West", "North", "South"}
+ assert.NoError(t, f.SetSheetRow("Sheet2", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
+ for row := 2; row < 32; row++ {
+ assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
+ assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("B%d", row), year[rand.Intn(3)]))
+ assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
+ assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("D%d", row), rand.Intn(5000)))
+ assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
+ }
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet2!A1:E31",
+ PivotTableRange: "Sheet2!G2:M34",
+ Name: "PivotTable1",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Filter: []PivotTableField{{Data: "Region"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ ShowError: true,
+ PivotTableStyleName: "PivotStyleLight16",
+ }))
+ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
+ DataRange: "Sheet2!A1:E31",
+ PivotTableRange: "Sheet2!U34:O2",
+ Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
+ Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
+ Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
+ RowGrandTotals: true,
+ ColGrandTotals: true,
+ ShowDrill: true,
+ ShowRowHeaders: true,
+ ShowColHeaders: true,
+ ShowLastColumn: true,
+ }))
+ // Test add a pivot table slicer
+ assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Month",
+ Cell: "G42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable1",
+ Caption: "Month",
+ }))
+ // Test add a pivot table slicer with duplicate field name
+ assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Month",
+ Cell: "K42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable1",
+ Caption: "Month",
+ }))
+ // Test add a pivot table slicer for another pivot table in a worksheet
+ assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Region",
+ Cell: "O42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable2",
+ Caption: "Region",
+ ItemDesc: true,
+ }))
+ // Test get pivot table slicers
+ slicers, err = f.GetSlicers("Sheet2")
+ assert.NoError(t, err)
+ assert.Equal(t, "Month", slicers[0].Name)
+ assert.Equal(t, "G42", slicers[0].Cell)
+ assert.Equal(t, "Sheet2", slicers[0].TableSheet)
+ assert.Equal(t, "PivotTable1", slicers[0].TableName)
+ assert.Equal(t, "Month", slicers[0].Caption)
+ assert.Equal(t, "Month 1", slicers[1].Name)
+ assert.Equal(t, "K42", slicers[1].Cell)
+ assert.Equal(t, "Sheet2", slicers[1].TableSheet)
+ assert.Equal(t, "PivotTable1", slicers[1].TableName)
+ assert.Equal(t, "Month", slicers[1].Caption)
+ assert.Equal(t, "Region", slicers[2].Name)
+ assert.Equal(t, "O42", slicers[2].Cell)
+ assert.Equal(t, "Sheet2", slicers[2].TableSheet)
+ assert.Equal(t, "PivotTable2", slicers[2].TableName)
+ assert.Equal(t, "Region", slicers[2].Caption)
+ assert.True(t, slicers[2].ItemDesc)
+ // Test add a table slicer with empty slicer options
+ assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil))
+ // Test add a table slicer with invalid slicer options
+ for _, opts := range []*SlicerOptions{
+ {Cell: "Q1", TableSheet: "Sheet1", TableName: "Table1"},
+ {Name: "Column", Cell: "Q1", TableSheet: "Sheet1"},
+ {Name: "Column", TableSheet: "Sheet1", TableName: "Table1"},
+ } {
+ assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts))
+ }
+ // Test add a table slicer with not exist worksheet
+ assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{
+ Name: "Column2",
+ Cell: "Q1",
+ TableSheet: "SheetN",
+ TableName: "Table1",
+ }), "sheet SheetN does not exist")
+ // Test add a table slicer with not exist table name
+ assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column2",
+ Cell: "Q1",
+ TableSheet: "Sheet1",
+ TableName: "Table2",
+ }))
+ // Test add a table slicer with invalid slicer name
+ assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column6",
+ Cell: "Q1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ }))
+ workbookPath := filepath.Join("test", "TestAddSlicer.xlsm")
+ file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddVBAProject(file))
+ assert.NoError(t, f.SaveAs(workbookPath))
+ assert.NoError(t, f.Close())
+
+ // Test add a pivot table slicer with unsupported charset pivot table
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/pivotTables/pivotTable2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Month",
+ Cell: "G42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable1",
+ Caption: "Month",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test open a workbook and get already exist slicers
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ slicers, err = f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Column1", slicers[0].Name)
+ assert.Equal(t, "E1", slicers[0].Cell)
+ assert.Equal(t, "Sheet1", slicers[0].TableSheet)
+ assert.Equal(t, "Table1", slicers[0].TableName)
+ assert.Equal(t, "Column1", slicers[0].Caption)
+ assert.Equal(t, "Column1 1", slicers[1].Name)
+ assert.Equal(t, "I1", slicers[1].Cell)
+ assert.Equal(t, "Sheet1", slicers[1].TableSheet)
+ assert.Equal(t, "Table1", slicers[1].TableName)
+ assert.Equal(t, "Column1", slicers[1].Caption)
+ assert.Equal(t, colName, slicers[2].Name)
+ assert.Equal(t, "M1", slicers[2].Cell)
+ assert.Equal(t, "Sheet1", slicers[2].TableSheet)
+ assert.Equal(t, "Table1", slicers[2].TableName)
+ assert.Equal(t, colName, slicers[2].Caption)
+ assert.Equal(t, "Button1_Click", slicers[2].Macro)
+ assert.False(t, *slicers[2].DisplayHeader)
+ assert.True(t, slicers[2].ItemDesc)
+ slicers, err = f.GetSlicers("Sheet2")
+ assert.NoError(t, err)
+ assert.Equal(t, "Month", slicers[0].Name)
+ assert.Equal(t, "G42", slicers[0].Cell)
+ assert.Equal(t, "Sheet2", slicers[0].TableSheet)
+ assert.Equal(t, "PivotTable1", slicers[0].TableName)
+ assert.Equal(t, "Month", slicers[0].Caption)
+ assert.Equal(t, "Month 1", slicers[1].Name)
+ assert.Equal(t, "K42", slicers[1].Cell)
+ assert.Equal(t, "Sheet2", slicers[1].TableSheet)
+ assert.Equal(t, "PivotTable1", slicers[1].TableName)
+ assert.Equal(t, "Month", slicers[1].Caption)
+ assert.Equal(t, "Region", slicers[2].Name)
+ assert.Equal(t, "O42", slicers[2].Cell)
+ assert.Equal(t, "Sheet2", slicers[2].TableSheet)
+ assert.Equal(t, "PivotTable2", slicers[2].TableName)
+ assert.Equal(t, "Region", slicers[2].Caption)
+ assert.True(t, slicers[2].ItemDesc)
+
+ // Test add a pivot table slicer with workbook which contains timeline
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/timelines/timeline1.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX15.Value)))
+ assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Month",
+ Cell: "G42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable1",
+ Caption: "Month",
+ }))
+ assert.NoError(t, f.Close())
+
+ // Test add a pivot table slicer with unsupported charset timeline
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/timelines/timeline1.xml", MacintoshCyrillicCharset)
+ assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
+ Name: "Month",
+ Cell: "G42",
+ TableSheet: "Sheet2",
+ TableName: "PivotTable1",
+ Caption: "Month",
+ }))
+ assert.NoError(t, f.Close())
+
+ // Test add a table slicer with invalid worksheet extension list
+ f = NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"}
+ assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ }))
+ assert.NoError(t, f.Close())
+
+ // Test add a table slicer with unsupported charset slicer
+ f = NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableName: "Table1",
+ TableSheet: "Sheet1",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test add a table slicer with read workbook error
+ f = NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"}
+ assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableName: "Table1",
+ TableSheet: "Sheet1",
+ }))
+ assert.NoError(t, f.Close())
+
+ // Test add a table slicer with unsupported charset content types
+ f = NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableName: "Table1",
+ TableSheet: "Sheet1",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addSlicer(0, xlsxSlicer{}), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ // Create table in a worksheet
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: "Column1",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ // Test get sheet slicers without slicer
+ slicers, err = f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ assert.Empty(t, slicers)
+ // Test get sheet slicers with not exist worksheet name
+ _, err = f.GetSlicers("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ // Test get sheet slicers with unsupported charset slicer cache
+ f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ // Test get sheet slicers with unsupported charset slicer
+ f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetSlicers("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get sheet slicers with invalid worksheet extension list
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
+ _, err = f.GetSlicers("Sheet1")
+ assert.Error(t, err)
+ assert.NoError(t, f.Close())
+
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ // Test get sheet slicers without slicer cache
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
+ f.Pkg.Delete(k.(string))
+ }
+ return true
+ })
+ slicers, err = f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ assert.Empty(t, slicers)
+ assert.NoError(t, f.Close())
+ // Test open a workbook and get sheet slicer with invalid cell reference in the drawing part
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`-1-1
`, NameSpaceDrawingMLSpreadSheet.Value, NameSpaceDrawingMLSlicerX15.Value)))
+ _, err = f.GetSlicers("Sheet1")
+ assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
+ // Test get sheet slicer without slicer shape in the drawing part
+ f.Drawings.Delete("xl/drawings/drawing1.xml")
+ f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(``, NameSpaceDrawingMLSpreadSheet.Value)))
+ _, err = f.GetSlicers("Sheet1")
+ assert.NoError(t, err)
+ f.Drawings.Delete("xl/drawings/drawing1.xml")
+ // Test get sheet slicers with unsupported charset drawing part
+ f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetSlicers("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get sheet slicers with unsupported charset table
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetSlicers("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get sheet slicers with unsupported charset pivot table
+ f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetSlicers("Sheet2")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+
+ // Test create a workbook and get sheet slicer with invalid cell reference in the drawing part
+ f = NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: "Column1",
+ }))
+ drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
+ assert.True(t, ok)
+ drawing.(*xlsxWsDr).TwoCellAnchor[0].From = &xlsxFrom{Col: -1, Row: -1}
+ _, err = f.GetSlicers("Sheet1")
+ assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
+ assert.NoError(t, f.Close())
+
+ // Test open a workbook and delete slicers
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ for _, name := range []string{colName, "Column1 1", "Column1"} {
+ assert.NoError(t, f.DeleteSlicer(name))
+ }
+ for _, name := range []string{"Month", "Month 1", "Region"} {
+ assert.NoError(t, f.DeleteSlicer(name))
+ }
+ // Test delete slicer with no exits slicer name
+ assert.Equal(t, newNoExistSlicerError("x"), f.DeleteSlicer("x"))
+ assert.NoError(t, f.Close())
+
+ // Test open a workbook and delete sheet slicer with unsupported charset slicer cache
+ f, err = OpenFile(workbookPath)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeleteSlicer("Column1"), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestAddSheetSlicer(t *testing.T) {
+ f := NewFile()
+ // Test add sheet slicer with not exist worksheet name
+ _, err := f.addSheetSlicer("SheetN", ExtURISlicerListX15)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ assert.NoError(t, f.Close())
+}
+
+func TestAddSheetTableSlicer(t *testing.T) {
+ f := NewFile()
+ // Test add sheet table slicer with invalid worksheet extension
+ assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0, ExtURISlicerListX15))
+ // Test add sheet table slicer with existing worksheet extension
+ assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1, ExtURISlicerListX15))
+ assert.NoError(t, f.Close())
+}
+
+func TestSetSlicerCache(t *testing.T) {
+ f := NewFile()
+ f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
+ _, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+
+ f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
+ _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
+ _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
+ _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+
+ f = NewFile()
+ f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value)))
+ _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, f.Close())
+}
+
+func TestDeleteSlicer(t *testing.T) {
+ f, slicerXML := NewFile(), "xl/slicers/slicer1.xml"
+ assert.NoError(t, f.AddTable("Sheet1", &Table{
+ Name: "Table1",
+ Range: "A1:D5",
+ }))
+ assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
+ Name: "Column1",
+ Cell: "E1",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ Caption: "Column1",
+ }))
+ // Test delete sheet slicers with invalid worksheet extension list
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
+ assert.Error(t, f.deleteSlicer(SlicerOptions{
+ slicerXML: slicerXML,
+ slicerSheetName: "Sheet1",
+ Name: "Column1",
+ }))
+ // Test delete slicer with unsupported charset worksheet
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteSlicer(SlicerOptions{
+ slicerXML: slicerXML,
+ slicerSheetName: "Sheet1",
+ Name: "Column1",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ // Test delete slicer with unsupported charset slicer
+ f.Pkg.Store(slicerXML, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteSlicer(SlicerOptions{slicerXML: slicerXML}), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestDeleteSlicerCache(t *testing.T) {
+ f := NewFile()
+ // Test delete slicer cache with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.deleteSlicerCache(nil, SlicerOptions{}), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestAddSlicerCache(t *testing.T) {
+ f := NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}, nil), "XML syntax error on line 1: invalid UTF-8")
+ // Test add a pivot table cache slicer with unsupported charset
+ pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
+ f.Pkg.Store(pivotCacheXML, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, nil,
+ &PivotTableOptions{pivotCacheXML: pivotCacheXML}), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestAddDrawingSlicer(t *testing.T) {
+ f := NewFile()
+ // Test add a drawing slicer with not exist worksheet
+ assert.EqualError(t, f.addDrawingSlicer("SheetN", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
+ Name: "Column2",
+ Cell: "Q1",
+ TableSheet: "SheetN",
+ TableName: "Table1",
+ }), "sheet SheetN does not exist")
+ // Test add a drawing slicer with invalid cell reference
+ assert.EqualError(t, f.addDrawingSlicer("Sheet1", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
+ Name: "Column2",
+ Cell: "A",
+ TableSheet: "Sheet1",
+ TableName: "Table1",
+ }), "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"")
+ assert.NoError(t, f.Close())
+}
+
+func TestAddWorkbookSlicerCache(t *testing.T) {
+ // Test add a workbook slicer cache with unsupported charset workbook
+ f := NewFile()
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
+
+func TestGenSlicerCacheName(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "Slicer_Column_1", RefersTo: formulaErrorNA}))
+ assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1"))
+ assert.NoError(t, f.Close())
+}
+
+func TestAddPivotCacheSlicer(t *testing.T) {
+ f := NewFile()
+ pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
+ // Test add a pivot table cache slicer with existing extension list
+ f.Pkg.Store(pivotCacheXML, []byte(fmt.Sprintf(``, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition)))
+ _, err := f.addPivotCacheSlicer(&PivotTableOptions{
+ pivotCacheXML: pivotCacheXML,
+ })
+ assert.NoError(t, err)
+}
diff --git a/sparkline.go b/sparkline.go
index 4004878e4e..4597317096 100644
--- a/sparkline.go
+++ b/sparkline.go
@@ -1,486 +1,468 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"encoding/xml"
- "errors"
"io"
+ "sort"
"strings"
)
-// addSparklineGroupByStyle provides a function to create x14:sparklineGroups
-// element by given sparkline style ID.
-func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup {
- groups := []*xlsxX14SparklineGroup{
- {
- ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 5},
- ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 4},
- ColorLow: &xlsxTabColor{Theme: 4},
+// getSparklineGroupPresets returns the preset list of sparkline group to create
+// x14:sparklineGroups element.
+func getSparklineGroupPresets() []*xlsxX14SparklineGroup {
+ return []*xlsxX14SparklineGroup{
+ {
+ ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(5)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(4)},
+ ColorLow: &xlsxColor{Theme: intPtr(4)},
}, // 0
{
- ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 5},
- ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 4},
- ColorLow: &xlsxTabColor{Theme: 4},
+ ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(5)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(4)},
+ ColorLow: &xlsxColor{Theme: intPtr(4)},
}, // 1
{
- ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 6},
- ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 5},
- ColorLow: &xlsxTabColor{Theme: 5},
+ ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(6)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(5)},
+ ColorLow: &xlsxColor{Theme: intPtr(5)},
}, // 2
{
- ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 7},
- ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 6},
- ColorLow: &xlsxTabColor{Theme: 6},
+ ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(7)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(6)},
+ ColorLow: &xlsxColor{Theme: intPtr(6)},
}, // 3
{
- ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 8},
- ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 7},
- ColorLow: &xlsxTabColor{Theme: 7},
+ ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(8)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(7)},
+ ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 4
{
- ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 9},
- ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 8},
- ColorLow: &xlsxTabColor{Theme: 8},
+ ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(9)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(8)},
+ ColorLow: &xlsxColor{Theme: intPtr(8)},
}, // 5
{
- ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 4},
- ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
- ColorFirst: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
- ColorLast: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
- ColorHigh: &xlsxTabColor{Theme: 9},
- ColorLow: &xlsxTabColor{Theme: 9},
+ ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(4)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
+ ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
+ ColorLast: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
+ ColorHigh: &xlsxColor{Theme: intPtr(9)},
+ ColorLow: &xlsxColor{Theme: intPtr(9)},
}, // 6
{
- ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 5},
- ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 5},
- ColorLow: &xlsxTabColor{Theme: 5},
+ ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(5)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(5)},
+ ColorLow: &xlsxColor{Theme: intPtr(5)},
}, // 7
{
- ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 6},
- ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(6)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
}, // 8
{
- ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 7},
- ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(7)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
}, // 9
{
- ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 8},
- ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(8)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
}, // 10
{
- ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 9},
- ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(9)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
}, // 11
{
- ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorNegative: &xlsxTabColor{Theme: 4},
- ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorNegative: &xlsxColor{Theme: intPtr(4)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
}, // 12
{
- ColorSeries: &xlsxTabColor{Theme: 4},
- ColorNegative: &xlsxTabColor{Theme: 5},
- ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(4)},
+ ColorNegative: &xlsxColor{Theme: intPtr(5)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
}, // 13
{
- ColorSeries: &xlsxTabColor{Theme: 5},
- ColorNegative: &xlsxTabColor{Theme: 6},
- ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(5)},
+ ColorNegative: &xlsxColor{Theme: intPtr(6)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
}, // 14
{
- ColorSeries: &xlsxTabColor{Theme: 6},
- ColorNegative: &xlsxTabColor{Theme: 7},
- ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(6)},
+ ColorNegative: &xlsxColor{Theme: intPtr(7)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
}, // 15
{
- ColorSeries: &xlsxTabColor{Theme: 7},
- ColorNegative: &xlsxTabColor{Theme: 8},
- ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(7)},
+ ColorNegative: &xlsxColor{Theme: intPtr(8)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
}, // 16
{
- ColorSeries: &xlsxTabColor{Theme: 8},
- ColorNegative: &xlsxTabColor{Theme: 9},
- ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(8)},
+ ColorNegative: &xlsxColor{Theme: intPtr(9)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
}, // 17
{
- ColorSeries: &xlsxTabColor{Theme: 9},
- ColorNegative: &xlsxTabColor{Theme: 4},
- ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(9)},
+ ColorNegative: &xlsxColor{Theme: intPtr(4)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
}, // 18
{
- ColorSeries: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 4, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
}, // 19
{
- ColorSeries: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 5, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
}, // 20
{
- ColorSeries: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 6, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
}, // 21
{
- ColorSeries: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 7, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
}, // 22
{
- ColorSeries: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 8, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
}, // 23
{
- ColorSeries: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262},
- ColorMarkers: &xlsxTabColor{Theme: 9, Tint: 0.79998168889431442},
- ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
- ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262},
+ ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
+ ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: 0.79998168889431442},
+ ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
+ ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
}, // 24
{
- ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.499984740745262},
- ColorNegative: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
- ColorMarkers: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.499984740745262},
+ ColorNegative: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
+ ColorMarkers: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
}, // 25
{
- ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.34998626667073579},
- ColorNegative: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
- ColorMarkers: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
- ColorFirst: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
- ColorLast: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
- ColorHigh: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
- ColorLow: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893},
+ ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.34998626667073579},
+ ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
+ ColorMarkers: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
+ ColorFirst: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
+ ColorLast: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
+ ColorHigh: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
+ ColorLow: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
}, // 26
{
- ColorSeries: &xlsxTabColor{RGB: "FF323232"},
- ColorNegative: &xlsxTabColor{RGB: "FFD00000"},
- ColorMarkers: &xlsxTabColor{RGB: "FFD00000"},
- ColorFirst: &xlsxTabColor{RGB: "FFD00000"},
- ColorLast: &xlsxTabColor{RGB: "FFD00000"},
- ColorHigh: &xlsxTabColor{RGB: "FFD00000"},
- ColorLow: &xlsxTabColor{RGB: "FFD00000"},
+ ColorSeries: &xlsxColor{RGB: "FF323232"},
+ ColorNegative: &xlsxColor{RGB: "FFD00000"},
+ ColorMarkers: &xlsxColor{RGB: "FFD00000"},
+ ColorFirst: &xlsxColor{RGB: "FFD00000"},
+ ColorLast: &xlsxColor{RGB: "FFD00000"},
+ ColorHigh: &xlsxColor{RGB: "FFD00000"},
+ ColorLow: &xlsxColor{RGB: "FFD00000"},
}, // 27
{
- ColorSeries: &xlsxTabColor{RGB: "FF000000"},
- ColorNegative: &xlsxTabColor{RGB: "FF0070C0"},
- ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"},
- ColorFirst: &xlsxTabColor{RGB: "FF0070C0"},
- ColorLast: &xlsxTabColor{RGB: "FF0070C0"},
- ColorHigh: &xlsxTabColor{RGB: "FF0070C0"},
- ColorLow: &xlsxTabColor{RGB: "FF0070C0"},
+ ColorSeries: &xlsxColor{RGB: "FF000000"},
+ ColorNegative: &xlsxColor{RGB: "FF0070C0"},
+ ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
+ ColorFirst: &xlsxColor{RGB: "FF0070C0"},
+ ColorLast: &xlsxColor{RGB: "FF0070C0"},
+ ColorHigh: &xlsxColor{RGB: "FF0070C0"},
+ ColorLow: &xlsxColor{RGB: "FF0070C0"},
}, // 28
{
- ColorSeries: &xlsxTabColor{RGB: "FF376092"},
- ColorNegative: &xlsxTabColor{RGB: "FFD00000"},
- ColorMarkers: &xlsxTabColor{RGB: "FFD00000"},
- ColorFirst: &xlsxTabColor{RGB: "FFD00000"},
- ColorLast: &xlsxTabColor{RGB: "FFD00000"},
- ColorHigh: &xlsxTabColor{RGB: "FFD00000"},
- ColorLow: &xlsxTabColor{RGB: "FFD00000"},
+ ColorSeries: &xlsxColor{RGB: "FF376092"},
+ ColorNegative: &xlsxColor{RGB: "FFD00000"},
+ ColorMarkers: &xlsxColor{RGB: "FFD00000"},
+ ColorFirst: &xlsxColor{RGB: "FFD00000"},
+ ColorLast: &xlsxColor{RGB: "FFD00000"},
+ ColorHigh: &xlsxColor{RGB: "FFD00000"},
+ ColorLow: &xlsxColor{RGB: "FFD00000"},
}, // 29
{
- ColorSeries: &xlsxTabColor{RGB: "FF0070C0"},
- ColorNegative: &xlsxTabColor{RGB: "FF000000"},
- ColorMarkers: &xlsxTabColor{RGB: "FF000000"},
- ColorFirst: &xlsxTabColor{RGB: "FF000000"},
- ColorLast: &xlsxTabColor{RGB: "FF000000"},
- ColorHigh: &xlsxTabColor{RGB: "FF000000"},
- ColorLow: &xlsxTabColor{RGB: "FF000000"},
+ ColorSeries: &xlsxColor{RGB: "FF0070C0"},
+ ColorNegative: &xlsxColor{RGB: "FF000000"},
+ ColorMarkers: &xlsxColor{RGB: "FF000000"},
+ ColorFirst: &xlsxColor{RGB: "FF000000"},
+ ColorLast: &xlsxColor{RGB: "FF000000"},
+ ColorHigh: &xlsxColor{RGB: "FF000000"},
+ ColorLow: &xlsxColor{RGB: "FF000000"},
}, // 30
{
- ColorSeries: &xlsxTabColor{RGB: "FF5F5F5F"},
- ColorNegative: &xlsxTabColor{RGB: "FFFFB620"},
- ColorMarkers: &xlsxTabColor{RGB: "FFD70077"},
- ColorFirst: &xlsxTabColor{RGB: "FF5687C2"},
- ColorLast: &xlsxTabColor{RGB: "FF359CEB"},
- ColorHigh: &xlsxTabColor{RGB: "FF56BE79"},
- ColorLow: &xlsxTabColor{RGB: "FFFF5055"},
+ ColorSeries: &xlsxColor{RGB: "FF5F5F5F"},
+ ColorNegative: &xlsxColor{RGB: "FFFFB620"},
+ ColorMarkers: &xlsxColor{RGB: "FFD70077"},
+ ColorFirst: &xlsxColor{RGB: "FF5687C2"},
+ ColorLast: &xlsxColor{RGB: "FF359CEB"},
+ ColorHigh: &xlsxColor{RGB: "FF56BE79"},
+ ColorLow: &xlsxColor{RGB: "FFFF5055"},
}, // 31
{
- ColorSeries: &xlsxTabColor{RGB: "FF5687C2"},
- ColorNegative: &xlsxTabColor{RGB: "FFFFB620"},
- ColorMarkers: &xlsxTabColor{RGB: "FFD70077"},
- ColorFirst: &xlsxTabColor{RGB: "FF777777"},
- ColorLast: &xlsxTabColor{RGB: "FF359CEB"},
- ColorHigh: &xlsxTabColor{RGB: "FF56BE79"},
- ColorLow: &xlsxTabColor{RGB: "FFFF5055"},
+ ColorSeries: &xlsxColor{RGB: "FF5687C2"},
+ ColorNegative: &xlsxColor{RGB: "FFFFB620"},
+ ColorMarkers: &xlsxColor{RGB: "FFD70077"},
+ ColorFirst: &xlsxColor{RGB: "FF777777"},
+ ColorLast: &xlsxColor{RGB: "FF359CEB"},
+ ColorHigh: &xlsxColor{RGB: "FF56BE79"},
+ ColorLow: &xlsxColor{RGB: "FFFF5055"},
}, // 32
{
- ColorSeries: &xlsxTabColor{RGB: "FFC6EFCE"},
- ColorNegative: &xlsxTabColor{RGB: "FFFFC7CE"},
- ColorMarkers: &xlsxTabColor{RGB: "FF8CADD6"},
- ColorFirst: &xlsxTabColor{RGB: "FFFFDC47"},
- ColorLast: &xlsxTabColor{RGB: "FFFFEB9C"},
- ColorHigh: &xlsxTabColor{RGB: "FF60D276"},
- ColorLow: &xlsxTabColor{RGB: "FFFF5367"},
+ ColorSeries: &xlsxColor{RGB: "FFC6EFCE"},
+ ColorNegative: &xlsxColor{RGB: "FFFFC7CE"},
+ ColorMarkers: &xlsxColor{RGB: "FF8CADD6"},
+ ColorFirst: &xlsxColor{RGB: "FFFFDC47"},
+ ColorLast: &xlsxColor{RGB: "FFFFEB9C"},
+ ColorHigh: &xlsxColor{RGB: "FF60D276"},
+ ColorLow: &xlsxColor{RGB: "FFFF5367"},
}, // 33
{
- ColorSeries: &xlsxTabColor{RGB: "FF00B050"},
- ColorNegative: &xlsxTabColor{RGB: "FFFF0000"},
- ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"},
- ColorFirst: &xlsxTabColor{RGB: "FFFFC000"},
- ColorLast: &xlsxTabColor{RGB: "FFFFC000"},
- ColorHigh: &xlsxTabColor{RGB: "FF00B050"},
- ColorLow: &xlsxTabColor{RGB: "FFFF0000"},
+ ColorSeries: &xlsxColor{RGB: "FF00B050"},
+ ColorNegative: &xlsxColor{RGB: "FFFF0000"},
+ ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
+ ColorFirst: &xlsxColor{RGB: "FFFFC000"},
+ ColorLast: &xlsxColor{RGB: "FFFFC000"},
+ ColorHigh: &xlsxColor{RGB: "FF00B050"},
+ ColorLow: &xlsxColor{RGB: "FFFF0000"},
}, // 34
{
- ColorSeries: &xlsxTabColor{Theme: 3},
- ColorNegative: &xlsxTabColor{Theme: 9},
- ColorMarkers: &xlsxTabColor{Theme: 8},
- ColorFirst: &xlsxTabColor{Theme: 4},
- ColorLast: &xlsxTabColor{Theme: 5},
- ColorHigh: &xlsxTabColor{Theme: 6},
- ColorLow: &xlsxTabColor{Theme: 7},
+ ColorSeries: &xlsxColor{Theme: intPtr(3)},
+ ColorNegative: &xlsxColor{Theme: intPtr(9)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8)},
+ ColorFirst: &xlsxColor{Theme: intPtr(4)},
+ ColorLast: &xlsxColor{Theme: intPtr(5)},
+ ColorHigh: &xlsxColor{Theme: intPtr(6)},
+ ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 35
{
- ColorSeries: &xlsxTabColor{Theme: 1},
- ColorNegative: &xlsxTabColor{Theme: 9},
- ColorMarkers: &xlsxTabColor{Theme: 8},
- ColorFirst: &xlsxTabColor{Theme: 4},
- ColorLast: &xlsxTabColor{Theme: 5},
- ColorHigh: &xlsxTabColor{Theme: 6},
- ColorLow: &xlsxTabColor{Theme: 7},
+ ColorSeries: &xlsxColor{Theme: intPtr(1)},
+ ColorNegative: &xlsxColor{Theme: intPtr(9)},
+ ColorMarkers: &xlsxColor{Theme: intPtr(8)},
+ ColorFirst: &xlsxColor{Theme: intPtr(4)},
+ ColorLast: &xlsxColor{Theme: intPtr(5)},
+ ColorHigh: &xlsxColor{Theme: intPtr(6)},
+ ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 36
}
- return groups[ID]
}
// AddSparkline provides a function to add sparklines to the worksheet by
// given formatting options. Sparklines are small charts that fit in a single
// cell and are used to show trends in data. Sparklines are a feature of Excel
-// 2010 and later only. You can write them to an XLSX file that can be read by
-// Excel 2007 but they won't be displayed. For example, add a grouped
-// sparkline. Changes are applied to all three:
+// 2010 and later only. You can write them to workbook that can be read by Excel
+// 2007, but they won't be displayed. For example, add a grouped sparkline.
+// Changes are applied to all three:
//
-// err := f.AddSparkline("Sheet1", &excelize.SparklineOption{
-// Location: []string{"A1", "A2", "A3"},
-// Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"},
-// Markers: true,
-// })
+// err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{
+// Location: []string{"A1", "A2", "A3"},
+// Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"},
+// Markers: true,
+// })
//
// The following shows the formatting options of sparkline supported by excelize:
//
-// Parameter | Description
-// -----------+--------------------------------------------
-// Location | Required, must have the same number with 'Range' parameter
-// Range | Required, must have the same number with 'Location' parameter
-// Type | Enumeration value: line, column, win_loss
-// Style | Value range: 0 - 35
-// Hight | Toggle sparkline high points
-// Low | Toggle sparkline low points
-// First | Toggle sparkline first points
-// Last | Toggle sparkline last points
-// Negative | Toggle sparkline negative points
-// Markers | Toggle sparkline markers
-// ColorAxis | An RGB Color is specified as RRGGBB
-// Axis | Show sparkline axis
-//
-func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) {
+// Parameter | Description
+// -------------+--------------------------------------------
+// Location | Required, must have the same number with 'Range' parameter
+// Range | Required, must have the same number with 'Location' parameter
+// Type | Enumeration value: line, column, win_loss
+// Style | Value range: 0 - 35
+// Hight | Toggle sparkline high points
+// Low | Toggle sparkline low points
+// First | Toggle sparkline first points
+// Last | Toggle sparkline last points
+// Negative | Toggle sparkline negative points
+// Markers | Toggle sparkline markers
+// Axis | Used to specify if show horizontal axis
+// Reverse | Used to specify if enable plot data right-to-left
+// SeriesColor | An RGB Color is specified as RRGGBB
+func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
var (
- ws *xlsxWorksheet
- sparkType string
- sparkTypes map[string]string
- specifiedSparkTypes string
- ok bool
- group *xlsxX14SparklineGroup
- groups *xlsxX14SparklineGroups
- sparklineGroupsBytes, extBytes []byte
+ err error
+ ws *xlsxWorksheet
+ sparkType string
+ sparkTypes map[string]string
+ specifiedSparkTypes string
+ ok bool
+ group *xlsxX14SparklineGroup
+ groups *xlsxX14SparklineGroups
)
// parameter validation
- if ws, err = f.parseFormatAddSparklineSet(sheet, opt); err != nil {
- return
+ if ws, err = f.parseFormatAddSparklineSet(sheet, opts); err != nil {
+ return err
}
// Handle the sparkline type
sparkType = "line"
sparkTypes = map[string]string{"line": "line", "column": "column", "win_loss": "stacked"}
- if opt.Type != "" {
- if specifiedSparkTypes, ok = sparkTypes[opt.Type]; !ok {
- err = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
- return
+ if opts.Type != "" {
+ if specifiedSparkTypes, ok = sparkTypes[opts.Type]; !ok {
+ err = ErrSparklineType
+ return err
}
sparkType = specifiedSparkTypes
}
- group = f.addSparklineGroupByStyle(opt.Style)
+ group = getSparklineGroupPresets()[opts.Style]
group.Type = sparkType
group.ColorAxis = &xlsxColor{RGB: "FF000000"}
group.DisplayEmptyCellsAs = "gap"
- group.High = opt.High
- group.Low = opt.Low
- group.First = opt.First
- group.Last = opt.Last
- group.Negative = opt.Negative
- group.DisplayXAxis = opt.Axis
- group.Markers = opt.Markers
- if opt.SeriesColor != "" {
- group.ColorSeries = &xlsxTabColor{
- RGB: getPaletteColor(opt.SeriesColor),
+ group.High = opts.High
+ group.Low = opts.Low
+ group.First = opts.First
+ group.Last = opts.Last
+ group.Negative = opts.Negative
+ group.DisplayXAxis = opts.Axis
+ group.Markers = opts.Markers
+ if opts.SeriesColor != "" {
+ group.ColorSeries = &xlsxColor{
+ RGB: getPaletteColor(opts.SeriesColor),
}
}
- if opt.Reverse {
- group.RightToLeft = opt.Reverse
+ if opts.Reverse {
+ group.RightToLeft = opts.Reverse
}
- f.addSparkline(opt, group)
- if ws.ExtLst.Ext != "" { // append mode ext
- if err = f.appendSparkline(ws, group, groups); err != nil {
- return
- }
- } else {
- groups = &xlsxX14SparklineGroups{
- XMLNSXM: NameSpaceSpreadSheetExcel2006Main,
- SparklineGroups: []*xlsxX14SparklineGroup{group},
- }
- if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil {
- return
- }
- if extBytes, err = xml.Marshal(&xlsxWorksheetExt{
- URI: ExtURISparklineGroups,
- Content: string(sparklineGroupsBytes),
- }); err != nil {
- return
- }
- ws.ExtLst.Ext = string(extBytes)
+ f.addSparkline(opts, group)
+ if err = f.appendSparkline(ws, group, groups); err != nil {
+ return err
}
f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14)
- return
+ return err
}
// parseFormatAddSparklineSet provides a function to validate sparkline
// properties.
-func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*xlsxWorksheet, error) {
+func (f *File) parseFormatAddSparklineSet(sheet string, opts *SparklineOptions) (*xlsxWorksheet, error) {
ws, err := f.workSheetReader(sheet)
if err != nil {
return ws, err
}
- if opt == nil {
- return ws, errors.New("parameter is required")
+ if opts == nil {
+ return ws, ErrParameterRequired
}
- if len(opt.Location) < 1 {
- return ws, errors.New("parameter 'Location' is required")
+ if len(opts.Location) < 1 {
+ return ws, ErrSparklineLocation
}
- if len(opt.Range) < 1 {
- return ws, errors.New("parameter 'Range' is required")
+ if len(opts.Range) < 1 {
+ return ws, ErrSparklineRange
}
- // The ranges and locations must match.\
- if len(opt.Location) != len(opt.Range) {
- return ws, errors.New(`must have the same number of 'Location' and 'Range' parameters`)
+ // The range and locations must match
+ if len(opts.Location) != len(opts.Range) {
+ return ws, ErrSparkline
}
- if opt.Style < 0 || opt.Style > 35 {
- return ws, errors.New("parameter 'Style' must betweent 0-35")
+ if opts.Style < 0 || opts.Style > 35 {
+ return ws, ErrSparklineStyle
}
if ws.ExtLst == nil {
ws.ExtLst = &xlsxExtLst{}
@@ -490,10 +472,10 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*
// addSparkline provides a function to create a sparkline in a sparkline group
// by given properties.
-func (f *File) addSparkline(opt *SparklineOption, group *xlsxX14SparklineGroup) {
- for idx, location := range opt.Location {
+func (f *File) addSparkline(opts *SparklineOptions, group *xlsxX14SparklineGroup) {
+ for idx, location := range opts.Location {
group.Sparklines.Sparkline = append(group.Sparklines.Sparkline, &xlsxX14Sparkline{
- F: opt.Range[idx],
+ F: opts.Range[idx],
Sqref: location,
})
}
@@ -501,44 +483,54 @@ func (f *File) addSparkline(opt *SparklineOption, group *xlsxX14SparklineGroup)
// appendSparkline provides a function to append sparkline to sparkline
// groups.
-func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups *xlsxX14SparklineGroups) (err error) {
+func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups *xlsxX14SparklineGroups) error {
var (
+ err error
idx int
- decodeExtLst *decodeWorksheetExt
+ appendMode bool
+ decodeExtLst = new(decodeExtLst)
decodeSparklineGroups *decodeX14SparklineGroups
- ext *xlsxWorksheetExt
+ ext *xlsxExt
sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte
)
- decodeExtLst = new(decodeWorksheetExt)
- if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
- Decode(decodeExtLst); err != nil && err != io.EOF {
- return
- }
- for idx, ext = range decodeExtLst.Ext {
- if ext.URI == ExtURISparklineGroups {
- decodeSparklineGroups = new(decodeX14SparklineGroups)
- if err = f.xmlNewDecoder(strings.NewReader(ext.Content)).
- Decode(decodeSparklineGroups); err != nil && err != io.EOF {
- return
- }
- if sparklineGroupBytes, err = xml.Marshal(group); err != nil {
- return
- }
- groups = &xlsxX14SparklineGroups{
- XMLNSXM: NameSpaceSpreadSheetExcel2006Main,
- Content: decodeSparklineGroups.Content + string(sparklineGroupBytes),
- }
- if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil {
- return
+ sparklineGroupBytes, _ = xml.Marshal(group)
+ if ws.ExtLst != nil { // append mode ext
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ for idx, ext = range decodeExtLst.Ext {
+ if ext.URI == ExtURISparklineGroups {
+ decodeSparklineGroups = new(decodeX14SparklineGroups)
+ if err = f.xmlNewDecoder(strings.NewReader(ext.Content)).
+ Decode(decodeSparklineGroups); err != nil && err != io.EOF {
+ return err
+ }
+ if groups == nil {
+ groups = &xlsxX14SparklineGroups{}
+ }
+ groups.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value
+ groups.Content = decodeSparklineGroups.Content + string(sparklineGroupBytes)
+ sparklineGroupsBytes, _ = xml.Marshal(groups)
+ decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes)
+ appendMode = true
}
- decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes)
}
}
- if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil {
- return
- }
- ws.ExtLst = &xlsxExtLst{
- Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""),
+ if !appendMode {
+ sparklineGroupsBytes, _ = xml.Marshal(&xlsxX14SparklineGroups{
+ XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value,
+ SparklineGroups: []*xlsxX14SparklineGroup{group},
+ })
+ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
+ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes),
+ })
}
- return
+ sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
+ return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
+ inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
+ })
+ extLstBytes, err = xml.Marshal(decodeExtLst)
+ ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ return err
}
diff --git a/sparkline_test.go b/sparkline_test.go
index 4b059ab982..27da1e694e 100644
--- a/sparkline_test.go
+++ b/sparkline_test.go
@@ -9,13 +9,17 @@ import (
)
func TestAddSparkline(t *testing.T) {
- f := prepareSparklineDataset()
+ f, err := prepareSparklineDataset()
+ assert.NoError(t, err)
// Set the columns widths to make the output clearer
- style, err := f.NewStyle(`{"font":{"bold":true}}`)
+ style, err := f.NewStyle(&Style{Font: &Font{Bold: true}})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style))
- assert.NoError(t, f.SetSheetViewOptions("Sheet1", 0, ZoomScale(150)))
+ viewOpts, err := f.GetSheetView("Sheet1", 0)
+ assert.NoError(t, err)
+ viewOpts.ZoomScale = float64Ptr(150)
+ assert.NoError(t, f.SetSheetView("Sheet1", 0, &viewOpts))
assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 14))
assert.NoError(t, f.SetColWidth("Sheet1", "B", "B", 50))
@@ -24,34 +28,34 @@ func TestAddSparkline(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Description"))
assert.NoError(t, f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A2"},
Range: []string{"Sheet3!A1:J1"},
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A3"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A4"},
Range: []string{"Sheet3!A3:J3"},
Type: "win_loss",
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B6", "Line with markers."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A6"},
Range: []string{"Sheet3!A1:J1"},
Markers: true,
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B7", "Line with high and low points."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A7"},
Range: []string{"Sheet3!A1:J1"},
High: true,
@@ -59,7 +63,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B8", "Line with first and last point markers."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A8"},
Range: []string{"Sheet3!A1:J1"},
First: true,
@@ -67,28 +71,28 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B9", "Line with negative point markers."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A9"},
Range: []string{"Sheet3!A1:J1"},
Negative: true,
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B10", "Line with axis."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A10"},
Range: []string{"Sheet3!A1:J1"},
Axis: true,
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B12", "Column with default style (1)."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A12"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B13", "Column with style 2."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A13"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
@@ -96,7 +100,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B14", "Column with style 3."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A14"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
@@ -104,7 +108,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B15", "Column with style 4."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A15"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
@@ -112,7 +116,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B16", "Column with style 5."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A16"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
@@ -120,7 +124,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B17", "Column with style 6."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A17"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
@@ -128,22 +132,22 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B18", "Column with a user defined color."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A18"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
- SeriesColor: "#E965E0",
+ SeriesColor: "E965E0",
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B20", "A win/loss sparkline."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A20"},
Range: []string{"Sheet3!A3:J3"},
Type: "win_loss",
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A21"},
Range: []string{"Sheet3!A3:J3"},
Type: "win_loss",
@@ -151,7 +155,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B23", "A left to right column (the default)."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A23"},
Range: []string{"Sheet3!A4:J4"},
Type: "column",
@@ -159,7 +163,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B24", "A right to left column."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A24"},
Range: []string{"Sheet3!A4:J4"},
Type: "column",
@@ -168,7 +172,7 @@ func TestAddSparkline(t *testing.T) {
}))
assert.NoError(t, f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A25"},
Range: []string{"Sheet3!A4:J4"},
Type: "column",
@@ -177,100 +181,108 @@ func TestAddSparkline(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "A25", "Growth"))
assert.NoError(t, f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three."))
- assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A27", "A28", "A29"},
Range: []string{"Sheet3!A5:J5", "Sheet3!A6:J6", "Sheet3!A7:J7"},
Markers: true,
}))
// Sheet2 sections
- assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Type: "win_loss",
Negative: true,
}))
- assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{
Location: []string{"F1"},
Range: []string{"Sheet2!A1:E1"},
Markers: true,
}))
- assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{
Location: []string{"F2"},
Range: []string{"Sheet2!A2:E2"},
Type: "column",
Style: 12,
}))
- assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{
+ assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Type: "win_loss",
Negative: true,
}))
- // Save xlsx file by the given path.
+ // Save spreadsheet by the given path
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx")))
// Test error exceptions
- assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOption{
+ assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
- }), "sheet SheetN is not exist")
+ }), "sheet SheetN does not exist")
+
+ assert.Equal(t, ErrParameterRequired, f.AddSparkline("Sheet1", nil))
- assert.EqualError(t, f.AddSparkline("Sheet1", nil), "parameter is required")
+ // Test add sparkline with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.AddSparkline("Sheet:1", &SparklineOptions{
+ Location: []string{"F3"},
+ Range: []string{"Sheet2!A3:E3"},
+ Type: "win_loss",
+ Negative: true,
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{
Range: []string{"Sheet2!A3:E3"},
- }), `parameter 'Location' is required`)
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparklineRange, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
- }), `parameter 'Range' is required`)
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F2", "F3"},
Range: []string{"Sheet2!A3:E3"},
- }), `must have the same number of 'Location' and 'Range' parameters`)
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparklineType, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Type: "unknown_type",
- }), `parameter 'Type' must be 'line', 'column' or 'win_loss'`)
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Style: -1,
- }), `parameter 'Style' must betweent 0-35`)
+ }))
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Style: -1,
- }), `parameter 'Style' must betweent 0-35`)
-
- f.Sheet["xl/worksheets/sheet1.xml"].ExtLst.Ext = `
-
-
-
-
-
-
-
- `
- assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{
+ }))
+ // Test creating a conditional format with existing extension lists
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURISlicerListX14, ExtURISparklineGroups)}
+ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
+ Location: []string{"A3"},
+ Range: []string{"Sheet3!A2:J2"},
+ Type: "column",
+ }))
+ // Test creating a conditional format with invalid extension list characters
+ ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(``, ExtURISparklineGroups)
+ assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A2"},
Range: []string{"Sheet3!A1:J1"},
- }), "XML syntax error on line 6: element closed by ")
+ }), "XML syntax error on line 1: element closed by ")
}
func TestAppendSparkline(t *testing.T) {
- // Test unsupport charset.
+ // Test unsupported charset.
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
@@ -278,7 +290,7 @@ func TestAppendSparkline(t *testing.T) {
assert.EqualError(t, f.appendSparkline(ws, &xlsxX14SparklineGroup{}, &xlsxX14SparklineGroups{}), "XML syntax error on line 1: invalid UTF-8")
}
-func prepareSparklineDataset() *File {
+func prepareSparklineDataset() (*File, error) {
f := NewFile()
sheet2 := [][]int{
{-2, 2, 3, -1, 0},
@@ -294,8 +306,12 @@ func prepareSparklineDataset() *File {
{3, -1, 0, -2, 3, 2, 1, 0, 2, 1},
{0, -2, 3, 2, 1, 0, 1, 2, 3, 1},
}
- f.NewSheet("Sheet2")
- f.NewSheet("Sheet3")
+ if _, err := f.NewSheet("Sheet2"); err != nil {
+ return f, err
+ }
+ if _, err := f.NewSheet("Sheet3"); err != nil {
+ return f, err
+ }
for row, data := range sheet2 {
if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil {
fmt.Println(err)
@@ -306,5 +322,5 @@ func prepareSparklineDataset() *File {
fmt.Println(err)
}
}
- return f
+ return f, nil
}
diff --git a/stream.go b/stream.go
index 19f5ca7625..89081b8dde 100644
--- a/stream.go
+++ b/stream.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -16,7 +16,6 @@ import (
"encoding/xml"
"fmt"
"io"
- "io/ioutil"
"os"
"reflect"
"strconv"
@@ -26,57 +25,102 @@ import (
// StreamWriter defined the type of stream writer.
type StreamWriter struct {
- File *File
- Sheet string
- SheetID int
- worksheet *xlsxWorksheet
- rawData bufferedWriter
- tableParts string
+ file *File
+ Sheet string
+ SheetID int
+ sheetWritten bool
+ worksheet *xlsxWorksheet
+ rawData bufferedWriter
+ rows int
+ mergeCellsCount int
+ mergeCells strings.Builder
+ tableParts string
}
-// NewStreamWriter return stream writer struct by given worksheet name for
-// generate new worksheet with large amounts of data. Note that after set
-// rows, you must call the 'Flush' method to end the streaming writing
-// process and ensure that the order of line numbers is ascending. For
-// example, set data for worksheet of size 102400 rows x 50 columns with
-// numbers and style:
+// NewStreamWriter returns stream writer struct by given worksheet name used for
+// writing data on a new existing empty worksheet with large amounts of data.
+// Note that after writing data with the stream writer for the worksheet, you
+// must call the 'Flush' method to end the streaming writing process, ensure
+// that the order of row numbers is ascending when set rows, and the normal
+// mode functions and stream mode functions can not be work mixed to writing
+// data on the worksheets. The stream writer will try to use temporary files on
+// disk to reduce the memory usage when in-memory chunks data over 16MB, and
+// you can't get cell value at this time. For example, set data for worksheet
+// of size 102400 rows x 50 columns with numbers and style:
//
-// file := excelize.NewFile()
-// streamWriter, err := file.NewStreamWriter("Sheet1")
-// if err != nil {
-// fmt.Println(err)
-// }
-// styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil {
-// fmt.Println(err)
-// }
-// for rowID := 2; rowID <= 102400; rowID++ {
-// row := make([]interface{}, 50)
-// for colID := 0; colID < 50; colID++ {
-// row[colID] = rand.Intn(640000)
-// }
-// cell, _ := excelize.CoordinatesToCellName(1, rowID)
-// if err := streamWriter.SetRow(cell, row); err != nil {
-// fmt.Println(err)
-// }
-// }
-// if err := streamWriter.Flush(); err != nil {
-// fmt.Println(err)
-// }
-// if err := file.SaveAs("Book1.xlsx"); err != nil {
-// fmt.Println(err)
-// }
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// sw, err := f.NewStreamWriter("Sheet1")
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// styleID, err := f.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "777777"}})
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := sw.SetRow("A1",
+// []interface{}{
+// excelize.Cell{StyleID: styleID, Value: "Data"},
+// []excelize.RichTextRun{
+// {Text: "Rich ", Font: &excelize.Font{Color: "2354e8"}},
+// {Text: "Text", Font: &excelize.Font{Color: "e83723"}},
+// },
+// },
+// excelize.RowOpts{Height: 45, Hidden: false}); err != nil {
+// fmt.Println(err)
+// return
+// }
+// for rowID := 2; rowID <= 102400; rowID++ {
+// row := make([]interface{}, 50)
+// for colID := 0; colID < 50; colID++ {
+// row[colID] = rand.Intn(640000)
+// }
+// cell, err := excelize.CoordinatesToCellName(1, rowID)
+// if err != nil {
+// fmt.Println(err)
+// break
+// }
+// if err := sw.SetRow(cell, row); err != nil {
+// fmt.Println(err)
+// break
+// }
+// }
+// if err := sw.Flush(); err != nil {
+// fmt.Println(err)
+// return
+// }
+// if err := f.SaveAs("Book1.xlsx"); err != nil {
+// fmt.Println(err)
+// }
//
+// Set cell value and cell formula for a worksheet with stream writer:
+//
+// err := sw.SetRow("A1", []interface{}{
+// excelize.Cell{Value: 1},
+// excelize.Cell{Value: 2},
+// excelize.Cell{Formula: "SUM(A1,B1)"}});
+//
+// Set cell value and rows style for a worksheet with stream writer:
+//
+// err := sw.SetRow("A1", []interface{}{
+// excelize.Cell{Value: 1}},
+// excelize.RowOpts{StyleID: styleID, Height: 20, Hidden: false});
func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
+ if err := checkSheetName(sheet); err != nil {
+ return nil, err
+ }
sheetID := f.getSheetID(sheet)
if sheetID == -1 {
- return nil, fmt.Errorf("sheet %s is not exist", sheet)
+ return nil, ErrSheetNotExist{sheet}
}
sw := &StreamWriter{
- File: f,
+ file: f,
Sheet: sheet,
SheetID: sheetID,
}
@@ -85,35 +129,49 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
if err != nil {
return nil, err
}
- sw.rawData.WriteString(XMLHeader + ``)
+
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ if f.streams == nil {
+ f.streams = make(map[string]*StreamWriter)
+ }
+ f.streams[sheetXMLPath] = sw
+
+ _, _ = sw.rawData.WriteString(xml.Header + ``, rID)
- sw.File.addContentTypePart(tableID, "table")
-
- b, _ := xml.Marshal(table)
- sw.File.saveFileList(tableXML, b)
- return nil
+ if err = sw.file.addContentTypePart(tableID, "table"); err != nil {
+ return err
+ }
+ b, _ := xml.Marshal(tbl)
+ sw.file.saveFileList(tableXML, b)
+ return err
}
// Extract values from a row in the StreamWriter.
-func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err error) {
- res = make([]string, vcol-hcol+1)
+func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err error) {
+ res = make([]string, vCol-hCol+1)
r, err := sw.rawData.Reader()
if err != nil {
return nil, err
}
- dec := sw.File.xmlNewDecoder(r)
+ dec := sw.file.xmlNewDecoder(r)
for {
token, err := dec.Token()
if err == io.EOF {
@@ -207,7 +266,7 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er
if err != nil {
return nil, err
}
- startElement, ok := getRowElement(token, hrow)
+ startElement, ok := getRowElement(token, hRow)
if !ok {
continue
}
@@ -221,17 +280,17 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er
if err != nil {
return nil, err
}
- if col < hcol || col > vcol {
+ if col < hCol || col > vCol {
continue
}
- res[col-hcol] = c.V
+ res[col-hCol], _ = c.getValueFrom(sw.file, nil, false)
}
return res, nil
}
}
-// Check if the token is an XLSX row with the matching row number.
-func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok bool) {
+// Check if the token is an worksheet row with the matching row number.
+func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) {
startElement, ok = token.(xml.StartElement)
if !ok {
return
@@ -246,7 +305,7 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok
continue
}
row, _ := strconv.Atoi(attr.Value)
- if row == hrow {
+ if row == hRow {
ok = true
return
}
@@ -258,142 +317,406 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok
// a value.
type Cell struct {
StyleID int
+ Formula string
Value interface{}
}
-// SetRow writes an array to stream rows by giving a worksheet name, starting
-// coordinate and a pointer to an array of values. Note that you must call the
-// 'Flush' method to end the streaming writing process.
+// RowOpts define the options for the set row, it can be used directly in
+// StreamWriter.SetRow to specify the style and properties of the row.
+type RowOpts struct {
+ Height float64
+ Hidden bool
+ StyleID int
+ OutlineLevel int
+}
+
+// marshalAttrs prepare attributes of the row.
+func (r *RowOpts) marshalAttrs() (strings.Builder, error) {
+ var (
+ err error
+ attrs strings.Builder
+ )
+ if r == nil {
+ return attrs, err
+ }
+ if r.Height > MaxRowHeight {
+ err = ErrMaxRowHeight
+ return attrs, err
+ }
+ if r.OutlineLevel > 7 {
+ err = ErrOutlineLevel
+ return attrs, err
+ }
+ if r.StyleID > 0 {
+ attrs.WriteString(` s="`)
+ attrs.WriteString(strconv.Itoa(r.StyleID))
+ attrs.WriteString(`" customFormat="1"`)
+ }
+ if r.Height > 0 {
+ attrs.WriteString(` ht="`)
+ attrs.WriteString(strconv.FormatFloat(r.Height, 'f', -1, 64))
+ attrs.WriteString(`" customHeight="1"`)
+ }
+ if r.OutlineLevel > 0 {
+ attrs.WriteString(` outlineLevel="`)
+ attrs.WriteString(strconv.Itoa(r.OutlineLevel))
+ attrs.WriteString(`"`)
+ }
+ if r.Hidden {
+ attrs.WriteString(` hidden="1"`)
+ }
+ return attrs, err
+}
+
+// parseRowOpts provides a function to parse the optional settings for
+// *StreamWriter.SetRow.
+func parseRowOpts(opts ...RowOpts) *RowOpts {
+ options := &RowOpts{}
+ for _, opt := range opts {
+ options = &opt
+ }
+ return options
+}
+
+// SetRow writes an array to stream rows by giving starting cell reference and a
+// pointer to an array of values. Note that you must call the 'Flush' function
+// to end the streaming writing process.
//
// As a special case, if Cell is used as a value, then the Cell.StyleID will be
// applied to that cell.
-func (sw *StreamWriter) SetRow(axis string, values []interface{}) error {
- col, row, err := CellNameToCoordinates(axis)
+func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpts) error {
+ col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
-
- fmt.Fprintf(&sw.rawData, ``, row)
+ if row <= sw.rows {
+ return newStreamSetRowError(row)
+ }
+ sw.rows = row
+ sw.writeSheetData()
+ options := parseRowOpts(opts...)
+ attrs, err := options.marshalAttrs()
+ if err != nil {
+ return err
+ }
+ _, _ = sw.rawData.WriteString(``)
for i, val := range values {
- axis, err := CoordinatesToCellName(col+i, row)
+ if val == nil {
+ continue
+ }
+ ref, err := CoordinatesToCellName(col+i, row)
if err != nil {
return err
}
- c := xlsxC{R: axis}
+ c := xlsxC{R: ref, S: sw.worksheet.prepareCellStyle(col, row, options.StyleID)}
+ var s int
if v, ok := val.(Cell); ok {
- c.S = v.StyleID
- val = v.Value
+ s, val = v.StyleID, v.Value
+ setCellFormula(&c, v.Formula)
} else if v, ok := val.(*Cell); ok && v != nil {
- c.S = v.StyleID
- val = v.Value
+ s, val = v.StyleID, v.Value
+ setCellFormula(&c, v.Formula)
+ }
+ if s > 0 {
+ c.S = s
}
- if err = setCellValFunc(&c, val); err != nil {
- sw.rawData.WriteString(`
`)
+ if err = sw.setCellValFunc(&c, val); err != nil {
+ _, _ = sw.rawData.WriteString(`
`)
return err
}
writeCell(&sw.rawData, c)
}
- sw.rawData.WriteString(``)
+ _, _ = sw.rawData.WriteString(``)
return sw.rawData.Sync()
}
+// SetColStyle provides a function to set the style of a single column or
+// multiple columns for the StreamWriter. Note that you must call
+// the 'SetColStyle' function before the 'SetRow' function. For example set
+// style of column H on Sheet1:
+//
+// err := sw.SetColStyle(8, 8, style)
+func (sw *StreamWriter) SetColStyle(minVal, maxVal, styleID int) error {
+ if sw.sheetWritten {
+ return ErrStreamSetColStyle
+ }
+ if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns {
+ return ErrColumnNumber
+ }
+ if maxVal < minVal {
+ minVal, maxVal = maxVal, minVal
+ }
+ s, err := sw.file.stylesReader()
+ if err != nil {
+ return err
+ }
+ if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
+ return newInvalidStyleID(styleID)
+ }
+ sw.worksheet.setColStyle(minVal, maxVal, styleID)
+ return nil
+}
+
+// SetColWidth provides a function to set the width of a single column or
+// multiple columns for the StreamWriter. Note that you must call
+// the 'SetColWidth' function before the 'SetRow' function. For example set
+// the width column B:C as 20:
+//
+// err := sw.SetColWidth(2, 3, 20)
+func (sw *StreamWriter) SetColWidth(minVal, maxVal int, width float64) error {
+ if sw.sheetWritten {
+ return ErrStreamSetColWidth
+ }
+ if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns {
+ return ErrColumnNumber
+ }
+ if width > MaxColumnWidth {
+ return ErrColumnWidth
+ }
+ if minVal > maxVal {
+ minVal, maxVal = maxVal, minVal
+ }
+ sw.worksheet.setColWidth(minVal, maxVal, width)
+ return nil
+}
+
+// InsertPageBreak creates a page break to determine where the printed page ends
+// and where begins the next one by a given cell reference, the content before
+// the page break will be printed on one page and after the page break on
+// another.
+func (sw *StreamWriter) InsertPageBreak(cell string) error {
+ return sw.worksheet.insertPageBreak(cell)
+}
+
+// SetPanes provides a function to create and remove freeze panes and split
+// panes by giving panes options for the StreamWriter. Note that you must call
+// the 'SetPanes' function before the 'SetRow' function.
+func (sw *StreamWriter) SetPanes(panes *Panes) error {
+ if sw.sheetWritten {
+ return ErrStreamSetPanes
+ }
+ return sw.worksheet.setPanes(panes)
+}
+
+// MergeCell provides a function to merge cells by a given range reference for
+// the StreamWriter. Don't create a merged cell that overlaps with another
+// existing merged cell.
+func (sw *StreamWriter) MergeCell(topLeftCell, bottomRightCell string) error {
+ _, err := cellRefsToCoordinates(topLeftCell, bottomRightCell)
+ if err != nil {
+ return err
+ }
+ sw.mergeCellsCount++
+ _, _ = sw.mergeCells.WriteString(``)
+ return nil
+}
+
+// setCellFormula provides a function to set formula of a cell.
+func setCellFormula(c *xlsxC, formula string) {
+ if formula != "" {
+ c.T, c.F = "str", &xlsxF{Content: formula}
+ }
+}
+
+// setCellTime provides a function to set number of a cell with a time.
+func (sw *StreamWriter) setCellTime(c *xlsxC, val time.Time) error {
+ var date1904, isNum bool
+ wb, err := sw.file.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb != nil && wb.WorkbookPr != nil {
+ date1904 = wb.WorkbookPr.Date1904
+ }
+ if isNum, err = c.setCellTime(val, date1904); err == nil && isNum && c.S == 0 {
+ style, _ := sw.file.NewStyle(&Style{NumFmt: 22})
+ c.S = style
+ }
+ return nil
+}
+
// setCellValFunc provides a function to set value of a cell.
-func setCellValFunc(c *xlsxC, val interface{}) (err error) {
+func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
+ var err error
switch val := val.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
- err = setCellIntFunc(c, val)
+ setCellIntFunc(c, val)
case float32:
- c.T, c.V = setCellFloat(float64(val), -1, 32)
+ c.setCellFloat(float64(val), -1, 32)
case float64:
- c.T, c.V = setCellFloat(val, -1, 64)
+ c.setCellFloat(val, -1, 64)
case string:
- c.T, c.V, c.XMLSpace = setCellStr(val)
+ c.setCellValue(val)
case []byte:
- c.T, c.V, c.XMLSpace = setCellStr(string(val))
+ c.setCellValue(string(val))
case time.Duration:
c.T, c.V = setCellDuration(val)
case time.Time:
- c.T, c.V, _, err = setCellTime(val)
+ err = sw.setCellTime(c, val)
case bool:
c.T, c.V = setCellBool(val)
case nil:
- c.T, c.V, c.XMLSpace = setCellStr("")
+ return err
+ case []RichTextRun:
+ c.T, c.IS = "inlineStr", &xlsxSI{}
+ c.IS.R, err = setRichText(val)
default:
- c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val))
+ c.setCellValue(fmt.Sprint(val))
}
return err
}
// setCellIntFunc is a wrapper of SetCellInt.
-func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
+func setCellIntFunc(c *xlsxC, val interface{}) {
switch val := val.(type) {
case int:
- c.T, c.V = setCellInt(val)
+ c.T, c.V = setCellInt(int64(val))
case int8:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellInt(int64(val))
case int16:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellInt(int64(val))
case int32:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellInt(int64(val))
case int64:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellInt(val)
case uint:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellUint(uint64(val))
case uint8:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellUint(uint64(val))
case uint16:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellUint(uint64(val))
case uint32:
- c.T, c.V = setCellInt(int(val))
+ c.T, c.V = setCellUint(uint64(val))
case uint64:
- c.T, c.V = setCellInt(int(val))
- default:
+ c.T, c.V = setCellUint(val)
}
- return
}
+// writeCell constructs a cell XML and writes it to the buffer.
func writeCell(buf *bufferedWriter, c xlsxC) {
- buf.WriteString(``)
+ if c.F != nil {
+ _, _ = buf.WriteString(``)
+ _ = xml.EscapeText(buf, []byte(c.F.Content))
+ _, _ = buf.WriteString(``)
}
- buf.WriteString(`>`)
if c.V != "" {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(c.V))
- buf.WriteString(``)
+ _, _ = buf.WriteString(``)
+ _ = xml.EscapeText(buf, []byte(c.V))
+ _, _ = buf.WriteString(``)
+ }
+ if c.IS != nil {
+ if len(c.IS.R) > 0 {
+ is, _ := xml.Marshal(c.IS.R)
+ _, _ = buf.WriteString(``)
+ _, _ = buf.Write(is)
+ _, _ = buf.WriteString(``)
+ }
+ if c.IS.T != nil {
+ _, _ = buf.WriteString(``)
+ _, _ = buf.Write([]byte(c.IS.T.Val))
+ _, _ = buf.WriteString(``)
+ }
+ }
+ _, _ = buf.WriteString(``)
+}
+
+// writeSheetData prepares the element preceding sheetData and writes the
+// sheetData XML start element to the buffer.
+func (sw *StreamWriter) writeSheetData() {
+ if !sw.sheetWritten {
+ bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5)
+ if sw.worksheet.Cols != nil {
+ _, _ = sw.rawData.WriteString("")
+ for _, col := range sw.worksheet.Cols.Col {
+ sw.rawData.WriteString(``)
+ }
+ _, _ = sw.rawData.WriteString("")
+ }
+ _, _ = sw.rawData.WriteString(``)
+ sw.sheetWritten = true
}
- buf.WriteString(``)
}
// Flush ending the streaming writing process.
func (sw *StreamWriter) Flush() error {
- sw.rawData.WriteString(``)
- bulkAppendFields(&sw.rawData, sw.worksheet, 7, 37)
- sw.rawData.WriteString(sw.tableParts)
- bulkAppendFields(&sw.rawData, sw.worksheet, 39, 39)
- sw.rawData.WriteString(``)
+ sw.writeSheetData()
+ _, _ = sw.rawData.WriteString(``)
+ bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15)
+ mergeCells := strings.Builder{}
+ if sw.mergeCellsCount > 0 {
+ _, _ = mergeCells.WriteString(``)
+ _, _ = mergeCells.WriteString(sw.mergeCells.String())
+ _, _ = mergeCells.WriteString(``)
+ }
+ _, _ = sw.rawData.WriteString(mergeCells.String())
+ bulkAppendFields(&sw.rawData, sw.worksheet, 17, 38)
+ _, _ = sw.rawData.WriteString(sw.tableParts)
+ bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40)
+ _, _ = sw.rawData.WriteString(``)
if err := sw.rawData.Flush(); err != nil {
return err
}
- sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)
- delete(sw.File.Sheet, sheetXML)
- delete(sw.File.checked, sheetXML)
+ sheetPath := sw.file.sheetMap[sw.Sheet]
+ sw.file.Sheet.Delete(sheetPath)
+ sw.file.checked.Delete(sheetPath)
+ sw.file.Pkg.Delete(sheetPath)
- defer sw.rawData.Close()
- b, err := sw.rawData.Bytes()
- if err != nil {
- return err
- }
- sw.File.XLSX[sheetXML] = b
return nil
}
@@ -404,7 +727,7 @@ func bulkAppendFields(w io.Writer, ws *xlsxWorksheet, from, to int) {
enc := xml.NewEncoder(w)
for i := 0; i < s.NumField(); i++ {
if from <= i && i <= to {
- enc.Encode(s.Field(i).Interface())
+ _ = enc.Encode(s.Field(i).Interface())
}
}
}
@@ -418,12 +741,12 @@ type bufferedWriter struct {
buf bytes.Buffer
}
-// Write to the in-memory buffer. The err is always nil.
+// Write to the in-memory buffer. The error is always nil.
func (bw *bufferedWriter) Write(p []byte) (n int, err error) {
return bw.buf.Write(p)
}
-// WriteString wites to the in-memory buffer. The err is always nil.
+// WriteString write to the in-memory buffer. The error is always nil.
func (bw *bufferedWriter) WriteString(p string) (n int, err error) {
return bw.buf.WriteString(p)
}
@@ -444,46 +767,15 @@ func (bw *bufferedWriter) Reader() (io.Reader, error) {
return io.NewSectionReader(bw.tmp, 0, fi.Size()), nil
}
-// Bytes returns the entire content of the bufferedWriter. If a temp file is
-// used, Bytes will efficiently allocate a buffer to prevent re-allocations.
-func (bw *bufferedWriter) Bytes() ([]byte, error) {
- if bw.tmp == nil {
- return bw.buf.Bytes(), nil
- }
-
- if err := bw.Flush(); err != nil {
- return nil, err
- }
-
- var buf bytes.Buffer
- if fi, err := bw.tmp.Stat(); err == nil {
- if size := fi.Size() + bytes.MinRead; size > bytes.MinRead {
- if int64(int(size)) == size {
- buf.Grow(int(size))
- } else {
- return nil, bytes.ErrTooLarge
- }
- }
- }
-
- if _, err := bw.tmp.Seek(0, 0); err != nil {
- return nil, err
- }
-
- _, err := buf.ReadFrom(bw.tmp)
- return buf.Bytes(), err
-}
-
// Sync will write the in-memory buffer to a temp file, if the in-memory
// buffer has grown large enough. Any error will be returned.
func (bw *bufferedWriter) Sync() (err error) {
// Try to use local storage
- const chunk = 1 << 24
- if bw.buf.Len() < chunk {
+ if bw.buf.Len() < StreamChunkSize {
return nil
}
if bw.tmp == nil {
- bw.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-")
+ bw.tmp, err = os.CreateTemp(os.TempDir(), "excelize-")
if err != nil {
// can not use local storage
return nil
diff --git a/stream_test.go b/stream_test.go
index d89dad845a..a2eea18391 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -3,7 +3,8 @@ package excelize
import (
"encoding/xml"
"fmt"
- "io/ioutil"
+ "io"
+ "math"
"math/rand"
"os"
"path/filepath"
@@ -16,7 +17,11 @@ import (
func BenchmarkStreamWriter(b *testing.B) {
file := NewFile()
-
+ defer func() {
+ if err := file.Close(); err != nil {
+ b.Error(err)
+ }
+ }()
row := make([]interface{}, 10)
for colID := 0; colID < 10; colID++ {
row[colID] = colID
@@ -26,7 +31,7 @@ func BenchmarkStreamWriter(b *testing.B) {
streamWriter, _ := file.NewStreamWriter("Sheet1")
for rowID := 10; rowID <= 110; rowID++ {
cell, _ := CoordinatesToCellName(1, rowID)
- streamWriter.SetRow(cell, row)
+ _ = streamWriter.SetRow(cell, row)
}
}
@@ -38,12 +43,12 @@ func TestStreamWriter(t *testing.T) {
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
- // Test max characters in a cell.
+ // Test max characters in a cell
row := make([]interface{}, 1)
- row[0] = strings.Repeat("c", 32769)
+ row[0] = strings.Repeat("c", TotalCellChars+2)
assert.NoError(t, streamWriter.SetRow("A1", row))
- // Test leading and ending space(s) character characters in a cell.
+ // Test leading and ending space(s) character characters in a cell
row = make([]interface{}, 1)
row[0] = " characters"
assert.NoError(t, streamWriter.SetRow("A2", row))
@@ -52,12 +57,27 @@ func TestStreamWriter(t *testing.T) {
row[0] = []byte("Word")
assert.NoError(t, streamWriter.SetRow("A3", row))
- // Test set cell with style.
- styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`)
+ // Test set cell with style and rich text
+ styleID, err := file.NewStyle(&Style{Font: &Font{Color: "777777"}})
assert.NoError(t, err)
- assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}}))
- assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}}))
- assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), "only UTC time expected")
+ assert.NoError(t, streamWriter.SetRow("A4", []interface{}{
+ Cell{StyleID: styleID},
+ Cell{Formula: "SUM(A10,B10)", Value: " preserve space "},
+ },
+ RowOpts{Height: 45, StyleID: styleID}))
+ assert.NoError(t, streamWriter.SetRow("A5", []interface{}{
+ &Cell{StyleID: styleID, Value: "cell <>&'\""},
+ &Cell{Formula: "SUM(A10,B10)"},
+ []RichTextRun{
+ {Text: "Rich ", Font: &Font{Color: "2354E8"}},
+ {Text: "Text", Font: &Font{Color: "E83723"}},
+ },
+ }))
+ assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}))
+ assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID}))
+ assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}))
+
+ assert.NoError(t, streamWriter.SetRow("A9", []interface{}{math.NaN(), math.Inf(0), math.Inf(-1)}))
for rowID := 10; rowID <= 51200; rowID++ {
row := make([]interface{}, 50)
@@ -69,14 +89,18 @@ func TestStreamWriter(t *testing.T) {
}
assert.NoError(t, streamWriter.Flush())
- // Save xlsx file by the given path.
+ // Save spreadsheet by the given path
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx")))
- // Test close temporary file error.
+ // Test set cell column overflow
+ assert.ErrorIs(t, streamWriter.SetRow("XFD51201", []interface{}{"A", "B", "C"}), ErrColumnNumber)
+ assert.NoError(t, file.Close())
+
+ // Test close temporary file error
file = NewFile()
streamWriter, err = file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
- for rowID := 10; rowID <= 51200; rowID++ {
+ for rowID := 10; rowID <= 25600; rowID++ {
row := make([]interface{}, 50)
for colID := 0; colID < 50; colID++ {
row[colID] = rand.Intn(640000)
@@ -87,88 +111,379 @@ func TestStreamWriter(t *testing.T) {
assert.NoError(t, streamWriter.rawData.Close())
assert.Error(t, streamWriter.Flush())
- streamWriter.rawData.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-")
+ streamWriter.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-")
assert.NoError(t, err)
_, err = streamWriter.rawData.Reader()
assert.NoError(t, err)
+ assert.NoError(t, streamWriter.rawData.tmp.Close())
assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name()))
- // Test unsupport charset
+ // Test create stream writer with unsupported charset
+ file = NewFile()
+ file.Sheet.Delete("xl/worksheets/sheet1.xml")
+ file.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ _, err = file.NewStreamWriter("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, file.Close())
+
+ // Test read cell
file = NewFile()
- delete(file.Sheet, "xl/worksheets/sheet1.xml")
- file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset
streamWriter, err = file.NewStreamWriter("Sheet1")
- assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{Cell{StyleID: styleID, Value: "Data"}}))
+ assert.NoError(t, streamWriter.Flush())
+ cellValue, err := file.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Data", cellValue)
+
+ // Test stream reader for a worksheet with huge amounts of data
+ file, err = OpenFile(filepath.Join("test", "TestStreamWriter.xlsx"))
+ assert.NoError(t, err)
+ rows, err := file.Rows("Sheet1")
+ assert.NoError(t, err)
+ cells := 0
+ for rows.Next() {
+ row, err := rows.Columns()
+ assert.NoError(t, err)
+ cells += len(row)
+ }
+ assert.NoError(t, rows.Close())
+ assert.Equal(t, 2559562, cells)
+ // Save spreadsheet with password.
+ assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"}))
+ assert.NoError(t, file.Close())
}
-func TestStreamTable(t *testing.T) {
+func TestStreamSetColStyle(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetColStyle(3, 2, 0))
+ assert.Equal(t, ErrColumnNumber, streamWriter.SetColStyle(0, 3, 20))
+ assert.Equal(t, ErrColumnNumber, streamWriter.SetColStyle(MaxColumns+1, 3, 20))
+ assert.Equal(t, newInvalidStyleID(2), streamWriter.SetColStyle(1, 3, 2))
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
+ assert.Equal(t, ErrStreamSetColStyle, streamWriter.SetColStyle(2, 3, 0))
+
+ file = NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ // Test set column style with unsupported charset style sheet
+ file.Styles = nil
+ file.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ streamWriter, err = file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.EqualError(t, streamWriter.SetColStyle(3, 2, 0), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestStreamSetColWidth(t *testing.T) {
file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ styleID, err := file.NewStyle(&Style{
+ Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1},
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetColWidth(3, 2, 20))
+ assert.NoError(t, streamWriter.SetColStyle(3, 2, styleID))
+ assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20))
+ assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20))
+ assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1))
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
+ assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20))
+ assert.NoError(t, streamWriter.Flush())
+}
+
+func TestStreamSetPanes(t *testing.T) {
+ file, paneOpts := NewFile(), &Panes{
+ Freeze: true,
+ Split: false,
+ XSplit: 1,
+ YSplit: 0,
+ TopLeftCell: "B1",
+ ActivePane: "topRight",
+ Selection: []Selection{
+ {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
+ },
+ }
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetPanes(paneOpts))
+ assert.Equal(t, ErrParameterInvalid, streamWriter.SetPanes(nil))
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
+ assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts))
+}
- // Write some rows. We want enough rows to force a temp file (>16MB).
+func TestStreamTable(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ // Test add table without table header
+ assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:C2"}), "XML syntax error on line 2: unexpected EOF")
+ // Write some rows. We want enough rows to force a temp file (>16MB)
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
row := []interface{}{1, 2, 3}
for r := 2; r < 10000; r++ {
assert.NoError(t, streamWriter.SetRow(fmt.Sprintf("A%d", r), row))
}
- // Write a table.
- assert.NoError(t, streamWriter.AddTable("A1", "C2", ``))
+ // Write a table
+ assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C2"}))
assert.NoError(t, streamWriter.Flush())
- // Verify the table has names.
+ // Verify the table has names
var table xlsxTable
- assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table))
+ val, ok := file.Pkg.Load("xl/tables/table1.xml")
+ assert.True(t, ok)
+ assert.NoError(t, xml.Unmarshal(val.([]byte), &table))
assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name)
assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name)
assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name)
- assert.NoError(t, streamWriter.AddTable("A1", "C1", ``))
+ assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"}))
+
+ // Test add table with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"}))
+ assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"}))
+ // Test add table with invalid table name
+ assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}))
+ // Test add table with row number exceeds maximum limit
+ assert.Equal(t, ErrMaxRows, streamWriter.AddTable(&Table{Range: "A1048576:C1048576"}))
+ // Test add table with unsupported charset content types
+ file.ContentTypes = nil
+ file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:C2"}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestStreamMergeCells(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.MergeCell("A1", "D1"))
+ // Test merge cells with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.MergeCell("A", "D1"))
+ assert.NoError(t, streamWriter.Flush())
+ // Save spreadsheet by the given path
+ assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx")))
+}
- // Test add table with illegal formatset.
- assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string")
- // Test add table with illegal cell coordinates.
- assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
+func TestStreamInsertPageBreak(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.InsertPageBreak("A1"))
+ assert.NoError(t, streamWriter.Flush())
+ // Save spreadsheet by the given path
+ assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamInsertPageBreak.xlsx")))
}
func TestNewStreamWriter(t *testing.T) {
// Test error exceptions
file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
_, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
_, err = file.NewStreamWriter("SheetN")
- assert.EqualError(t, err, "sheet SheetN is not exist")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test new stream write with invalid sheet name
+ _, err = file.NewStreamWriter("Sheet:1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
+}
+
+func TestStreamMarshalAttrs(t *testing.T) {
+ var r *RowOpts
+ attrs, err := r.marshalAttrs()
+ assert.NoError(t, err)
+ assert.Empty(t, attrs)
}
-func TestSetRow(t *testing.T) {
+func TestStreamSetRow(t *testing.T) {
// Test error exceptions
file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.SetRow("A", []interface{}{}))
+ // Test set row with non-ascending row number
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{}))
+ assert.Equal(t, newStreamSetRowError(1), streamWriter.SetRow("A1", []interface{}{}))
+ // Test set row with unsupported charset workbook
+ file.WorkBook = nil
+ file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, streamWriter.SetRow("A2", []interface{}{time.Now()}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestStreamSetRowNilValues(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}}))
+ streamWriter.Flush()
+ ws, err := file.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ assert.NotEqual(t, ws.SheetData.Row[0].C[0].XMLName.Local, "c")
+}
+
+func TestStreamSetRowWithStyle(t *testing.T) {
+ file := NewFile()
+ defer func() {
+ assert.NoError(t, file.Close())
+ }()
+ grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "777777"}})
+ assert.NoError(t, err)
+ blueStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "0000FF"}})
+ assert.NoError(t, err)
+
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
- assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
+ assert.NoError(t, streamWriter.SetRow("A1", []interface{}{
+ "value1",
+ Cell{Value: "value2"},
+ &Cell{Value: "value2"},
+ Cell{StyleID: blueStyleID, Value: "value3"},
+ &Cell{StyleID: blueStyleID, Value: "value3"},
+ }, RowOpts{StyleID: grayStyleID}))
+ assert.NoError(t, streamWriter.Flush())
+
+ ws, err := file.workSheetReader("Sheet1")
+ assert.NoError(t, err)
+ for colIdx, expected := range []int{grayStyleID, grayStyleID, grayStyleID, blueStyleID, blueStyleID} {
+ assert.Equal(t, expected, ws.SheetData.Row[0].C[colIdx].S)
+ }
}
-func TestSetCellValFunc(t *testing.T) {
+func TestStreamSetCellValFunc(t *testing.T) {
+ f := NewFile()
+ defer func() {
+ assert.NoError(t, f.Close())
+ }()
+ sw, err := f.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
c := &xlsxC{}
- assert.NoError(t, setCellValFunc(c, 128))
- assert.NoError(t, setCellValFunc(c, int8(-128)))
- assert.NoError(t, setCellValFunc(c, int16(-32768)))
- assert.NoError(t, setCellValFunc(c, int32(-2147483648)))
- assert.NoError(t, setCellValFunc(c, int64(-9223372036854775808)))
- assert.NoError(t, setCellValFunc(c, uint(128)))
- assert.NoError(t, setCellValFunc(c, uint8(255)))
- assert.NoError(t, setCellValFunc(c, uint16(65535)))
- assert.NoError(t, setCellValFunc(c, uint32(4294967295)))
- assert.NoError(t, setCellValFunc(c, uint64(18446744073709551615)))
- assert.NoError(t, setCellValFunc(c, float32(100.1588)))
- assert.NoError(t, setCellValFunc(c, float64(100.1588)))
- assert.NoError(t, setCellValFunc(c, " Hello"))
- assert.NoError(t, setCellValFunc(c, []byte(" Hello")))
- assert.NoError(t, setCellValFunc(c, time.Now().UTC()))
- assert.NoError(t, setCellValFunc(c, time.Duration(1e13)))
- assert.NoError(t, setCellValFunc(c, true))
- assert.NoError(t, setCellValFunc(c, nil))
- assert.NoError(t, setCellValFunc(c, complex64(5+10i)))
+ for _, val := range []interface{}{
+ 128,
+ int8(-128),
+ int16(-32768),
+ int32(-2147483648),
+ int64(-9223372036854775808),
+ uint(128),
+ uint8(255),
+ uint16(65535),
+ uint32(4294967295),
+ uint64(18446744073709551615),
+ float32(100.1588),
+ 100.1588,
+ " Hello",
+ []byte(" Hello"),
+ time.Now().UTC(),
+ time.Duration(1e13),
+ true,
+ nil,
+ complex64(5 + 10i),
+ } {
+ assert.NoError(t, sw.setCellValFunc(c, val))
+ }
+}
+
+func TestStreamWriterOutlineLevel(t *testing.T) {
+ file := NewFile()
+ streamWriter, err := file.NewStreamWriter("Sheet1")
+ assert.NoError(t, err)
+
+ // Test set outlineLevel in row
+ assert.NoError(t, streamWriter.SetRow("A1", nil, RowOpts{OutlineLevel: 1}))
+ assert.NoError(t, streamWriter.SetRow("A2", nil, RowOpts{OutlineLevel: 7}))
+ assert.ErrorIs(t, ErrOutlineLevel, streamWriter.SetRow("A3", nil, RowOpts{OutlineLevel: 8}))
+
+ assert.NoError(t, streamWriter.Flush())
+ // Save spreadsheet by the given path
+ assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx")))
+
+ file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx"))
+ assert.NoError(t, err)
+ for rowIdx, expected := range []uint8{1, 7, 0} {
+ level, err := file.GetRowOutlineLevel("Sheet1", rowIdx+1)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, level)
+ }
+ assert.NoError(t, file.Close())
+}
+
+func TestStreamWriterReader(t *testing.T) {
+ var (
+ err error
+ sw = StreamWriter{
+ rawData: bufferedWriter{},
+ }
+ )
+ sw.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-")
+ assert.NoError(t, err)
+ assert.NoError(t, sw.rawData.tmp.Close())
+ // Test reader stat a closed temp file
+ _, err = sw.rawData.Reader()
+ assert.Error(t, err)
+ _, err = sw.getRowValues(1, 1, 1)
+ assert.Error(t, err)
+ os.Remove(sw.rawData.tmp.Name())
+
+ sw = StreamWriter{
+ file: NewFile(),
+ rawData: bufferedWriter{},
+ }
+ // Test getRowValues without expected row
+ sw.rawData.buf.WriteString("
")
+ _, err = sw.getRowValues(1, 1, 1)
+ assert.NoError(t, err)
+ sw.rawData.buf.Reset()
+ // Test getRowValues with illegal cell reference
+ sw.rawData.buf.WriteString("
")
+ _, err = sw.getRowValues(1, 1, 1)
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
+ sw.rawData.buf.Reset()
+ // Test getRowValues with invalid c element characters
+ sw.rawData.buf.WriteString("
")
+ _, err = sw.getRowValues(1, 1, 1)
+ assert.EqualError(t, err, "XML syntax error on line 1: element closed by ")
+ sw.rawData.buf.Reset()
+}
+
+func TestStreamWriterGetRowElement(t *testing.T) {
+ // Test get row element without r attribute
+ dec := xml.NewDecoder(strings.NewReader("
"))
+ for {
+ token, err := dec.Token()
+ if err == io.EOF {
+ break
+ }
+ _, ok := getRowElement(token, 0)
+ assert.False(t, ok)
+ }
}
diff --git a/styles.go b/styles.go
index c3a2393160..5992cc39e0 100644
--- a/styles.go
+++ b/styles.go
@@ -1,1020 +1,39 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"bytes"
- "encoding/json"
"encoding/xml"
- "errors"
"fmt"
"io"
- "log"
"math"
"reflect"
+ "sort"
"strconv"
"strings"
)
-// Excel styles can reference number formats that are built-in, all of which
-// have an id less than 164. This is a possibly incomplete list comprised of
-// as many of them as I could find.
-var builtInNumFmt = map[int]string{
- 0: "general",
- 1: "0",
- 2: "0.00",
- 3: "#,##0",
- 4: "#,##0.00",
- 9: "0%",
- 10: "0.00%",
- 11: "0.00e+00",
- 12: "# ?/?",
- 13: "# ??/??",
- 14: "mm-dd-yy",
- 15: "d-mmm-yy",
- 16: "d-mmm",
- 17: "mmm-yy",
- 18: "h:mm am/pm",
- 19: "h:mm:ss am/pm",
- 20: "h:mm",
- 21: "h:mm:ss",
- 22: "m/d/yy h:mm",
- 37: "#,##0 ;(#,##0)",
- 38: "#,##0 ;[red](#,##0)",
- 39: "#,##0.00;(#,##0.00)",
- 40: "#,##0.00;[red](#,##0.00)",
- 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`,
- 42: `_("$"* #,##0_);_("$* \(#,##0\);_("$"* "-"_);_(@_)`,
- 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`,
- 44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`,
- 45: "mm:ss",
- 46: "[h]:mm:ss",
- 47: "mmss.0",
- 48: "##0.0e+0",
- 49: "@",
-}
-
-// langNumFmt defined number format code (with unicode values provided for
-// language glyphs where they occur) in different language.
-var langNumFmt = map[string]map[int]string{
- "zh-tw": {
- 27: "[$-404]e/m/d",
- 28: `[$-404]e"年"m"月"d"日"`,
- 29: `[$-404]e"年"m"月"d"日"`,
- 30: "m/d/yy",
- 31: `yyyy"年"m"月"d"日"`,
- 32: `hh"時"mm"分"`,
- 33: `hh"時"mm"分"ss"秒"`,
- 34: `上午/下午 hh"時"mm"分"`,
- 35: `上午/下午 hh"時"mm"分"ss"秒"`,
- 36: "[$-404]e/m/d",
- 50: "[$-404]e/m/d",
- 51: `[$-404]e"年"m"月"d"日"`,
- 52: `上午/下午 hh"時"mm"分"`,
- 53: `上午/下午 hh"時"mm"分"ss"秒"`,
- 54: `[$-404]e"年"m"月"d"日"`,
- 55: `上午/下午 hh"時"mm"分"`,
- 56: `上午/下午 hh"時"mm"分"ss"秒"`,
- 57: "[$-404]e/m/d",
- 58: `[$-404]e"年"m"月"d"日"`,
- },
- "zh-cn": {
- 27: `yyyy"年"m"月"`,
- 28: `m"月"d"日"`,
- 29: `m"月"d"日"`,
- 30: "m-d-yy",
- 31: `yyyy"年"m"月"d"日"`,
- 32: `h"时"mm"分"`,
- 33: `h"时"mm"分"ss"秒"`,
- 34: `上午/下午 h"时"mm"分"`,
- 35: `上午/下午 h"时"mm"分"ss"秒"`,
- 36: `yyyy"年"m"月"`,
- 50: `yyyy"年"m"月"`,
- 51: `m"月"d"日"`,
- 52: `yyyy"年"m"月"`,
- 53: `m"月"d"日"`,
- 54: `m"月"d"日"`,
- 55: `上午/下午 h"时"mm"分"`,
- 56: `上午/下午 h"时"mm"分"ss"秒"`,
- 57: `yyyy"年"m"月"`,
- 58: `m"月"d"日"`,
- },
- "zh-tw_unicode": {
- 27: "[$-404]e/m/d",
- 28: `[$-404]e"5E74"m"6708"d"65E5"`,
- 29: `[$-404]e"5E74"m"6708"d"65E5"`,
- 30: "m/d/yy",
- 31: `yyyy"5E74"m"6708"d"65E5"`,
- 32: `hh"6642"mm"5206"`,
- 33: `hh"6642"mm"5206"ss"79D2"`,
- 34: `4E0A5348/4E0B5348hh"6642"mm"5206"`,
- 35: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`,
- 36: "[$-404]e/m/d",
- 50: "[$-404]e/m/d",
- 51: `[$-404]e"5E74"m"6708"d"65E5"`,
- 52: `4E0A5348/4E0B5348hh"6642"mm"5206"`,
- 53: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`,
- 54: `[$-404]e"5E74"m"6708"d"65E5"`,
- 55: `4E0A5348/4E0B5348hh"6642"mm"5206"`,
- 56: `4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2"`,
- 57: "[$-404]e/m/d",
- 58: `[$-404]e"5E74"m"6708"d"65E5"`,
- },
- "zh-cn_unicode": {
- 27: `yyyy"5E74"m"6708"`,
- 28: `m"6708"d"65E5"`,
- 29: `m"6708"d"65E5"`,
- 30: "m-d-yy",
- 31: `yyyy"5E74"m"6708"d"65E5"`,
- 32: `h"65F6"mm"5206"`,
- 33: `h"65F6"mm"5206"ss"79D2"`,
- 34: `4E0A5348/4E0B5348h"65F6"mm"5206"`,
- 35: `4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2"`,
- 36: `yyyy"5E74"m"6708"`,
- 50: `yyyy"5E74"m"6708"`,
- 51: `m"6708"d"65E5"`,
- 52: `yyyy"5E74"m"6708"`,
- 53: `m"6708"d"65E5"`,
- 54: `m"6708"d"65E5"`,
- 55: `4E0A5348/4E0B5348h"65F6"mm"5206"`,
- 56: `4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2"`,
- 57: `yyyy"5E74"m"6708"`,
- 58: `m"6708"d"65E5"`,
- },
- "ja-jp": {
- 27: "[$-411]ge.m.d",
- 28: `[$-411]ggge"年"m"月"d"日"`,
- 29: `[$-411]ggge"年"m"月"d"日"`,
- 30: "m/d/yy",
- 31: `yyyy"年"m"月"d"日"`,
- 32: `h"時"mm"分"`,
- 33: `h"時"mm"分"ss"秒"`,
- 34: `yyyy"年"m"月"`,
- 35: `m"月"d"日"`,
- 36: "[$-411]ge.m.d",
- 50: "[$-411]ge.m.d",
- 51: `[$-411]ggge"年"m"月"d"日"`,
- 52: `yyyy"年"m"月"`,
- 53: `m"月"d"日"`,
- 54: `[$-411]ggge"年"m"月"d"日"`,
- 55: `yyyy"年"m"月"`,
- 56: `m"月"d"日"`,
- 57: "[$-411]ge.m.d",
- 58: `[$-411]ggge"年"m"月"d"日"`,
- },
- "ko-kr": {
- 27: `yyyy"年" mm"月" dd"日"`,
- 28: "mm-dd",
- 29: "mm-dd",
- 30: "mm-dd-yy",
- 31: `yyyy"년" mm"월" dd"일"`,
- 32: `h"시" mm"분"`,
- 33: `h"시" mm"분" ss"초"`,
- 34: `yyyy-mm-dd`,
- 35: `yyyy-mm-dd`,
- 36: `yyyy"年" mm"月" dd"日"`,
- 50: `yyyy"年" mm"月" dd"日"`,
- 51: "mm-dd",
- 52: "yyyy-mm-dd",
- 53: "yyyy-mm-dd",
- 54: "mm-dd",
- 55: "yyyy-mm-dd",
- 56: "yyyy-mm-dd",
- 57: `yyyy"年" mm"月" dd"日"`,
- 58: "mm-dd",
- },
- "ja-jp_unicode": {
- 27: "[$-411]ge.m.d",
- 28: `[$-411]ggge"5E74"m"6708"d"65E5"`,
- 29: `[$-411]ggge"5E74"m"6708"d"65E5"`,
- 30: "m/d/yy",
- 31: `yyyy"5E74"m"6708"d"65E5"`,
- 32: `h"6642"mm"5206"`,
- 33: `h"6642"mm"5206"ss"79D2"`,
- 34: `yyyy"5E74"m"6708"`,
- 35: `m"6708"d"65E5"`,
- 36: "[$-411]ge.m.d",
- 50: "[$-411]ge.m.d",
- 51: `[$-411]ggge"5E74"m"6708"d"65E5"`,
- 52: `yyyy"5E74"m"6708"`,
- 53: `m"6708"d"65E5"`,
- 54: `[$-411]ggge"5E74"m"6708"d"65E5"`,
- 55: `yyyy"5E74"m"6708"`,
- 56: `m"6708"d"65E5"`,
- 57: "[$-411]ge.m.d",
- 58: `[$-411]ggge"5E74"m"6708"d"65E5"`,
- },
- "ko-kr_unicode": {
- 27: `yyyy"5E74" mm"6708" dd"65E5"`,
- 28: "mm-dd",
- 29: "mm-dd",
- 30: "mm-dd-yy",
- 31: `yyyy"B144" mm"C6D4" dd"C77C"`,
- 32: `h"C2DC" mm"BD84"`,
- 33: `h"C2DC" mm"BD84" ss"CD08"`,
- 34: "yyyy-mm-dd",
- 35: "yyyy-mm-dd",
- 36: `yyyy"5E74" mm"6708" dd"65E5"`,
- 50: `yyyy"5E74" mm"6708" dd"65E5"`,
- 51: "mm-dd",
- 52: "yyyy-mm-dd",
- 53: "yyyy-mm-dd",
- 54: "mm-dd",
- 55: "yyyy-mm-dd",
- 56: "yyyy-mm-dd",
- 57: `yyyy"5E74" mm"6708" dd"65E5"`,
- 58: "mm-dd",
- },
- "th-th": {
- 59: "t0",
- 60: "t0.00",
- 61: "t#,##0",
- 62: "t#,##0.00",
- 67: "t0%",
- 68: "t0.00%",
- 69: "t# ?/?",
- 70: "t# ??/??",
- 71: "ว/ด/ปปปป",
- 72: "ว-ดดด-ปป",
- 73: "ว-ดดด",
- 74: "ดดด-ปป",
- 75: "ช:นน",
- 76: "ช:นน:ทท",
- 77: "ว/ด/ปปปป ช:นน",
- 78: "นน:ทท",
- 79: "[ช]:นน:ทท",
- 80: "นน:ทท.0",
- 81: "d/m/bb",
- },
- "th-th_unicode": {
- 59: "t0",
- 60: "t0.00",
- 61: "t#,##0",
- 62: "t#,##0.00",
- 67: "t0%",
- 68: "t0.00%",
- 69: "t# ?/?",
- 70: "t# ??/??",
- 71: "0E27/0E14/0E1B0E1B0E1B0E1B",
- 72: "0E27-0E140E140E14-0E1B0E1B",
- 73: "0E27-0E140E140E14",
- 74: "0E140E140E14-0E1B0E1B",
- 75: "0E0A:0E190E19",
- 76: "0E0A:0E190E19:0E170E17",
- 77: "0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E19",
- 78: "0E190E19:0E170E17",
- 79: "[0E0A]:0E190E19:0E170E17",
- 80: "0E190E19:0E170E17.0",
- 81: "d/m/bb",
- },
-}
-
-// currencyNumFmt defined the currency number format map.
-var currencyNumFmt = map[int]string{
- 164: `"CN¥",##0.00`,
- 165: "[$$-409]#,##0.00",
- 166: "[$$-45C]#,##0.00",
- 167: "[$$-1004]#,##0.00",
- 168: "[$$-404]#,##0.00",
- 169: "[$$-C09]#,##0.00",
- 170: "[$$-2809]#,##0.00",
- 171: "[$$-1009]#,##0.00",
- 172: "[$$-2009]#,##0.00",
- 173: "[$$-1409]#,##0.00",
- 174: "[$$-4809]#,##0.00",
- 175: "[$$-2C09]#,##0.00",
- 176: "[$$-2409]#,##0.00",
- 177: "[$$-1000]#,##0.00",
- 178: `#,##0.00\ [$$-C0C]`,
- 179: "[$$-475]#,##0.00",
- 180: "[$$-83E]#,##0.00",
- 181: `[$$-86B]\ #,##0.00`,
- 182: `[$$-340A]\ #,##0.00`,
- 183: "[$$-240A]#,##0.00",
- 184: `[$$-300A]\ #,##0.00`,
- 185: "[$$-440A]#,##0.00",
- 186: "[$$-80A]#,##0.00",
- 187: "[$$-500A]#,##0.00",
- 188: "[$$-540A]#,##0.00",
- 189: `[$$-380A]\ #,##0.00`,
- 190: "[$£-809]#,##0.00",
- 191: "[$£-491]#,##0.00",
- 192: "[$£-452]#,##0.00",
- 193: "[$¥-804]#,##0.00",
- 194: "[$¥-411]#,##0.00",
- 195: "[$¥-478]#,##0.00",
- 196: "[$¥-451]#,##0.00",
- 197: "[$¥-480]#,##0.00",
- 198: "#,##0.00\\ [$\u058F-42B]",
- 199: "[$\u060B-463]#,##0.00",
- 200: "[$\u060B-48C]#,##0.00",
- 201: "[$\u09F3-845]\\ #,##0.00",
- 202: "#,##0.00[$\u17DB-453]",
- 203: "[$\u20A1-140A]#,##0.00",
- 204: "[$\u20A6-468]\\ #,##0.00",
- 205: "[$\u20A6-470]\\ #,##0.00",
- 206: "[$\u20A9-412]#,##0.00",
- 207: "[$\u20AA-40D]\\ #,##0.00",
- 208: "#,##0.00\\ [$\u20AB-42A]",
- 209: "#,##0.00\\ [$\u20AC-42D]",
- 210: "#,##0.00\\ [$\u20AC-47E]",
- 211: "#,##0.00\\ [$\u20AC-403]",
- 212: "#,##0.00\\ [$\u20AC-483]",
- 213: "[$\u20AC-813]\\ #,##0.00",
- 214: "[$\u20AC-413]\\ #,##0.00",
- 215: "[$\u20AC-1809]#,##0.00",
- 216: "#,##0.00\\ [$\u20AC-425]",
- 217: "[$\u20AC-2]\\ #,##0.00",
- 218: "#,##0.00\\ [$\u20AC-1]",
- 219: "#,##0.00\\ [$\u20AC-40B]",
- 220: "#,##0.00\\ [$\u20AC-80C]",
- 221: "#,##0.00\\ [$\u20AC-40C]",
- 222: "#,##0.00\\ [$\u20AC-140C]",
- 223: "#,##0.00\\ [$\u20AC-180C]",
- 224: "[$\u20AC-200C]#,##0.00",
- 225: "#,##0.00\\ [$\u20AC-456]",
- 226: "#,##0.00\\ [$\u20AC-C07]",
- 227: "#,##0.00\\ [$\u20AC-407]",
- 228: "#,##0.00\\ [$\u20AC-1007]",
- 229: "#,##0.00\\ [$\u20AC-408]",
- 230: "#,##0.00\\ [$\u20AC-243B]",
- 231: "[$\u20AC-83C]#,##0.00",
- 232: "[$\u20AC-410]\\ #,##0.00",
- 233: "[$\u20AC-476]#,##0.00",
- 234: "#,##0.00\\ [$\u20AC-2C1A]",
- 235: "[$\u20AC-426]\\ #,##0.00",
- 236: "#,##0.00\\ [$\u20AC-427]",
- 237: "#,##0.00\\ [$\u20AC-82E]",
- 238: "#,##0.00\\ [$\u20AC-46E]",
- 239: "[$\u20AC-43A]#,##0.00",
- 240: "#,##0.00\\ [$\u20AC-C3B]",
- 241: "#,##0.00\\ [$\u20AC-482]",
- 242: "#,##0.00\\ [$\u20AC-816]",
- 243: "#,##0.00\\ [$\u20AC-301A]",
- 244: "#,##0.00\\ [$\u20AC-203B]",
- 245: "#,##0.00\\ [$\u20AC-41B]",
- 246: "#,##0.00\\ [$\u20AC-424]",
- 247: "#,##0.00\\ [$\u20AC-C0A]",
- 248: "#,##0.00\\ [$\u20AC-81D]",
- 249: "#,##0.00\\ [$\u20AC-484]",
- 250: "#,##0.00\\ [$\u20AC-42E]",
- 251: "[$\u20AC-462]\\ #,##0.00",
- 252: "#,##0.00\\ [$₭-454]",
- 253: "#,##0.00\\ [$₮-450]",
- 254: "[$\u20AE-C50]#,##0.00",
- 255: "[$\u20B1-3409]#,##0.00",
- 256: "[$\u20B1-464]#,##0.00",
- 257: "#,##0.00[$\u20B4-422]",
- 258: "[$\u20B8-43F]#,##0.00",
- 259: "[$\u20B9-460]#,##0.00",
- 260: "[$\u20B9-4009]\\ #,##0.00",
- 261: "[$\u20B9-447]\\ #,##0.00",
- 262: "[$\u20B9-439]\\ #,##0.00",
- 263: "[$\u20B9-44B]\\ #,##0.00",
- 264: "[$\u20B9-860]#,##0.00",
- 265: "[$\u20B9-457]\\ #,##0.00",
- 266: "[$\u20B9-458]#,##0.00",
- 267: "[$\u20B9-44E]\\ #,##0.00",
- 268: "[$\u20B9-861]#,##0.00",
- 269: "[$\u20B9-448]\\ #,##0.00",
- 270: "[$\u20B9-446]\\ #,##0.00",
- 271: "[$\u20B9-44F]\\ #,##0.00",
- 272: "[$\u20B9-459]#,##0.00",
- 273: "[$\u20B9-449]\\ #,##0.00",
- 274: "[$\u20B9-820]#,##0.00",
- 275: "#,##0.00\\ [$\u20BA-41F]",
- 276: "#,##0.00\\ [$\u20BC-42C]",
- 277: "#,##0.00\\ [$\u20BC-82C]",
- 278: "#,##0.00\\ [$\u20BD-419]",
- 279: "#,##0.00[$\u20BD-485]",
- 280: "#,##0.00\\ [$\u20BE-437]",
- 281: "[$B/.-180A]\\ #,##0.00",
- 282: "[$Br-472]#,##0.00",
- 283: "[$Br-477]#,##0.00",
- 284: "#,##0.00[$Br-473]",
- 285: "[$Bs-46B]\\ #,##0.00",
- 286: "[$Bs-400A]\\ #,##0.00",
- 287: "[$Bs.-200A]\\ #,##0.00",
- 288: "[$BWP-832]\\ #,##0.00",
- 289: "[$C$-4C0A]#,##0.00",
- 290: "[$CA$-85D]#,##0.00",
- 291: "[$CA$-47C]#,##0.00",
- 292: "[$CA$-45D]#,##0.00",
- 293: "[$CFA-340C]#,##0.00",
- 294: "[$CFA-280C]#,##0.00",
- 295: "#,##0.00\\ [$CFA-867]",
- 296: "#,##0.00\\ [$CFA-488]",
- 297: "#,##0.00\\ [$CHF-100C]",
- 298: "[$CHF-1407]\\ #,##0.00",
- 299: "[$CHF-807]\\ #,##0.00",
- 300: "[$CHF-810]\\ #,##0.00",
- 301: "[$CHF-417]\\ #,##0.00",
- 302: "[$CLP-47A]\\ #,##0.00",
- 303: "[$CN¥-850]#,##0.00",
- 304: "#,##0.00\\ [$DZD-85F]",
- 305: "[$FCFA-2C0C]#,##0.00",
- 306: "#,##0.00\\ [$Ft-40E]",
- 307: "[$G-3C0C]#,##0.00",
- 308: "[$Gs.-3C0A]\\ #,##0.00",
- 309: "[$GTQ-486]#,##0.00",
- 310: "[$HK$-C04]#,##0.00",
- 311: "[$HK$-3C09]#,##0.00",
- 312: "#,##0.00\\ [$HRK-41A]",
- 313: "[$IDR-3809]#,##0.00",
- 314: "[$IQD-492]#,##0.00",
- 315: "#,##0.00\\ [$ISK-40F]",
- 316: "[$K-455]#,##0.00",
- 317: "#,##0.00\\ [$K\u010D-405]",
- 318: "#,##0.00\\ [$KM-141A]",
- 319: "#,##0.00\\ [$KM-101A]",
- 320: "#,##0.00\\ [$KM-181A]",
- 321: "[$kr-438]\\ #,##0.00",
- 322: "[$kr-43B]\\ #,##0.00",
- 323: "#,##0.00\\ [$kr-83B]",
- 324: "[$kr-414]\\ #,##0.00",
- 325: "[$kr-814]\\ #,##0.00",
- 326: "#,##0.00\\ [$kr-41D]",
- 327: "[$kr.-406]\\ #,##0.00",
- 328: "[$kr.-46F]\\ #,##0.00",
- 329: "[$Ksh-441]#,##0.00",
- 330: "[$L-818]#,##0.00",
- 331: "[$L-819]#,##0.00",
- 332: "[$L-480A]\\ #,##0.00",
- 333: "#,##0.00\\ [$Lek\u00EB-41C]",
- 334: "[$MAD-45F]#,##0.00",
- 335: "[$MAD-380C]#,##0.00",
- 336: "#,##0.00\\ [$MAD-105F]",
- 337: "[$MOP$-1404]#,##0.00",
- 338: "#,##0.00\\ [$MVR-465]_-",
- 339: "#,##0.00[$Nfk-873]",
- 340: "[$NGN-466]#,##0.00",
- 341: "[$NGN-467]#,##0.00",
- 342: "[$NGN-469]#,##0.00",
- 343: "[$NGN-471]#,##0.00",
- 344: "[$NOK-103B]\\ #,##0.00",
- 345: "[$NOK-183B]\\ #,##0.00",
- 346: "[$NZ$-481]#,##0.00",
- 347: "[$PKR-859]\\ #,##0.00",
- 348: "[$PYG-474]#,##0.00",
- 349: "[$Q-100A]#,##0.00",
- 350: "[$R-436]\\ #,##0.00",
- 351: "[$R-1C09]\\ #,##0.00",
- 352: "[$R-435]\\ #,##0.00",
- 353: "[$R$-416]\\ #,##0.00",
- 354: "[$RD$-1C0A]#,##0.00",
- 355: "#,##0.00\\ [$RF-487]",
- 356: "[$RM-4409]#,##0.00",
- 357: "[$RM-43E]#,##0.00",
- 358: "#,##0.00\\ [$RON-418]",
- 359: "[$Rp-421]#,##0.00",
- 360: "[$Rs-420]#,##0.00_-",
- 361: "[$Rs.-849]\\ #,##0.00",
- 362: "#,##0.00\\ [$RSD-81A]",
- 363: "#,##0.00\\ [$RSD-C1A]",
- 364: "#,##0.00\\ [$RUB-46D]",
- 365: "#,##0.00\\ [$RUB-444]",
- 366: "[$S/.-C6B]\\ #,##0.00",
- 367: "[$S/.-280A]\\ #,##0.00",
- 368: "#,##0.00\\ [$SEK-143B]",
- 369: "#,##0.00\\ [$SEK-1C3B]",
- 370: "#,##0.00\\ [$so\u02BBm-443]",
- 371: "#,##0.00\\ [$so\u02BBm-843]",
- 372: "#,##0.00\\ [$SYP-45A]",
- 373: "[$THB-41E]#,##0.00",
- 374: "#,##0.00[$TMT-442]",
- 375: "[$US$-3009]#,##0.00",
- 376: "[$ZAR-46C]\\ #,##0.00",
- 377: "[$ZAR-430]#,##0.00",
- 378: "[$ZAR-431]#,##0.00",
- 379: "[$ZAR-432]\\ #,##0.00",
- 380: "[$ZAR-433]#,##0.00",
- 381: "[$ZAR-434]\\ #,##0.00",
- 382: "#,##0.00\\ [$z\u0142-415]",
- 383: "#,##0.00\\ [$\u0434\u0435\u043D-42F]",
- 384: "#,##0.00\\ [$КМ-201A]",
- 385: "#,##0.00\\ [$КМ-1C1A]",
- 386: "#,##0.00\\ [$\u043B\u0432.-402]",
- 387: "#,##0.00\\ [$р.-423]",
- 388: "#,##0.00\\ [$\u0441\u043E\u043C-440]",
- 389: "#,##0.00\\ [$\u0441\u043E\u043C-428]",
- 390: "[$\u062C.\u0645.-C01]\\ #,##0.00_-",
- 391: "[$\u062F.\u0623.-2C01]\\ #,##0.00_-",
- 392: "[$\u062F.\u0625.-3801]\\ #,##0.00_-",
- 393: "[$\u062F.\u0628.-3C01]\\ #,##0.00_-",
- 394: "[$\u062F.\u062A.-1C01]\\ #,##0.00_-",
- 395: "[$\u062F.\u062C.-1401]\\ #,##0.00_-",
- 396: "[$\u062F.\u0639.-801]\\ #,##0.00_-",
- 397: "[$\u062F.\u0643.-3401]\\ #,##0.00_-",
- 398: "[$\u062F.\u0644.-1001]#,##0.00_-",
- 399: "[$\u062F.\u0645.-1801]\\ #,##0.00_-",
- 400: "[$\u0631-846]\\ #,##0.00",
- 401: "[$\u0631.\u0633.-401]\\ #,##0.00_-",
- 402: "[$\u0631.\u0639.-2001]\\ #,##0.00_-",
- 403: "[$\u0631.\u0642.-4001]\\ #,##0.00_-",
- 404: "[$\u0631.\u064A.-2401]\\ #,##0.00_-",
- 405: "[$\u0631\u06CC\u0627\u0644-429]#,##0.00_-",
- 406: "[$\u0644.\u0633.-2801]\\ #,##0.00_-",
- 407: "[$\u0644.\u0644.-3001]\\ #,##0.00_-",
- 408: "[$\u1265\u122D-45E]#,##0.00",
- 409: "[$\u0930\u0942-461]#,##0.00",
- 410: "[$\u0DBB\u0DD4.-45B]\\ #,##0.00",
- 411: "[$ADP]\\ #,##0.00",
- 412: "[$AED]\\ #,##0.00",
- 413: "[$AFA]\\ #,##0.00",
- 414: "[$AFN]\\ #,##0.00",
- 415: "[$ALL]\\ #,##0.00",
- 416: "[$AMD]\\ #,##0.00",
- 417: "[$ANG]\\ #,##0.00",
- 418: "[$AOA]\\ #,##0.00",
- 419: "[$ARS]\\ #,##0.00",
- 420: "[$ATS]\\ #,##0.00",
- 421: "[$AUD]\\ #,##0.00",
- 422: "[$AWG]\\ #,##0.00",
- 423: "[$AZM]\\ #,##0.00",
- 424: "[$AZN]\\ #,##0.00",
- 425: "[$BAM]\\ #,##0.00",
- 426: "[$BBD]\\ #,##0.00",
- 427: "[$BDT]\\ #,##0.00",
- 428: "[$BEF]\\ #,##0.00",
- 429: "[$BGL]\\ #,##0.00",
- 430: "[$BGN]\\ #,##0.00",
- 431: "[$BHD]\\ #,##0.00",
- 432: "[$BIF]\\ #,##0.00",
- 433: "[$BMD]\\ #,##0.00",
- 434: "[$BND]\\ #,##0.00",
- 435: "[$BOB]\\ #,##0.00",
- 436: "[$BOV]\\ #,##0.00",
- 437: "[$BRL]\\ #,##0.00",
- 438: "[$BSD]\\ #,##0.00",
- 439: "[$BTN]\\ #,##0.00",
- 440: "[$BWP]\\ #,##0.00",
- 441: "[$BYR]\\ #,##0.00",
- 442: "[$BZD]\\ #,##0.00",
- 443: "[$CAD]\\ #,##0.00",
- 444: "[$CDF]\\ #,##0.00",
- 445: "[$CHE]\\ #,##0.00",
- 446: "[$CHF]\\ #,##0.00",
- 447: "[$CHW]\\ #,##0.00",
- 448: "[$CLF]\\ #,##0.00",
- 449: "[$CLP]\\ #,##0.00",
- 450: "[$CNY]\\ #,##0.00",
- 451: "[$COP]\\ #,##0.00",
- 452: "[$COU]\\ #,##0.00",
- 453: "[$CRC]\\ #,##0.00",
- 454: "[$CSD]\\ #,##0.00",
- 455: "[$CUC]\\ #,##0.00",
- 456: "[$CVE]\\ #,##0.00",
- 457: "[$CYP]\\ #,##0.00",
- 458: "[$CZK]\\ #,##0.00",
- 459: "[$DEM]\\ #,##0.00",
- 460: "[$DJF]\\ #,##0.00",
- 461: "[$DKK]\\ #,##0.00",
- 462: "[$DOP]\\ #,##0.00",
- 463: "[$DZD]\\ #,##0.00",
- 464: "[$ECS]\\ #,##0.00",
- 465: "[$ECV]\\ #,##0.00",
- 466: "[$EEK]\\ #,##0.00",
- 467: "[$EGP]\\ #,##0.00",
- 468: "[$ERN]\\ #,##0.00",
- 469: "[$ESP]\\ #,##0.00",
- 470: "[$ETB]\\ #,##0.00",
- 471: "[$EUR]\\ #,##0.00",
- 472: "[$FIM]\\ #,##0.00",
- 473: "[$FJD]\\ #,##0.00",
- 474: "[$FKP]\\ #,##0.00",
- 475: "[$FRF]\\ #,##0.00",
- 476: "[$GBP]\\ #,##0.00",
- 477: "[$GEL]\\ #,##0.00",
- 478: "[$GHC]\\ #,##0.00",
- 479: "[$GHS]\\ #,##0.00",
- 480: "[$GIP]\\ #,##0.00",
- 481: "[$GMD]\\ #,##0.00",
- 482: "[$GNF]\\ #,##0.00",
- 483: "[$GRD]\\ #,##0.00",
- 484: "[$GTQ]\\ #,##0.00",
- 485: "[$GYD]\\ #,##0.00",
- 486: "[$HKD]\\ #,##0.00",
- 487: "[$HNL]\\ #,##0.00",
- 488: "[$HRK]\\ #,##0.00",
- 489: "[$HTG]\\ #,##0.00",
- 490: "[$HUF]\\ #,##0.00",
- 491: "[$IDR]\\ #,##0.00",
- 492: "[$IEP]\\ #,##0.00",
- 493: "[$ILS]\\ #,##0.00",
- 494: "[$INR]\\ #,##0.00",
- 495: "[$IQD]\\ #,##0.00",
- 496: "[$IRR]\\ #,##0.00",
- 497: "[$ISK]\\ #,##0.00",
- 498: "[$ITL]\\ #,##0.00",
- 499: "[$JMD]\\ #,##0.00",
- 500: "[$JOD]\\ #,##0.00",
- 501: "[$JPY]\\ #,##0.00",
- 502: "[$KAF]\\ #,##0.00",
- 503: "[$KES]\\ #,##0.00",
- 504: "[$KGS]\\ #,##0.00",
- 505: "[$KHR]\\ #,##0.00",
- 506: "[$KMF]\\ #,##0.00",
- 507: "[$KPW]\\ #,##0.00",
- 508: "[$KRW]\\ #,##0.00",
- 509: "[$KWD]\\ #,##0.00",
- 510: "[$KYD]\\ #,##0.00",
- 511: "[$KZT]\\ #,##0.00",
- 512: "[$LAK]\\ #,##0.00",
- 513: "[$LBP]\\ #,##0.00",
- 514: "[$LKR]\\ #,##0.00",
- 515: "[$LRD]\\ #,##0.00",
- 516: "[$LSL]\\ #,##0.00",
- 517: "[$LTL]\\ #,##0.00",
- 518: "[$LUF]\\ #,##0.00",
- 519: "[$LVL]\\ #,##0.00",
- 520: "[$LYD]\\ #,##0.00",
- 521: "[$MAD]\\ #,##0.00",
- 522: "[$MDL]\\ #,##0.00",
- 523: "[$MGA]\\ #,##0.00",
- 524: "[$MGF]\\ #,##0.00",
- 525: "[$MKD]\\ #,##0.00",
- 526: "[$MMK]\\ #,##0.00",
- 527: "[$MNT]\\ #,##0.00",
- 528: "[$MOP]\\ #,##0.00",
- 529: "[$MRO]\\ #,##0.00",
- 530: "[$MTL]\\ #,##0.00",
- 531: "[$MUR]\\ #,##0.00",
- 532: "[$MVR]\\ #,##0.00",
- 533: "[$MWK]\\ #,##0.00",
- 534: "[$MXN]\\ #,##0.00",
- 535: "[$MXV]\\ #,##0.00",
- 536: "[$MYR]\\ #,##0.00",
- 537: "[$MZM]\\ #,##0.00",
- 538: "[$MZN]\\ #,##0.00",
- 539: "[$NAD]\\ #,##0.00",
- 540: "[$NGN]\\ #,##0.00",
- 541: "[$NIO]\\ #,##0.00",
- 542: "[$NLG]\\ #,##0.00",
- 543: "[$NOK]\\ #,##0.00",
- 544: "[$NPR]\\ #,##0.00",
- 545: "[$NTD]\\ #,##0.00",
- 546: "[$NZD]\\ #,##0.00",
- 547: "[$OMR]\\ #,##0.00",
- 548: "[$PAB]\\ #,##0.00",
- 549: "[$PEN]\\ #,##0.00",
- 550: "[$PGK]\\ #,##0.00",
- 551: "[$PHP]\\ #,##0.00",
- 552: "[$PKR]\\ #,##0.00",
- 553: "[$PLN]\\ #,##0.00",
- 554: "[$PTE]\\ #,##0.00",
- 555: "[$PYG]\\ #,##0.00",
- 556: "[$QAR]\\ #,##0.00",
- 557: "[$ROL]\\ #,##0.00",
- 558: "[$RON]\\ #,##0.00",
- 559: "[$RSD]\\ #,##0.00",
- 560: "[$RUB]\\ #,##0.00",
- 561: "[$RUR]\\ #,##0.00",
- 562: "[$RWF]\\ #,##0.00",
- 563: "[$SAR]\\ #,##0.00",
- 564: "[$SBD]\\ #,##0.00",
- 565: "[$SCR]\\ #,##0.00",
- 566: "[$SDD]\\ #,##0.00",
- 567: "[$SDG]\\ #,##0.00",
- 568: "[$SDP]\\ #,##0.00",
- 569: "[$SEK]\\ #,##0.00",
- 570: "[$SGD]\\ #,##0.00",
- 571: "[$SHP]\\ #,##0.00",
- 572: "[$SIT]\\ #,##0.00",
- 573: "[$SKK]\\ #,##0.00",
- 574: "[$SLL]\\ #,##0.00",
- 575: "[$SOS]\\ #,##0.00",
- 576: "[$SPL]\\ #,##0.00",
- 577: "[$SRD]\\ #,##0.00",
- 578: "[$SRG]\\ #,##0.00",
- 579: "[$STD]\\ #,##0.00",
- 580: "[$SVC]\\ #,##0.00",
- 581: "[$SYP]\\ #,##0.00",
- 582: "[$SZL]\\ #,##0.00",
- 583: "[$THB]\\ #,##0.00",
- 584: "[$TJR]\\ #,##0.00",
- 585: "[$TJS]\\ #,##0.00",
- 586: "[$TMM]\\ #,##0.00",
- 587: "[$TMT]\\ #,##0.00",
- 588: "[$TND]\\ #,##0.00",
- 589: "[$TOP]\\ #,##0.00",
- 590: "[$TRL]\\ #,##0.00",
- 591: "[$TRY]\\ #,##0.00",
- 592: "[$TTD]\\ #,##0.00",
- 593: "[$TWD]\\ #,##0.00",
- 594: "[$TZS]\\ #,##0.00",
- 595: "[$UAH]\\ #,##0.00",
- 596: "[$UGX]\\ #,##0.00",
- 597: "[$USD]\\ #,##0.00",
- 598: "[$USN]\\ #,##0.00",
- 599: "[$USS]\\ #,##0.00",
- 600: "[$UYI]\\ #,##0.00",
- 601: "[$UYU]\\ #,##0.00",
- 602: "[$UZS]\\ #,##0.00",
- 603: "[$VEB]\\ #,##0.00",
- 604: "[$VEF]\\ #,##0.00",
- 605: "[$VND]\\ #,##0.00",
- 606: "[$VUV]\\ #,##0.00",
- 607: "[$WST]\\ #,##0.00",
- 608: "[$XAF]\\ #,##0.00",
- 609: "[$XAG]\\ #,##0.00",
- 610: "[$XAU]\\ #,##0.00",
- 611: "[$XB5]\\ #,##0.00",
- 612: "[$XBA]\\ #,##0.00",
- 613: "[$XBB]\\ #,##0.00",
- 614: "[$XBC]\\ #,##0.00",
- 615: "[$XBD]\\ #,##0.00",
- 616: "[$XCD]\\ #,##0.00",
- 617: "[$XDR]\\ #,##0.00",
- 618: "[$XFO]\\ #,##0.00",
- 619: "[$XFU]\\ #,##0.00",
- 620: "[$XOF]\\ #,##0.00",
- 621: "[$XPD]\\ #,##0.00",
- 622: "[$XPF]\\ #,##0.00",
- 623: "[$XPT]\\ #,##0.00",
- 624: "[$XTS]\\ #,##0.00",
- 625: "[$XXX]\\ #,##0.00",
- 626: "[$YER]\\ #,##0.00",
- 627: "[$YUM]\\ #,##0.00",
- 628: "[$ZAR]\\ #,##0.00",
- 629: "[$ZMK]\\ #,##0.00",
- 630: "[$ZMW]\\ #,##0.00",
- 631: "[$ZWD]\\ #,##0.00",
- 632: "[$ZWL]\\ #,##0.00",
- 633: "[$ZWN]\\ #,##0.00",
- 634: "[$ZWR]\\ #,##0.00",
-}
-
-// builtInNumFmtFunc defined the format conversion functions map. Partial format
-// code doesn't support currently and will return original string.
-var builtInNumFmtFunc = map[int]func(i int, v string) string{
- 0: formatToString,
- 1: formatToInt,
- 2: formatToFloat,
- 3: formatToInt,
- 4: formatToFloat,
- 9: formatToC,
- 10: formatToD,
- 11: formatToE,
- 12: formatToString, // Doesn't support currently
- 13: formatToString, // Doesn't support currently
- 14: parseTime,
- 15: parseTime,
- 16: parseTime,
- 17: parseTime,
- 18: parseTime,
- 19: parseTime,
- 20: parseTime,
- 21: parseTime,
- 22: parseTime,
- 37: formatToA,
- 38: formatToA,
- 39: formatToB,
- 40: formatToB,
- 41: formatToString, // Doesn't support currently
- 42: formatToString, // Doesn't support currently
- 43: formatToString, // Doesn't support currently
- 44: formatToString, // Doesn't support currently
- 45: parseTime,
- 46: parseTime,
- 47: parseTime,
- 48: formatToE,
- 49: formatToString,
-}
-
-// validType defined the list of valid validation types.
-var validType = map[string]string{
- "cell": "cellIs",
- "date": "date", // Doesn't support currently
- "time": "time", // Doesn't support currently
- "average": "aboveAverage",
- "duplicate": "duplicateValues",
- "unique": "uniqueValues",
- "top": "top10",
- "bottom": "top10",
- "text": "text", // Doesn't support currently
- "time_period": "timePeriod", // Doesn't support currently
- "blanks": "containsBlanks", // Doesn't support currently
- "no_blanks": "notContainsBlanks", // Doesn't support currently
- "errors": "containsErrors", // Doesn't support currently
- "no_errors": "notContainsErrors", // Doesn't support currently
- "2_color_scale": "2_color_scale",
- "3_color_scale": "3_color_scale",
- "data_bar": "dataBar",
- "formula": "expression",
-}
-
-// criteriaType defined the list of valid criteria types.
-var criteriaType = map[string]string{
- "between": "between",
- "not between": "notBetween",
- "equal to": "equal",
- "=": "equal",
- "==": "equal",
- "not equal to": "notEqual",
- "!=": "notEqual",
- "<>": "notEqual",
- "greater than": "greaterThan",
- ">": "greaterThan",
- "less than": "lessThan",
- "<": "lessThan",
- "greater than or equal to": "greaterThanOrEqual",
- ">=": "greaterThanOrEqual",
- "less than or equal to": "lessThanOrEqual",
- "<=": "lessThanOrEqual",
- "containing": "containsText",
- "not containing": "notContains",
- "begins with": "beginsWith",
- "ends with": "endsWith",
- "yesterday": "yesterday",
- "today": "today",
- "last 7 days": "last7Days",
- "last week": "lastWeek",
- "this week": "thisWeek",
- "continue week": "continueWeek",
- "last month": "lastMonth",
- "this month": "thisMonth",
- "continue month": "continueMonth",
-}
-
-// formatToString provides a function to return original string by given
-// built-in number formats code and cell string.
-func formatToString(i int, v string) string {
- return v
-}
-
-// formatToInt provides a function to convert original string to integer
-// format as string type by given built-in number formats code and cell
-// string.
-func formatToInt(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- return fmt.Sprintf("%d", int64(f))
-}
-
-// formatToFloat provides a function to convert original string to float
-// format as string type by given built-in number formats code and cell
-// string.
-func formatToFloat(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- return fmt.Sprintf("%.2f", f)
-}
-
-// formatToA provides a function to convert original string to special format
-// as string type by given built-in number formats code and cell string.
-func formatToA(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- if f < 0 {
- t := int(math.Abs(f))
- return fmt.Sprintf("(%d)", t)
- }
- t := int(f)
- return fmt.Sprintf("%d", t)
-}
-
-// formatToB provides a function to convert original string to special format
-// as string type by given built-in number formats code and cell string.
-func formatToB(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- if f < 0 {
- return fmt.Sprintf("(%.2f)", f)
- }
- return fmt.Sprintf("%.2f", f)
-}
-
-// formatToC provides a function to convert original string to special format
-// as string type by given built-in number formats code and cell string.
-func formatToC(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- f = f * 100
- return fmt.Sprintf("%.f%%", f)
-}
-
-// formatToD provides a function to convert original string to special format
-// as string type by given built-in number formats code and cell string.
-func formatToD(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- f = f * 100
- return fmt.Sprintf("%.2f%%", f)
-}
-
-// formatToE provides a function to convert original string to special format
-// as string type by given built-in number formats code and cell string.
-func formatToE(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- return fmt.Sprintf("%.e", f)
-}
-
-// parseTime provides a function to returns a string parsed using time.Time.
-// Replace Excel placeholders with Go time placeholders. For example, replace
-// yyyy with 2006. These are in a specific order, due to the fact that m is
-// used in month, minute, and am/pm. It would be easier to fix that with
-// regular expressions, but if it's possible to keep this simple it would be
-// easier to maintain. Full-length month and days (e.g. March, Tuesday) have
-// letters in them that would be replaced by other characters below (such as
-// the 'h' in March, or the 'd' in Tuesday) below. First we convert them to
-// arbitrary characters unused in Excel Date formats, and then at the end,
-// turn them to what they should actually be. Based off:
-// http://www.ozgrid.com/Excel/CustomFormats.htm
-func parseTime(i int, v string) string {
- f, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- val := timeFromExcelTime(f, false)
- format := builtInNumFmt[i]
-
- replacements := []struct{ xltime, gotime string }{
- {"yyyy", "2006"},
- {"yy", "06"},
- {"mmmm", "%%%%"},
- {"dddd", "&&&&"},
- {"dd", "02"},
- {"d", "2"},
- {"mmm", "Jan"},
- {"mmss", "0405"},
- {"ss", "05"},
- {"mm:", "04:"},
- {":mm", ":04"},
- {"mm", "01"},
- {"am/pm", "pm"},
- {"m/", "1/"},
- {"%%%%", "January"},
- {"&&&&", "Monday"},
- }
- // It is the presence of the "am/pm" indicator that determines if this is
- // a 12 hour or 24 hours time format, not the number of 'h' characters.
- if is12HourTime(format) {
- format = strings.Replace(format, "hh", "03", 1)
- format = strings.Replace(format, "h", "3", 1)
- } else {
- format = strings.Replace(format, "hh", "15", 1)
- format = strings.Replace(format, "h", "15", 1)
- }
- for _, repl := range replacements {
- format = strings.Replace(format, repl.xltime, repl.gotime, 1)
- }
- // If the hour is optional, strip it out, along with the possible dangling
- // colon that would remain.
- if val.Hour() < 1 {
- format = strings.Replace(format, "]:", "]", 1)
- format = strings.Replace(format, "[03]", "", 1)
- format = strings.Replace(format, "[3]", "", 1)
- format = strings.Replace(format, "[15]", "", 1)
- } else {
- format = strings.Replace(format, "[3]", "3", 1)
- format = strings.Replace(format, "[15]", "15", 1)
- }
- return val.Format(format)
-}
-
-// is12HourTime checks whether an Excel time format string is a 12 hours form.
-func is12HourTime(format string) bool {
- return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
-}
-
// stylesReader provides a function to get the pointer to the structure after
// deserialization of xl/styles.xml.
-func (f *File) stylesReader() *xlsxStyleSheet {
- var err error
-
+func (f *File) stylesReader() (*xlsxStyleSheet, error) {
if f.Styles == nil {
f.Styles = new(xlsxStyleSheet)
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/styles.xml")))).
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathStyles)))).
Decode(f.Styles); err != nil && err != io.EOF {
- log.Printf("xml decode error: %s", err)
+ return f.Styles, err
}
}
-
- return f.Styles
+ return f.Styles, nil
}
// styleSheetWriter provides a function to save xl/styles.xml after serialize
@@ -1022,7 +41,75 @@ func (f *File) stylesReader() *xlsxStyleSheet {
func (f *File) styleSheetWriter() {
if f.Styles != nil {
output, _ := xml.Marshal(f.Styles)
- f.saveFileList("xl/styles.xml", f.replaceNameSpaceBytes("xl/styles.xml", output))
+ f.saveFileList(defaultXMLPathStyles, f.replaceNameSpaceBytes(defaultXMLPathStyles, output))
+ }
+}
+
+// themeWriter provides a function to save xl/theme/theme1.xml after serialize
+// structure.
+func (f *File) themeWriter() {
+ newColor := func(c *decodeCTColor) xlsxCTColor {
+ return xlsxCTColor{
+ ScrgbClr: c.ScrgbClr,
+ SrgbClr: c.SrgbClr,
+ HslClr: c.HslClr,
+ SysClr: c.SysClr,
+ SchemeClr: c.SchemeClr,
+ PrstClr: c.PrstClr,
+ }
+ }
+ newFontScheme := func(c *decodeFontCollection) xlsxFontCollection {
+ return xlsxFontCollection{
+ Latin: c.Latin,
+ Ea: c.Ea,
+ Cs: c.Cs,
+ Font: c.Font,
+ ExtLst: c.ExtLst,
+ }
+ }
+ if f.Theme != nil {
+ output, _ := xml.Marshal(xlsxTheme{
+ XMLNSa: NameSpaceDrawingML.Value,
+ XMLNSr: SourceRelationship.Value,
+ Name: f.Theme.Name,
+ ThemeElements: xlsxBaseStyles{
+ ClrScheme: xlsxColorScheme{
+ Name: f.Theme.ThemeElements.ClrScheme.Name,
+ Dk1: newColor(&f.Theme.ThemeElements.ClrScheme.Dk1),
+ Lt1: newColor(&f.Theme.ThemeElements.ClrScheme.Lt1),
+ Dk2: newColor(&f.Theme.ThemeElements.ClrScheme.Dk2),
+ Lt2: newColor(&f.Theme.ThemeElements.ClrScheme.Lt2),
+ Accent1: newColor(&f.Theme.ThemeElements.ClrScheme.Accent1),
+ Accent2: newColor(&f.Theme.ThemeElements.ClrScheme.Accent2),
+ Accent3: newColor(&f.Theme.ThemeElements.ClrScheme.Accent3),
+ Accent4: newColor(&f.Theme.ThemeElements.ClrScheme.Accent4),
+ Accent5: newColor(&f.Theme.ThemeElements.ClrScheme.Accent5),
+ Accent6: newColor(&f.Theme.ThemeElements.ClrScheme.Accent6),
+ Hlink: newColor(&f.Theme.ThemeElements.ClrScheme.Hlink),
+ FolHlink: newColor(&f.Theme.ThemeElements.ClrScheme.FolHlink),
+ ExtLst: f.Theme.ThemeElements.ClrScheme.ExtLst,
+ },
+ FontScheme: xlsxFontScheme{
+ Name: f.Theme.ThemeElements.FontScheme.Name,
+ MajorFont: newFontScheme(&f.Theme.ThemeElements.FontScheme.MajorFont),
+ MinorFont: newFontScheme(&f.Theme.ThemeElements.FontScheme.MinorFont),
+ ExtLst: f.Theme.ThemeElements.FontScheme.ExtLst,
+ },
+ FmtScheme: xlsxStyleMatrix{
+ Name: f.Theme.ThemeElements.FmtScheme.Name,
+ FillStyleLst: f.Theme.ThemeElements.FmtScheme.FillStyleLst,
+ LnStyleLst: f.Theme.ThemeElements.FmtScheme.LnStyleLst,
+ EffectStyleLst: f.Theme.ThemeElements.FmtScheme.EffectStyleLst,
+ BgFillStyleLst: f.Theme.ThemeElements.FmtScheme.BgFillStyleLst,
+ },
+ ExtLst: f.Theme.ThemeElements.ExtLst,
+ },
+ ObjectDefaults: f.Theme.ObjectDefaults,
+ ExtraClrSchemeLst: f.Theme.ExtraClrSchemeLst,
+ CustClrLst: f.Theme.CustClrLst,
+ ExtLst: f.Theme.ExtLst,
+ })
+ f.saveFileList(defaultXMLPathTheme, f.replaceNameSpaceBytes(defaultXMLPathTheme, output))
}
}
@@ -1031,911 +118,884 @@ func (f *File) styleSheetWriter() {
func (f *File) sharedStringsWriter() {
if f.SharedStrings != nil {
output, _ := xml.Marshal(f.SharedStrings)
- f.saveFileList("xl/sharedStrings.xml", f.replaceNameSpaceBytes("xl/sharedStrings.xml", output))
+ f.saveFileList(defaultXMLPathSharedStrings, f.replaceNameSpaceBytes(defaultXMLPathSharedStrings, output))
}
}
// parseFormatStyleSet provides a function to parse the format settings of the
// cells and conditional formats.
-func parseFormatStyleSet(style string) (*Style, error) {
- format := Style{}
- err := json.Unmarshal([]byte(style), &format)
- return &format, err
-}
-
-// NewStyle provides a function to create the style for cells by given JSON or
-// structure pointer. Note that the color field uses RGB color code.
-//
-// The following shows the border styles sorted by excelize index number:
-//
-// Index | Name | Weight | Style
-// -------+---------------+--------+-------------
-// 0 | None | 0 |
-// 1 | Continuous | 1 | -----------
-// 2 | Continuous | 2 | -----------
-// 3 | Dash | 1 | - - - - - -
-// 4 | Dot | 1 | . . . . . .
-// 5 | Continuous | 3 | -----------
-// 6 | Double | 3 | ===========
-// 7 | Continuous | 0 | -----------
-// 8 | Dash | 2 | - - - - - -
-// 9 | Dash Dot | 1 | - . - . - .
-// 10 | Dash Dot | 2 | - . - . - .
-// 11 | Dash Dot Dot | 1 | - . . - . .
-// 12 | Dash Dot Dot | 2 | - . . - . .
-// 13 | SlantDash Dot | 2 | / - . / - .
-//
-// The following shows the borders in the order shown in the Excel dialog:
-//
-// Index | Style | Index | Style
-// -------+-------------+-------+-------------
-// 0 | None | 12 | - . . - . .
-// 7 | ----------- | 13 | / - . / - .
-// 4 | . . . . . . | 10 | - . - . - .
-// 11 | - . . - . . | 8 | - - - - - -
-// 9 | - . - . - . | 2 | -----------
-// 3 | - - - - - - | 5 | -----------
-// 1 | ----------- | 6 | ===========
-//
-// The following shows the shading styles sorted by excelize index number:
-//
-// Index | Style | Index | Style
-// -------+-----------------+-------+-----------------
-// 0 | Horizontal | 3 | Diagonal down
-// 1 | Vertical | 4 | From corner
-// 2 | Diagonal Up | 5 | From center
-//
-// The following shows the patterns styles sorted by excelize index number:
-//
-// Index | Style | Index | Style
-// -------+-----------------+-------+-----------------
-// 0 | None | 10 | darkTrellis
-// 1 | solid | 11 | lightHorizontal
-// 2 | mediumGray | 12 | lightVertical
-// 3 | darkGray | 13 | lightDown
-// 4 | lightGray | 14 | lightUp
-// 5 | darkHorizontal | 15 | lightGrid
-// 6 | darkVertical | 16 | lightTrellis
-// 7 | darkDown | 17 | gray125
-// 8 | darkUp | 18 | gray0625
-// 9 | darkGrid | |
-//
-// The following the type of horizontal alignment in cells:
-//
-// Style
-// ------------------
-// left
-// center
-// right
-// fill
-// justify
-// centerContinuous
-// distributed
-//
-// The following the type of vertical alignment in cells:
-//
-// Style
-// ------------------
-// top
-// center
-// justify
-// distributed
-//
-// The following the type of font underline style:
-//
-// Style
-// ------------------
-// single
-// double
+func parseFormatStyleSet(style *Style) (*Style, error) {
+ var err error
+ if style.Font != nil {
+ if len(style.Font.Family) > MaxFontFamilyLength {
+ return style, ErrFontLength
+ }
+ if style.Font.Size > MaxFontSize {
+ return style, ErrFontSize
+ }
+ }
+ if style.CustomNumFmt != nil && len(*style.CustomNumFmt) == 0 {
+ err = ErrCustomNumFmt
+ }
+ return style, err
+}
+
+// NewStyle provides a function to create the style for cells by a given style
+// options, and returns style index. The same style index can not be used
+// across different workbook. This function is concurrency safe. Note that
+// the 'Font.Color' field uses an RGB color represented in 'RRGGBB' hexadecimal
+// notation.
+//
+// The following table shows the border types used in 'Border.Type' supported by
+// excelize:
+//
+// Type | Description
+// --------------+------------------
+// left | Left border
+// top | Top border
+// right | Right border
+// bottom | Bottom border
+// diagonalDown | Diagonal down border
+// diagonalUp | Diagonal up border
+//
+// The following table shows the border styles used in 'Border.Style' supported
+// by excelize index number:
+//
+// Index | Name | Weight | Style
+// -------+---------------+--------+-------------
+// 0 | None | 0 |
+// 1 | Continuous | 1 | -----------
+// 2 | Continuous | 2 | -----------
+// 3 | Dash | 1 | - - - - - -
+// 4 | Dot | 1 | . . . . . .
+// 5 | Continuous | 3 | -----------
+// 6 | Double | 3 | ===========
+// 7 | Continuous | 0 | -----------
+// 8 | Dash | 2 | - - - - - -
+// 9 | Dash Dot | 1 | - . - . - .
+// 10 | Dash Dot | 2 | - . - . - .
+// 11 | Dash Dot Dot | 1 | - . . - . .
+// 12 | Dash Dot Dot | 2 | - . . - . .
+// 13 | SlantDash Dot | 2 | / - . / - .
+//
+// The following table shows the border styles used in 'Border.Style' in the
+// order shown in the Excel dialog:
+//
+// Index | Style | Index | Style
+// -------+-------------+-------+-------------
+// 0 | None | 12 | - . . - . .
+// 7 | ----------- | 13 | / - . / - .
+// 4 | . . . . . . | 10 | - . - . - .
+// 11 | - . . - . . | 8 | - - - - - -
+// 9 | - . - . - . | 2 | -----------
+// 3 | - - - - - - | 5 | -----------
+// 1 | ----------- | 6 | ===========
+//
+// The following table shows the shading styles used in 'Fill.Shading' supported
+// by excelize index number:
+//
+// Index | Style | Index | Style
+// -------+-----------------+-------+-----------------
+// 0-2 | Horizontal | 9-11 | Diagonal down
+// 3-5 | Vertical | 12-15 | From corner
+// 6-8 | Diagonal Up | 16 | From center
+//
+// The following table shows the pattern styles used in 'Fill.Pattern' supported
+// by excelize index number:
+//
+// Index | Style | Index | Style
+// -------+-----------------+-------+-----------------
+// 0 | None | 10 | darkTrellis
+// 1 | solid | 11 | lightHorizontal
+// 2 | mediumGray | 12 | lightVertical
+// 3 | darkGray | 13 | lightDown
+// 4 | lightGray | 14 | lightUp
+// 5 | darkHorizontal | 15 | lightGrid
+// 6 | darkVertical | 16 | lightTrellis
+// 7 | darkDown | 17 | gray125
+// 8 | darkUp | 18 | gray0625
+// 9 | darkGrid | |
+//
+// The 'Alignment.Indent' is an integer value, where an increment of 1
+// represents 3 spaces. Indicates the number of spaces (of the normal style
+// font) of indentation for text in a cell. The number of spaces to indent is
+// calculated as following:
+//
+// Number of spaces to indent = indent value * 3
+//
+// For example, an indent value of 1 means that the text begins 3 space widths
+// (of the normal style font) from the edge of the cell. Note: The width of one
+// space character is defined by the font. Only left, right, and distributed
+// horizontal alignments are supported.
+//
+// The following table shows the type of cells' horizontal alignment used
+// in 'Alignment.Horizontal':
+//
+// Style
+// ------------------
+// left
+// center
+// right
+// fill
+// justify
+// centerContinuous
+// distributed
+//
+// The following table shows the type of cells' vertical alignment used in
+// 'Alignment.Vertical':
+//
+// Style
+// ------------------
+// top
+// center
+// justify
+// distributed
+//
+// The 'Alignment.ReadingOrder' is an uint64 value indicating whether the
+// reading order of the cell is left-to-right, right-to-left, or context
+// dependent. the valid value of this field was:
+//
+// Value | Description
+// -------+----------------------------------------------------
+// 0 | Context Dependent - reading order is determined by scanning the
+// | text for the first non-whitespace character: if it is a strong
+// | right-to-left character, the reading order is right-to-left;
+// | otherwise, the reading order left-to-right.
+// 1 | Left-to-Right: reading order is left-to-right in the cell, as in
+// | English.
+// 2 | Right-to-Left: reading order is right-to-left in the cell, as in
+// | Hebrew.
+//
+// The 'Alignment.RelativeIndent' is an integer value to indicate the additional
+// number of spaces of indentation to adjust for text in a cell.
+//
+// The following table shows the type of font underline style used in
+// 'Font.Underline':
+//
+// Style
+// ------------------
+// none
+// single
+// double
+//
+// NumFmt is used to set the built-in all languages formats index, built-in
+// language formats index, or built-in currency formats index, it doesn't work
+// when you specify the custom number format by CustomNumFmt. When you get
+// style definition by the GetStyle or GetConditionalStyle function, the NumFmt
+// only works if the number format code is exactly equal with any built-in all
+// languages format code, built-in language formats code, or built-in currency
+// format code.
//
// Excel's built-in all languages formats are shown in the following table:
//
-// Index | Format String
-// -------+----------------------------------------------------
-// 0 | General
-// 1 | 0
-// 2 | 0.00
-// 3 | #,##0
-// 4 | #,##0.00
-// 5 | ($#,##0_);($#,##0)
-// 6 | ($#,##0_);[Red]($#,##0)
-// 7 | ($#,##0.00_);($#,##0.00)
-// 8 | ($#,##0.00_);[Red]($#,##0.00)
-// 9 | 0%
-// 10 | 0.00%
-// 11 | 0.00E+00
-// 12 | # ?/?
-// 13 | # ??/??
-// 14 | m/d/yy
-// 15 | d-mmm-yy
-// 16 | d-mmm
-// 17 | mmm-yy
-// 18 | h:mm AM/PM
-// 19 | h:mm:ss AM/PM
-// 20 | h:mm
-// 21 | h:mm:ss
-// 22 | m/d/yy h:mm
-// ... | ...
-// 37 | (#,##0_);(#,##0)
-// 38 | (#,##0_);[Red](#,##0)
-// 39 | (#,##0.00_);(#,##0.00)
-// 40 | (#,##0.00_);[Red](#,##0.00)
-// 41 | _(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)
-// 42 | _($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)
-// 43 | _(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)
-// 44 | _($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)
-// 45 | mm:ss
-// 46 | [h]:mm:ss
-// 47 | mm:ss.0
-// 48 | ##0.0E+0
-// 49 | @
+// Index | Format String
+// -------+----------------------------------------------------
+// 0 | General
+// 1 | 0
+// 2 | 0.00
+// 3 | #,##0
+// 4 | #,##0.00
+// 5 | ($#,##0_);($#,##0)
+// 6 | ($#,##0_);[Red]($#,##0)
+// 7 | ($#,##0.00_);($#,##0.00)
+// 8 | ($#,##0.00_);[Red]($#,##0.00)
+// 9 | 0%
+// 10 | 0.00%
+// 11 | 0.00E+00
+// 12 | # ?/?
+// 13 | # ??/??
+// 14 | m/d/yy
+// 15 | d-mmm-yy
+// 16 | d-mmm
+// 17 | mmm-yy
+// 18 | h:mm AM/PM
+// 19 | h:mm:ss AM/PM
+// 20 | h:mm
+// 21 | h:mm:ss
+// 22 | m/d/yy h:mm
+// ... | ...
+// 37 | (#,##0_);(#,##0)
+// 38 | (#,##0_);[Red](#,##0)
+// 39 | (#,##0.00_);(#,##0.00)
+// 40 | (#,##0.00_);[Red](#,##0.00)
+// 41 | _(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)
+// 42 | _($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)
+// 43 | _(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)
+// 44 | _($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)
+// 45 | mm:ss
+// 46 | [h]:mm:ss
+// 47 | mm:ss.0
+// 48 | ##0.0E+0
+// 49 | @
//
// Number format code in zh-tw language:
//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | [$-404]e/m/d
-// 28 | [$-404]e"年"m"月"d"日"
-// 29 | [$-404]e"年"m"月"d"日"
-// 30 | m/d/yy
-// 31 | yyyy"年"m"月"d"日"
-// 32 | hh"時"mm"分"
-// 33 | hh"時"mm"分"ss"秒"
-// 34 | 上午/下午 hh"時"mm"分"
-// 35 | 上午/下午 hh"時"mm"分"ss"秒"
-// 36 | [$-404]e/m/d
-// 50 | [$-404]e/m/d
-// 51 | [$-404]e"年"m"月"d"日"
-// 52 | 上午/下午 hh"時"mm"分"
-// 53 | 上午/下午 hh"時"mm"分"ss"秒"
-// 54 | [$-404]e"年"m"月"d"日"
-// 55 | 上午/下午 hh"時"mm"分"
-// 56 | 上午/下午 hh"時"mm"分"ss"秒"
-// 57 | [$-404]e/m/d
-// 58 | [$-404]e"年"m"月"d"日"
+// Index | Symbol
+// -------+-------------------------------------------
+// 27 | [$-404]e/m/d
+// 28 | [$-404]e"年"m"月"d"日"
+// 29 | [$-404]e"年"m"月"d"日"
+// 30 | m/d/yy
+// 31 | yyyy"年"m"月"d"日"
+// 32 | hh"時"mm"分"
+// 33 | hh"時"mm"分"ss"秒"
+// 34 | 上午/下午 hh"時"mm"分"
+// 35 | 上午/下午 hh"時"mm"分"ss"秒"
+// 36 | [$-404]e/m/d
+// 50 | [$-404]e/m/d
+// 51 | [$-404]e"年"m"月"d"日"
+// 52 | 上午/下午 hh"時"mm"分"
+// 53 | 上午/下午 hh"時"mm"分"ss"秒"
+// 54 | [$-404]e"年"m"月"d"日"
+// 55 | 上午/下午 hh"時"mm"分"
+// 56 | 上午/下午 hh"時"mm"分"ss"秒"
+// 57 | [$-404]e/m/d
+// 58 | [$-404]e"年"m"月"d"日"
//
// Number format code in zh-cn language:
//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | yyyy"年"m"月"
-// 28 | m"月"d"日"
-// 29 | m"月"d"日"
-// 30 | m-d-yy
-// 31 | yyyy"年"m"月"d"日"
-// 32 | h"时"mm"分"
-// 33 | h"时"mm"分"ss"秒"
-// 34 | 上午/下午 h"时"mm"分"
-// 35 | 上午/下午 h"时"mm"分"ss"秒
-// 36 | yyyy"年"m"月
-// 50 | yyyy"年"m"月
-// 51 | m"月"d"日
-// 52 | yyyy"年"m"月
-// 53 | m"月"d"日
-// 54 | m"月"d"日
-// 55 | 上午/下午 h"时"mm"分
-// 56 | 上午/下午 h"时"mm"分"ss"秒
-// 57 | yyyy"年"m"月
-// 58 | m"月"d"日"
-//
-// Number format code with unicode values provided for language glyphs where
-// they occur in zh-tw language:
-//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | [$-404]e/m/
-// 28 | [$-404]e"5E74"m"6708"d"65E5
-// 29 | [$-404]e"5E74"m"6708"d"65E5
-// 30 | m/d/y
-// 31 | yyyy"5E74"m"6708"d"65E5
-// 32 | hh"6642"mm"5206
-// 33 | hh"6642"mm"5206"ss"79D2
-// 34 | 4E0A5348/4E0B5348hh"6642"mm"5206
-// 35 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2
-// 36 | [$-404]e/m/
-// 50 | [$-404]e/m/
-// 51 | [$-404]e"5E74"m"6708"d"65E5
-// 52 | 4E0A5348/4E0B5348hh"6642"mm"5206
-// 53 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2
-// 54 | [$-404]e"5E74"m"6708"d"65E5
-// 55 | 4E0A5348/4E0B5348hh"6642"mm"5206
-// 56 | 4E0A5348/4E0B5348hh"6642"mm"5206"ss"79D2
-// 57 | [$-404]e/m/
-// 58 | [$-404]e"5E74"m"6708"d"65E5"
-//
-// Number format code with unicode values provided for language glyphs where
-// they occur in zh-cn language:
-//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | yyyy"5E74"m"6708
-// 28 | m"6708"d"65E5
-// 29 | m"6708"d"65E5
-// 30 | m-d-y
-// 31 | yyyy"5E74"m"6708"d"65E5
-// 32 | h"65F6"mm"5206
-// 33 | h"65F6"mm"5206"ss"79D2
-// 34 | 4E0A5348/4E0B5348h"65F6"mm"5206
-// 35 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2
-// 36 | yyyy"5E74"m"6708
-// 50 | yyyy"5E74"m"6708
-// 51 | m"6708"d"65E5
-// 52 | yyyy"5E74"m"6708
-// 53 | m"6708"d"65E5
-// 54 | m"6708"d"65E5
-// 55 | 4E0A5348/4E0B5348h"65F6"mm"5206
-// 56 | 4E0A5348/4E0B5348h"65F6"mm"5206"ss"79D2
-// 57 | yyyy"5E74"m"6708
-// 58 | m"6708"d"65E5"
+// Index | Symbol
+// -------+-------------------------------------------
+// 27 | yyyy"年"m"月"
+// 28 | m"月"d"日"
+// 29 | m"月"d"日"
+// 30 | m-d-yy
+// 31 | yyyy"年"m"月"d"日"
+// 32 | h"时"mm"分"
+// 33 | h"时"mm"分"ss"秒"
+// 34 | 上午/下午 h"时"mm"分"
+// 35 | 上午/下午 h"时"mm"分"ss"秒
+// 36 | yyyy"年"m"月
+// 50 | yyyy"年"m"月
+// 51 | m"月"d"日
+// 52 | yyyy"年"m"月
+// 53 | m"月"d"日
+// 54 | m"月"d"日
+// 55 | 上午/下午 h"时"mm"分
+// 56 | 上午/下午 h"时"mm"分"ss"秒
+// 57 | yyyy"年"m"月
+// 58 | m"月"d"日"
//
// Number format code in ja-jp language:
//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | [$-411]ge.m.d
-// 28 | [$-411]ggge"年"m"月"d"日
-// 29 | [$-411]ggge"年"m"月"d"日
-// 30 | m/d/y
-// 31 | yyyy"年"m"月"d"日
-// 32 | h"時"mm"分
-// 33 | h"時"mm"分"ss"秒
-// 34 | yyyy"年"m"月
-// 35 | m"月"d"日
-// 36 | [$-411]ge.m.d
-// 50 | [$-411]ge.m.d
-// 51 | [$-411]ggge"年"m"月"d"日
-// 52 | yyyy"年"m"月
-// 53 | m"月"d"日
-// 54 | [$-411]ggge"年"m"月"d"日
-// 55 | yyyy"年"m"月
-// 56 | m"月"d"日
-// 57 | [$-411]ge.m.d
-// 58 | [$-411]ggge"年"m"月"d"日"
+// Index | Symbol
+// -------+-------------------------------------------
+// 27 | [$-411]ge.m.d
+// 28 | [$-411]ggge"年"m"月"d"日
+// 29 | [$-411]ggge"年"m"月"d"日
+// 30 | m/d/y
+// 31 | yyyy"年"m"月"d"日
+// 32 | h"時"mm"分
+// 33 | h"時"mm"分"ss"秒
+// 34 | yyyy"年"m"月
+// 35 | m"月"d"日
+// 36 | [$-411]ge.m.d
+// 50 | [$-411]ge.m.d
+// 51 | [$-411]ggge"年"m"月"d"日
+// 52 | yyyy"年"m"月
+// 53 | m"月"d"日
+// 54 | [$-411]ggge"年"m"月"d"日
+// 55 | yyyy"年"m"月
+// 56 | m"月"d"日
+// 57 | [$-411]ge.m.d
+// 58 | [$-411]ggge"年"m"月"d"日"
//
// Number format code in ko-kr language:
//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | yyyy"年" mm"月" dd"日
-// 28 | mm-d
-// 29 | mm-d
-// 30 | mm-dd-y
-// 31 | yyyy"년" mm"월" dd"일
-// 32 | h"시" mm"분
-// 33 | h"시" mm"분" ss"초
-// 34 | yyyy-mm-d
-// 35 | yyyy-mm-d
-// 36 | yyyy"年" mm"月" dd"日
-// 50 | yyyy"年" mm"月" dd"日
-// 51 | mm-d
-// 52 | yyyy-mm-d
-// 53 | yyyy-mm-d
-// 54 | mm-d
-// 55 | yyyy-mm-d
-// 56 | yyyy-mm-d
-// 57 | yyyy"年" mm"月" dd"日
-// 58 | mm-dd
-//
-// Number format code with unicode values provided for language glyphs where
-// they occur in ja-jp language:
-//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | [$-411]ge.m.d
-// 28 | [$-411]ggge"5E74"m"6708"d"65E5
-// 29 | [$-411]ggge"5E74"m"6708"d"65E5
-// 30 | m/d/y
-// 31 | yyyy"5E74"m"6708"d"65E5
-// 32 | h"6642"mm"5206
-// 33 | h"6642"mm"5206"ss"79D2
-// 34 | yyyy"5E74"m"6708
-// 35 | m"6708"d"65E5
-// 36 | [$-411]ge.m.d
-// 50 | [$-411]ge.m.d
-// 51 | [$-411]ggge"5E74"m"6708"d"65E5
-// 52 | yyyy"5E74"m"6708
-// 53 | m"6708"d"65E5
-// 54 | [$-411]ggge"5E74"m"6708"d"65E5
-// 55 | yyyy"5E74"m"6708
-// 56 | m"6708"d"65E5
-// 57 | [$-411]ge.m.d
-// 58 | [$-411]ggge"5E74"m"6708"d"65E5"
-//
-// Number format code with unicode values provided for language glyphs where
-// they occur in ko-kr language:
-//
-// Index | Symbol
-// -------+-------------------------------------------
-// 27 | yyyy"5E74" mm"6708" dd"65E5
-// 28 | mm-d
-// 29 | mm-d
-// 30 | mm-dd-y
-// 31 | yyyy"B144" mm"C6D4" dd"C77C
-// 32 | h"C2DC" mm"BD84
-// 33 | h"C2DC" mm"BD84" ss"CD08
-// 34 | yyyy-mm-d
-// 35 | yyyy-mm-d
-// 36 | yyyy"5E74" mm"6708" dd"65E5
-// 50 | yyyy"5E74" mm"6708" dd"65E5
-// 51 | mm-d
-// 52 | yyyy-mm-d
-// 53 | yyyy-mm-d
-// 54 | mm-d
-// 55 | yyyy-mm-d
-// 56 | yyyy-mm-d
-// 57 | yyyy"5E74" mm"6708" dd"65E5
-// 58 | mm-dd
+// Index | Symbol
+// -------+-------------------------------------------
+// 27 | yyyy"年" mm"月" dd"日
+// 28 | mm-d
+// 29 | mm-d
+// 30 | mm-dd-y
+// 31 | yyyy"년" mm"월" dd"일
+// 32 | h"시" mm"분
+// 33 | h"시" mm"분" ss"초
+// 34 | yyyy-mm-d
+// 35 | yyyy-mm-d
+// 36 | yyyy"年" mm"月" dd"日
+// 50 | yyyy"年" mm"月" dd"日
+// 51 | mm-d
+// 52 | yyyy-mm-d
+// 53 | yyyy-mm-d
+// 54 | mm-d
+// 55 | yyyy-mm-d
+// 56 | yyyy-mm-d
+// 57 | yyyy"年" mm"月" dd"日
+// 58 | mm-dd
//
// Number format code in th-th language:
//
-// Index | Symbol
-// -------+-------------------------------------------
-// 59 | t
-// 60 | t0.0
-// 61 | t#,##
-// 62 | t#,##0.0
-// 67 | t0
-// 68 | t0.00
-// 69 | t# ?/
-// 70 | t# ??/?
-// 71 | ว/ด/ปปป
-// 72 | ว-ดดด-ป
-// 73 | ว-ดด
-// 74 | ดดด-ป
-// 75 | ช:น
-// 76 | ช:นน:ท
-// 77 | ว/ด/ปปปป ช:น
-// 78 | นน:ท
-// 79 | [ช]:นน:ท
-// 80 | นน:ทท.
-// 81 | d/m/bb
-//
-// Number format code with unicode values provided for language glyphs where
-// they occur in th-th language:
-//
-// Index | Symbol
-// -------+-------------------------------------------
-// 59 | t
-// 60 | t0.0
-// 61 | t#,##
-// 62 | t#,##0.0
-// 67 | t0
-// 68 | t0.00
-// 69 | t# ?/
-// 70 | t# ??/?
-// 71 | 0E27/0E14/0E1B0E1B0E1B0E1
-// 72 | 0E27-0E140E140E14-0E1B0E1
-// 73 | 0E27-0E140E140E1
-// 74 | 0E140E140E14-0E1B0E1
-// 75 | 0E0A:0E190E1
-// 76 | 0E0A:0E190E19:0E170E1
-// 77 | 0E27/0E14/0E1B0E1B0E1B0E1B 0E0A:0E190E1
-// 78 | 0E190E19:0E170E1
-// 79 | [0E0A]:0E190E19:0E170E1
-// 80 | 0E190E19:0E170E17.
-// 81 | d/m/bb
+// Index | Symbol
+// -------+-------------------------------------------
+// 59 | t
+// 60 | t0.0
+// 61 | t#,##
+// 62 | t#,##0.0
+// 67 | t0
+// 68 | t0.00
+// 69 | t# ?/
+// 70 | t# ??/?
+// 71 | ว/ด/ปปป
+// 72 | ว-ดดด-ป
+// 73 | ว-ดด
+// 74 | ดดด-ป
+// 75 | ช:น
+// 76 | ช:นน:ท
+// 77 | ว/ด/ปปปป ช:น
+// 78 | นน:ท
+// 79 | [ช]:นน:ท
+// 80 | นน:ทท.
+// 81 | d/m/bb
//
// Excelize built-in currency formats are shown in the following table, only
// support these types in the following table (Index number is used only for
// markup and is not used inside an Excel file and you can't get formatted value
// by the function GetCellValue) currently:
//
-// Index | Symbol
-// -------+---------------------------------------------------------------
-// 164 | CN¥
-// 165 | $ English (China)
-// 166 | $ Cherokee (United States)
-// 167 | $ Chinese (Singapore)
-// 168 | $ Chinese (Taiwan)
-// 169 | $ English (Australia)
-// 170 | $ English (Belize)
-// 171 | $ English (Canada)
-// 172 | $ English (Jamaica)
-// 173 | $ English (New Zealand)
-// 174 | $ English (Singapore)
-// 175 | $ English (Trinidad & Tobago)
-// 176 | $ English (U.S. Virgin Islands)
-// 177 | $ English (United States)
-// 178 | $ French (Canada)
-// 179 | $ Hawaiian (United States)
-// 180 | $ Malay (Brunei)
-// 181 | $ Quechua (Ecuador)
-// 182 | $ Spanish (Chile)
-// 183 | $ Spanish (Colombia)
-// 184 | $ Spanish (Ecuador)
-// 185 | $ Spanish (El Salvador)
-// 186 | $ Spanish (Mexico)
-// 187 | $ Spanish (Puerto Rico)
-// 188 | $ Spanish (United States)
-// 189 | $ Spanish (Uruguay)
-// 190 | £ English (United Kingdom)
-// 191 | £ Scottish Gaelic (United Kingdom)
-// 192 | £ Welsh (United Kindom)
-// 193 | ¥ Chinese (China)
-// 194 | ¥ Japanese (Japan)
-// 195 | ¥ Sichuan Yi (China)
-// 196 | ¥ Tibetan (China)
-// 197 | ¥ Uyghur (China)
-// 198 | ֏ Armenian (Armenia)
-// 199 | ؋ Pashto (Afghanistan)
-// 200 | ؋ Persian (Afghanistan)
-// 201 | ৳ Bengali (Bangladesh)
-// 202 | ៛ Khmer (Cambodia)
-// 203 | ₡ Spanish (Costa Rica)
-// 204 | ₦ Hausa (Nigeria)
-// 205 | ₦ Igbo (Nigeria)
-// 206 | ₦ Yoruba (Nigeria)
-// 207 | ₩ Korean (South Korea)
-// 208 | ₪ Hebrew (Israel)
-// 209 | ₫ Vietnamese (Vietnam)
-// 210 | € Basque (Spain)
-// 211 | € Breton (France)
-// 212 | € Catalan (Spain)
-// 213 | € Corsican (France)
-// 214 | € Dutch (Belgium)
-// 215 | € Dutch (Netherlands)
-// 216 | € English (Ireland)
-// 217 | € Estonian (Estonia)
-// 218 | € Euro (€ 123)
-// 219 | € Euro (123 €)
-// 220 | € Finnish (Finland)
-// 221 | € French (Belgium)
-// 222 | € French (France)
-// 223 | € French (Luxembourg)
-// 224 | € French (Monaco)
-// 225 | € French (Réunion)
-// 226 | € Galician (Spain)
-// 227 | € German (Austria)
-// 228 | € German (Luxembourg)
-// 229 | € Greek (Greece)
-// 230 | € Inari Sami (Finland)
-// 231 | € Irish (Ireland)
-// 232 | € Italian (Italy)
-// 233 | € Latin (Italy)
-// 234 | € Latin, Serbian (Montenegro)
-// 235 | € Larvian (Latvia)
-// 236 | € Lithuanian (Lithuania)
-// 237 | € Lower Sorbian (Germany)
-// 238 | € Luxembourgish (Luxembourg)
-// 239 | € Maltese (Malta)
-// 240 | € Northern Sami (Finland)
-// 241 | € Occitan (France)
-// 242 | € Portuguese (Portugal)
-// 243 | € Serbian (Montenegro)
-// 244 | € Skolt Sami (Finland)
-// 245 | € Slovak (Slovakia)
-// 246 | € Slovenian (Slovenia)
-// 247 | € Spanish (Spain)
-// 248 | € Swedish (Finland)
-// 249 | € Swiss German (France)
-// 250 | € Upper Sorbian (Germany)
-// 251 | € Western Frisian (Netherlands)
-// 252 | ₭ Lao (Laos)
-// 253 | ₮ Mongolian (Mongolia)
-// 254 | ₮ Mongolian, Mongolian (Mongolia)
-// 255 | ₱ English (Philippines)
-// 256 | ₱ Filipino (Philippines)
-// 257 | ₴ Ukrainian (Ukraine)
-// 258 | ₸ Kazakh (Kazakhstan)
-// 259 | ₹ Arabic, Kashmiri (India)
-// 260 | ₹ English (India)
-// 261 | ₹ Gujarati (India)
-// 262 | ₹ Hindi (India)
-// 263 | ₹ Kannada (India)
-// 264 | ₹ Kashmiri (India)
-// 265 | ₹ Konkani (India)
-// 266 | ₹ Manipuri (India)
-// 267 | ₹ Marathi (India)
-// 268 | ₹ Nepali (India)
-// 269 | ₹ Oriya (India)
-// 270 | ₹ Punjabi (India)
-// 271 | ₹ Sanskrit (India)
-// 272 | ₹ Sindhi (India)
-// 273 | ₹ Tamil (India)
-// 274 | ₹ Urdu (India)
-// 275 | ₺ Turkish (Turkey)
-// 276 | ₼ Azerbaijani (Azerbaijan)
-// 277 | ₼ Cyrillic, Azerbaijani (Azerbaijan)
-// 278 | ₽ Russian (Russia)
-// 279 | ₽ Sakha (Russia)
-// 280 | ₾ Georgian (Georgia)
-// 281 | B/. Spanish (Panama)
-// 282 | Br Oromo (Ethiopia)
-// 283 | Br Somali (Ethiopia)
-// 284 | Br Tigrinya (Ethiopia)
-// 285 | Bs Quechua (Bolivia)
-// 286 | Bs Spanish (Bolivia)
-// 287 | BS. Spanish (Venezuela)
-// 288 | BWP Tswana (Botswana)
-// 289 | C$ Spanish (Nicaragua)
-// 290 | CA$ Latin, Inuktitut (Canada)
-// 291 | CA$ Mohawk (Canada)
-// 292 | CA$ Unified Canadian Aboriginal Syllabics, Inuktitut (Canada)
-// 293 | CFA French (Mali)
-// 294 | CFA French (Senegal)
-// 295 | CFA Fulah (Senegal)
-// 296 | CFA Wolof (Senegal)
-// 297 | CHF French (Switzerland)
-// 298 | CHF German (Liechtenstein)
-// 299 | CHF German (Switzerland)
-// 300 | CHF Italian (Switzerland)
-// 301 | CHF Romansh (Switzerland)
-// 302 | CLP Mapuche (Chile)
-// 303 | CN¥ Mongolian, Mongolian (China)
-// 304 | DZD Central Atlas Tamazight (Algeria)
-// 305 | FCFA French (Cameroon)
-// 306 | Ft Hungarian (Hungary)
-// 307 | G French (Haiti)
-// 308 | Gs. Spanish (Paraguay)
-// 309 | GTQ K'iche' (Guatemala)
-// 310 | HK$ Chinese (Hong Kong (China))
-// 311 | HK$ English (Hong Kong (China))
-// 312 | HRK Croatian (Croatia)
-// 313 | IDR English (Indonesia)
-// 314 | IQD Arbic, Central Kurdish (Iraq)
-// 315 | ISK Icelandic (Iceland)
-// 316 | K Burmese (Myanmar (Burma))
-// 317 | Kč Czech (Czech Republic)
-// 318 | KM Bosnian (Bosnia & Herzegovina)
-// 319 | KM Croatian (Bosnia & Herzegovina)
-// 320 | KM Latin, Serbian (Bosnia & Herzegovina)
-// 321 | kr Faroese (Faroe Islands)
-// 322 | kr Northern Sami (Norway)
-// 323 | kr Northern Sami (Sweden)
-// 324 | kr Norwegian Bokmål (Norway)
-// 325 | kr Norwegian Nynorsk (Norway)
-// 326 | kr Swedish (Sweden)
-// 327 | kr. Danish (Denmark)
-// 328 | kr. Kalaallisut (Greenland)
-// 329 | Ksh Swahili (kenya)
-// 330 | L Romanian (Moldova)
-// 331 | L Russian (Moldova)
-// 332 | L Spanish (Honduras)
-// 333 | Lekë Albanian (Albania)
-// 334 | MAD Arabic, Central Atlas Tamazight (Morocco)
-// 335 | MAD French (Morocco)
-// 336 | MAD Tifinagh, Central Atlas Tamazight (Morocco)
-// 337 | MOP$ Chinese (Macau (China))
-// 338 | MVR Divehi (Maldives)
-// 339 | Nfk Tigrinya (Eritrea)
-// 340 | NGN Bini (Nigeria)
-// 341 | NGN Fulah (Nigeria)
-// 342 | NGN Ibibio (Nigeria)
-// 343 | NGN Kanuri (Nigeria)
-// 344 | NOK Lule Sami (Norway)
-// 345 | NOK Southern Sami (Norway)
-// 346 | NZ$ Maori (New Zealand)
-// 347 | PKR Sindhi (Pakistan)
-// 348 | PYG Guarani (Paraguay)
-// 349 | Q Spanish (Guatemala)
-// 350 | R Afrikaans (South Africa)
-// 351 | R English (South Africa)
-// 352 | R Zulu (South Africa)
-// 353 | R$ Portuguese (Brazil)
-// 354 | RD$ Spanish (Dominican Republic)
-// 355 | RF Kinyarwanda (Rwanda)
-// 356 | RM English (Malaysia)
-// 357 | RM Malay (Malaysia)
-// 358 | RON Romanian (Romania)
-// 359 | Rp Indonesoan (Indonesia)
-// 360 | Rs Urdu (Pakistan)
-// 361 | Rs. Tamil (Sri Lanka)
-// 362 | RSD Latin, Serbian (Serbia)
-// 363 | RSD Serbian (Serbia)
-// 364 | RUB Bashkir (Russia)
-// 365 | RUB Tatar (Russia)
-// 366 | S/. Quechua (Peru)
-// 367 | S/. Spanish (Peru)
-// 368 | SEK Lule Sami (Sweden)
-// 369 | SEK Southern Sami (Sweden)
-// 370 | soʻm Latin, Uzbek (Uzbekistan)
-// 371 | soʻm Uzbek (Uzbekistan)
-// 372 | SYP Syriac (Syria)
-// 373 | THB Thai (Thailand)
-// 374 | TMT Turkmen (Turkmenistan)
-// 375 | US$ English (Zimbabwe)
-// 376 | ZAR Northern Sotho (South Africa)
-// 377 | ZAR Southern Sotho (South Africa)
-// 378 | ZAR Tsonga (South Africa)
-// 379 | ZAR Tswana (south Africa)
-// 380 | ZAR Venda (South Africa)
-// 381 | ZAR Xhosa (South Africa)
-// 382 | zł Polish (Poland)
-// 383 | ден Macedonian (Macedonia)
-// 384 | KM Cyrillic, Bosnian (Bosnia & Herzegovina)
-// 385 | KM Serbian (Bosnia & Herzegovina)
-// 386 | лв. Bulgarian (Bulgaria)
-// 387 | p. Belarusian (Belarus)
-// 388 | сом Kyrgyz (Kyrgyzstan)
-// 389 | сом Tajik (Tajikistan)
-// 390 | ج.م. Arabic (Egypt)
-// 391 | د.أ. Arabic (Jordan)
-// 392 | د.أ. Arabic (United Arab Emirates)
-// 393 | د.ب. Arabic (Bahrain)
-// 394 | د.ت. Arabic (Tunisia)
-// 395 | د.ج. Arabic (Algeria)
-// 396 | د.ع. Arabic (Iraq)
-// 397 | د.ك. Arabic (Kuwait)
-// 398 | د.ل. Arabic (Libya)
-// 399 | د.م. Arabic (Morocco)
-// 400 | ر Punjabi (Pakistan)
-// 401 | ر.س. Arabic (Saudi Arabia)
-// 402 | ر.ع. Arabic (Oman)
-// 403 | ر.ق. Arabic (Qatar)
-// 404 | ر.ي. Arabic (Yemen)
-// 405 | ریال Persian (Iran)
-// 406 | ل.س. Arabic (Syria)
-// 407 | ل.ل. Arabic (Lebanon)
-// 408 | ብር Amharic (Ethiopia)
-// 409 | रू Nepaol (Nepal)
-// 410 | රු. Sinhala (Sri Lanka)
-// 411 | ADP
-// 412 | AED
-// 413 | AFA
-// 414 | AFN
-// 415 | ALL
-// 416 | AMD
-// 417 | ANG
-// 418 | AOA
-// 419 | ARS
-// 420 | ATS
-// 421 | AUD
-// 422 | AWG
-// 423 | AZM
-// 424 | AZN
-// 425 | BAM
-// 426 | BBD
-// 427 | BDT
-// 428 | BEF
-// 429 | BGL
-// 430 | BGN
-// 431 | BHD
-// 432 | BIF
-// 433 | BMD
-// 434 | BND
-// 435 | BOB
-// 436 | BOV
-// 437 | BRL
-// 438 | BSD
-// 439 | BTN
-// 440 | BWP
-// 441 | BYR
-// 442 | BZD
-// 443 | CAD
-// 444 | CDF
-// 445 | CHE
-// 446 | CHF
-// 447 | CHW
-// 448 | CLF
-// 449 | CLP
-// 450 | CNY
-// 451 | COP
-// 452 | COU
-// 453 | CRC
-// 454 | CSD
-// 455 | CUC
-// 456 | CVE
-// 457 | CYP
-// 458 | CZK
-// 459 | DEM
-// 460 | DJF
-// 461 | DKK
-// 462 | DOP
-// 463 | DZD
-// 464 | ECS
-// 465 | ECV
-// 466 | EEK
-// 467 | EGP
-// 468 | ERN
-// 469 | ESP
-// 470 | ETB
-// 471 | EUR
-// 472 | FIM
-// 473 | FJD
-// 474 | FKP
-// 475 | FRF
-// 476 | GBP
-// 477 | GEL
-// 478 | GHC
-// 479 | GHS
-// 480 | GIP
-// 481 | GMD
-// 482 | GNF
-// 483 | GRD
-// 484 | GTQ
-// 485 | GYD
-// 486 | HKD
-// 487 | HNL
-// 488 | HRK
-// 489 | HTG
-// 490 | HUF
-// 491 | IDR
-// 492 | IEP
-// 493 | ILS
-// 494 | INR
-// 495 | IQD
-// 496 | IRR
-// 497 | ISK
-// 498 | ITL
-// 499 | JMD
-// 500 | JOD
-// 501 | JPY
-// 502 | KAF
-// 503 | KES
-// 504 | KGS
-// 505 | KHR
-// 506 | KMF
-// 507 | KPW
-// 508 | KRW
-// 509 | KWD
-// 510 | KYD
-// 511 | KZT
-// 512 | LAK
-// 513 | LBP
-// 514 | LKR
-// 515 | LRD
-// 516 | LSL
-// 517 | LTL
-// 518 | LUF
-// 519 | LVL
-// 520 | LYD
-// 521 | MAD
-// 522 | MDL
-// 523 | MGA
-// 524 | MGF
-// 525 | MKD
-// 526 | MMK
-// 527 | MNT
-// 528 | MOP
-// 529 | MRO
-// 530 | MTL
-// 531 | MUR
-// 532 | MVR
-// 533 | MWK
-// 534 | MXN
-// 535 | MXV
-// 536 | MYR
-// 537 | MZM
-// 538 | MZN
-// 539 | NAD
-// 540 | NGN
-// 541 | NIO
-// 542 | NLG
-// 543 | NOK
-// 544 | NPR
-// 545 | NTD
-// 546 | NZD
-// 547 | OMR
-// 548 | PAB
-// 549 | PEN
-// 550 | PGK
-// 551 | PHP
-// 552 | PKR
-// 553 | PLN
-// 554 | PTE
-// 555 | PYG
-// 556 | QAR
-// 557 | ROL
-// 558 | RON
-// 559 | RSD
-// 560 | RUB
-// 561 | RUR
-// 562 | RWF
-// 563 | SAR
-// 564 | SBD
-// 565 | SCR
-// 566 | SDD
-// 567 | SDG
-// 568 | SDP
-// 569 | SEK
-// 570 | SGD
-// 571 | SHP
-// 572 | SIT
-// 573 | SKK
-// 574 | SLL
-// 575 | SOS
-// 576 | SPL
-// 577 | SRD
-// 578 | SRG
-// 579 | STD
-// 580 | SVC
-// 581 | SYP
-// 582 | SZL
-// 583 | THB
-// 584 | TJR
-// 585 | TJS
-// 586 | TMM
-// 587 | TMT
-// 588 | TND
-// 589 | TOP
-// 590 | TRL
-// 591 | TRY
-// 592 | TTD
-// 593 | TWD
-// 594 | TZS
-// 595 | UAH
-// 596 | UGX
-// 597 | USD
-// 598 | USN
-// 599 | USS
-// 600 | UYI
-// 601 | UYU
-// 602 | UZS
-// 603 | VEB
-// 604 | VEF
-// 605 | VND
-// 606 | VUV
-// 607 | WST
-// 608 | XAF
-// 609 | XAG
-// 610 | XAU
-// 611 | XB5
-// 612 | XBA
-// 613 | XBB
-// 614 | XBC
-// 615 | XBD
-// 616 | XCD
-// 617 | XDR
-// 618 | XFO
-// 619 | XFU
-// 620 | XOF
-// 621 | XPD
-// 622 | XPF
-// 623 | XPT
-// 624 | XTS
-// 625 | XXX
-// 626 | YER
-// 627 | YUM
-// 628 | ZAR
-// 629 | ZMK
-// 630 | ZMW
-// 631 | ZWD
-// 632 | ZWL
-// 633 | ZWN
-// 634 | ZWR
-//
-// Excelize support set custom number format for cell. For example, set number
-// as date type in Uruguay (Spanish) format for Sheet1!A6:
-//
-// f := excelize.NewFile()
-// f.SetCellValue("Sheet1", "A6", 42920.5)
-// exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"
-// style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp})
-// err = f.SetCellStyle("Sheet1", "A6", "A6", style)
-//
-// Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017
-//
-func (f *File) NewStyle(style interface{}) (int, error) {
- var fs *Style
- var err error
- var cellXfsID, fontID, borderID, fillID int
- switch v := style.(type) {
- case string:
- fs, err = parseFormatStyleSet(v)
- if err != nil {
- return cellXfsID, err
- }
- case *Style:
- fs = v
- default:
- return cellXfsID, errors.New("invalid parameter type")
+// Index | Symbol
+// -------+---------------------------------------------------------------
+// 164 | ¥
+// 165 | $ English (United States)
+// 166 | $ Cherokee (United States)
+// 167 | $ Chinese (Singapore)
+// 168 | $ Chinese (Taiwan)
+// 169 | $ English (Australia)
+// 170 | $ English (Belize)
+// 171 | $ English (Canada)
+// 172 | $ English (Jamaica)
+// 173 | $ English (New Zealand)
+// 174 | $ English (Singapore)
+// 175 | $ English (Trinidad & Tobago)
+// 176 | $ English (U.S. Virgin Islands)
+// 177 | $ English (United States)
+// 178 | $ French (Canada)
+// 179 | $ Hawaiian (United States)
+// 180 | $ Malay (Brunei)
+// 181 | $ Quechua (Ecuador)
+// 182 | $ Spanish (Chile)
+// 183 | $ Spanish (Colombia)
+// 184 | $ Spanish (Ecuador)
+// 185 | $ Spanish (El Salvador)
+// 186 | $ Spanish (Mexico)
+// 187 | $ Spanish (Puerto Rico)
+// 188 | $ Spanish (United States)
+// 189 | $ Spanish (Uruguay)
+// 190 | £ English (United Kingdom)
+// 191 | £ Scottish Gaelic (United Kingdom)
+// 192 | £ Welsh (United Kindom)
+// 193 | ¥ Chinese (China)
+// 194 | ¥ Japanese (Japan)
+// 195 | ¥ Sichuan Yi (China)
+// 196 | ¥ Tibetan (China)
+// 197 | ¥ Uyghur (China)
+// 198 | ֏ Armenian (Armenia)
+// 199 | ؋ Pashto (Afghanistan)
+// 200 | ؋ Persian (Afghanistan)
+// 201 | ৳ Bengali (Bangladesh)
+// 202 | ៛ Khmer (Cambodia)
+// 203 | ₡ Spanish (Costa Rica)
+// 204 | ₦ Hausa (Nigeria)
+// 205 | ₦ Igbo (Nigeria)
+// 206 | ₩ Korean (South Korea)
+// 207 | ₪ Hebrew (Israel)
+// 208 | ₫ Vietnamese (Vietnam)
+// 209 | € Basque (Spain)
+// 210 | € Breton (France)
+// 211 | € Catalan (Spain)
+// 212 | € Corsican (France)
+// 213 | € Dutch (Belgium)
+// 214 | € Dutch (Netherlands)
+// 215 | € English (Ireland)
+// 216 | € Estonian (Estonia)
+// 217 | € Euro (€ 123)
+// 218 | € Euro (123 €)
+// 219 | € Finnish (Finland)
+// 220 | € French (Belgium)
+// 221 | € French (France)
+// 222 | € French (Luxembourg)
+// 223 | € French (Monaco)
+// 224 | € French (Réunion)
+// 225 | € Galician (Spain)
+// 226 | € German (Austria)
+// 227 | € German (German)
+// 228 | € German (Luxembourg)
+// 229 | € Greek (Greece)
+// 230 | € Inari Sami (Finland)
+// 231 | € Irish (Ireland)
+// 232 | € Italian (Italy)
+// 233 | € Latin (Italy)
+// 234 | € Latin, Serbian (Montenegro)
+// 235 | € Larvian (Latvia)
+// 236 | € Lithuanian (Lithuania)
+// 237 | € Lower Sorbian (Germany)
+// 238 | € Luxembourgish (Luxembourg)
+// 239 | € Maltese (Malta)
+// 240 | € Northern Sami (Finland)
+// 241 | € Occitan (France)
+// 242 | € Portuguese (Portugal)
+// 243 | € Serbian (Montenegro)
+// 244 | € Skolt Sami (Finland)
+// 245 | € Slovak (Slovakia)
+// 246 | € Slovenian (Slovenia)
+// 247 | € Spanish (Spain)
+// 248 | € Swedish (Finland)
+// 249 | € Swiss German (France)
+// 250 | € Upper Sorbian (Germany)
+// 251 | € Western Frisian (Netherlands)
+// 252 | ₭ Lao (Laos)
+// 253 | ₮ Mongolian (Mongolia)
+// 254 | ₮ Mongolian, Mongolian (Mongolia)
+// 255 | ₱ English (Philippines)
+// 256 | ₱ Filipino (Philippines)
+// 257 | ₴ Ukrainian (Ukraine)
+// 258 | ₸ Kazakh (Kazakhstan)
+// 259 | ₹ Arabic, Kashmiri (India)
+// 260 | ₹ English (India)
+// 261 | ₹ Gujarati (India)
+// 262 | ₹ Hindi (India)
+// 263 | ₹ Kannada (India)
+// 264 | ₹ Kashmiri (India)
+// 265 | ₹ Konkani (India)
+// 266 | ₹ Manipuri (India)
+// 267 | ₹ Marathi (India)
+// 268 | ₹ Nepali (India)
+// 269 | ₹ Oriya (India)
+// 270 | ₹ Punjabi (India)
+// 271 | ₹ Sanskrit (India)
+// 272 | ₹ Sindhi (India)
+// 273 | ₹ Tamil (India)
+// 274 | ₹ Urdu (India)
+// 275 | ₺ Turkish (Turkey)
+// 276 | ₼ Azerbaijani (Azerbaijan)
+// 277 | ₼ Cyrillic, Azerbaijani (Azerbaijan)
+// 278 | ₽ Russian (Russia)
+// 279 | ₽ Sakha (Russia)
+// 280 | ₾ Georgian (Georgia)
+// 281 | B/. Spanish (Panama)
+// 282 | Br Oromo (Ethiopia)
+// 283 | Br Somali (Ethiopia)
+// 284 | Br Tigrinya (Ethiopia)
+// 285 | Bs Quechua (Bolivia)
+// 286 | Bs Spanish (Bolivia)
+// 287 | BS. Spanish (Venezuela)
+// 288 | BWP Tswana (Botswana)
+// 289 | C$ Spanish (Nicaragua)
+// 290 | CA$ Latin, Inuktitut (Canada)
+// 291 | CA$ Mohawk (Canada)
+// 292 | CA$ Unified Canadian Aboriginal Syllabics, Inuktitut (Canada)
+// 293 | CFA French (Mali)
+// 294 | CFA French (Senegal)
+// 295 | CFA Fulah (Senegal)
+// 296 | CFA Wolof (Senegal)
+// 297 | CHF French (Switzerland)
+// 298 | CHF German (Liechtenstein)
+// 299 | CHF German (Switzerland)
+// 300 | CHF Italian (Switzerland)
+// 301 | CHF Romansh (Switzerland)
+// 302 | CLP Mapuche (Chile)
+// 303 | CN¥ Mongolian, Mongolian (China)
+// 304 | DZD Central Atlas Tamazight (Algeria)
+// 305 | FCFA French (Cameroon)
+// 306 | Ft Hungarian (Hungary)
+// 307 | G French (Haiti)
+// 308 | Gs. Spanish (Paraguay)
+// 309 | GTQ K'iche' (Guatemala)
+// 310 | HK$ Chinese (Hong Kong (China))
+// 311 | HK$ English (Hong Kong (China))
+// 312 | HRK Croatian (Croatia)
+// 313 | IDR English (Indonesia)
+// 314 | IQD Arbic, Central Kurdish (Iraq)
+// 315 | ISK Icelandic (Iceland)
+// 316 | K Burmese (Myanmar (Burma))
+// 317 | Kč Czech (Czech Republic)
+// 318 | KM Bosnian (Bosnia & Herzegovina)
+// 319 | KM Croatian (Bosnia & Herzegovina)
+// 320 | KM Latin, Serbian (Bosnia & Herzegovina)
+// 321 | kr Faroese (Faroe Islands)
+// 322 | kr Northern Sami (Norway)
+// 323 | kr Northern Sami (Sweden)
+// 324 | kr Norwegian Bokmål (Norway)
+// 325 | kr Norwegian Nynorsk (Norway)
+// 326 | kr Swedish (Sweden)
+// 327 | kr. Danish (Denmark)
+// 328 | kr. Kalaallisut (Greenland)
+// 329 | Ksh Swahili (kenya)
+// 330 | L Romanian (Moldova)
+// 331 | L Russian (Moldova)
+// 332 | L Spanish (Honduras)
+// 333 | Lekë Albanian (Albania)
+// 334 | MAD Arabic, Central Atlas Tamazight (Morocco)
+// 335 | MAD French (Morocco)
+// 336 | MAD Tifinagh, Central Atlas Tamazight (Morocco)
+// 337 | MOP$ Chinese (Macau (China))
+// 338 | MVR Divehi (Maldives)
+// 339 | Nfk Tigrinya (Eritrea)
+// 340 | NGN Bini (Nigeria)
+// 341 | NGN Fulah (Nigeria)
+// 342 | NGN Ibibio (Nigeria)
+// 343 | NGN Kanuri (Nigeria)
+// 344 | NOK Lule Sami (Norway)
+// 345 | NOK Southern Sami (Norway)
+// 346 | NZ$ Maori (New Zealand)
+// 347 | PKR Sindhi (Pakistan)
+// 348 | PYG Guarani (Paraguay)
+// 349 | Q Spanish (Guatemala)
+// 350 | R Afrikaans (South Africa)
+// 351 | R English (South Africa)
+// 352 | R Zulu (South Africa)
+// 353 | R$ Portuguese (Brazil)
+// 354 | RD$ Spanish (Dominican Republic)
+// 355 | RF Kinyarwanda (Rwanda)
+// 356 | RM English (Malaysia)
+// 357 | RM Malay (Malaysia)
+// 358 | RON Romanian (Romania)
+// 359 | Rp Indonesoan (Indonesia)
+// 360 | Rs Urdu (Pakistan)
+// 361 | Rs. Tamil (Sri Lanka)
+// 362 | RSD Latin, Serbian (Serbia)
+// 363 | RSD Serbian (Serbia)
+// 364 | RUB Bashkir (Russia)
+// 365 | RUB Tatar (Russia)
+// 366 | S/. Quechua (Peru)
+// 367 | S/. Spanish (Peru)
+// 368 | SEK Lule Sami (Sweden)
+// 369 | SEK Southern Sami (Sweden)
+// 370 | soʻm Latin, Uzbek (Uzbekistan)
+// 371 | soʻm Uzbek (Uzbekistan)
+// 372 | SYP Syriac (Syria)
+// 373 | THB Thai (Thailand)
+// 374 | TMT Turkmen (Turkmenistan)
+// 375 | US$ English (Zimbabwe)
+// 376 | ZAR Northern Sotho (South Africa)
+// 377 | ZAR Southern Sotho (South Africa)
+// 378 | ZAR Tsonga (South Africa)
+// 379 | ZAR Tswana (south Africa)
+// 380 | ZAR Venda (South Africa)
+// 381 | ZAR Xhosa (South Africa)
+// 382 | zł Polish (Poland)
+// 383 | ден Macedonian (Macedonia)
+// 384 | KM Cyrillic, Bosnian (Bosnia & Herzegovina)
+// 385 | KM Serbian (Bosnia & Herzegovina)
+// 386 | лв. Bulgarian (Bulgaria)
+// 387 | p. Belarusian (Belarus)
+// 388 | сом Kyrgyz (Kyrgyzstan)
+// 389 | сом Tajik (Tajikistan)
+// 390 | ج.م. Arabic (Egypt)
+// 391 | د.أ. Arabic (Jordan)
+// 392 | د.أ. Arabic (United Arab Emirates)
+// 393 | د.ب. Arabic (Bahrain)
+// 394 | د.ت. Arabic (Tunisia)
+// 395 | د.ج. Arabic (Algeria)
+// 396 | د.ع. Arabic (Iraq)
+// 397 | د.ك. Arabic (Kuwait)
+// 398 | د.ل. Arabic (Libya)
+// 399 | د.م. Arabic (Morocco)
+// 400 | ر Punjabi (Pakistan)
+// 401 | ر.س. Arabic (Saudi Arabia)
+// 402 | ر.ع. Arabic (Oman)
+// 403 | ر.ق. Arabic (Qatar)
+// 404 | ر.ي. Arabic (Yemen)
+// 405 | ریال Persian (Iran)
+// 406 | ل.س. Arabic (Syria)
+// 407 | ل.ل. Arabic (Lebanon)
+// 408 | ብር Amharic (Ethiopia)
+// 409 | रू Nepaol (Nepal)
+// 410 | රු. Sinhala (Sri Lanka)
+// 411 | ADP
+// 412 | AED
+// 413 | AFA
+// 414 | AFN
+// 415 | ALL
+// 416 | AMD
+// 417 | ANG
+// 418 | AOA
+// 419 | ARS
+// 420 | ATS
+// 421 | AUD
+// 422 | AWG
+// 423 | AZM
+// 424 | AZN
+// 425 | BAM
+// 426 | BBD
+// 427 | BDT
+// 428 | BEF
+// 429 | BGL
+// 430 | BGN
+// 431 | BHD
+// 432 | BIF
+// 433 | BMD
+// 434 | BND
+// 435 | BOB
+// 436 | BOV
+// 437 | BRL
+// 438 | BSD
+// 439 | BTN
+// 440 | BWP
+// 441 | BYR
+// 442 | BZD
+// 443 | CAD
+// 444 | CDF
+// 445 | CHE
+// 446 | CHF
+// 447 | CHW
+// 448 | CLF
+// 449 | CLP
+// 450 | CNY
+// 451 | COP
+// 452 | COU
+// 453 | CRC
+// 454 | CSD
+// 455 | CUC
+// 456 | CVE
+// 457 | CYP
+// 458 | CZK
+// 459 | DEM
+// 460 | DJF
+// 461 | DKK
+// 462 | DOP
+// 463 | DZD
+// 464 | ECS
+// 465 | ECV
+// 466 | EEK
+// 467 | EGP
+// 468 | ERN
+// 469 | ESP
+// 470 | ETB
+// 471 | EUR
+// 472 | FIM
+// 473 | FJD
+// 474 | FKP
+// 475 | FRF
+// 476 | GBP
+// 477 | GEL
+// 478 | GHC
+// 479 | GHS
+// 480 | GIP
+// 481 | GMD
+// 482 | GNF
+// 483 | GRD
+// 484 | GTQ
+// 485 | GYD
+// 486 | HKD
+// 487 | HNL
+// 488 | HRK
+// 489 | HTG
+// 490 | HUF
+// 491 | IDR
+// 492 | IEP
+// 493 | ILS
+// 494 | INR
+// 495 | IQD
+// 496 | IRR
+// 497 | ISK
+// 498 | ITL
+// 499 | JMD
+// 500 | JOD
+// 501 | JPY
+// 502 | KAF
+// 503 | KES
+// 504 | KGS
+// 505 | KHR
+// 506 | KMF
+// 507 | KPW
+// 508 | KRW
+// 509 | KWD
+// 510 | KYD
+// 511 | KZT
+// 512 | LAK
+// 513 | LBP
+// 514 | LKR
+// 515 | LRD
+// 516 | LSL
+// 517 | LTL
+// 518 | LUF
+// 519 | LVL
+// 520 | LYD
+// 521 | MAD
+// 522 | MDL
+// 523 | MGA
+// 524 | MGF
+// 525 | MKD
+// 526 | MMK
+// 527 | MNT
+// 528 | MOP
+// 529 | MRO
+// 530 | MTL
+// 531 | MUR
+// 532 | MVR
+// 533 | MWK
+// 534 | MXN
+// 535 | MXV
+// 536 | MYR
+// 537 | MZM
+// 538 | MZN
+// 539 | NAD
+// 540 | NGN
+// 541 | NIO
+// 542 | NLG
+// 543 | NOK
+// 544 | NPR
+// 545 | NTD
+// 546 | NZD
+// 547 | OMR
+// 548 | PAB
+// 549 | PEN
+// 550 | PGK
+// 551 | PHP
+// 552 | PKR
+// 553 | PLN
+// 554 | PTE
+// 555 | PYG
+// 556 | QAR
+// 557 | ROL
+// 558 | RON
+// 559 | RSD
+// 560 | RUB
+// 561 | RUR
+// 562 | RWF
+// 563 | SAR
+// 564 | SBD
+// 565 | SCR
+// 566 | SDD
+// 567 | SDG
+// 568 | SDP
+// 569 | SEK
+// 570 | SGD
+// 571 | SHP
+// 572 | SIT
+// 573 | SKK
+// 574 | SLL
+// 575 | SOS
+// 576 | SPL
+// 577 | SRD
+// 578 | SRG
+// 579 | STD
+// 580 | SVC
+// 581 | SYP
+// 582 | SZL
+// 583 | THB
+// 584 | TJR
+// 585 | TJS
+// 586 | TMM
+// 587 | TMT
+// 588 | TND
+// 589 | TOP
+// 590 | TRL
+// 591 | TRY
+// 592 | TTD
+// 593 | TWD
+// 594 | TZS
+// 595 | UAH
+// 596 | UGX
+// 597 | USD
+// 598 | USN
+// 599 | USS
+// 600 | UYI
+// 601 | UYU
+// 602 | UZS
+// 603 | VEB
+// 604 | VEF
+// 605 | VND
+// 606 | VUV
+// 607 | WST
+// 608 | XAF
+// 609 | XAG
+// 610 | XAU
+// 611 | XB5
+// 612 | XBA
+// 613 | XBB
+// 614 | XBC
+// 615 | XBD
+// 616 | XCD
+// 617 | XDR
+// 618 | XFO
+// 619 | XFU
+// 620 | XOF
+// 621 | XPD
+// 622 | XPF
+// 623 | XPT
+// 624 | XTS
+// 625 | XXX
+// 626 | YER
+// 627 | YUM
+// 628 | ZAR
+// 629 | ZMK
+// 630 | ZMW
+// 631 | ZWD
+// 632 | ZWL
+// 633 | ZWN
+// 634 | ZWR
+//
+// Excelize support set custom number format for cell by CustomNumFmt field. For
+// example, set number as date type in Uruguay (Spanish) format for Sheet1!A6:
+//
+// f := excelize.NewFile()
+// defer func() {
+// if err := f.Close(); err != nil {
+// fmt.Println(err)
+// }
+// }()
+// if err := f.SetCellValue("Sheet1", "A6", 42920.5); err != nil {
+// fmt.Println(err)
+// return
+// }
+// exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"
+// style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp})
+// if err != nil {
+// fmt.Println(err)
+// return
+// }
+// err = f.SetCellStyle("Sheet1", "A6", "A6", style)
+//
+// Cell Sheet1!A6 in the spreadsheet application: martes, 04 de Julio de 2017
+//
+// DecimalPlaces is used to set the decimal places for built-in currency
+// formats, it doesn't work if you have specified the built-in all languages
+// formats or built-in language formats by NumFmt field, or specify the custom
+// number format by CustomNumFmt. When you get style definition by the GetStyle
+// or GetConditionalStyle function, the DecimalPlaces only doesn't nil if a
+// number format code has the same decimal places in the positive part negative
+// part, or only the positive part.
+func (f *File) NewStyle(style *Style) (int, error) {
+ var (
+ fs *Style
+ font *xlsxFont
+ err error
+ cellXfsID, fontID, borderID, fillID int
+ )
+ if style == nil {
+ return cellXfsID, err
+ }
+ fs, err = parseFormatStyleSet(style)
+ if err != nil {
+ return cellXfsID, err
}
- if fs.DecimalPlaces == 0 {
- fs.DecimalPlaces = 2
+ if fs.DecimalPlaces != nil && (*fs.DecimalPlaces < 0 || *fs.DecimalPlaces > 30) {
+ fs.DecimalPlaces = intPtr(2)
}
- s := f.stylesReader()
+ f.mu.Lock()
+ s, err := f.stylesReader()
+ if err != nil {
+ f.mu.Unlock()
+ return cellXfsID, err
+ }
+ f.mu.Unlock()
+ s.mu.Lock()
+ defer s.mu.Unlock()
// check given style already exist.
- if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 {
+ if cellXfsID, err = f.getStyleID(s, fs); err != nil || cellXfsID != -1 {
return cellXfsID, err
}
numFmtID := newNumFmt(s, fs)
if fs.Font != nil {
- fontID = f.getFontID(s, fs)
+ fontID, _ = f.getFontID(s, fs)
if fontID == -1 {
s.Fonts.Count++
- s.Fonts.Font = append(s.Fonts.Font, f.newFont(fs))
+ font, _ = f.newFont(fs)
+ s.Fonts.Font = append(s.Fonts.Font, font)
fontID = s.Fonts.Count - 1
}
}
@@ -1963,60 +1023,638 @@ func (f *File) NewStyle(style interface{}) (int, error) {
applyAlignment, alignment := fs.Alignment != nil, newAlignment(fs)
applyProtection, protection := fs.Protection != nil, newProtection(fs)
- cellXfsID = setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection)
- return cellXfsID, nil
+ return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection)
}
-var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{
- "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool {
- if style.NumFmt == 0 && style.CustomNumFmt == nil && numFmtID == -1 {
- return xf.NumFmtID != nil || *xf.NumFmtID == 0
+var (
+ // styleBorders list all types of the cell border style.
+ styleBorders = []string{
+ "none",
+ "thin",
+ "medium",
+ "dashed",
+ "dotted",
+ "thick",
+ "double",
+ "hair",
+ "mediumDashed",
+ "dashDot",
+ "mediumDashDot",
+ "dashDotDot",
+ "mediumDashDotDot",
+ "slantDashDot",
+ }
+ // styleBorderTypes list all types of the cell border.
+ styleBorderTypes = []string{
+ "left", "right", "top", "bottom", "diagonalUp", "diagonalDown",
+ }
+ // styleFillPatterns list all types of the cell fill style.
+ styleFillPatterns = []string{
+ "none",
+ "solid",
+ "mediumGray",
+ "darkGray",
+ "lightGray",
+ "darkHorizontal",
+ "darkVertical",
+ "darkDown",
+ "darkUp",
+ "darkGrid",
+ "darkTrellis",
+ "lightHorizontal",
+ "lightVertical",
+ "lightDown",
+ "lightUp",
+ "lightGrid",
+ "lightTrellis",
+ "gray125",
+ "gray0625",
+ }
+ // styleFillVariants list all preset variants of the fill style.
+ styleFillVariants = func() []xlsxGradientFill {
+ return []xlsxGradientFill{
+ {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 270, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 180, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
+ {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 255, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
+ {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 315, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
+ {Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path"},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Left: 1, Right: 1},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Top: 1},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Left: 1, Right: 1, Top: 1},
+ {Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 0.5, Left: 0.5, Right: 0.5, Top: 0.5},
}
- if style.NegRed || style.Lang != "" || style.DecimalPlaces != 2 {
- return false
+ }
+
+ // getXfIDFuncs provides a function to get xfID by given style.
+ getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{
+ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool {
+ if style.CustomNumFmt == nil && numFmtID == -1 {
+ return xf.NumFmtID != nil && *xf.NumFmtID == 0
+ }
+ if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) {
+ return false
+ }
+ return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID
+ },
+ "font": func(fontID int, xf xlsxXf, style *Style) bool {
+ if style.Font == nil {
+ return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont)
+ }
+ return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont
+ },
+ "fill": func(fillID int, xf xlsxXf, style *Style) bool {
+ if style.Fill.Type == "" {
+ return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill)
+ }
+ return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill
+ },
+ "border": func(borderID int, xf xlsxXf, style *Style) bool {
+ if len(style.Border) == 0 {
+ return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder)
+ }
+ return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder
+ },
+ "alignment": func(ID int, xf xlsxXf, style *Style) bool {
+ if style.Alignment == nil {
+ return xf.ApplyAlignment == nil || !*xf.ApplyAlignment
+ }
+ return reflect.DeepEqual(xf.Alignment, newAlignment(style))
+ },
+ "protection": func(ID int, xf xlsxXf, style *Style) bool {
+ if style.Protection == nil {
+ return xf.ApplyProtection == nil || !*xf.ApplyProtection
+ }
+ return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection
+ },
+ }
+
+ // extractStyleCondFuncs provides a function set to returns if should be
+ // extract style definition by given style.
+ extractStyleCondFuncs = map[string]func(xlsxXf, *xlsxStyleSheet) bool{
+ "fill": func(xf xlsxXf, s *xlsxStyleSheet) bool {
+ return (xf.ApplyFill == nil || (xf.ApplyFill != nil && *xf.ApplyFill)) &&
+ xf.FillID != nil && s.Fills != nil &&
+ *xf.FillID < len(s.Fills.Fill)
+ },
+ "border": func(xf xlsxXf, s *xlsxStyleSheet) bool {
+ return (xf.ApplyBorder == nil || (xf.ApplyBorder != nil && *xf.ApplyBorder)) &&
+ xf.BorderID != nil && s.Borders != nil &&
+ *xf.BorderID < len(s.Borders.Border)
+ },
+ "font": func(xf xlsxXf, s *xlsxStyleSheet) bool {
+ return (xf.ApplyFont == nil || (xf.ApplyFont != nil && *xf.ApplyFont)) &&
+ xf.FontID != nil && s.Fonts != nil &&
+ *xf.FontID < len(s.Fonts.Font)
+ },
+ "alignment": func(xf xlsxXf, s *xlsxStyleSheet) bool {
+ return xf.ApplyAlignment == nil || (xf.ApplyAlignment != nil && *xf.ApplyAlignment)
+ },
+ "protection": func(xf xlsxXf, s *xlsxStyleSheet) bool {
+ return xf.ApplyProtection == nil || (xf.ApplyProtection != nil && *xf.ApplyProtection)
+ },
+ }
+
+ // drawContFmtFunc defines functions to create conditional formats.
+ drawContFmtFunc = map[string]func(p int, ct, ref, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){
+ "cellIs": drawCondFmtCellIs,
+ "timePeriod": drawCondFmtTimePeriod,
+ "text": drawCondFmtText,
+ "top10": drawCondFmtTop10,
+ "aboveAverage": drawCondFmtAboveAverage,
+ "duplicateValues": drawCondFmtDuplicateUniqueValues,
+ "uniqueValues": drawCondFmtDuplicateUniqueValues,
+ "containsBlanks": drawCondFmtBlanks,
+ "notContainsBlanks": drawCondFmtNoBlanks,
+ "containsErrors": drawCondFmtErrors,
+ "notContainsErrors": drawCondFmtNoErrors,
+ "2_color_scale": drawCondFmtColorScale,
+ "3_color_scale": drawCondFmtColorScale,
+ "dataBar": drawCondFmtDataBar,
+ "expression": drawCondFmtExp,
+ "iconSet": drawCondFmtIconSet,
+ }
+
+ // extractContFmtFunc defines functions to get conditional formats.
+ extractContFmtFunc = map[string]func(*File, *xlsxCfRule, *xlsxExtLst) ConditionalFormatOptions{
+ "cellIs": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtCellIs(c, extLst)
+ },
+ "timePeriod": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtTimePeriod(c, extLst)
+ },
+ "containsText": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtText(c, extLst)
+ },
+ "notContainsText": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtText(c, extLst)
+ },
+ "beginsWith": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtText(c, extLst)
+ },
+ "endsWith": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtText(c, extLst)
+ },
+ "top10": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtTop10(c, extLst)
+ },
+ "aboveAverage": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtAboveAverage(c, extLst)
+ },
+ "duplicateValues": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtDuplicateUniqueValues(c, extLst)
+ },
+ "uniqueValues": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtDuplicateUniqueValues(c, extLst)
+ },
+ "containsBlanks": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtBlanks(c, extLst)
+ },
+ "notContainsBlanks": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtNoBlanks(c, extLst)
+ },
+ "containsErrors": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtErrors(c, extLst)
+ },
+ "notContainsErrors": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtNoErrors(c, extLst)
+ },
+ "colorScale": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtColorScale(c, extLst)
+ },
+ "dataBar": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtDataBar(c, extLst)
+ },
+ "expression": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtExp(c, extLst)
+ },
+ "iconSet": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return f.extractCondFmtIconSet(c, extLst)
+ },
+ }
+
+ // validType defined the list of valid validation types.
+ validType = map[string]string{
+ "cell": "cellIs",
+ "average": "aboveAverage",
+ "duplicate": "duplicateValues",
+ "unique": "uniqueValues",
+ "top": "top10",
+ "bottom": "top10",
+ "text": "text",
+ "time_period": "timePeriod",
+ "blanks": "containsBlanks",
+ "no_blanks": "notContainsBlanks",
+ "errors": "containsErrors",
+ "no_errors": "notContainsErrors",
+ "2_color_scale": "2_color_scale",
+ "3_color_scale": "3_color_scale",
+ "data_bar": "dataBar",
+ "formula": "expression",
+ "icon_set": "iconSet",
+ }
+ // criteriaType defined the list of valid criteria types.
+ criteriaType = map[string]string{
+ "!=": "notEqual",
+ "<": "lessThan",
+ "<=": "lessThanOrEqual",
+ "<>": "notEqual",
+ "=": "equal",
+ "==": "equal",
+ ">": "greaterThan",
+ ">=": "greaterThanOrEqual",
+ "begins with": "beginsWith",
+ "between": "between",
+ "containing": "containsText",
+ "continue month": "nextMonth",
+ "continue week": "nextWeek",
+ "ends with": "endsWith",
+ "equal to": "equal",
+ "greater than or equal to": "greaterThanOrEqual",
+ "greater than": "greaterThan",
+ "last 7 days": "last7Days",
+ "last month": "lastMonth",
+ "last week": "lastWeek",
+ "less than or equal to": "lessThanOrEqual",
+ "less than": "lessThan",
+ "not between": "notBetween",
+ "not containing": "notContains",
+ "not equal to": "notEqual",
+ "this month": "thisMonth",
+ "this week": "thisWeek",
+ "today": "today",
+ "tomorrow": "tomorrow",
+ "yesterday": "yesterday",
+ }
+ // operatorType defined the list of valid operator types.
+ operatorType = map[string]string{
+ "beginsWith": "begins with",
+ "between": "between",
+ "containsText": "containing",
+ "endsWith": "ends with",
+ "equal": "equal to",
+ "greaterThan": "greater than",
+ "greaterThanOrEqual": "greater than or equal to",
+ "last7Days": "last 7 days",
+ "lastMonth": "last month",
+ "lastWeek": "last week",
+ "lessThan": "less than",
+ "lessThanOrEqual": "less than or equal to",
+ "nextMonth": "continue month",
+ "nextWeek": "continue week",
+ "notBetween": "not between",
+ "notContains": "not containing",
+ "notEqual": "not equal to",
+ "thisMonth": "this month",
+ "thisWeek": "this week",
+ "today": "today",
+ "tomorrow": "tomorrow",
+ "yesterday": "yesterday",
+ }
+ // cellIsCriteriaType defined the list of valid criteria types used for
+ // cellIs conditional formats.
+ cellIsCriteriaType = []string{
+ "equal",
+ "notEqual",
+ "greaterThan",
+ "lessThan",
+ "greaterThanOrEqual",
+ "lessThanOrEqual",
+ "containsText",
+ "notContains",
+ "beginsWith",
+ "endsWith",
+ }
+ // cfvo3 defined the icon set conditional formatting rules.
+ cfvo3 = &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{
+ {Type: "percent", Val: "0"},
+ {Type: "percent", Val: "33"},
+ {Type: "percent", Val: "67"},
+ }}}
+ // cfvo4 defined the icon set conditional formatting rules.
+ cfvo4 = &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{
+ {Type: "percent", Val: "0"},
+ {Type: "percent", Val: "25"},
+ {Type: "percent", Val: "50"},
+ {Type: "percent", Val: "75"},
+ }}}
+ // cfvo5 defined the icon set conditional formatting rules.
+ cfvo5 = &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{
+ {Type: "percent", Val: "0"},
+ {Type: "percent", Val: "20"},
+ {Type: "percent", Val: "40"},
+ {Type: "percent", Val: "60"},
+ {Type: "percent", Val: "80"},
+ }}}
+ // condFmtIconSetPresets defined the list of icon set conditional formatting
+ // rules.
+ condFmtIconSetPresets = map[string]*xlsxCfRule{
+ "3Arrows": cfvo3,
+ "3ArrowsGray": cfvo3,
+ "3Flags": cfvo3,
+ "3Signs": cfvo3,
+ "3Symbols": cfvo3,
+ "3Symbols2": cfvo3,
+ "3TrafficLights1": cfvo3,
+ "3TrafficLights2": cfvo3,
+ "4Arrows": cfvo4,
+ "4ArrowsGray": cfvo4,
+ "4Rating": cfvo4,
+ "4RedToBlack": cfvo4,
+ "4TrafficLights": cfvo4,
+ "5Arrows": cfvo5,
+ "5ArrowsGray": cfvo5,
+ "5Quarters": cfvo5,
+ "5Rating": cfvo5,
+ }
+)
+
+// colorChoice returns a hex color code from the actual color values.
+func (clr *decodeCTColor) colorChoice() *string {
+ if clr.SrgbClr != nil {
+ return clr.SrgbClr.Val
+ }
+ if clr.SysClr != nil {
+ return &clr.SysClr.LastClr
+ }
+ return nil
+}
+
+// GetBaseColor returns the preferred hex color code by giving hex color code,
+// indexed color, and theme color.
+func (f *File) GetBaseColor(hexColor string, indexedColor int, themeColor *int) string {
+ if f.Theme != nil && themeColor != nil {
+ clrScheme := f.Theme.ThemeElements.ClrScheme
+ if val, ok := map[int]*string{
+ 0: clrScheme.Lt1.colorChoice(),
+ 1: clrScheme.Dk1.colorChoice(),
+ 2: clrScheme.Lt2.colorChoice(),
+ 3: clrScheme.Dk2.colorChoice(),
+ 4: clrScheme.Accent1.colorChoice(),
+ 5: clrScheme.Accent2.colorChoice(),
+ 6: clrScheme.Accent3.colorChoice(),
+ 7: clrScheme.Accent4.colorChoice(),
+ 8: clrScheme.Accent5.colorChoice(),
+ 9: clrScheme.Accent6.colorChoice(),
+ }[*themeColor]; ok && val != nil {
+ return *val
}
- return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID
- },
- "font": func(fontID int, xf xlsxXf, style *Style) bool {
- if style.Font == nil {
- return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || *xf.ApplyFont == false)
+ }
+ if len(hexColor) == 6 {
+ return hexColor
+ }
+ if len(hexColor) == 8 {
+ return strings.TrimPrefix(hexColor, "FF")
+ }
+ if f.Styles != nil && f.Styles.Colors != nil && f.Styles.Colors.IndexedColors != nil &&
+ indexedColor < len(f.Styles.Colors.IndexedColors.RgbColor) {
+ return strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[indexedColor].RGB, "FF")
+ }
+ if indexedColor < len(IndexedColorMapping) {
+ return IndexedColorMapping[indexedColor]
+ }
+ return hexColor
+}
+
+// getThemeColor provides a function to convert theme color or index color to
+// RGB color.
+func (f *File) getThemeColor(clr *xlsxColor) string {
+ var RGB string
+ if clr == nil || f.Theme == nil {
+ return RGB
+ }
+ if RGB = f.GetBaseColor(clr.RGB, clr.Indexed, clr.Theme); RGB != "" {
+ RGB = strings.TrimPrefix(ThemeColor(RGB, clr.Tint), "FF")
+ }
+ return RGB
+}
+
+// extractBorders provides a function to extract borders styles settings by
+// given border styles definition.
+func (f *File) extractBorders(bdr *xlsxBorder, s *xlsxStyleSheet, style *Style) {
+ if bdr != nil {
+ var borders []Border
+ extractBorder := func(lineType string, line *xlsxLine) {
+ if line != nil && line.Style != "" {
+ borders = append(borders, Border{
+ Type: lineType,
+ Color: f.getThemeColor(line.Color),
+ Style: inStrSlice(styleBorders, line.Style, false),
+ })
+ }
}
- return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont == true
- },
- "fill": func(fillID int, xf xlsxXf, style *Style) bool {
- if style.Fill.Type == "" {
- return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || *xf.ApplyFill == false)
+ for i, line := range []*xlsxLine{
+ bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal,
+ } {
+ if i < 4 {
+ extractBorder(styleBorderTypes[i], line)
+ }
+ if i == 4 && bdr.DiagonalUp {
+ extractBorder(styleBorderTypes[i], line)
+ }
+ if i == 5 && bdr.DiagonalDown {
+ extractBorder(styleBorderTypes[i], line)
+ }
+ }
+ style.Border = borders
+ }
+}
+
+// extractFills provides a function to extract fill styles settings by
+// given fill styles definition.
+func (f *File) extractFills(fl *xlsxFill, s *xlsxStyleSheet, style *Style) {
+ if fl != nil {
+ var fill Fill
+ if fl.GradientFill != nil {
+ fill.Type = "gradient"
+ for shading, variants := range styleFillVariants() {
+ if fl.GradientFill.Bottom == variants.Bottom &&
+ fl.GradientFill.Degree == variants.Degree &&
+ fl.GradientFill.Left == variants.Left &&
+ fl.GradientFill.Right == variants.Right &&
+ fl.GradientFill.Top == variants.Top &&
+ fl.GradientFill.Type == variants.Type {
+ fill.Shading = shading
+ break
+ }
+ }
+ for _, stop := range fl.GradientFill.Stop {
+ fill.Color = append(fill.Color, f.getThemeColor(&stop.Color))
+ }
+ }
+ if fl.PatternFill != nil {
+ fill.Type = "pattern"
+ fill.Pattern = inStrSlice(styleFillPatterns, fl.PatternFill.PatternType, false)
+ if fl.PatternFill.BgColor != nil {
+ fill.Color = []string{f.getThemeColor(fl.PatternFill.BgColor)}
+ }
+ if fl.PatternFill.FgColor != nil {
+ fill.Color = []string{f.getThemeColor(fl.PatternFill.FgColor)}
+ }
+ }
+ style.Fill = fill
+ }
+}
+
+// extractFont provides a function to extract font styles settings by given
+// font styles definition.
+func (f *File) extractFont(fnt *xlsxFont, s *xlsxStyleSheet, style *Style) {
+ if fnt != nil {
+ var font Font
+ if fnt.B != nil {
+ font.Bold = fnt.B.Value()
+ }
+ if fnt.I != nil {
+ font.Italic = fnt.I.Value()
+ }
+ if fnt.U != nil {
+ if font.Underline = fnt.U.Value(); font.Underline == "" {
+ font.Underline = "single"
+ }
+ }
+ if fnt.Name != nil {
+ font.Family = fnt.Name.Value()
+ }
+ if fnt.Sz != nil {
+ font.Size = fnt.Sz.Value()
+ }
+ if fnt.Strike != nil {
+ font.Strike = fnt.Strike.Value()
+ }
+ if fnt.Color != nil {
+ font.Color = strings.TrimPrefix(fnt.Color.RGB, "FF")
+ font.ColorIndexed = fnt.Color.Indexed
+ font.ColorTheme = fnt.Color.Theme
+ font.ColorTint = fnt.Color.Tint
+ }
+ style.Font = &font
+ }
+}
+
+// extractNumFmt provides a function to extract number format by given styles
+// definition.
+func (f *File) extractNumFmt(n *int, s *xlsxStyleSheet, style *Style) {
+ if n != nil {
+ numFmtID := *n
+ if builtInFmtCode, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) {
+ style.NumFmt = numFmtID
+ if decimalPlaces := f.extractNumFmtDecimal(builtInFmtCode); decimalPlaces != -1 {
+ style.DecimalPlaces = &decimalPlaces
+ }
+ return
+ }
+ if s.NumFmts != nil {
+ for _, numFmt := range s.NumFmts.NumFmt {
+ if numFmt.NumFmtID != numFmtID {
+ continue
+ }
+ if decimalPlaces := f.extractNumFmtDecimal(numFmt.FormatCode); decimalPlaces != -1 {
+ style.DecimalPlaces = &decimalPlaces
+ }
+ style.CustomNumFmt = &numFmt.FormatCode
+ if strings.Contains(numFmt.FormatCode, ";[Red]") {
+ style.NegRed = true
+ }
+ for numFmtID, fmtCode := range currencyNumFmt {
+ if style.NegRed {
+ fmtCode += ";[Red]" + fmtCode
+ }
+ if numFmt.FormatCode == fmtCode {
+ style.NumFmt = numFmtID
+ }
+ }
+ }
}
- return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill == true
- },
- "border": func(borderID int, xf xlsxXf, style *Style) bool {
- if len(style.Border) == 0 {
- return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || *xf.ApplyBorder == false)
+ }
+}
+
+// extractAlignment provides a function to extract alignment format by
+// given style definition.
+func (f *File) extractAlignment(a *xlsxAlignment, s *xlsxStyleSheet, style *Style) {
+ if a != nil {
+ style.Alignment = &Alignment{
+ Horizontal: a.Horizontal,
+ Indent: a.Indent,
+ JustifyLastLine: a.JustifyLastLine,
+ ReadingOrder: a.ReadingOrder,
+ RelativeIndent: a.RelativeIndent,
+ ShrinkToFit: a.ShrinkToFit,
+ TextRotation: a.TextRotation,
+ Vertical: a.Vertical,
+ WrapText: a.WrapText,
}
- return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder == true
- },
- "alignment": func(ID int, xf xlsxXf, style *Style) bool {
- if style.Alignment == nil {
- return xf.ApplyAlignment == nil || *xf.ApplyAlignment == false
+ }
+}
+
+// extractProtection provides a function to extract protection settings by
+// given format definition.
+func (f *File) extractProtection(p *xlsxProtection, s *xlsxStyleSheet, style *Style) {
+ if p != nil {
+ style.Protection = &Protection{}
+ if p.Hidden != nil {
+ style.Protection.Hidden = *p.Hidden
}
- return reflect.DeepEqual(xf.Alignment, newAlignment(style)) && xf.ApplyBorder != nil && *xf.ApplyBorder == true
- },
- "protection": func(ID int, xf xlsxXf, style *Style) bool {
- if style.Protection == nil {
- return xf.ApplyProtection == nil || *xf.ApplyProtection == false
+ if p.Locked != nil {
+ style.Protection.Locked = *p.Locked
}
- return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection == true
- },
+ }
+}
+
+// GetStyle provides a function to get style definition by given style index.
+func (f *File) GetStyle(idx int) (*Style, error) {
+ var style *Style
+ f.mu.Lock()
+ s, err := f.stylesReader()
+ if err != nil {
+ return style, err
+ }
+ f.mu.Unlock()
+ if idx < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= idx {
+ return style, newInvalidStyleID(idx)
+ }
+ style = &Style{}
+ xf := s.CellXfs.Xf[idx]
+ if extractStyleCondFuncs["fill"](xf, s) {
+ f.extractFills(s.Fills.Fill[*xf.FillID], s, style)
+ }
+ if extractStyleCondFuncs["border"](xf, s) {
+ f.extractBorders(s.Borders.Border[*xf.BorderID], s, style)
+ }
+ if extractStyleCondFuncs["font"](xf, s) {
+ f.extractFont(s.Fonts.Font[*xf.FontID], s, style)
+ }
+ if extractStyleCondFuncs["alignment"](xf, s) {
+ f.extractAlignment(xf.Alignment, s, style)
+ }
+ if extractStyleCondFuncs["protection"](xf, s) {
+ f.extractProtection(xf.Protection, s, style)
+ }
+ f.extractNumFmt(xf.NumFmtID, s, style)
+ return style, nil
}
// getStyleID provides a function to get styleID by given style. If given
-// style is not exist, will return -1.
-func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) {
- styleID = -1
+// style does not exist, will return -1.
+func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (int, error) {
+ var (
+ err error
+ fontID int
+ styleID = -1
+ )
if ss.CellXfs == nil {
- return
+ return styleID, err
+ }
+ numFmtID, borderID, fillID := getNumFmtID(ss, style), getBorderID(ss, style), getFillID(ss, style)
+ if fontID, err = f.getFontID(ss, style); err != nil {
+ return styleID, err
}
- numFmtID, borderID, fillID, fontID := getNumFmtID(ss, style), getBorderID(ss, style), getFillID(ss, style), f.getFontID(ss, style)
if style.CustomNumFmt != nil {
numFmtID = getCustomNumFmtID(ss, style)
}
@@ -2028,23 +1666,31 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) {
getXfIDFuncs["alignment"](0, xf, style) &&
getXfIDFuncs["protection"](0, xf, style) {
styleID = xfID
- return
+ return styleID, err
}
}
- return
+ return styleID, err
}
// NewConditionalStyle provides a function to create style for conditional
-// format by given style format. The parameters are the same as function
-// NewStyle(). Note that the color field uses RGB color code and only support
-// to set font, fills, alignment and borders currently.
-func (f *File) NewConditionalStyle(style string) (int, error) {
- s := f.stylesReader()
+// format by given style format. The parameters are the same with the NewStyle
+// function.
+func (f *File) NewConditionalStyle(style *Style) (int, error) {
+ f.mu.Lock()
+ s, err := f.stylesReader()
+ if err != nil {
+ f.mu.Unlock()
+ return 0, err
+ }
+ f.mu.Unlock()
fs, err := parseFormatStyleSet(style)
if err != nil {
return 0, err
}
- dxf := dxf{
+ if fs.DecimalPlaces != nil && (*fs.DecimalPlaces < 0 || *fs.DecimalPlaces > 30) {
+ fs.DecimalPlaces = intPtr(2)
+ }
+ dxf := xlsxDxf{
Fill: newFills(fs, false),
}
if fs.Alignment != nil {
@@ -2054,125 +1700,238 @@ func (f *File) NewConditionalStyle(style string) (int, error) {
dxf.Border = newBorders(fs)
}
if fs.Font != nil {
- dxf.Font = f.newFont(fs)
+ dxf.Font, _ = f.newFont(fs)
}
- dxfStr, _ := xml.Marshal(dxf)
+ if fs.Protection != nil {
+ dxf.Protection = newProtection(fs)
+ }
+ dxf.NumFmt = newDxfNumFmt(s, style, &dxf)
if s.Dxfs == nil {
s.Dxfs = &xlsxDxfs{}
}
s.Dxfs.Count++
- s.Dxfs.Dxfs = append(s.Dxfs.Dxfs, &xlsxDxf{
- Dxf: string(dxfStr[5 : len(dxfStr)-6]),
- })
+ s.Dxfs.Dxfs = append(s.Dxfs.Dxfs, &dxf)
return s.Dxfs.Count - 1, nil
}
-// GetDefaultFont provides the default font name currently set in the workbook
-// Documents generated by excelize start with Calibri.
-func (f *File) GetDefaultFont() string {
- font := f.readDefaultFont()
- return *font.Name.Val
+// GetConditionalStyle returns conditional format style definition by specified
+// style index.
+func (f *File) GetConditionalStyle(idx int) (*Style, error) {
+ var style *Style
+ f.mu.Lock()
+ s, err := f.stylesReader()
+ if err != nil {
+ return style, err
+ }
+ f.mu.Unlock()
+ if idx < 0 || s.Dxfs == nil || len(s.Dxfs.Dxfs) <= idx {
+ return style, newInvalidStyleID(idx)
+ }
+ style = &Style{}
+ xf := s.Dxfs.Dxfs[idx]
+ // The default pattern fill type of conditional format style is solid
+ if xf.Fill != nil && xf.Fill.PatternFill != nil && xf.Fill.PatternFill.PatternType == "" {
+ xf.Fill.PatternFill.PatternType = "solid"
+ }
+ f.extractFills(xf.Fill, s, style)
+ f.extractBorders(xf.Border, s, style)
+ f.extractFont(xf.Font, s, style)
+ f.extractAlignment(xf.Alignment, s, style)
+ f.extractProtection(xf.Protection, s, style)
+ if xf.NumFmt != nil {
+ f.extractNumFmt(&xf.NumFmt.NumFmtID, s, style)
+ }
+ return style, nil
+}
+
+// newDxfNumFmt provides a function to create number format for conditional
+// format styles.
+func newDxfNumFmt(styleSheet *xlsxStyleSheet, style *Style, dxf *xlsxDxf) *xlsxNumFmt {
+ dp, numFmtID := "0", 164 // Default custom number format code from 164.
+ if style.DecimalPlaces != nil && *style.DecimalPlaces > 0 {
+ dp += "."
+ for i := 0; i < *style.DecimalPlaces; i++ {
+ dp += "0"
+ }
+ }
+ if style.CustomNumFmt != nil {
+ if styleSheet.Dxfs != nil {
+ for _, d := range styleSheet.Dxfs.Dxfs {
+ if d != nil && d.NumFmt != nil && d.NumFmt.NumFmtID > numFmtID {
+ numFmtID = d.NumFmt.NumFmtID
+ }
+ }
+ }
+ return &xlsxNumFmt{NumFmtID: numFmtID + 1, FormatCode: *style.CustomNumFmt}
+ }
+ numFmtCode, ok := builtInNumFmt[style.NumFmt]
+ if style.NumFmt > 0 && ok {
+ return &xlsxNumFmt{NumFmtID: style.NumFmt, FormatCode: numFmtCode}
+ }
+ fc, currency := currencyNumFmt[style.NumFmt]
+ if !currency {
+ return nil
+ }
+ if style.DecimalPlaces != nil {
+ fc = strings.ReplaceAll(fc, "0.00", dp)
+ }
+ if style.NegRed {
+ fc = fc + ";[Red]" + fc
+ }
+ return &xlsxNumFmt{NumFmtID: numFmtID, FormatCode: fc}
+}
+
+// GetDefaultFont provides the default font name currently set in the
+// workbook. The spreadsheet generated by excelize default font is Calibri.
+func (f *File) GetDefaultFont() (string, error) {
+ font, err := f.readDefaultFont()
+ if err != nil {
+ return "", err
+ }
+ return *font.Name.Val, err
}
// SetDefaultFont changes the default font in the workbook.
-func (f *File) SetDefaultFont(fontName string) {
- font := f.readDefaultFont()
+func (f *File) SetDefaultFont(fontName string) error {
+ font, err := f.readDefaultFont()
+ if err != nil {
+ return err
+ }
font.Name.Val = stringPtr(fontName)
- s := f.stylesReader()
+ f.mu.Lock()
+ s, _ := f.stylesReader()
+ f.mu.Unlock()
s.Fonts.Font[0] = font
custom := true
s.CellStyles.CellStyle[0].CustomBuiltIn = &custom
+ return err
}
-// readDefaultFont provides an unmarshalled font value.
-func (f *File) readDefaultFont() *xlsxFont {
- s := f.stylesReader()
- return s.Fonts.Font[0]
+// readDefaultFont provides an un-marshalled font value.
+func (f *File) readDefaultFont() (*xlsxFont, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ s, err := f.stylesReader()
+ if err != nil {
+ return nil, err
+ }
+ return s.Fonts.Font[0], err
}
// getFontID provides a function to get font ID.
-// If given font is not exist, will return -1.
-func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) {
- fontID = -1
+// If given font does not exist, will return -1.
+func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (int, error) {
+ var err error
+ fontID := -1
if styleSheet.Fonts == nil || style.Font == nil {
- return
+ return fontID, err
}
for idx, fnt := range styleSheet.Fonts.Font {
- if reflect.DeepEqual(*fnt, *f.newFont(style)) {
+ font, err := f.newFont(style)
+ if err != nil {
+ return fontID, err
+ }
+ if reflect.DeepEqual(*fnt, *font) {
fontID = idx
+ return fontID, err
+ }
+ }
+ return fontID, err
+}
+
+// newFontColor set font color by given styles.
+func newFontColor(font *Font) *xlsxColor {
+ var fontColor *xlsxColor
+ prepareFontColor := func() {
+ if fontColor != nil {
return
}
+ fontColor = &xlsxColor{}
+ }
+ if font.Color != "" {
+ prepareFontColor()
+ fontColor.RGB = getPaletteColor(font.Color)
+ }
+ if font.ColorIndexed >= 0 && font.ColorIndexed <= len(IndexedColorMapping)+1 {
+ prepareFontColor()
+ fontColor.Indexed = font.ColorIndexed
}
- return
+ if font.ColorTheme != nil {
+ prepareFontColor()
+ fontColor.Theme = font.ColorTheme
+ }
+ if font.ColorTint != 0 {
+ prepareFontColor()
+ fontColor.Tint = font.ColorTint
+ }
+ return fontColor
}
// newFont provides a function to add font style by given cell format
// settings.
-func (f *File) newFont(style *Style) *xlsxFont {
- fontUnderlineType := map[string]string{"single": "single", "double": "double"}
- if style.Font.Size < 1 {
+func (f *File) newFont(style *Style) (*xlsxFont, error) {
+ var err error
+ if style.Font.Size < MinFontSize {
style.Font.Size = 11
}
- if style.Font.Color == "" {
- style.Font.Color = "#000000"
- }
fnt := xlsxFont{
Sz: &attrValFloat{Val: float64Ptr(style.Font.Size)},
- Color: &xlsxColor{RGB: getPaletteColor(style.Font.Color)},
Name: &attrValString{Val: stringPtr(style.Font.Family)},
Family: &attrValInt{Val: intPtr(2)},
}
+ fnt.Color = newFontColor(style.Font)
if style.Font.Bold {
- fnt.B = &style.Font.Bold
+ fnt.B = &attrValBool{Val: &style.Font.Bold}
}
if style.Font.Italic {
- fnt.I = &style.Font.Italic
+ fnt.I = &attrValBool{Val: &style.Font.Italic}
}
if *fnt.Name.Val == "" {
- *fnt.Name.Val = f.GetDefaultFont()
+ if *fnt.Name.Val, err = f.GetDefaultFont(); err != nil {
+ return &fnt, err
+ }
}
if style.Font.Strike {
- strike := true
- fnt.Strike = &strike
+ fnt.Strike = &attrValBool{Val: &style.Font.Strike}
}
- val, ok := fontUnderlineType[style.Font.Underline]
- if ok {
- fnt.U = &attrValString{Val: stringPtr(val)}
+ if idx := inStrSlice(supportedUnderlineTypes, style.Font.Underline, true); idx != -1 {
+ fnt.U = &attrValString{Val: stringPtr(supportedUnderlineTypes[idx])}
}
- return &fnt
+ return &fnt, err
}
// getNumFmtID provides a function to get number format code ID.
-// If given number format code is not exist, will return -1.
-func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) {
- numFmtID = -1
- if styleSheet.NumFmts == nil {
- return
- }
+// If given number format code does not exist, will return -1.
+func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) int {
+ numFmtID := -1
if _, ok := builtInNumFmt[style.NumFmt]; ok {
return style.NumFmt
}
+ if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) {
+ return style.NumFmt
+ }
if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok {
- for _, numFmt := range styleSheet.NumFmts.NumFmt {
- if numFmt.FormatCode == fmtCode {
- numFmtID = numFmt.NumFmtID
- return
+ numFmtID = style.NumFmt
+ if styleSheet.NumFmts != nil {
+ for _, numFmt := range styleSheet.NumFmts.NumFmt {
+ if numFmt.FormatCode == fmtCode {
+ return numFmt.NumFmtID
+ }
}
}
}
- return
+ return numFmtID
}
// newNumFmt provides a function to check if number format code in the range
// of built-in values.
func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
- dp := "0."
- numFmtID := 164 // Default custom number format code from 164.
- if style.DecimalPlaces < 0 || style.DecimalPlaces > 30 {
- style.DecimalPlaces = 2
- }
- for i := 0; i < style.DecimalPlaces; i++ {
- dp += "0"
+ dp, numFmtID := "0", 164 // Default custom number format code from 164.
+ if style.DecimalPlaces != nil && *style.DecimalPlaces > 0 {
+ dp += "."
+ for i := 0; i < *style.DecimalPlaces; i++ {
+ dp += "0"
+ }
}
if style.CustomNumFmt != nil {
if customNumFmtID := getCustomNumFmtID(styleSheet, style); customNumFmtID != -1 {
@@ -2180,35 +1939,26 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
}
return setCustomNumFmt(styleSheet, style)
}
- _, ok := builtInNumFmt[style.NumFmt]
- if !ok {
+ if _, ok := builtInNumFmt[style.NumFmt]; !ok {
fc, currency := currencyNumFmt[style.NumFmt]
if !currency {
- return setLangNumFmt(styleSheet, style)
+ return setLangNumFmt(style)
+ }
+ if style.DecimalPlaces != nil {
+ fc = strings.ReplaceAll(fc, "0.00", dp)
}
- fc = strings.Replace(fc, "0.00", dp, -1)
if style.NegRed {
fc = fc + ";[Red]" + fc
}
- if styleSheet.NumFmts != nil {
- numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1
- nf := xlsxNumFmt{
- FormatCode: fc,
- NumFmtID: numFmtID,
- }
- styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf)
- styleSheet.NumFmts.Count++
+ if styleSheet.NumFmts == nil {
+ styleSheet.NumFmts = &xlsxNumFmts{NumFmt: []*xlsxNumFmt{}}
} else {
- nf := xlsxNumFmt{
- FormatCode: fc,
- NumFmtID: numFmtID,
- }
- numFmts := xlsxNumFmts{
- NumFmt: []*xlsxNumFmt{&nf},
- Count: 1,
- }
- styleSheet.NumFmts = &numFmts
+ numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1
}
+ styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &xlsxNumFmt{
+ FormatCode: fc, NumFmtID: numFmtID,
+ })
+ styleSheet.NumFmts.Count++
return numFmtID
}
return style.NumFmt
@@ -2216,24 +1966,23 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
// setCustomNumFmt provides a function to set custom number format code.
func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
- nf := xlsxNumFmt{FormatCode: *style.CustomNumFmt}
- if styleSheet.NumFmts != nil {
- nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1
- styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf)
- styleSheet.NumFmts.Count++
- } else {
- nf.NumFmtID = 164
- numFmts := xlsxNumFmts{
- NumFmt: []*xlsxNumFmt{&nf},
- Count: 1,
+ nf := xlsxNumFmt{NumFmtID: 163, FormatCode: *style.CustomNumFmt}
+ if styleSheet.NumFmts == nil {
+ styleSheet.NumFmts = &xlsxNumFmts{}
+ }
+ for _, numFmt := range styleSheet.NumFmts.NumFmt {
+ if numFmt != nil && nf.NumFmtID < numFmt.NumFmtID {
+ nf.NumFmtID = numFmt.NumFmtID
}
- styleSheet.NumFmts = &numFmts
}
+ nf.NumFmtID++
+ styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf)
+ styleSheet.NumFmts.Count = len(styleSheet.NumFmts.NumFmt)
return nf.NumFmtID
}
// getCustomNumFmtID provides a function to get custom number format code ID.
-// If given custom number format code is not exist, will return -1.
+// If given custom number format code does not exist, will return -1.
func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID int) {
customNumFmtID = -1
if styleSheet.NumFmts == nil {
@@ -2248,31 +1997,18 @@ func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID
return
}
+// isLangNumFmt provides a function to returns if a given number format ID is a
+// built-in language glyphs number format code.
+func isLangNumFmt(ID int) bool {
+ return (27 <= ID && ID <= 36) || (50 <= ID && ID <= 62) || (67 <= ID && ID <= 81)
+}
+
// setLangNumFmt provides a function to set number format code with language.
-func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
- numFmts, ok := langNumFmt[style.Lang]
- if !ok {
- return 0
- }
- var fc string
- fc, ok = numFmts[style.NumFmt]
- if !ok {
- return 0
- }
- nf := xlsxNumFmt{FormatCode: fc}
- if styleSheet.NumFmts != nil {
- nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1
- styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf)
- styleSheet.NumFmts.Count++
- } else {
- nf.NumFmtID = style.NumFmt
- numFmts := xlsxNumFmts{
- NumFmt: []*xlsxNumFmt{&nf},
- Count: 1,
- }
- styleSheet.NumFmts = &numFmts
+func setLangNumFmt(style *Style) int {
+ if isLangNumFmt(style.NumFmt) {
+ return style.NumFmt
}
- return nf.NumFmtID
+ return 0
}
// getFillID provides a function to get fill ID. If given fill is not
@@ -2298,77 +2034,38 @@ func getFillID(styleSheet *xlsxStyleSheet, style *Style) (fillID int) {
// newFills provides a function to add fill elements in the styles.xml by
// given cell format settings.
func newFills(style *Style, fg bool) *xlsxFill {
- var patterns = []string{
- "none",
- "solid",
- "mediumGray",
- "darkGray",
- "lightGray",
- "darkHorizontal",
- "darkVertical",
- "darkDown",
- "darkUp",
- "darkGrid",
- "darkTrellis",
- "lightHorizontal",
- "lightVertical",
- "lightDown",
- "lightUp",
- "lightGrid",
- "lightTrellis",
- "gray125",
- "gray0625",
- }
-
- var variants = []float64{
- 90,
- 0,
- 45,
- 135,
- }
-
var fill xlsxFill
switch style.Fill.Type {
case "gradient":
- if len(style.Fill.Color) != 2 {
+ if len(style.Fill.Color) != 2 || style.Fill.Shading < 0 || style.Fill.Shading > 16 {
break
}
- var gradient xlsxGradientFill
- switch style.Fill.Shading {
- case 0, 1, 2, 3:
- gradient.Degree = variants[style.Fill.Shading]
- case 4:
- gradient.Type = "path"
- case 5:
- gradient.Type = "path"
- gradient.Bottom = 0.5
- gradient.Left = 0.5
- gradient.Right = 0.5
- gradient.Top = 0.5
- default:
- break
- }
- var stops []*xlsxGradientFillStop
- for index, color := range style.Fill.Color {
- var stop xlsxGradientFillStop
- stop.Position = float64(index)
- stop.Color.RGB = getPaletteColor(color)
- stops = append(stops, &stop)
+ gradient := styleFillVariants()[style.Fill.Shading]
+ gradient.Stop[0].Color.RGB = getPaletteColor(style.Fill.Color[0])
+ gradient.Stop[1].Color.RGB = getPaletteColor(style.Fill.Color[1])
+ if len(gradient.Stop) == 3 {
+ gradient.Stop[2].Color.RGB = getPaletteColor(style.Fill.Color[0])
}
- gradient.Stop = stops
fill.GradientFill = &gradient
case "pattern":
if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 {
break
}
+ var pattern xlsxPatternFill
+ pattern.PatternType = styleFillPatterns[style.Fill.Pattern]
if len(style.Fill.Color) < 1 {
+ fill.PatternFill = &pattern
break
}
- var pattern xlsxPatternFill
- pattern.PatternType = patterns[style.Fill.Pattern]
if fg {
+ if pattern.FgColor == nil {
+ pattern.FgColor = new(xlsxColor)
+ }
pattern.FgColor.RGB = getPaletteColor(style.Fill.Color[0])
} else {
+ if pattern.BgColor == nil {
+ pattern.BgColor = new(xlsxColor)
+ }
pattern.BgColor.RGB = getPaletteColor(style.Fill.Color[0])
}
fill.PatternFill = &pattern
@@ -2403,8 +2100,8 @@ func newAlignment(style *Style) *xlsxAlignment {
func newProtection(style *Style) *xlsxProtection {
var protection xlsxProtection
if style.Protection != nil {
- protection.Hidden = style.Protection.Hidden
- protection.Locked = style.Protection.Locked
+ protection.Hidden = &style.Protection.Hidden
+ protection.Locked = &style.Protection.Locked
}
return &protection
}
@@ -2428,58 +2125,32 @@ func getBorderID(styleSheet *xlsxStyleSheet, style *Style) (borderID int) {
// newBorders provides a function to add border elements in the styles.xml by
// given borders format settings.
func newBorders(style *Style) *xlsxBorder {
- var styles = []string{
- "none",
- "thin",
- "medium",
- "dashed",
- "dotted",
- "thick",
- "double",
- "hair",
- "mediumDashed",
- "dashDot",
- "mediumDashDot",
- "dashDotDot",
- "mediumDashDotDot",
- "slantDashDot",
- }
-
var border xlsxBorder
for _, v := range style.Border {
if 0 <= v.Style && v.Style < 14 {
- var color xlsxColor
- color.RGB = getPaletteColor(v.Color)
+ line := &xlsxLine{Style: styleBorders[v.Style], Color: &xlsxColor{RGB: getPaletteColor(v.Color)}}
switch v.Type {
case "left":
- border.Left.Style = styles[v.Style]
- border.Left.Color = &color
+ border.Left = line
case "right":
- border.Right.Style = styles[v.Style]
- border.Right.Color = &color
+ border.Right = line
case "top":
- border.Top.Style = styles[v.Style]
- border.Top.Color = &color
+ border.Top = line
case "bottom":
- border.Bottom.Style = styles[v.Style]
- border.Bottom.Color = &color
+ border.Bottom = line
case "diagonalUp":
- border.Diagonal.Style = styles[v.Style]
- border.Diagonal.Color = &color
- border.DiagonalUp = true
+ border.Diagonal, border.DiagonalUp = line, true
case "diagonalDown":
- border.Diagonal.Style = styles[v.Style]
- border.Diagonal.Color = &color
- border.DiagonalDown = true
+ border.Diagonal, border.DiagonalDown = line, true
}
}
}
return &border
}
-// setCellXfs provides a function to set describes all of the formatting for a
+// setCellXfs provides a function to set describes all the formatting for a
// cell.
-func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) int {
+func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) (int, error) {
var xf xlsxXf
xf.FontID = intPtr(fontID)
if fontID != 0 {
@@ -2497,7 +2168,10 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a
if borderID != 0 {
xf.ApplyBorder = boolPtr(true)
}
- style.CellXfs.Count++
+ if len(style.CellXfs.Xf) == MaxCellStyles {
+ return 0, ErrCellStyles
+ }
+ style.CellXfs.Count = len(style.CellXfs.Xf) + 1
xf.Alignment = alignment
if alignment != nil {
xf.ApplyAlignment = boolPtr(applyAlignment)
@@ -2509,124 +2183,183 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a
xfID := 0
xf.XfID = &xfID
style.CellXfs.Xf = append(style.CellXfs.Xf, xf)
- return style.CellXfs.Count - 1
+ return style.CellXfs.Count - 1, nil
}
// GetCellStyle provides a function to get cell style index by given worksheet
-// name and cell coordinates.
-func (f *File) GetCellStyle(sheet, axis string) (int, error) {
- xlsx, err := f.workSheetReader(sheet)
+// name and cell reference. This function is concurrency safe.
+func (f *File) GetCellStyle(sheet, cell string) (int, error) {
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
if err != nil {
+ f.mu.Unlock()
return 0, err
}
- cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
+ f.mu.Unlock()
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+ col, row, err := CellNameToCoordinates(cell)
if err != nil {
return 0, err
}
- return f.prepareCellStyle(xlsx, col, cellData.S), err
+ ws.prepareSheetXML(col, row)
+ return ws.prepareCellStyle(col, row, ws.SheetData.Row[row-1].C[col-1].S), err
}
// SetCellStyle provides a function to add style attribute for cells by given
-// worksheet name, coordinate area and style ID. Note that diagonalDown and
-// diagonalUp type border should be use same color in the same coordinate
-// area.
+// worksheet name, range reference and style ID. This function is concurrency
+// safe. Note that diagonalDown and diagonalUp type border should be use same
+// color in the same range. SetCellStyle will overwrite the existing
+// styles for the cell, it won't append or merge style with existing styles.
//
// For example create a borders of cell H9 on Sheet1:
//
-// style, err := f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":3},{"type":"top","color":"00FF00","style":4},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":7},{"type":"diagonalUp","color":"A020F0","style":8}]}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// style, err := f.NewStyle(&excelize.Style{
+// Border: []excelize.Border{
+// {Type: "left", Color: "0000FF", Style: 3},
+// {Type: "top", Color: "00FF00", Style: 4},
+// {Type: "bottom", Color: "FFFF00", Style: 5},
+// {Type: "right", Color: "FF0000", Style: 6},
+// {Type: "diagonalDown", Color: "A020F0", Style: 7},
+// {Type: "diagonalUp", Color: "A020F0", Style: 8},
+// },
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Set gradient fill with vertical variants shading styles for cell H9 on
// Sheet1:
//
-// style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// style, err := f.NewStyle(&excelize.Style{
+// Fill: excelize.Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 1},
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Set solid style pattern fill for cell H9 on Sheet1:
//
-// style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// style, err := f.NewStyle(&excelize.Style{
+// Fill: excelize.Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1},
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Set alignment style for cell H9 on Sheet1:
//
-// style, err := f.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"","wrap_text":true}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// style, err := f.NewStyle(&excelize.Style{
+// Alignment: &excelize.Alignment{
+// Horizontal: "center",
+// Indent: 1,
+// JustifyLastLine: true,
+// ReadingOrder: 0,
+// RelativeIndent: 1,
+// ShrinkToFit: true,
+// TextRotation: 45,
+// Vertical: "",
+// WrapText: true,
+// },
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Dates and times in Excel are represented by real numbers, for example "Apr 7
// 2017 12:00 PM" is represented by the number 42920.5. Set date and time format
// for cell H9 on Sheet1:
//
-// f.SetCellValue("Sheet1", "H9", 42920.5)
-// style, err := f.NewStyle(`{"number_format": 22}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// f.SetCellValue("Sheet1", "H9", 42920.5)
+// style, err := f.NewStyle(&excelize.Style{NumFmt: 22})
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Set font style for cell H9 on Sheet1:
//
-// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+// style, err := f.NewStyle(&excelize.Style{
+// Font: &excelize.Font{
+// Bold: true,
+// Italic: true,
+// Family: "Times New Roman",
+// Size: 36,
+// Color: "777777",
+// },
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
//
// Hide and lock for cell H9 on Sheet1:
//
-// style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
-//
-func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error {
- hcol, hrow, err := CellNameToCoordinates(hcell)
+// style, err := f.NewStyle(&excelize.Style{
+// Protection: &excelize.Protection{
+// Hidden: true,
+// Locked: true,
+// },
+// })
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetCellStyle("Sheet1", "H9", "H9", style)
+func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID int) error {
+ hCol, hRow, err := CellNameToCoordinates(topLeftCell)
if err != nil {
return err
}
- vcol, vrow, err := CellNameToCoordinates(vcell)
+ vCol, vRow, err := CellNameToCoordinates(bottomRightCell)
if err != nil {
return err
}
- // Normalize the coordinate area, such correct C1:B3 to B1:C3.
- if vcol < hcol {
- vcol, hcol = hcol, vcol
+ // Normalize the range, such correct C1:B3 to B1:C3.
+ if vCol < hCol {
+ vCol, hCol = hCol, vCol
}
- if vrow < hrow {
- vrow, hrow = hrow, vrow
+ if vRow < hRow {
+ vRow, hRow = hRow, vRow
}
- hcolIdx := hcol - 1
- hrowIdx := hrow - 1
-
- vcolIdx := vcol - 1
- vrowIdx := vrow - 1
+ hColIdx := hCol - 1
+ hRowIdx := hRow - 1
- xlsx, err := f.workSheetReader(sheet)
+ vColIdx := vCol - 1
+ vRowIdx := vRow - 1
+ f.mu.Lock()
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ f.mu.Unlock()
+ return err
+ }
+ s, err := f.stylesReader()
if err != nil {
+ f.mu.Unlock()
return err
}
- prepareSheetXML(xlsx, vcol, vrow)
- makeContiguousColumns(xlsx, hrow, vrow, vcol)
+ f.mu.Unlock()
+
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+
+ ws.prepareSheetXML(vCol, vRow)
+ ws.makeContiguousColumns(hRow, vRow, vCol)
+
+ if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
+ return newInvalidStyleID(styleID)
+ }
- for r := hrowIdx; r <= vrowIdx; r++ {
- for k := hcolIdx; k <= vcolIdx; k++ {
- xlsx.SheetData.Row[r].C[k].S = styleID
+ for r := hRowIdx; r <= vRowIdx; r++ {
+ for k := hColIdx; k <= vColIdx; k++ {
+ ws.SheetData.Row[r].C[k].S = styleID
}
}
return err
@@ -2640,64 +2373,67 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error {
// The type option is a required parameter and it has no default value.
// Allowable type values and their associated parameters are:
//
-// Type | Parameters
-// ---------------+------------------------------------
-// cell | criteria
-// | value
-// | minimum
-// | maximum
-// date | criteria
-// | value
-// | minimum
-// | maximum
-// time_period | criteria
-// text | criteria
-// | value
-// average | criteria
-// duplicate | (none)
-// unique | (none)
-// top | criteria
-// | value
-// bottom | criteria
-// | value
-// blanks | (none)
-// no_blanks | (none)
-// errors | (none)
-// no_errors | (none)
-// 2_color_scale | min_type
-// | max_type
-// | min_value
-// | max_value
-// | min_color
-// | max_color
-// 3_color_scale | min_type
-// | mid_type
-// | max_type
-// | min_value
-// | mid_value
-// | max_value
-// | min_color
-// | mid_color
-// | max_color
-// data_bar | min_type
-// | max_type
-// | min_value
-// | max_value
-// | bar_color
-// formula | criteria
-//
-// The criteria parameter is used to set the criteria by which the cell data
+// Type | Parameters
+// ---------------+------------------------------------
+// cell | Criteria
+// | Value
+// | MinValue
+// | MaxValue
+// time_period | Criteria
+// text | Criteria
+// | Value
+// average | Criteria
+// duplicate | (none)
+// unique | (none)
+// top | Criteria
+// | Value
+// bottom | Criteria
+// | Value
+// blanks | (none)
+// no_blanks | (none)
+// errors | (none)
+// no_errors | (none)
+// 2_color_scale | MinType
+// | MaxType
+// | MinValue
+// | MaxValue
+// | MinColor
+// | MaxColor
+// 3_color_scale | MinType
+// | MidType
+// | MaxType
+// | MinValue
+// | MidValue
+// | MaxValue
+// | MinColor
+// | MidColor
+// | MaxColor
+// data_bar | MinType
+// | MaxType
+// | MinValue
+// | MaxValue
+// | BarBorderColor
+// | BarColor
+// | BarDirection
+// | BarOnly
+// | BarSolid
+// icon_set | IconStyle
+// | ReverseIcons
+// | IconsOnly
+// formula | Criteria
+//
+// The 'Criteria' parameter is used to set the criteria by which the cell data
// will be evaluated. It has no default value. The most common criteria as
-// applied to {"type":"cell"} are:
+// applied to {Type: "cell"} are:
//
-// between |
-// not between |
-// equal to | ==
-// not equal to | !=
-// greater than | >
-// less than | <
-// greater than or equal to | >=
-// less than or equal to | <=
+// between |
+// not between |
+// equal to | ==
+// not equal to | !=
+// greater than | >
+// less than | <
+// greater than or equal to | >=
+// less than or equal to | <=
//
// You can either use Excel's textual description strings, in the first column
// above, or the more common symbolic alternatives.
@@ -2708,22 +2444,51 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error {
// value: The value is generally used along with the criteria parameter to set
// the rule by which the cell data will be evaluated:
//
-// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))
+// err := f.SetConditionalFormat("Sheet1", "D1:D10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "cell",
+// Criteria: ">",
+// Format: &format,
+// Value: "6",
+// },
+// },
+// )
//
// The value property can also be an cell reference:
//
-// f.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"$C$1"}]`, format))
+// err := f.SetConditionalFormat("Sheet1", "D1:D10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "cell",
+// Criteria: ">",
+// Format: &format,
+// Value: "$C$1",
+// },
+// },
+// )
//
// type: format - The format parameter is used to specify the format that will
// be applied to the cell when the conditional formatting criterion is met. The
-// format is created using the NewConditionalStyle() method in the same way as
+// format is created using the NewConditionalStyle function in the same way as
// cell formats:
//
-// format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`)
-// if err != nil {
-// fmt.Println(err)
-// }
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))
+// format, err := f.NewConditionalStyle(
+// &excelize.Style{
+// Font: &excelize.Font{Color: "9A0511"},
+// Fill: excelize.Fill{
+// Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1,
+// },
+// },
+// )
+// if err != nil {
+// fmt.Println(err)
+// }
+// err = f.SetConditionalFormat("Sheet1", "D1:D10",
+// []excelize.ConditionalFormatOptions{
+// {Type: "cell", Criteria: ">", Format: &format, Value: "6"},
+// },
+// )
//
// Note: In Excel, a conditional format is superimposed over the existing cell
// format and not all cell format properties can be modified. Properties that
@@ -2734,171 +2499,696 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error {
// Excel specifies some default formats to be used with conditional formatting.
// These can be replicated using the following excelize formats:
//
-// // Rose format for bad conditional.
-// format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`)
-//
-// // Light yellow format for neutral conditional.
-// format2, err = f.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`)
-//
-// // Light green format for good conditional.
-// format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`)
-//
-// type: minimum - The minimum parameter is used to set the lower limiting value
-// when the criteria is either "between" or "not between".
-//
-// // Hightlight cells rules: between...
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format))
-//
-// type: maximum - The maximum parameter is used to set the upper limiting value
-// when the criteria is either "between" or "not between". See the previous
-// example.
+// // Rose format for bad conditional.
+// format1, err := f.NewConditionalStyle(
+// &excelize.Style{
+// Font: &excelize.Font{Color: "9A0511"},
+// Fill: excelize.Fill{
+// Type: "pattern", Color: []string{"#FEC7CE"}, Pattern: 1,
+// },
+// },
+// )
+//
+// // Light yellow format for neutral conditional.
+// format2, err := f.NewConditionalStyle(
+// &excelize.Style{
+// Font: &excelize.Font{Color: "9B5713"},
+// Fill: excelize.Fill{
+// Type: "pattern", Color: []string{"FEEAA0"}, Pattern: 1,
+// },
+// },
+// )
+//
+// // Light green format for good conditional.
+// format3, err := f.NewConditionalStyle(
+// &excelize.Style{
+// Font: &excelize.Font{Color: "09600B"},
+// Fill: excelize.Fill{
+// Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1,
+// },
+// },
+// )
+//
+// type: MinValue - The 'MinValue' parameter is used to set the lower limiting
+// value when the criteria is either "between" or "not between".
+//
+// // Highlight cells rules: between...
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "cell",
+// Criteria: "between",
+// Format: &format,
+// MinValue: 6",
+// MaxValue: 8",
+// },
+// },
+// )
+//
+// type: MaxValue - The 'MaxValue' parameter is used to set the upper limiting
+// value when the criteria is either "between" or "not between". See the
+// previous example.
//
// type: average - The average type is used to specify Excel's "Average" style
// conditional format:
//
-// // Top/Bottom rules: Above Average...
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1))
-//
-// // Top/Bottom rules: Below Average...
-// f.SetConditionalFormat("Sheet1", "B1:B10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format2))
-//
-// type: duplicate - The duplicate type is used to highlight duplicate cells in a range:
-//
-// // Hightlight cells rules: Duplicate Values...
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format))
+// // Top/Bottom rules: Above Average...
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "average",
+// Criteria: "=",
+// Format: &format1,
+// AboveAverage: true,
+// },
+// },
+// )
+//
+// // Top/Bottom rules: Below Average...
+// err := f.SetConditionalFormat("Sheet1", "B1:B10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "average",
+// Criteria: "=",
+// Format: &format2,
+// AboveAverage: false,
+// },
+// },
+// )
+//
+// type: duplicate - The duplicate type is used to highlight duplicate cells in
+// a range:
+//
+// // Highlight cells rules: Duplicate Values...
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {Type: "duplicate", Criteria: "=", Format: &format},
+// },
+// )
//
// type: unique - The unique type is used to highlight unique cells in a range:
//
-// // Hightlight cells rules: Not Equal To...
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format))
-//
-// type: top - The top type is used to specify the top n values by number or percentage in a range:
-//
-// // Top/Bottom rules: Top 10.
-// f.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6"}]`, format))
+// // Highlight cells rules: Not Equal To...
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {Type: "unique", Criteria: "=", Format: &format},
+// },
+// )
+//
+// type: top - The top type is used to specify the top n values by number or
+// percentage in a range:
+//
+// // Top/Bottom rules: Top 10.
+// err := f.SetConditionalFormat("Sheet1", "H1:H10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "top",
+// Criteria: "=",
+// Format: &format,
+// Value: "6",
+// },
+// },
+// )
//
// The criteria can be used to indicate that a percentage condition is required:
//
-// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format))
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "top",
+// Criteria: "=",
+// Format: &format,
+// Value: "6",
+// Percent: true,
+// },
+// },
+// )
//
// type: 2_color_scale - The 2_color_scale type is used to specify Excel's "2
// Color Scale" style conditional format:
//
-// // Color scales: 2 color.
-// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`)
-//
-// This conditional type can be modified with min_type, max_type, min_value,
-// max_value, min_color and max_color, see below.
+// // Color scales: 2 color.
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "2_color_scale",
+// Criteria: "=",
+// MinType: "min",
+// MaxType: "max",
+// MinColor: "#F8696B",
+// MaxColor: "#63BE7B",
+// },
+// },
+// )
+//
+// This conditional type can be modified with MinType, MaxType, MinValue,
+// MaxValue, MinColor and MaxColor, see below.
//
// type: 3_color_scale - The 3_color_scale type is used to specify Excel's "3
// Color Scale" style conditional format:
//
-// // Color scales: 3 color.
-// f.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`)
-//
-// This conditional type can be modified with min_type, mid_type, max_type,
-// min_value, mid_value, max_value, min_color, mid_color and max_color, see
+// // Color scales: 3 color.
+// err := f.SetConditionalFormat("Sheet1", "A1:A10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "3_color_scale",
+// Criteria: "=",
+// MinType: "min",
+// MidType: "percentile",
+// MaxType: "max",
+// MinColor: "#F8696B",
+// MidColor: "#FFEB84",
+// MaxColor: "#63BE7B",
+// },
+// },
+// )
+//
+// This conditional type can be modified with MinType, MidType, MaxType,
+// MinValue, MidValue, MaxValue, MinColor, MidColor and MaxColor, see
// below.
//
// type: data_bar - The data_bar type is used to specify Excel's "Data Bar"
// style conditional format.
//
-// min_type - The min_type and max_type properties are available when the conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The mid_type is available for 3_color_scale. The properties are used as follows:
+// MinType - The MinType and MaxType properties are available when the
+// conditional formatting type is 2_color_scale, 3_color_scale or data_bar.
+// The MidType is available for 3_color_scale. The properties are used as
+// follows:
//
-// // Data Bars: Gradient Fill.
-// f.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)
+// // Data Bars: Gradient Fill.
+// err := f.SetConditionalFormat("Sheet1", "K1:K10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "data_bar",
+// Criteria: "=",
+// MinType: "min",
+// MaxType: "max",
+// BarColor: "#638EC6",
+// },
+// },
+// )
//
// The available min/mid/max types are:
//
-// min (for min_type only)
-// num
-// percent
-// percentile
-// formula
-// max (for max_type only)
+// min (for MinType only)
+// num
+// percent
+// percentile
+// formula
+// max (for MaxType only)
//
-// mid_type - Used for 3_color_scale. Same as min_type, see above.
+// MidType - Used for 3_color_scale. Same as MinType, see above.
//
-// max_type - Same as min_type, see above.
+// MaxType - Same as MinType, see above.
//
-// min_value - The min_value and max_value properties are available when the
-// conditional formatting type is 2_color_scale, 3_color_scale or data_bar. The
-// mid_value is available for 3_color_scale.
-//
-// mid_value - Used for 3_color_scale. Same as min_value, see above.
-//
-// max_value - Same as min_value, see above.
-//
-// min_color - The min_color and max_color properties are available when the
+// MinValue - The MinValue and MaxValue properties are available when the
// conditional formatting type is 2_color_scale, 3_color_scale or data_bar.
-// The mid_color is available for 3_color_scale. The properties are used as
-// follows:
//
-// // Color scales: 3 color.
-// f.SetConditionalFormat("Sheet1", "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`)
+// MidValue - The MidValue is available for 3_color_scale. Same as MinValue,
+// see above.
//
-// mid_color - Used for 3_color_scale. Same as min_color, see above.
+// MaxValue - Same as MinValue, see above.
//
-// max_color - Same as min_color, see above.
-//
-// bar_color - Used for data_bar. Same as min_color, see above.
+// MinColor - The MinColor and MaxColor properties are available when the
+// conditional formatting type is 2_color_scale, 3_color_scale or data_bar.
//
-func (f *File) SetConditionalFormat(sheet, area, formatSet string) error {
- var format []*formatConditional
- err := json.Unmarshal([]byte(formatSet), &format)
+// MidColor - The MidColor is available for 3_color_scale. The properties
+// are used as follows:
+//
+// // Color scales: 3 color.
+// err := f.SetConditionalFormat("Sheet1", "B1:B10",
+// []excelize.ConditionalFormatOptions{
+// {
+// Type: "3_color_scale",
+// Criteria: "=",
+// MinType: "min",
+// MidType: "percentile",
+// MaxType: "max",
+// MinColor: "#F8696B",
+// MidColor: "#FFEB84",
+// MaxColor: "#63BE7B",
+// },
+// },
+// )
+//
+// MaxColor - Same as MinColor, see above.
+//
+// BarColor - Used for data_bar. Same as MinColor, see above.
+//
+// BarBorderColor - Used for sets the color for the border line of a data bar,
+// this is only visible in Excel 2010 and later.
+//
+// BarDirection - sets the direction for data bars. The available options are:
+//
+// context - Data bar direction is set by spreadsheet application based on the context of the data displayed.
+// leftToRight - Data bar direction is from right to left.
+// rightToLeft - Data bar direction is from left to right.
+//
+// BarOnly - Used for set displays a bar data but not the data in the cells.
+//
+// BarSolid - Used for turns on a solid (non-gradient) fill for data bars, this
+// is only visible in Excel 2010 and later.
+//
+// IconStyle - The available options are:
+//
+// 3Arrows
+// 3ArrowsGray
+// 3Flags
+// 3Signs
+// 3Symbols
+// 3Symbols2
+// 3TrafficLights1
+// 3TrafficLights2
+// 4Arrows
+// 4ArrowsGray
+// 4Rating
+// 4RedToBlack
+// 4TrafficLights
+// 5Arrows
+// 5ArrowsGray
+// 5Quarters
+// 5Rating
+//
+// ReverseIcons - Used for set reversed icons sets.
+//
+// IconsOnly - Used for set displayed without the cell value.
+//
+// StopIfTrue - used to set the "stop if true" feature of a conditional
+// formatting rule when more than one rule is applied to a cell or a range of
+// cells. When this parameter is set then subsequent rules are not evaluated
+// if the current rule is true.
+func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFormatOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- drawContFmtFunc := map[string]func(p int, ct string, fmtCond *formatConditional) *xlsxCfRule{
- "cellIs": drawCondFmtCellIs,
- "top10": drawCondFmtTop10,
- "aboveAverage": drawCondFmtAboveAverage,
- "duplicateValues": drawCondFmtDuplicateUniqueValues,
- "uniqueValues": drawCondFmtDuplicateUniqueValues,
- "2_color_scale": drawCondFmtColorScale,
- "3_color_scale": drawCondFmtColorScale,
- "dataBar": drawCondFmtDataBar,
- "expression": drawConfFmtExp,
- }
-
- xlsx, err := f.workSheetReader(sheet)
+ SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef)
if err != nil {
return err
}
- cfRule := []*xlsxCfRule{}
- for p, v := range format {
+ // Create a pseudo GUID for each unique rule.
+ var rules int
+ for _, cf := range ws.ConditionalFormatting {
+ rules += len(cf.CfRule)
+ }
+ var (
+ cfRule []*xlsxCfRule
+ noCriteriaTypes = []string{
+ "containsBlanks",
+ "notContainsBlanks",
+ "containsErrors",
+ "notContainsErrors",
+ "expression",
+ "iconSet",
+ }
+ )
+ for i, opt := range opts {
var vt, ct string
var ok bool
// "type" is a required parameter, check for valid validation types.
- vt, ok = validType[v.Type]
+ vt, ok = validType[opt.Type]
if ok {
// Check for valid criteria types.
- ct, ok = criteriaType[v.Criteria]
- if ok || vt == "expression" {
- drawfunc, ok := drawContFmtFunc[vt]
+ ct, ok = criteriaType[opt.Criteria]
+ if ok || inStrSlice(noCriteriaTypes, vt, true) != -1 {
+ drawFunc, ok := drawContFmtFunc[vt]
if ok {
- cfRule = append(cfRule, drawfunc(p, ct, v))
+ priority := rules + i
+ rule, x14rule := drawFunc(priority, ct, mastCell,
+ fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), priority), &opt)
+ if rule == nil {
+ return ErrParameterInvalid
+ }
+ if x14rule != nil {
+ if err = f.appendCfRule(ws, x14rule); err != nil {
+ return err
+ }
+ f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14)
+ }
+ cfRule = append(cfRule, rule)
+ continue
}
}
+ return ErrParameterInvalid
}
+ return ErrParameterInvalid
}
- xlsx.ConditionalFormatting = append(xlsx.ConditionalFormatting, &xlsxConditionalFormatting{
- SQRef: area,
+ ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{
+ SQRef: SQRef,
CfRule: cfRule,
})
return err
}
+// prepareConditionalFormatRange returns checked cell range and master cell
+// reference by giving conditional formatting range reference.
+func prepareConditionalFormatRange(rangeRef string) (string, string, error) {
+ var SQRef, mastCell string
+ if rangeRef == "" {
+ return SQRef, mastCell, ErrParameterRequired
+ }
+ rangeRef = strings.ReplaceAll(rangeRef, ",", " ")
+ for i, cellRange := range strings.Split(rangeRef, " ") {
+ var cellNames []string
+ for j, ref := range strings.Split(cellRange, ":") {
+ if j > 1 {
+ return SQRef, mastCell, ErrParameterInvalid
+ }
+ cellRef, col, row, err := parseRef(ref)
+ if err != nil {
+ return SQRef, mastCell, err
+ }
+ var c, r int
+ if col {
+ if cellRef.Row = TotalRows; j == 0 {
+ cellRef.Row = 1
+ }
+ }
+ if row {
+ if cellRef.Col = MaxColumns; j == 0 {
+ cellRef.Col = 1
+ }
+ }
+ c, r = cellRef.Col, cellRef.Row
+ cellName, _ := CoordinatesToCellName(c, r)
+ cellNames = append(cellNames, cellName)
+ if i == 0 && j == 0 {
+ mastCell = cellName
+ }
+ }
+ SQRef += strings.Join(cellNames, ":") + " "
+ }
+ return strings.TrimSuffix(SQRef, " "), mastCell, nil
+}
+
+// appendCfRule provides a function to append rules to conditional formatting.
+func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error {
+ var (
+ err error
+ idx int
+ appendMode bool
+ decodeExtLst = new(decodeExtLst)
+ condFmts *xlsxX14ConditionalFormattings
+ decodeCondFmts *decodeX14ConditionalFormattings
+ ext *xlsxExt
+ condFmtBytes, condFmtsBytes, extLstBytes []byte
+ )
+ condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{
+ {XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, CfRule: []*xlsxX14CfRule{rule}},
+ })
+ if ws.ExtLst != nil { // append mode ext
+ if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")).
+ Decode(decodeExtLst); err != nil && err != io.EOF {
+ return err
+ }
+ for idx, ext = range decodeExtLst.Ext {
+ if ext.URI == ExtURIConditionalFormattings {
+ decodeCondFmts = new(decodeX14ConditionalFormattings)
+ _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeCondFmts)
+ if condFmts == nil {
+ condFmts = &xlsxX14ConditionalFormattings{}
+ }
+ condFmts.Content = decodeCondFmts.Content + string(condFmtBytes)
+ condFmtsBytes, _ = xml.Marshal(condFmts)
+ decodeExtLst.Ext[idx].Content = string(condFmtsBytes)
+ appendMode = true
+ }
+ }
+ }
+ if !appendMode {
+ condFmtsBytes, _ = xml.Marshal(&xlsxX14ConditionalFormattings{Content: string(condFmtBytes)})
+ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
+ URI: ExtURIConditionalFormattings, Content: string(condFmtsBytes),
+ })
+ }
+ sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
+ return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
+ inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
+ })
+ extLstBytes, err = xml.Marshal(decodeExtLst)
+ ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")}
+ return err
+}
+
+// extractCondFmtCellIs provides a function to extract conditional format
+// settings for cell value (include between, not between, equal, not equal,
+// greater than and less than) by given conditional formatting rule.
+func (f *File) extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator]}
+ if len(c.Formula) == 2 {
+ format.MinValue, format.MaxValue = c.Formula[0], c.Formula[1]
+ return format
+ }
+ format.Value = c.Formula[0]
+ return format
+}
+
+// extractCondFmtTimePeriod provides a function to extract conditional format
+// settings for time period by given conditional formatting rule.
+func (f *File) extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator]}
+}
+
+// extractCondFmtText provides a function to extract conditional format
+// settings for text cell values by given conditional formatting rule.
+func (f *File) extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Value: c.Text}
+}
+
+// extractCondFmtTop10 provides a function to extract conditional format
+// settings for top N (default is top 10) by given conditional formatting
+// rule.
+func (f *File) extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "top",
+ Criteria: "=",
+ Percent: c.Percent,
+ Value: strconv.Itoa(c.Rank),
+ }
+ if c.Bottom {
+ format.Type = "bottom"
+ }
+ return format
+}
+
+// extractCondFmtAboveAverage provides a function to extract conditional format
+// settings for above average and below average by given conditional formatting
+// rule.
+func (f *File) extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "average",
+ Criteria: "=",
+ }
+ if c.AboveAverage != nil {
+ format.AboveAverage = *c.AboveAverage
+ }
+ return format
+}
+
+// extractCondFmtDuplicateUniqueValues provides a function to extract
+// conditional format settings for duplicate and unique values by given
+// conditional formatting rule.
+func (f *File) extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: map[string]string{
+ "duplicateValues": "duplicate",
+ "uniqueValues": "unique",
+ }[c.Type],
+ Criteria: "=",
+ }
+}
+
+// extractCondFmtBlanks provides a function to extract conditional format
+// settings for blank cells by given conditional formatting rule.
+func (f *File) extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "blanks",
+ }
+}
+
+// extractCondFmtNoBlanks provides a function to extract conditional format
+// settings for no blank cells by given conditional formatting rule.
+func (f *File) extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "no_blanks",
+ }
+}
+
+// extractCondFmtErrors provides a function to extract conditional format
+// settings for cells with errors by given conditional formatting rule.
+func (f *File) extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "errors",
+ }
+}
+
+// extractCondFmtNoErrors provides a function to extract conditional format
+// settings for cells without errors by given conditional formatting rule.
+func (f *File) extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ return ConditionalFormatOptions{
+ Format: c.DxfID,
+ StopIfTrue: c.StopIfTrue,
+ Type: "no_errors",
+ }
+}
+
+// extractCondFmtColorScale provides a function to extract conditional format
+// settings for color scale (include 2 color scale and 3 color scale) by given
+// conditional formatting rule.
+func (f *File) extractCondFmtColorScale(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue}
+ format.Type, format.Criteria = "2_color_scale", "="
+ values := len(c.ColorScale.Cfvo)
+ colors := len(c.ColorScale.Color)
+ if colors > 1 && values > 1 {
+ format.MinType = c.ColorScale.Cfvo[0].Type
+ if c.ColorScale.Cfvo[0].Val != "0" {
+ format.MinValue = c.ColorScale.Cfvo[0].Val
+ }
+ format.MinColor = "#" + f.getThemeColor(c.ColorScale.Color[0])
+ format.MaxType = c.ColorScale.Cfvo[1].Type
+ if c.ColorScale.Cfvo[1].Val != "0" {
+ format.MaxValue = c.ColorScale.Cfvo[1].Val
+ }
+ format.MaxColor = "#" + f.getThemeColor(c.ColorScale.Color[1])
+ }
+ if colors == 3 {
+ format.Type = "3_color_scale"
+ format.MidType = c.ColorScale.Cfvo[1].Type
+ if c.ColorScale.Cfvo[1].Val != "0" {
+ format.MidValue = c.ColorScale.Cfvo[1].Val
+ }
+ format.MidColor = "#" + f.getThemeColor(c.ColorScale.Color[1])
+ format.MaxType = c.ColorScale.Cfvo[2].Type
+ if c.ColorScale.Cfvo[2].Val != "0" {
+ format.MaxValue = c.ColorScale.Cfvo[2].Val
+ }
+ format.MaxColor = "#" + f.getThemeColor(c.ColorScale.Color[2])
+ }
+ return format
+}
+
+// extractCondFmtDataBarRule provides a function to extract conditional format
+// settings for data bar by given conditional formatting rule extension list.
+func (f *File) extractCondFmtDataBarRule(ID string, format *ConditionalFormatOptions, condFmts []decodeX14ConditionalFormatting) {
+ for _, condFmt := range condFmts {
+ for _, rule := range condFmt.CfRule {
+ if rule.DataBar != nil && rule.ID == ID {
+ format.BarDirection = rule.DataBar.Direction
+ if rule.DataBar.Gradient != nil && !*rule.DataBar.Gradient {
+ format.BarSolid = true
+ }
+ if rule.DataBar.BorderColor != nil {
+ format.BarBorderColor = "#" + f.getThemeColor(rule.DataBar.BorderColor)
+ }
+ }
+ }
+ }
+}
+
+// extractCondFmtDataBar provides a function to extract conditional format
+// settings for data bar by given conditional formatting rule.
+func (f *File) extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{Type: "data_bar", Criteria: "="}
+ if c.DataBar != nil {
+ format.StopIfTrue = c.StopIfTrue
+ format.MinType = c.DataBar.Cfvo[0].Type
+ format.MinValue = c.DataBar.Cfvo[0].Val
+ format.MaxType = c.DataBar.Cfvo[1].Type
+ format.MaxValue = c.DataBar.Cfvo[1].Val
+ format.BarColor = "#" + f.getThemeColor(c.DataBar.Color[0])
+ if c.DataBar.ShowValue != nil {
+ format.BarOnly = !*c.DataBar.ShowValue
+ }
+ }
+ extractExtLst := func(ID string, extLst *decodeExtLst) {
+ for _, ext := range extLst.Ext {
+ if ext.URI == ExtURIConditionalFormattings {
+ decodeCondFmts := new(decodeX14ConditionalFormattingRules)
+ if err := xml.Unmarshal([]byte(ext.Content), &decodeCondFmts); err == nil {
+ f.extractCondFmtDataBarRule(ID, &format, decodeCondFmts.CondFmt)
+ }
+ }
+ }
+ }
+ if c.ExtLst != nil {
+ ext := decodeX14ConditionalFormattingExt{}
+ if err := xml.Unmarshal([]byte(c.ExtLst.Ext), &ext); err == nil && extLst != nil {
+ decodeExtLst := new(decodeExtLst)
+ if err = xml.Unmarshal([]byte(""+extLst.Ext+""), decodeExtLst); err == nil {
+ extractExtLst(ext.ID, decodeExtLst)
+ }
+ }
+ }
+ return format
+}
+
+// extractCondFmtExp provides a function to extract conditional format settings
+// for expression by given conditional formatting rule.
+func (f *File) extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "formula"}
+ if len(c.Formula) > 0 {
+ format.Criteria = c.Formula[0]
+ }
+ return format
+}
+
+// extractCondFmtIconSet provides a function to extract conditional format
+// settings for icon sets by given conditional formatting rule.
+func (f *File) extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions {
+ format := ConditionalFormatOptions{Type: "icon_set"}
+ if c.IconSet != nil {
+ if c.IconSet.ShowValue != nil {
+ format.IconsOnly = !*c.IconSet.ShowValue
+ }
+ format.IconStyle = c.IconSet.IconSet
+ format.ReverseIcons = c.IconSet.Reverse
+ }
+ return format
+}
+
+// GetConditionalFormats returns conditional format settings by given worksheet
+// name.
+func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalFormatOptions, error) {
+ conditionalFormats := make(map[string][]ConditionalFormatOptions)
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return conditionalFormats, err
+ }
+ for _, cf := range ws.ConditionalFormatting {
+ var opts []ConditionalFormatOptions
+ for _, cr := range cf.CfRule {
+ if extractFunc, ok := extractContFmtFunc[cr.Type]; ok {
+ opts = append(opts, extractFunc(f, cr, ws.ExtLst))
+ }
+ }
+ conditionalFormats[cf.SQRef] = opts
+ }
+ return conditionalFormats, err
+}
+
// UnsetConditionalFormat provides a function to unset the conditional format
-// by given worksheet name and range.
-func (f *File) UnsetConditionalFormat(sheet, area string) error {
+// by given worksheet name and range reference.
+func (f *File) UnsetConditionalFormat(sheet, rangeRef string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
for i, cf := range ws.ConditionalFormatting {
- if cf.SQRef == area {
+ if cf.SQRef == rangeRef {
ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...)
return nil
}
@@ -2909,71 +3199,128 @@ func (f *File) UnsetConditionalFormat(sheet, area string) error {
// drawCondFmtCellIs provides a function to create conditional formatting rule
// for cell value (include between, not between, equal, not equal, greater
// than and less than) by given priority, criteria type and format settings.
-func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtCellIs(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
c := &xlsxCfRule{
- Priority: p + 1,
- Type: validType[format.Type],
- Operator: ct,
- DxfID: &format.Format,
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Operator: ct,
+ DxfID: format.Format,
}
// "between" and "not between" criteria require 2 values.
- _, ok := map[string]bool{"between": true, "notBetween": true}[ct]
- if ok {
- c.Formula = append(c.Formula, format.Minimum)
- c.Formula = append(c.Formula, format.Maximum)
+ if ct == "between" || ct == "notBetween" {
+ c.Formula = append(c.Formula, []string{format.MinValue, format.MaxValue}...)
}
- _, ok = map[string]bool{"equal": true, "notEqual": true, "greaterThan": true, "lessThan": true, "greaterThanOrEqual": true, "lessThanOrEqual": true, "containsText": true, "notContains": true, "beginsWith": true, "endsWith": true}[ct]
- if ok {
+ if inStrSlice(cellIsCriteriaType, ct, true) != -1 {
c.Formula = append(c.Formula, format.Value)
}
- return c
+ return c, nil
+}
+
+// drawCondFmtTimePeriod provides a function to create conditional formatting
+// rule for time period by given priority, criteria type and format settings.
+func drawCondFmtTimePeriod(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: "timePeriod",
+ Operator: ct,
+ Formula: []string{
+ map[string]string{
+ "yesterday": fmt.Sprintf("FLOOR(%s,1)=TODAY()-1", ref),
+ "today": fmt.Sprintf("FLOOR(%s,1)=TODAY()", ref),
+ "tomorrow": fmt.Sprintf("FLOOR(%s,1)=TODAY()+1", ref),
+ "last 7 days": fmt.Sprintf("AND(TODAY()-FLOOR(%[1]s,1)<=6,FLOOR(%[1]s,1)<=TODAY())", ref),
+ "last week": fmt.Sprintf("AND(TODAY()-ROUNDDOWN(%[1]s,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(%[1]s,0)<(WEEKDAY(TODAY())+7))", ref),
+ "this week": fmt.Sprintf("AND(TODAY()-ROUNDDOWN(%[1]s,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(%[1]s,0)-TODAY()>=7-WEEKDAY(TODAY()))", ref),
+ "continue week": fmt.Sprintf("AND(ROUNDDOWN(%[1]s,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(%[1]s,0)-TODAY()<(15-WEEKDAY(TODAY())))", ref),
+ "last month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY())-1,OR(YEAR(%[1]s)=YEAR(TODAY()),AND(MONTH(%[1]s)=1,YEAR(%[1]s)=YEAR(TODAY())-1)))", ref),
+ "this month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY()),YEAR(%[1]s)=YEAR(TODAY()))", ref),
+ "continue month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY())+1,OR(YEAR(%[1]s)=YEAR(TODAY()),AND(MONTH(%[1]s)=12,YEAR(%[1]s)=YEAR(TODAY())+1)))", ref),
+ }[ct],
+ },
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtText provides a function to create conditional formatting rule for
+// text cell values by given priority, criteria type and format settings.
+func drawCondFmtText(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: map[string]string{
+ "containsText": "containsText",
+ "notContains": "notContainsText",
+ "beginsWith": "beginsWith",
+ "endsWith": "endsWith",
+ }[ct],
+ Text: format.Value,
+ Operator: ct,
+ Formula: []string{
+ map[string]string{
+ "containsText": fmt.Sprintf("NOT(ISERROR(SEARCH(\"%s\",%s)))",
+ strings.NewReplacer(`"`, `""`).Replace(format.Value), ref),
+ "notContains": fmt.Sprintf("ISERROR(SEARCH(\"%s\",%s))",
+ strings.NewReplacer(`"`, `""`).Replace(format.Value), ref),
+ "beginsWith": fmt.Sprintf("LEFT(%[2]s,LEN(\"%[1]s\"))=\"%[1]s\"",
+ strings.NewReplacer(`"`, `""`).Replace(format.Value), ref),
+ "endsWith": fmt.Sprintf("RIGHT(%[2]s,LEN(\"%[1]s\"))=\"%[1]s\"",
+ strings.NewReplacer(`"`, `""`).Replace(format.Value), ref),
+ }[ct],
+ },
+ DxfID: format.Format,
+ }, nil
}
// drawCondFmtTop10 provides a function to create conditional formatting rule
// for top N (default is top 10) by given priority, criteria type and format
// settings.
-func drawCondFmtTop10(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtTop10(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
c := &xlsxCfRule{
- Priority: p + 1,
- Type: validType[format.Type],
- Rank: 10,
- DxfID: &format.Format,
- Percent: format.Percent,
- }
- rank, err := strconv.Atoi(format.Value)
- if err == nil {
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Bottom: format.Type == "bottom",
+ Type: validType[format.Type],
+ Rank: 10,
+ DxfID: format.Format,
+ Percent: format.Percent,
+ }
+ if rank, err := strconv.Atoi(format.Value); err == nil {
c.Rank = rank
}
- return c
+ return c, nil
}
// drawCondFmtAboveAverage provides a function to create conditional
// formatting rule for above average and below average by given priority,
// criteria type and format settings.
-func drawCondFmtAboveAverage(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtAboveAverage(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
return &xlsxCfRule{
Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
Type: validType[format.Type],
- AboveAverage: &format.AboveAverage,
- DxfID: &format.Format,
- }
+ AboveAverage: boolPtr(format.AboveAverage),
+ DxfID: format.Format,
+ }, nil
}
// drawCondFmtDuplicateUniqueValues provides a function to create conditional
// formatting rule for duplicate and unique values by given priority, criteria
// type and format settings.
-func drawCondFmtDuplicateUniqueValues(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtDuplicateUniqueValues(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
return &xlsxCfRule{
- Priority: p + 1,
- Type: validType[format.Type],
- DxfID: &format.Format,
- }
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ DxfID: format.Format,
+ }, nil
}
// drawCondFmtColorScale provides a function to create conditional formatting
// rule for color scale (include 2 color scale and 3 color scale) by given
// priority, criteria type and format settings.
-func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtColorScale(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
minValue := format.MinValue
if minValue == "" {
minValue = "0"
@@ -2988,8 +3335,9 @@ func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfR
}
c := &xlsxCfRule{
- Priority: p + 1,
- Type: "colorScale",
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: "colorScale",
ColorScale: &xlsxColorScale{
Cfvo: []*xlsxCfvo{
{Type: format.MinType, Val: minValue},
@@ -3005,53 +3353,139 @@ func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfR
}
c.ColorScale.Cfvo = append(c.ColorScale.Cfvo, &xlsxCfvo{Type: format.MaxType, Val: maxValue})
c.ColorScale.Color = append(c.ColorScale.Color, &xlsxColor{RGB: getPaletteColor(format.MaxColor)})
- return c
+ return c, nil
}
// drawCondFmtDataBar provides a function to create conditional formatting
// rule for data bar by given priority, criteria type and format settings.
-func drawCondFmtDataBar(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtDataBar(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ var x14CfRule *xlsxX14CfRule
+ var extLst *xlsxExtLst
+ if format.BarSolid || format.BarDirection == "leftToRight" || format.BarDirection == "rightToLeft" || format.BarBorderColor != "" {
+ extLst = &xlsxExtLst{Ext: fmt.Sprintf(`%s`, ExtURIConditionalFormattingRuleID, NameSpaceSpreadSheetX14.Value, GUID)}
+ x14CfRule = &xlsxX14CfRule{
+ Type: validType[format.Type],
+ ID: GUID,
+ DataBar: &xlsx14DataBar{
+ MaxLength: 100,
+ Border: format.BarBorderColor != "",
+ Gradient: !format.BarSolid,
+ Direction: format.BarDirection,
+ Cfvo: []*xlsxCfvo{{Type: "autoMin"}, {Type: "autoMax"}},
+ NegativeFillColor: &xlsxColor{RGB: "FFFF0000"},
+ AxisColor: &xlsxColor{RGB: "FFFF0000"},
+ },
+ }
+ if x14CfRule.DataBar.Border {
+ x14CfRule.DataBar.BorderColor = &xlsxColor{RGB: getPaletteColor(format.BarBorderColor)}
+ }
+ }
return &xlsxCfRule{
- Priority: p + 1,
- Type: validType[format.Type],
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
DataBar: &xlsxDataBar{
- Cfvo: []*xlsxCfvo{{Type: format.MinType}, {Type: format.MaxType}},
- Color: []*xlsxColor{{RGB: getPaletteColor(format.BarColor)}},
+ ShowValue: boolPtr(!format.BarOnly),
+ Cfvo: []*xlsxCfvo{{Type: format.MinType, Val: format.MinValue}, {Type: format.MaxType, Val: format.MaxValue}},
+ Color: []*xlsxColor{{RGB: getPaletteColor(format.BarColor)}},
},
- }
+ ExtLst: extLst,
+ }, x14CfRule
}
-// drawConfFmtExp provides a function to create conditional formatting rule
+// drawCondFmtExp provides a function to create conditional formatting rule
// for expression by given priority, criteria type and format settings.
-func drawConfFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule {
+func drawCondFmtExp(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
return &xlsxCfRule{
- Priority: p + 1,
- Type: validType[format.Type],
- Formula: []string{format.Criteria},
- DxfID: &format.Format,
- }
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Formula: []string{format.Criteria},
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtErrors provides a function to create conditional formatting rule
+// for cells with errors by given priority, criteria type and format settings.
+func drawCondFmtErrors(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Formula: []string{fmt.Sprintf("ISERROR(%s)", ref)},
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtNoErrors provides a function to create conditional formatting rule
+// for cells without errors by given priority, criteria type and format settings.
+func drawCondFmtNoErrors(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Formula: []string{fmt.Sprintf("NOT(ISERROR(%s))", ref)},
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtBlanks provides a function to create conditional formatting rule
+// for blank cells by given priority, criteria type and format settings.
+func drawCondFmtBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Formula: []string{fmt.Sprintf("LEN(TRIM(%s))=0", ref)},
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtNoBlanks provides a function to create conditional formatting rule
+// for no blanks cells by given priority, criteria type and format settings.
+func drawCondFmtNoBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ return &xlsxCfRule{
+ Priority: p + 1,
+ StopIfTrue: format.StopIfTrue,
+ Type: validType[format.Type],
+ Formula: []string{fmt.Sprintf("LEN(TRIM(%s))>0", ref)},
+ DxfID: format.Format,
+ }, nil
+}
+
+// drawCondFmtIconSet provides a function to create conditional formatting rule
+// for icon set by given priority, criteria type and format settings.
+func drawCondFmtIconSet(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) {
+ cfRule, ok := condFmtIconSetPresets[format.IconStyle]
+ if !ok {
+ return nil, nil
+ }
+ cfRule.Priority = p + 1
+ cfRule.IconSet.IconSet = format.IconStyle
+ cfRule.IconSet.Reverse = format.ReverseIcons
+ cfRule.IconSet.ShowValue = boolPtr(!format.IconsOnly)
+ cfRule.Type = validType[format.Type]
+ return cfRule, nil
}
// getPaletteColor provides a function to convert the RBG color by given
// string.
func getPaletteColor(color string) string {
- return "FF" + strings.Replace(strings.ToUpper(color), "#", "", -1)
+ return "FF" + strings.ReplaceAll(strings.ToUpper(color), "#", "")
}
// themeReader provides a function to get the pointer to the xl/theme/theme1.xml
// structure after deserialization.
-func (f *File) themeReader() *xlsxTheme {
- var (
- err error
- theme xlsxTheme
- )
-
- if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")))).
+func (f *File) themeReader() (*decodeTheme, error) {
+ if _, ok := f.Pkg.Load(defaultXMLPathTheme); !ok {
+ return nil, nil
+ }
+ theme := decodeTheme{}
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathTheme)))).
Decode(&theme); err != nil && err != io.EOF {
- log.Printf("xml decoder error: %s", err)
+ return &theme, err
}
-
- return &theme
+ return &theme, nil
}
// ThemeColor applied the color with tint value.
@@ -3059,12 +3493,15 @@ func ThemeColor(baseColor string, tint float64) string {
if tint == 0 {
return "FF" + baseColor
}
- r, _ := strconv.ParseInt(baseColor[0:2], 16, 64)
- g, _ := strconv.ParseInt(baseColor[2:4], 16, 64)
- b, _ := strconv.ParseInt(baseColor[4:6], 16, 64)
- h, s, l := RGBToHSL(uint8(r), uint8(g), uint8(b))
+ r, _ := strconv.ParseUint(baseColor[:2], 16, 64)
+ g, _ := strconv.ParseUint(baseColor[2:4], 16, 64)
+ b, _ := strconv.ParseUint(baseColor[4:6], 16, 64)
+ var h, s, l float64
+ if r <= math.MaxUint8 && g <= math.MaxUint8 && b <= math.MaxUint8 {
+ h, s, l = RGBToHSL(uint8(r), uint8(g), uint8(b))
+ }
if tint < 0 {
- l *= (1 + tint)
+ l *= 1 + tint
} else {
l = l*(1-tint) + (1 - (1 - tint))
}
diff --git a/styles_test.go b/styles_test.go
index 9b8ba39e19..1b5d3a254e 100644
--- a/styles_test.go
+++ b/styles_test.go
@@ -2,7 +2,9 @@ package excelize
import (
"fmt"
+ "math"
"path/filepath"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -11,15 +13,15 @@ import (
func TestStyleFill(t *testing.T) {
cases := []struct {
label string
- format string
+ format *Style
expectFill bool
}{{
label: "no_fill",
- format: `{"alignment":{"wrap_text":true}}`,
+ format: &Style{Alignment: &Alignment{WrapText: true}},
expectFill: false,
}, {
label: "fill",
- format: `{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`,
+ format: &Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}},
expectFill: true,
}}
@@ -28,7 +30,8 @@ func TestStyleFill(t *testing.T) {
styleID, err := xl.NewStyle(testCase.format)
assert.NoError(t, err)
- styles := xl.stylesReader()
+ styles, err := xl.stylesReader()
+ assert.NoError(t, err)
style := styles.CellXfs.Xf[styleID]
if testCase.expectFill {
assert.NotEqual(t, *style.FillID, 0, testCase.label)
@@ -37,9 +40,9 @@ func TestStyleFill(t *testing.T) {
}
}
f := NewFile()
- styleID1, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`)
+ styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}})
assert.NoError(t, err)
- styleID2, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`)
+ styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"000000"}}})
assert.NoError(t, err)
assert.Equal(t, styleID1, styleID2)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleFill.xlsx")))
@@ -48,23 +51,23 @@ func TestStyleFill(t *testing.T) {
func TestSetConditionalFormat(t *testing.T) {
cases := []struct {
label string
- format string
+ format []ConditionalFormatOptions
rules []*xlsxCfRule
}{{
label: "3_color_scale",
- format: `[{
- "type":"3_color_scale",
- "criteria":"=",
- "min_type":"num",
- "mid_type":"num",
- "max_type":"num",
- "min_value": "-10",
- "mid_value": "0",
- "max_value": "10",
- "min_color":"ff0000",
- "mid_color":"00ff00",
- "max_color":"0000ff"
- }]`,
+ format: []ConditionalFormatOptions{{
+ Type: "3_color_scale",
+ Criteria: "=",
+ MinType: "num",
+ MidType: "num",
+ MaxType: "num",
+ MinValue: "-10",
+ MidValue: "0",
+ MaxValue: "10",
+ MinColor: "ff0000",
+ MidColor: "00ff00",
+ MaxColor: "0000ff",
+ }},
rules: []*xlsxCfRule{{
Priority: 1,
Type: "colorScale",
@@ -90,16 +93,16 @@ func TestSetConditionalFormat(t *testing.T) {
}},
}, {
label: "3_color_scale default min/mid/max",
- format: `[{
- "type":"3_color_scale",
- "criteria":"=",
- "min_type":"num",
- "mid_type":"num",
- "max_type":"num",
- "min_color":"ff0000",
- "mid_color":"00ff00",
- "max_color":"0000ff"
- }]`,
+ format: []ConditionalFormatOptions{{
+ Type: "3_color_scale",
+ Criteria: "=",
+ MinType: "num",
+ MidType: "num",
+ MaxType: "num",
+ MinColor: "ff0000",
+ MidColor: "00ff00",
+ MaxColor: "0000ff",
+ }},
rules: []*xlsxCfRule{{
Priority: 1,
Type: "colorScale",
@@ -125,14 +128,14 @@ func TestSetConditionalFormat(t *testing.T) {
}},
}, {
label: "2_color_scale default min/max",
- format: `[{
- "type":"2_color_scale",
- "criteria":"=",
- "min_type":"num",
- "max_type":"num",
- "min_color":"ff0000",
- "max_color":"0000ff"
- }]`,
+ format: []ConditionalFormatOptions{{
+ Type: "2_color_scale",
+ Criteria: "=",
+ MinType: "num",
+ MaxType: "num",
+ MinColor: "ff0000",
+ MaxColor: "0000ff",
+ }},
rules: []*xlsxCfRule{{
Priority: 1,
Type: "colorScale",
@@ -154,94 +157,607 @@ func TestSetConditionalFormat(t *testing.T) {
}}
for _, testCase := range cases {
- xl := NewFile()
+ f := NewFile()
const sheet = "Sheet1"
- const cellRange = "A1:A1"
-
- err := xl.SetConditionalFormat(sheet, cellRange, testCase.format)
- if err != nil {
- t.Fatalf("%s", err)
- }
-
- xlsx, err := xl.workSheetReader(sheet)
+ const rangeRef = "A1:A1"
+ assert.NoError(t, f.SetConditionalFormat(sheet, rangeRef, testCase.format))
+ ws, err := f.workSheetReader(sheet)
assert.NoError(t, err)
- cf := xlsx.ConditionalFormatting
+ cf := ws.ConditionalFormatting
assert.Len(t, cf, 1, testCase.label)
assert.Len(t, cf[0].CfRule, 1, testCase.label)
- assert.Equal(t, cellRange, cf[0].SQRef, testCase.label)
+ assert.Equal(t, rangeRef, cf[0].SQRef, testCase.label)
assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label)
}
+ // Test creating a conditional format with a solid color data bar style
+ f := NewFile()
+ condFmts := []ConditionalFormatOptions{
+ {Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: intPtr(0), Criteria: "=", MinType: "min", MaxType: "max"},
+ }
+ for _, ref := range []string{"A1:A2", "B1:B2"} {
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts))
+ }
+ f = NewFile()
+ // Test creating a conditional format without cell reference
+ assert.Equal(t, ErrParameterRequired, f.SetConditionalFormat("Sheet1", "", nil))
+ // Test creating a conditional format with invalid cell reference
+ assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2:A3", nil))
+ // Test creating a conditional format with existing extension lists
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURISlicerListX14, ExtURISparklineGroups)}
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}}))
+ f = NewFile()
+ // Test creating a conditional format with invalid extension list characters
+ ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ""}
+ assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element closed by ")
+ // Test creating a conditional format with invalid icon set style
+ assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}))
+ // Test unsupported conditional formatting rule types
+ assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: "unsupported"}}))
+
+ t.Run("multi_conditional_formatting_rules_priority", func(t *testing.T) {
+ f := NewFile()
+ var condFmts []ConditionalFormatOptions
+ for _, color := range []string{
+ "#264B96", // Blue
+ "#F9A73E", // Yellow
+ "#006F3C", // Green
+ } {
+ condFmts = append(condFmts, ConditionalFormatOptions{
+ Type: "data_bar",
+ Criteria: "=",
+ MinType: "num",
+ MaxType: "num",
+ MinValue: "0",
+ MaxValue: "5",
+ BarColor: color,
+ BarSolid: true,
+ })
+ }
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A5", condFmts))
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "B1:B5", condFmts))
+ for r := 1; r <= 20; r++ {
+ cell, err := CoordinatesToCellName(1, r)
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
+ cell, err = CoordinatesToCellName(2, r)
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
+ }
+ ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
+ assert.True(t, ok)
+ var priorities []int
+ expected := []int{1, 2, 3, 4, 5, 6}
+ for _, condFmt := range ws.(*xlsxWorksheet).ConditionalFormatting {
+ for _, rule := range condFmt.CfRule {
+ priorities = append(priorities, rule.Priority)
+ }
+ }
+ assert.Equal(t, expected, priorities)
+ assert.NoError(t, f.Close())
+ })
+}
+
+func TestGetConditionalFormats(t *testing.T) {
+ for _, format := range [][]ConditionalFormatOptions{
+ {{Type: "cell", Format: intPtr(1), Criteria: "greater than", Value: "6"}},
+ {{Type: "cell", Format: intPtr(1), Criteria: "between", MinValue: "6", MaxValue: "8"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "yesterday"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "today"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "tomorrow"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "last 7 days"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "last week"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "this week"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "continue week"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "last month"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "this month"}},
+ {{Type: "time_period", Format: intPtr(1), Criteria: "continue month"}},
+ {{Type: "text", Format: intPtr(1), Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}},
+ {{Type: "text", Format: intPtr(1), Criteria: "not containing", Value: "text"}},
+ {{Type: "text", Format: intPtr(1), Criteria: "begins with", Value: "prefix"}},
+ {{Type: "text", Format: intPtr(1), Criteria: "ends with", Value: "suffix"}},
+ {{Type: "top", Format: intPtr(1), Criteria: "=", Value: "6"}},
+ {{Type: "bottom", Format: intPtr(1), Criteria: "=", Value: "6"}},
+ {{Type: "average", AboveAverage: true, Format: intPtr(1), Criteria: "="}},
+ {{Type: "duplicate", Format: intPtr(1), Criteria: "="}},
+ {{Type: "unique", Format: intPtr(1), Criteria: "="}},
+ {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}},
+ {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}},
+ {{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}},
+ {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}},
+ {{Type: "formula", Format: intPtr(1), Criteria: "="}},
+ {{Type: "blanks", Format: intPtr(1)}},
+ {{Type: "no_blanks", Format: intPtr(1)}},
+ {{Type: "errors", Format: intPtr(1)}},
+ {{Type: "no_errors", Format: intPtr(1)}},
+ {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}},
+ } {
+ f := NewFile()
+ err := f.SetConditionalFormat("Sheet1", "A2:A1,B:B,2:2", format)
+ assert.NoError(t, err)
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, format, opts["A2:A1 B1:B1048576 A2:XFD2"])
+ }
+ // Test get multiple conditional formats
+ f := NewFile()
+ expected := []ConditionalFormatOptions{
+ {Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true},
+ {Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: false, StopIfTrue: true},
+ }
+ err := f.SetConditionalFormat("Sheet1", "A1:A2", expected)
+ assert.NoError(t, err)
+ opts, err := f.GetConditionalFormats("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts["A1:A2"])
+
+ // Test get conditional formats on no exists worksheet
+ f = NewFile()
+ _, err = f.GetConditionalFormats("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test get conditional formats with invalid sheet name
+ _, err = f.GetConditionalFormats("Sheet:1")
+ assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestUnsetConditionalFormat(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 7))
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
- format, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`)
+ format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
assert.NoError(t, err)
- assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)))
+ assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: &format, Value: "6"}}))
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
- // Test unset conditional format on not exists worksheet.
- assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN is not exist")
- // Save xlsx file by the given path.
+ // Test unset conditional format on not exists worksheet
+ assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist")
+ // Test unset conditional format with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10"))
+ // Save spreadsheet by the given path
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx")))
}
func TestNewStyle(t *testing.T) {
f := NewFile()
- styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`)
+ for i := 0; i < 18; i++ {
+ _, err := f.NewStyle(&Style{
+ Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: i},
+ })
+ assert.NoError(t, err)
+ }
+ f = NewFile()
+ styleID, err := f.NewStyle(&Style{Font: &Font{Bold: true, Italic: true, Family: "Times New Roman", Size: 36, Color: "777777"}})
+ assert.NoError(t, err)
+ styles, err := f.stylesReader()
assert.NoError(t, err)
- styles := f.stylesReader()
fontID := styles.CellXfs.Xf[styleID].FontID
font := styles.Fonts.Font[*fontID]
assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name")
assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles")
_, err = f.NewStyle(&Style{})
assert.NoError(t, err)
- _, err = f.NewStyle(Style{})
- assert.EqualError(t, err, "invalid parameter type")
+ _, err = f.NewStyle(nil)
+ assert.NoError(t, err)
+
+ // Test gradient fills
+ f = NewFile()
+ styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: 1, Pattern: 1}})
+ assert.NoError(t, err)
+ styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FF0000", "4E71BE"}, Shading: 1, Pattern: 1}})
+ assert.NoError(t, err)
+ assert.NotEqual(t, styleID1, styleID2)
+
+ var exp string
+ f = NewFile()
+ _, err = f.NewStyle(&Style{CustomNumFmt: &exp})
+ assert.Equal(t, ErrCustomNumFmt, err)
+ _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}})
+ assert.Equal(t, ErrFontLength, err)
+ _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}})
+ assert.Equal(t, ErrFontSize, err)
+
+ // Test create numeric custom style
+ numFmt := "####;####"
+ f.Styles.NumFmts = nil
+ styleID, err = f.NewStyle(&Style{
+ CustomNumFmt: &numFmt,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, 1, styleID)
+
+ assert.NotNil(t, f.Styles)
+ assert.NotNil(t, f.Styles.CellXfs)
+ assert.NotNil(t, f.Styles.CellXfs.Xf)
+
+ nf := f.Styles.CellXfs.Xf[styleID]
+ assert.Equal(t, 164, *nf.NumFmtID)
+
+ // Test create currency custom style
+ f.Styles.NumFmts = nil
+ styleID, err = f.NewStyle(&Style{
+ NumFmt: 32, // must not be in currencyNumFmt
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, 2, styleID)
+
+ assert.NotNil(t, f.Styles)
+ assert.NotNil(t, f.Styles.CellXfs)
+ assert.NotNil(t, f.Styles.CellXfs.Xf)
+
+ nf = f.Styles.CellXfs.Xf[styleID]
+ assert.Equal(t, 32, *nf.NumFmtID)
+
+ // Test set build-in scientific number format
+ styleID, err = f.NewStyle(&Style{NumFmt: 11})
+ assert.NoError(t, err)
+ assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", styleID))
+ assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]float64{1.23, 1.234}))
+ rows, err := f.GetRows("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, [][]string{{"1.23E+00", "1.23E+00"}}, rows)
+
+ f = NewFile()
+ // Test currency number format
+ customNumFmt := "[$$-409]#,##0.00"
+ style1, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt})
+ assert.NoError(t, err)
+ style2, err := f.NewStyle(&Style{NumFmt: 165})
+ assert.NoError(t, err)
+ assert.Equal(t, style1, style2)
+
+ style3, err := f.NewStyle(&Style{NumFmt: 166})
+ assert.NoError(t, err)
+ assert.Equal(t, 2, style3)
+
+ f = NewFile()
+ f.Styles.NumFmts = nil
+ f.Styles.CellXfs.Xf = nil
+ style4, err := f.NewStyle(&Style{NumFmt: 160})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, style4)
+
+ f = NewFile()
+ f.Styles.NumFmts = nil
+ f.Styles.CellXfs.Xf = nil
+ style5, err := f.NewStyle(&Style{NumFmt: 160})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, style5)
+
+ // Test create style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.NewStyle(&Style{NumFmt: 165})
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ // Test create cell styles reach maximum
+ f = NewFile()
+ f.Styles.CellXfs.Xf = make([]xlsxXf, MaxCellStyles)
+ f.Styles.CellXfs.Count = MaxCellStyles
+ _, err = f.NewStyle(&Style{NumFmt: 0})
+ assert.Equal(t, ErrCellStyles, err)
+}
+
+func TestConditionalStyle(t *testing.T) {
+ f := NewFile()
+ expected := &Style{Protection: &Protection{Hidden: true, Locked: true}}
+ idx, err := f.NewConditionalStyle(expected)
+ assert.NoError(t, err)
+ style, err := f.GetConditionalStyle(idx)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, style)
+ _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(4), NumFmt: 165, NegRed: true})
+ assert.NoError(t, err)
+ _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(-1)})
+ assert.NoError(t, err)
+ expected = &Style{NumFmt: 1}
+ idx, err = f.NewConditionalStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetConditionalStyle(idx)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.NumFmt, style.NumFmt)
+ assert.Zero(t, *style.DecimalPlaces)
+ _, err = f.NewConditionalStyle(&Style{NumFmt: 27})
+ assert.NoError(t, err)
+ numFmt := "general"
+ _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt})
+ assert.NoError(t, err)
+ numFmt1 := "0.00"
+ _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt1})
+ assert.NoError(t, err)
+ // Test create conditional style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get conditional style with invalid style index
+ _, err = f.GetConditionalStyle(1)
+ assert.Equal(t, newInvalidStyleID(1), err)
+ // Test get conditional style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.GetConditionalStyle(1)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ f = NewFile()
+ // Test get conditional style with background color and empty pattern type
+ idx, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
+ assert.NoError(t, err)
+ f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.PatternType = ""
+ f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.FgColor = nil
+ f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.BgColor = &xlsxColor{Theme: intPtr(6)}
+ style, err = f.GetConditionalStyle(idx)
+ assert.NoError(t, err)
+ assert.Equal(t, "pattern", style.Fill.Type)
+ assert.Equal(t, []string{"A5A5A5"}, style.Fill.Color)
}
func TestGetDefaultFont(t *testing.T) {
f := NewFile()
- s := f.GetDefaultFont()
+ s, err := f.GetDefaultFont()
+ assert.NoError(t, err)
assert.Equal(t, s, "Calibri", "Default font should be Calibri")
+ // Test get default font with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.GetDefaultFont()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestSetDefaultFont(t *testing.T) {
f := NewFile()
- f.SetDefaultFont("Ariel")
- styles := f.stylesReader()
- s := f.GetDefaultFont()
- assert.Equal(t, s, "Ariel", "Default font should change to Ariel")
+ assert.NoError(t, f.SetDefaultFont("Arial"))
+ styles, err := f.stylesReader()
+ assert.NoError(t, err)
+ s, err := f.GetDefaultFont()
+ assert.NoError(t, err)
+ assert.Equal(t, s, "Arial", "Default font should change to Arial")
assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true)
+ // Test set default font with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetDefaultFont("Arial"), "XML syntax error on line 1: invalid UTF-8")
}
func TestStylesReader(t *testing.T) {
f := NewFile()
- // Test read styles with unsupport charset.
+ // Test read styles with unsupported charset
f.Styles = nil
- f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset
- assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader())
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ styles, err := f.stylesReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.EqualValues(t, new(xlsxStyleSheet), styles)
}
func TestThemeReader(t *testing.T) {
f := NewFile()
- // Test read theme with unsupport charset.
- f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset
- assert.EqualValues(t, new(xlsxTheme), f.themeReader())
+ // Test read theme with unsupported charset
+ f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset)
+ theme, err := f.themeReader()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ assert.EqualValues(t, &decodeTheme{}, theme)
}
func TestSetCellStyle(t *testing.T) {
f := NewFile()
- // Test set cell style on not exists worksheet.
- assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN is not exist")
+ // Test set cell style on not exists worksheet
+ assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist")
+ // Test set cell style with invalid style ID
+ assert.Equal(t, newInvalidStyleID(-1), f.SetCellStyle("Sheet1", "A1", "A2", -1))
+ // Test set cell style with not exists style ID
+ assert.Equal(t, newInvalidStyleID(10), f.SetCellStyle("Sheet1", "A1", "A2", 10))
+ // Test set cell style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 1), "XML syntax error on line 1: invalid UTF-8")
}
func TestGetStyleID(t *testing.T) {
- assert.Equal(t, -1, NewFile().getStyleID(&xlsxStyleSheet{}, nil))
+ f := NewFile()
+ styleID, err := f.getStyleID(&xlsxStyleSheet{}, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, -1, styleID)
+ // Test get style ID with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ _, err = f.getStyleID(&xlsxStyleSheet{
+ CellXfs: &xlsxCellXfs{},
+ Fonts: &xlsxFonts{
+ Font: []*xlsxFont{{}},
+ },
+ }, &Style{NumFmt: 0, Font: &Font{}})
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestGetFillID(t *testing.T) {
- assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}}))
+ styles, err := NewFile().stylesReader()
+ assert.NoError(t, err)
+ assert.Equal(t, -1, getFillID(styles, &Style{Fill: Fill{Type: "unknown"}}))
+}
+
+func TestThemeColor(t *testing.T) {
+ for _, clr := range [][]string{
+ {"FF000000", ThemeColor("000000", -0.1)},
+ {"FF000000", ThemeColor("000000", 0)},
+ {"FF33FF33", ThemeColor("00FF00", 0.2)},
+ {"FFFFFFFF", ThemeColor("000000", 1)},
+ {"FFFFFFFF", ThemeColor(strings.Repeat(string(rune(math.MaxUint8+1)), 6), 1)},
+ {"FFFFFFFF", ThemeColor(strings.Repeat(string(rune(-1)), 6), 1)},
+ } {
+ assert.Equal(t, clr[0], clr[1])
+ }
+}
+
+func TestGetNumFmtID(t *testing.T) {
+ f := NewFile()
+
+ fs1, err := parseFormatStyleSet(&Style{Protection: &Protection{Hidden: false, Locked: false}, NumFmt: 10})
+ assert.NoError(t, err)
+ id1 := getNumFmtID(&xlsxStyleSheet{}, fs1)
+
+ fs2, err := parseFormatStyleSet(&Style{Protection: &Protection{Hidden: false, Locked: false}, NumFmt: 0})
+ assert.NoError(t, err)
+ id2 := getNumFmtID(&xlsxStyleSheet{}, fs2)
+
+ assert.NotEqual(t, id1, id2)
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx")))
+}
+
+func TestGetThemeColor(t *testing.T) {
+ assert.Empty(t, (&File{}).getThemeColor(&xlsxColor{}))
+ f := NewFile()
+ assert.Empty(t, f.getThemeColor(nil))
+ var theme int
+ assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{Theme: &theme}))
+ assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"}))
+ assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5}))
+ assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5}))
+ clr := &decodeCTColor{}
+ assert.Nil(t, clr.colorChoice())
+}
+
+func TestGetStyle(t *testing.T) {
+ f := NewFile()
+ expected := &Style{
+ Border: []Border{
+ {Type: "left", Color: "0000FF", Style: 3},
+ {Type: "right", Color: "FF0000", Style: 6},
+ {Type: "top", Color: "00FF00", Style: 4},
+ {Type: "bottom", Color: "FFFF00", Style: 5},
+ {Type: "diagonalUp", Color: "A020F0", Style: 7},
+ {Type: "diagonalDown", Color: "A020F0", Style: 7},
+ },
+ Fill: Fill{Type: "gradient", Shading: 16, Color: []string{"0000FF", "00FF00"}},
+ Font: &Font{
+ Bold: true, Italic: true, Underline: "single", Family: "Arial",
+ Size: 8.5, Strike: true, Color: "777777", ColorIndexed: 1, ColorTint: 0.1,
+ },
+ Alignment: &Alignment{
+ Horizontal: "center",
+ Indent: 1,
+ JustifyLastLine: true,
+ ReadingOrder: 1,
+ RelativeIndent: 1,
+ ShrinkToFit: true,
+ TextRotation: 180,
+ Vertical: "center",
+ WrapText: true,
+ },
+ Protection: &Protection{Hidden: true, Locked: true},
+ NumFmt: 49,
+ }
+ styleID, err := f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err := f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.Border, style.Border)
+ assert.Equal(t, expected.Fill, style.Fill)
+ assert.Equal(t, expected.Font, style.Font)
+ assert.Equal(t, expected.Alignment, style.Alignment)
+ assert.Equal(t, expected.Protection, style.Protection)
+ assert.Equal(t, expected.NumFmt, style.NumFmt)
+ assert.Nil(t, style.DecimalPlaces)
+
+ expected = &Style{
+ Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}},
+ }
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.Fill, style.Fill)
+ assert.Nil(t, style.DecimalPlaces)
+
+ expected = &Style{NumFmt: 2}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.NumFmt, style.NumFmt)
+ assert.Equal(t, 2, *style.DecimalPlaces)
+
+ expected = &Style{NumFmt: 27}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.NumFmt, style.NumFmt)
+ assert.Nil(t, style.DecimalPlaces)
+
+ expected = &Style{NumFmt: 165}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, expected.NumFmt, style.NumFmt)
+ assert.Equal(t, 2, *style.DecimalPlaces)
+
+ decimal := 4
+ expected = &Style{NumFmt: 165, DecimalPlaces: &decimal, NegRed: true}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, style.NumFmt)
+ assert.Equal(t, *expected.DecimalPlaces, *style.DecimalPlaces)
+ assert.Equal(t, "[$$-409]#,##0.0000;[Red][$$-409]#,##0.0000", *style.CustomNumFmt)
+
+ for _, val := range [][]interface{}{
+ {"$#,##0", 0},
+ {"$#,##0.0", 1},
+ {"_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", 0},
+ {"_($* #,##000_);_($* (#,##000);_($* \"-\"_);_(@_)", 0},
+ {"_($* #,##0.0000_);_($* (#,##0.0000);_($* \"-\"????_);_(@_)", 4},
+ } {
+ numFmtCode := val[0].(string)
+ expected = &Style{CustomNumFmt: &numFmtCode}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, val[1].(int), *style.DecimalPlaces, numFmtCode)
+ }
+
+ for _, val := range []string{
+ ";$#,##0",
+ ";$#,##0;",
+ ";$#,##0.0",
+ ";$#,##0.0;",
+ "$#,##0;0.0",
+ "_($* #,##0_);;_($* \"-\"_);_(@_)",
+ "_($* #,##0.0_);_($* (#,##0.00);_($* \"-\"_);_(@_)",
+ } {
+ expected = &Style{CustomNumFmt: &val}
+ styleID, err = f.NewStyle(expected)
+ assert.NoError(t, err)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Nil(t, style.DecimalPlaces)
+ }
+
+ // Test get style with custom color index
+ f.Styles.Colors = &xlsxStyleColors{
+ IndexedColors: &xlsxIndexedColors{
+ RgbColor: []xlsxColor{{RGB: "FF012345"}},
+ },
+ }
+ assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0}))
+
+ f.Styles.Fonts.Font[0].U = &attrValString{}
+ f.Styles.CellXfs.Xf[0].FontID = intPtr(0)
+ style, err = f.GetStyle(styleID)
+ assert.NoError(t, err)
+ assert.Equal(t, "single", style.Font.Underline)
+
+ // Test get style with invalid style index
+ style, err = f.GetStyle(-1)
+ assert.Nil(t, style)
+ assert.Equal(t, err, newInvalidStyleID(-1))
+ // Test get style with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ style, err = f.GetStyle(1)
+ assert.Nil(t, style)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
diff --git a/table.go b/table.go
index 64c87b16e6..1d47e21e85 100644
--- a/table.go
+++ b/table.go
@@ -1,108 +1,251 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
- "encoding/json"
+ "bytes"
"encoding/xml"
"fmt"
+ "io"
"regexp"
"strconv"
"strings"
+ "unicode/utf8"
)
-// parseFormatTableSet provides a function to parse the format settings of the
+var (
+ expressionFormat = regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
+ conditionFormat = regexp.MustCompile(`(or|\|\|)`)
+ blankFormat = regexp.MustCompile("blanks|nonblanks")
+ matchFormat = regexp.MustCompile("[*?]")
+)
+
+// parseTableOptions provides a function to parse the format settings of the
// table with default value.
-func parseFormatTableSet(formatSet string) (*formatTable, error) {
- format := formatTable{
- TableStyle: "",
- ShowRowStripes: true,
+func parseTableOptions(opts *Table) (*Table, error) {
+ var err error
+ if opts == nil {
+ return &Table{ShowRowStripes: boolPtr(true)}, err
+ }
+ if opts.ShowRowStripes == nil {
+ opts.ShowRowStripes = boolPtr(true)
}
- err := json.Unmarshal(parseFormatSet(formatSet), &format)
- return &format, err
+ if err = checkDefinedName(opts.Name); err != nil {
+ return opts, err
+ }
+ return opts, err
}
// AddTable provides the method to add table in a worksheet by given worksheet
-// name, coordinate area and format set. For example, create a table of A1:D5
+// name, range reference and format set. For example, create a table of A1:D5
// on Sheet1:
//
-// err := f.AddTable("Sheet1", "A1", "D5", ``)
+// err := f.AddTable("Sheet1", &excelize.Table{Range: "A1:D5"})
//
// Create a table of F2:H6 on Sheet2 with format set:
//
-// err := f.AddTable("Sheet2", "F2", "H6", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)
+// disable := false
+// err := f.AddTable("Sheet2", &excelize.Table{
+// Range: "F2:H6",
+// Name: "table",
+// StyleName: "TableStyleMedium2",
+// ShowFirstColumn: true,
+// ShowLastColumn: true,
+// ShowRowStripes: &disable,
+// ShowColumnStripes: true,
+// })
//
// Note that the table must be at least two lines including the header. The
// header cells must contain strings and must be unique, and must set the
// header row data of the table before calling the AddTable function. Multiple
-// tables coordinate areas that can't have an intersection.
-//
-// table_name: The name of the table, in the same worksheet name of the table should be unique
+// tables range reference that can't have an intersection.
//
-// table_style: The built-in table style names
+// Name: The name of the table, in the same worksheet name of the table should
+// be unique, starts with a letter or underscore (_), doesn't include a
+// space or character, and should be no more than 255 characters
//
-// TableStyleLight1 - TableStyleLight21
-// TableStyleMedium1 - TableStyleMedium28
-// TableStyleDark1 - TableStyleDark11
+// StyleName: The built-in table style names
//
-func (f *File) AddTable(sheet, hcell, vcell, format string) error {
- formatSet, err := parseFormatTableSet(format)
+// TableStyleLight1 - TableStyleLight21
+// TableStyleMedium1 - TableStyleMedium28
+// TableStyleDark1 - TableStyleDark11
+func (f *File) AddTable(sheet string, table *Table) error {
+ options, err := parseTableOptions(table)
if err != nil {
return err
}
- // Coordinate conversion, convert C1:B3 to 2,0,1,2.
- hcol, hrow, err := CellNameToCoordinates(hcell)
- if err != nil {
- return err
+ var exist bool
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/tables/table") {
+ var t xlsxTable
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
+ Decode(&t); err != nil && err != io.EOF {
+ return true
+ }
+ if exist = t.Name == options.Name; exist {
+ return false
+ }
+ }
+ return true
+ })
+ if exist {
+ return ErrExistsTableName
}
- vcol, vrow, err := CellNameToCoordinates(vcell)
+ // Coordinate conversion, convert C1:B3 to 2,0,1,2.
+ coordinates, err := rangeRefToCoordinates(options.Range)
if err != nil {
return err
}
-
- if vcol < hcol {
- vcol, hcol = hcol, vcol
- }
-
- if vrow < hrow {
- vrow, hrow = hrow, vrow
- }
-
+ // Correct table reference range, such correct C1:B3 to B1:C3.
+ _ = sortCoordinates(coordinates)
tableID := f.countTables() + 1
sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml"
- tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1)
+ tableXML := strings.ReplaceAll(sheetRelationshipsTableXML, "..", "xl")
// Add first table for given sheet.
- sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "")
if err = f.addSheetTable(sheet, rID); err != nil {
return err
}
f.addSheetNameSpace(sheet, SourceRelationship)
- if err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet); err != nil {
+ if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil {
return err
}
- f.addContentTypePart(tableID, "table")
- return err
+ return f.addContentTypePart(tableID, "table")
+}
+
+// GetTables provides the method to get all tables in a worksheet by given
+// worksheet name.
+func (f *File) GetTables(sheet string) ([]Table, error) {
+ var tables []Table
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return tables, err
+ }
+ if ws.TableParts == nil {
+ return tables, err
+ }
+ for _, tbl := range ws.TableParts.TableParts {
+ if tbl != nil {
+ target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID)
+ tableXML := strings.ReplaceAll(target, "..", "xl")
+ content, ok := f.Pkg.Load(tableXML)
+ if !ok {
+ continue
+ }
+ var t xlsxTable
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(&t); err != nil && err != io.EOF {
+ return tables, err
+ }
+ table := Table{
+ rID: tbl.RID,
+ tID: t.ID,
+ tableXML: tableXML,
+ Range: t.Ref,
+ Name: t.Name,
+ }
+ if t.TableStyleInfo != nil {
+ table.StyleName = t.TableStyleInfo.Name
+ table.ShowColumnStripes = t.TableStyleInfo.ShowColumnStripes
+ table.ShowFirstColumn = t.TableStyleInfo.ShowFirstColumn
+ table.ShowLastColumn = t.TableStyleInfo.ShowLastColumn
+ table.ShowRowStripes = &t.TableStyleInfo.ShowRowStripes
+ }
+ tables = append(tables, table)
+ }
+ }
+ return tables, err
+}
+
+// DeleteTable provides the method to delete table by given table name.
+func (f *File) DeleteTable(name string) error {
+ if err := checkDefinedName(name); err != nil {
+ return err
+ }
+ tbls, err := f.getTables()
+ if err != nil {
+ return err
+ }
+ for sheet, tables := range tbls {
+ for _, table := range tables {
+ if table.Name != name {
+ continue
+ }
+ ws, _ := f.workSheetReader(sheet)
+ for i, tbl := range ws.TableParts.TableParts {
+ if tbl.RID == table.rID {
+ ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...)
+ f.Pkg.Delete(table.tableXML)
+ _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLTable, "/"+table.tableXML)
+ f.deleteSheetRelationships(sheet, tbl.RID)
+ break
+ }
+ }
+ if ws.TableParts.Count = len(ws.TableParts.TableParts); ws.TableParts.Count == 0 {
+ ws.TableParts = nil
+ }
+ return err
+ }
+ }
+ return newNoExistTableError(name)
+}
+
+// getTables provides a function to get all tables in a workbook.
+func (f *File) getTables() (map[string][]Table, error) {
+ tables := map[string][]Table{}
+ for _, sheetName := range f.GetSheetList() {
+ tbls, err := f.GetTables(sheetName)
+ e := ErrSheetNotExist{sheetName}
+ if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
+ return tables, err
+ }
+ tables[sheetName] = append(tables[sheetName], tbls...)
+ }
+ return tables, nil
}
// countTables provides a function to get table files count storage in the
// folder xl/tables.
func (f *File) countTables() int {
count := 0
- for k := range f.XLSX {
- if strings.Contains(k, "xl/tables/table") {
- count++
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/tables/tableSingleCells") {
+ var cells xlsxSingleXMLCells
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
+ Decode(&cells); err != nil && err != io.EOF {
+ count++
+ return true
+ }
+ for _, cell := range cells.SingleXmlCell {
+ if count < cell.ID {
+ count = cell.ID
+ }
+ }
}
- }
+ if strings.Contains(k.(string), "xl/tables/table") {
+ var t xlsxTable
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
+ Decode(&t); err != nil && err != io.EOF {
+ count++
+ return true
+ }
+ if count < t.ID {
+ count = t.ID
+ }
+ }
+ return true
+ })
return count
}
@@ -124,43 +267,108 @@ func (f *File) addSheetTable(sheet string, rID int) error {
return err
}
-// addTable provides a function to add table by given worksheet name,
-// coordinate area and format set.
-func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error {
- // Correct the minimum number of rows, the table at least two lines.
- if y1 == y2 {
- y2++
- }
-
- // Correct table reference coordinate area, such correct C1:B3 to B1:C3.
- ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2})
- if err != nil {
- return err
- }
-
- var tableColumn []*xlsxTableColumn
-
- idx := 0
+// setTableColumns provides a function to set cells value in header row for the
+// table.
+func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, tbl *xlsxTable) error {
+ var (
+ idx int
+ header []string
+ tableColumns []*xlsxTableColumn
+ getTableColumn = func(name string) *xlsxTableColumn {
+ if tbl != nil && tbl.TableColumns != nil {
+ for _, column := range tbl.TableColumns.TableColumn {
+ if column.Name == name {
+ return column
+ }
+ }
+ }
+ return nil
+ }
+ )
for i := x1; i <= x2; i++ {
idx++
cell, err := CoordinatesToCellName(i, y1)
if err != nil {
return err
}
- name, _ := f.GetCellValue(sheet, cell)
+ name, _ := f.GetCellValue(sheet, cell, Options{RawCellValue: true})
if _, err := strconv.Atoi(name); err == nil {
- _ = f.SetCellStr(sheet, cell, name)
+ if showHeaderRow {
+ _ = f.SetCellStr(sheet, cell, name)
+ }
}
- if name == "" {
+ if name == "" || inStrSlice(header, name, true) != -1 {
name = "Column" + strconv.Itoa(idx)
- f.SetCellStr(sheet, cell, name)
+ if showHeaderRow {
+ _ = f.SetCellStr(sheet, cell, name)
+ }
}
- tableColumn = append(tableColumn, &xlsxTableColumn{
+ header = append(header, name)
+ if column := getTableColumn(name); column != nil {
+ column.ID, column.DataDxfID, column.QueryTableFieldID = idx, 0, 0
+ tableColumns = append(tableColumns, column)
+ continue
+ }
+ tableColumns = append(tableColumns, &xlsxTableColumn{
ID: idx,
Name: name,
})
}
- name := formatSet.TableName
+ tbl.TableColumns = &xlsxTableColumns{
+ Count: len(tableColumns),
+ TableColumn: tableColumns,
+ }
+ return nil
+}
+
+// checkDefinedName check whether there are illegal characters in the defined
+// name or table name. Verify that the name:
+// 1. Starts with a letter or underscore (_)
+// 2. Doesn't include a space or character that isn't allowed
+func checkDefinedName(name string) error {
+ if utf8.RuneCountInString(name) > MaxFieldLength {
+ return ErrNameLength
+ }
+ inCodeRange := func(code int, tbl []int) bool {
+ for i := 0; i < len(tbl); i += 2 {
+ if tbl[i] <= code && code <= tbl[i+1] {
+ return true
+ }
+ }
+ return false
+ }
+ for i, c := range name {
+ if i == 0 {
+ if inCodeRange(int(c), supportedDefinedNameAtStartCharCodeRange) {
+ continue
+ }
+ return newInvalidNameError(name)
+ }
+ if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) {
+ continue
+ }
+ return newInvalidNameError(name)
+ }
+ return nil
+}
+
+// addTable provides a function to add table by given worksheet name,
+// range reference and format set.
+func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Table) error {
+ // Correct the minimum number of rows, the table at least two lines.
+ if y1 == y2 {
+ y2++
+ }
+ hideHeaderRow := opts != nil && opts.ShowHeaderRow != nil && !*opts.ShowHeaderRow
+ if hideHeaderRow {
+ y1++
+ }
+ // Correct table range reference, such correct C1:B3 to B1:C3.
+ ref, err := coordinatesToRangeRef([]int{x1, y1, x2, y2})
+ if err != nil {
+ return err
+ }
+ name := opts.Name
if name == "" {
name = "Table" + strconv.Itoa(i)
}
@@ -173,87 +381,82 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet
AutoFilter: &xlsxAutoFilter{
Ref: ref,
},
- TableColumns: &xlsxTableColumns{
- Count: idx,
- TableColumn: tableColumn,
- },
TableStyleInfo: &xlsxTableStyleInfo{
- Name: formatSet.TableStyle,
- ShowFirstColumn: formatSet.ShowFirstColumn,
- ShowLastColumn: formatSet.ShowLastColumn,
- ShowRowStripes: formatSet.ShowRowStripes,
- ShowColumnStripes: formatSet.ShowColumnStripes,
+ Name: opts.StyleName,
+ ShowFirstColumn: opts.ShowFirstColumn,
+ ShowLastColumn: opts.ShowLastColumn,
+ ShowRowStripes: *opts.ShowRowStripes,
+ ShowColumnStripes: opts.ShowColumnStripes,
},
}
- table, _ := xml.Marshal(t)
+ _ = f.setTableColumns(sheet, !hideHeaderRow, x1, y1, x2, &t)
+ if hideHeaderRow {
+ t.AutoFilter = nil
+ t.HeaderRowCount = intPtr(0)
+ }
+ table, err := xml.Marshal(t)
f.saveFileList(tableXML, table)
- return nil
-}
-
-// parseAutoFilterSet provides a function to parse the settings of the auto
-// filter.
-func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) {
- format := formatAutoFilter{}
- err := json.Unmarshal([]byte(formatSet), &format)
- return &format, err
+ return err
}
// AutoFilter provides the method to add auto filter in a worksheet by given
-// worksheet name, coordinate area and settings. An autofilter in Excel is a
+// worksheet name, range reference and settings. An auto filter in Excel is a
// way of filtering a 2D range of data based on some simple criteria. For
-// example applying an autofilter to a cell range A1:D4 in the Sheet1:
+// example applying an auto filter to a cell range A1:D4 in the Sheet1:
//
-// err := f.AutoFilter("Sheet1", "A1", "D4", "")
+// err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{})
//
-// Filter data in an autofilter:
+// Filter data in an auto filter:
//
-// err := f.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`)
+// err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{
+// {Column: "B", Expression: "x != blanks"},
+// })
//
-// column defines the filter columns in a autofilter range based on simple
+// Column defines the filter columns in an auto filter range based on simple
// criteria
//
// It isn't sufficient to just specify the filter condition. You must also
// hide any rows that don't match the filter condition. Rows are hidden using
-// the SetRowVisible() method. Excelize can't filter rows automatically since
+// the SetRowVisible function. Excelize can't filter rows automatically since
// this isn't part of the file format.
//
// Setting a filter criteria for a column:
//
-// expression defines the conditions, the following operators are available
+// Expression defines the conditions, the following operators are available
// for setting the filter criteria:
//
-// ==
-// !=
-// >
-// <
-// >=
-// <=
-// and
-// or
+// ==
+// !=
+// >
+// <
+// >=
+// <=
+// and
+// or
//
// An expression can comprise a single statement or two statements separated
// by the 'and' and 'or' operators. For example:
//
-// x < 2000
-// x > 2000
-// x == 2000
-// x > 2000 and x < 5000
-// x == 2000 or x == 5000
+// x < 2000
+// x > 2000
+// x == 2000
+// x > 2000 and x < 5000
+// x == 2000 or x == 5000
//
// Filtering of blank or non-blank data can be achieved by using a value of
// Blanks or NonBlanks in the expression:
//
-// x == Blanks
-// x == NonBlanks
+// x == Blanks
+// x == NonBlanks
//
// Excel also allows some simple string matching operations:
//
-// x == b* // begins with b
-// x != b* // doesnt begin with b
-// x == *b // ends with b
-// x != *b // doesnt end with b
-// x == *b* // contains b
-// x != *b* // doesn't contains b
+// x == b* // begins with b
+// x != b* // doesn't begin with b
+// x == *b // ends with b
+// x != *b // doesn't end with b
+// x == *b* // contains b
+// x != *b* // doesn't contain b
//
// You can also use '*' to match any character or number and '?' to match any
// single character or number. No other regular expression quantifier is
@@ -264,37 +467,28 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) {
// simple string. The actual placeholder name is ignored internally so the
// following are all equivalent:
//
-// x < 2000
-// col < 2000
-// Price < 2000
-//
-func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
- hcol, hrow, err := CellNameToCoordinates(hcell)
+// x < 2000
+// col < 2000
+// Price < 2000
+func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) error {
+ coordinates, err := rangeRefToCoordinates(rangeRef)
if err != nil {
return err
}
- vcol, vrow, err := CellNameToCoordinates(vcell)
+ _ = sortCoordinates(coordinates)
+ // Correct reference range, such correct C1:B3 to B1:C3.
+ ref, _ := coordinatesToRangeRef(coordinates, true)
+ wb, err := f.workbookReader()
if err != nil {
return err
}
-
- if vcol < hcol {
- vcol, hcol = hcol, vcol
- }
-
- if vrow < hrow {
- vrow, hrow = hrow, vrow
+ sheetID, err := f.GetSheetIndex(sheet)
+ if err != nil {
+ return err
}
-
- formatSet, _ := parseAutoFilterSet(format)
- cellStart, _ := CoordinatesToCellName(hcol, hrow)
- cellEnd, _ := CoordinatesToCellName(vcol, vrow)
- ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase"
- wb := f.workbookReader()
- sheetID := f.GetSheetIndex(sheet)
- filterRange := fmt.Sprintf("%s!%s", sheet, ref)
+ filterRange := fmt.Sprintf("'%s'!%s", sheet, ref)
d := xlsxDefinedName{
- Name: filterDB,
+ Name: builtInDefinedNames[3],
Hidden: true,
LocalSheetID: intPtr(sheetID),
Data: filterRange,
@@ -306,8 +500,11 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
} else {
var definedNameExists bool
for idx := range wb.DefinedNames.DefinedName {
- definedName := wb.DefinedNames.DefinedName[idx]
- if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden {
+ definedName, localSheetID := wb.DefinedNames.DefinedName[idx], 0
+ if definedName.LocalSheetID != nil {
+ localSheetID = *definedName.LocalSheetID
+ }
+ if definedName.Name == builtInDefinedNames[3] && localSheetID == sheetID && definedName.Hidden {
wb.DefinedNames.DefinedName[idx].Data = filterRange
definedNameExists = true
}
@@ -316,85 +513,84 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d)
}
}
- refRange := vcol - hcol
- return f.autoFilter(sheet, ref, refRange, hcol, formatSet)
+ columns := coordinates[2] - coordinates[0]
+ return f.autoFilter(sheet, ref, columns, coordinates[0], opts)
}
// autoFilter provides a function to extract the tokens from the filter
// expression. The tokens are mainly non-whitespace groups.
-func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *formatAutoFilter) error {
- xlsx, err := f.workSheetReader(sheet)
+func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilterOptions) error {
+ ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
- if xlsx.SheetPr != nil {
- xlsx.SheetPr.FilterMode = true
+ if ws.SheetPr != nil {
+ ws.SheetPr.FilterMode = true
}
- xlsx.SheetPr = &xlsxSheetPr{FilterMode: true}
+ ws.SheetPr = &xlsxSheetPr{FilterMode: true}
filter := &xlsxAutoFilter{
Ref: ref,
}
- xlsx.AutoFilter = filter
- if formatSet.Column == "" || formatSet.Expression == "" {
- return nil
- }
-
- fsCol, err := ColumnNameToNumber(formatSet.Column)
- if err != nil {
- return err
- }
- offset := fsCol - col
- if offset < 0 || offset > refRange {
- return fmt.Errorf("incorrect index of column '%s'", formatSet.Column)
- }
-
- filter.FilterColumn = &xlsxFilterColumn{
- ColID: offset,
- }
- re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
- token := re.FindAllString(formatSet.Expression, -1)
- if len(token) != 3 && len(token) != 7 {
- return fmt.Errorf("incorrect number of tokens in criteria '%s'", formatSet.Expression)
- }
- expressions, tokens, err := f.parseFilterExpression(formatSet.Expression, token)
- if err != nil {
- return err
+ ws.AutoFilter = filter
+ for _, opt := range opts {
+ if opt.Column == "" || opt.Expression == "" {
+ continue
+ }
+ fsCol, err := ColumnNameToNumber(opt.Column)
+ if err != nil {
+ return err
+ }
+ offset := fsCol - col
+ if offset < 0 || offset > columns {
+ return newInvalidAutoFilterColumnError(opt.Column)
+ }
+ fc := &xlsxFilterColumn{ColID: offset}
+ token := expressionFormat.FindAllString(opt.Expression, -1)
+ if len(token) != 3 && len(token) != 7 {
+ return newInvalidAutoFilterExpError(opt.Expression)
+ }
+ expressions, tokens, err := f.parseFilterExpression(opt.Expression, token)
+ if err != nil {
+ return err
+ }
+ f.writeAutoFilter(fc, expressions, tokens)
+ filter.FilterColumn = append(filter.FilterColumn, fc)
}
- f.writeAutoFilter(filter, expressions, tokens)
- xlsx.AutoFilter = filter
+ ws.AutoFilter = filter
return nil
}
// writeAutoFilter provides a function to check for single or double custom
// filters as default filters and handle them accordingly.
-func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) {
+func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string) {
if len(exp) == 1 && exp[0] == 2 {
// Single equality.
var filters []*xlsxFilter
filters = append(filters, &xlsxFilter{Val: tokens[0]})
- filter.FilterColumn.Filters = &xlsxFilters{Filter: filters}
- } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
+ fc.Filters = &xlsxFilters{Filter: filters}
+ return
+ }
+ if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
// Double equality with "or" operator.
- filters := []*xlsxFilter{}
+ var filters []*xlsxFilter
for _, v := range tokens {
filters = append(filters, &xlsxFilter{Val: v})
}
- filter.FilterColumn.Filters = &xlsxFilters{Filter: filters}
- } else {
- // Non default custom filter.
- expRel := map[int]int{0: 0, 1: 2}
- andRel := map[int]bool{0: true, 1: false}
- for k, v := range tokens {
- f.writeCustomFilter(filter, exp[expRel[k]], v)
- if k == 1 {
- filter.FilterColumn.CustomFilters.And = andRel[exp[k]]
- }
+ fc.Filters = &xlsxFilters{Filter: filters}
+ return
+ }
+ // Non default custom filter.
+ expRel, andRel := map[int]int{0: 0, 1: 2}, map[int]bool{0: true, 1: false}
+ for k, v := range tokens {
+ f.writeCustomFilter(fc, exp[expRel[k]], v)
+ if k == 1 {
+ fc.CustomFilters.And = andRel[exp[k]]
}
}
}
// writeCustomFilter provides a function to write the element.
-func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) {
+func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) {
operators := map[int]string{
1: "lessThan",
2: "equal",
@@ -408,13 +604,13 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin
Operator: operators[operator],
Val: val,
}
- if filter.FilterColumn.CustomFilters != nil {
- filter.FilterColumn.CustomFilters.CustomFilter = append(filter.FilterColumn.CustomFilters.CustomFilter, &customFilter)
- } else {
- customFilters := []*xlsxCustomFilter{}
- customFilters = append(customFilters, &customFilter)
- filter.FilterColumn.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
+ if fc.CustomFilters != nil {
+ fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter)
+ return
}
+ var customFilters []*xlsxCustomFilter
+ customFilters = append(customFilters, &customFilter)
+ fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
}
// parseFilterExpression provides a function to converts the tokens of a
@@ -423,22 +619,19 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin
//
// Examples:
//
-// ('x', '==', 2000) -> exp1
-// ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2
-//
+// ('x', '==', 2000) -> exp1
+// ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2
func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, []string, error) {
- expressions := []int{}
- t := []string{}
+ var expressions []int
+ var t []string
if len(tokens) == 7 {
// The number of tokens will be either 3 (for 1 expression) or 7 (for 2
// expressions).
- conditional := 0
- c := tokens[3]
- re, _ := regexp.Match(`(or|\|\|)`, []byte(c))
- if re {
+ conditional, c := 0, tokens[3]
+ if conditionFormat.MatchString(c) {
conditional = 1
}
- expression1, token1, err := f.parseFilterTokens(expression, tokens[0:3])
+ expression1, token1, err := f.parseFilterTokens(expression, tokens[:3])
if err != nil {
return expressions, t, err
}
@@ -446,17 +639,13 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int,
if err != nil {
return expressions, t, err
}
- expressions = []int{expression1[0], conditional, expression2[0]}
- t = []string{token1, token2}
- } else {
- exp, token, err := f.parseFilterTokens(expression, tokens)
- if err != nil {
- return expressions, t, err
- }
- expressions = exp
- t = []string{token}
+ return []int{expression1[0], conditional, expression2[0]}, []string{token1, token2}, nil
+ }
+ exp, token, err := f.parseFilterTokens(expression, tokens)
+ if err != nil {
+ return expressions, t, err
}
- return expressions, t, nil
+ return exp, []string{token}, nil
}
// parseFilterTokens provides a function to parse the 3 tokens of a filter
@@ -479,15 +668,15 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
operator, ok := operators[strings.ToLower(tokens[1])]
if !ok {
// Convert the operator from a number to a descriptive string.
- return []int{}, "", fmt.Errorf("unknown operator: %s", tokens[1])
+ return []int{}, "", newUnknownFilterTokenError(tokens[1])
}
token := tokens[2]
// Special handling for Blanks/NonBlanks.
- re, _ := regexp.Match("blanks|nonblanks", []byte(strings.ToLower(token)))
+ re := blankFormat.MatchString(strings.ToLower(token))
if re {
// Only allow Equals or NotEqual in this context.
if operator != 2 && operator != 5 {
- return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression)
+ return []int{operator}, token, newInvalidAutoFilterOperatorError(tokens[1], expression)
}
token = strings.ToLower(token)
// The operator should always be 2 (=) to flag a "simple" equality in
@@ -506,10 +695,9 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
}
}
}
- // if the string token contains an Excel match character then change the
+ // If the string token contains an Excel match character then change the
// operator type to indicate a non "simple" equality.
- re, _ = regexp.Match("[*?]", []byte(token))
- if operator == 2 && re {
+ if re = matchFormat.MatchString(token); operator == 2 && re {
operator = 22
}
return []int{operator}, token, nil
diff --git a/table_test.go b/table_test.go
index 95738e1375..8abfe1fb40 100644
--- a/table_test.go
+++ b/table_test.go
@@ -3,6 +3,7 @@ package excelize
import (
"fmt"
"path/filepath"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,122 +11,216 @@ import (
func TestAddTable(t *testing.T) {
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- err = f.AddTable("Sheet1", "B26", "A21", `{}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"}))
+ assert.NoError(t, f.AddTable("Sheet2", &Table{
+ Range: "A2:B5",
+ Name: "table",
+ StyleName: "TableStyleMedium2",
+ ShowColumnStripes: true,
+ ShowFirstColumn: true,
+ ShowLastColumn: true,
+ ShowRowStripes: boolPtr(true),
+ }))
+ assert.NoError(t, f.AddTable("Sheet2", &Table{
+ Range: "D1:D11",
+ ShowHeaderRow: boolPtr(false),
+ }))
+ assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"}))
+ // Test get tables in worksheet
+ tables, err := f.GetTables("Sheet2")
+ assert.Len(t, tables, 3)
+ assert.NoError(t, err)
+
+ // Test add table with already exist table name
+ assert.Equal(t, f.AddTable("Sheet2", &Table{Name: "Table1"}), ErrExistsTableName)
+ // Test add table with invalid table options
+ assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid)
+ // Test add table in not exist worksheet
+ assert.EqualError(t, f.AddTable("SheetN", &Table{Range: "B26:A21"}), "sheet SheetN does not exist")
+ // Test add table with illegal cell reference
+ assert.Equal(t, f.AddTable("Sheet1", &Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
+ assert.Equal(t, f.AddTable("Sheet1", &Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
- err = f.AddTable("Sheet2", "A2", "B5", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx")))
- err = f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`)
- if !assert.NoError(t, err) {
- t.FailNow()
+ // Test add table with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}))
+ // Test addTable with illegal cell reference
+ f = NewFile()
+ assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil))
+ assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil))
+ // Test set defined name and add table with invalid name
+ for _, cases := range []struct {
+ name string
+ err error
+ }{
+ {name: "1Table", err: newInvalidNameError("1Table")},
+ {name: "-Table", err: newInvalidNameError("-Table")},
+ {name: "'Table", err: newInvalidNameError("'Table")},
+ {name: "Table 1", err: newInvalidNameError("Table 1")},
+ {name: "A&B", err: newInvalidNameError("A&B")},
+ {name: "_1Table'", err: newInvalidNameError("_1Table'")},
+ {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")},
+ {name: strings.Repeat("c", MaxFieldLength+1), err: ErrNameLength},
+ } {
+ assert.Equal(t, cases.err, f.AddTable("Sheet1", &Table{
+ Range: "A1:B2",
+ Name: cases.name,
+ }))
+ assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{
+ Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5",
+ }))
}
+ // Test check duplicate table name with unsupported charset table parts
+ f = NewFile()
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
+ assert.NoError(t, f.Close())
+ f = NewFile()
+ // Test add table with workbook with single cells parts
+ f.Pkg.Store("xl/tables/tableSingleCells1.xml", []byte(""))
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
+ // Test add table with workbook with unsupported charset single cells parts
+ f.Pkg.Store("xl/tables/tableSingleCells1.xml", MacintoshCyrillicCharset)
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
+ assert.NoError(t, f.Close())
+}
- // Test add table in not exist worksheet.
- assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN is not exist")
- // Test add table with illegal formatset.
- assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string")
- // Test add table with illegal cell coordinates.
- assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
-
- assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx")))
+func TestGetTables(t *testing.T) {
+ f := NewFile()
+ // Test get tables in none table worksheet
+ tables, err := f.GetTables("Sheet1")
+ assert.Len(t, tables, 0)
+ assert.NoError(t, err)
+ // Test get tables in not exist worksheet
+ _, err = f.GetTables("SheetN")
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+ // Test adjust table with unsupported charset
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"}))
+ f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
+ _, err = f.GetTables("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test adjust table with no exist table parts
+ f.Pkg.Delete("xl/tables/table1.xml")
+ tables, err = f.GetTables("Sheet1")
+ assert.Len(t, tables, 0)
+ assert.NoError(t, err)
+}
- // Test addTable with illegal cell coordinates.
+func TestDeleteTable(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"}))
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21", Name: "Table2"}))
+ assert.NoError(t, f.DeleteTable("Table2"))
+ assert.NoError(t, f.DeleteTable("Table1"))
+ // Test delete table with invalid table name
+ assert.Equal(t, newInvalidNameError("Table 1"), f.DeleteTable("Table 1"))
+ // Test delete table with no exist table name
+ assert.Equal(t, newNoExistTableError("Table"), f.DeleteTable("Table"))
+ // Test delete table with unsupported charset
+ f.Sheet.Delete("xl/worksheets/sheet1.xml")
+ f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeleteTable("Table1"), "XML syntax error on line 1: invalid UTF-8")
+ // Test delete table without deleting table header
f = NewFile()
- assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell coordinates [0, 0]")
- assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]")
+ assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Date"))
+ assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Values"))
+ assert.NoError(t, f.UpdateLinkedValue())
+ assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2", Name: "Table1"}))
+ assert.NoError(t, f.DeleteTable("Table1"))
+ val, err := f.GetCellValue("Sheet1", "A1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Date", val)
+ val, err = f.GetCellValue("Sheet1", "B1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Values", val)
+}
+
+func TestSetTableColumns(t *testing.T) {
+ f := NewFile()
+ assert.Equal(t, newCoordinatesToCellNameError(1, 0), f.setTableColumns("Sheet1", true, 1, 0, 1, nil))
}
func TestAutoFilter(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilter%d.xlsx")
-
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- formats := []string{
- ``,
- `{"column":"B","expression":"x != blanks"}`,
- `{"column":"B","expression":"x == blanks"}`,
- `{"column":"B","expression":"x != nonblanks"}`,
- `{"column":"B","expression":"x == nonblanks"}`,
- `{"column":"B","expression":"x <= 1 and x >= 2"}`,
- `{"column":"B","expression":"x == 1 or x == 2"}`,
- `{"column":"B","expression":"x == 1 or x == 2*"}`,
- }
-
- for i, format := range formats {
+ assert.NoError(t, err)
+ for i, opts := range [][]AutoFilterOptions{
+ {},
+ {{Column: "B", Expression: ""}},
+ {{Column: "B", Expression: "x != blanks"}},
+ {{Column: "B", Expression: "x == blanks"}},
+ {{Column: "B", Expression: "x != nonblanks"}},
+ {{Column: "B", Expression: "x == nonblanks"}},
+ {{Column: "B", Expression: "x <= 1 and x >= 2"}},
+ {{Column: "B", Expression: "x == 1 or x == 2"}},
+ {{Column: "B", Expression: "x == 1 or x == 2*"}},
+ } {
t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) {
- err = f.AutoFilter("Sheet1", "D4", "B1", format)
- assert.NoError(t, err)
+ assert.NoError(t, f.AutoFilter("Sheet1", "D4:B1", opts))
assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1)))
})
}
- // testing AutoFilter with illegal cell coordinates.
- assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
- assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
+ // Test add auto filter with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.AutoFilter("Sheet:1", "A1:B1", nil))
+ // Test add auto filter with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil))
+ assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil))
+ // Test add auto filter with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8")
+ // Test add auto filter with empty local sheet ID
+ f = NewFile()
+ f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[3], Hidden: true}}}}
+ assert.NoError(t, f.AutoFilter("Sheet1", "A1:B1", nil))
}
func TestAutoFilterError(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx")
-
f, err := prepareTestBook1()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
-
- formats := []string{
- `{"column":"B","expression":"x <= 1 and x >= blanks"}`,
- `{"column":"B","expression":"x -- y or x == *2*"}`,
- `{"column":"B","expression":"x != y or x ? *2"}`,
- `{"column":"B","expression":"x -- y o r x == *2"}`,
- `{"column":"B","expression":"x -- y"}`,
- `{"column":"A","expression":"x -- y"}`,
- }
- for i, format := range formats {
+ assert.NoError(t, err)
+ for i, opts := range [][]AutoFilterOptions{
+ {{Column: "B", Expression: "x <= 1 and x >= blanks"}},
+ {{Column: "B", Expression: "x -- y or x == *2*"}},
+ {{Column: "B", Expression: "x != y or x ? *2"}},
+ {{Column: "B", Expression: "x -- y o r x == *2"}},
+ {{Column: "B", Expression: "x -- y"}},
+ {{Column: "A", Expression: "x -- y"}},
+ } {
t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) {
- err = f.AutoFilter("Sheet2", "D4", "B1", format)
- if assert.Error(t, err) {
+ if assert.Error(t, f.AutoFilter("Sheet2", "D4:B1", opts)) {
assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1)))
}
})
}
- assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &formatAutoFilter{
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{
Column: "A",
Expression: "",
- }), "sheet SheetN is not exist")
- assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{
+ }}))
+ assert.Equal(t, newInvalidColumnNameError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "-",
Expression: "-",
- }), `invalid column name "-"`)
- assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &formatAutoFilter{
+ }}))
+ assert.Equal(t, newInvalidAutoFilterColumnError("A"), f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
Column: "A",
Expression: "-",
- }), `incorrect index of column 'A'`)
- assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{
+ }}))
+ assert.Equal(t, newInvalidAutoFilterExpError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "A",
Expression: "-",
- }), `incorrect number of tokens in criteria '-'`)
+ }}))
}
func TestParseFilterTokens(t *testing.T) {
f := NewFile()
- // Test with unknown operator.
+ // Test with unknown operator
_, _, err := f.parseFilterTokens("", []string{"", "!"})
assert.EqualError(t, err, "unknown operator: !")
- // Test invalid operator in context.
+ // Test invalid operator in context
_, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"})
- assert.EqualError(t, err, "the operator '<' in expression '' is not valid in relation to Blanks/NonBlanks'")
+ assert.Equal(t, newInvalidAutoFilterOperatorError("<", ""), err)
}
diff --git a/templates.go b/templates.go
index 5721150ce8..b8cf159131 100644
--- a/templates.go
+++ b/templates.go
@@ -1,28 +1,522 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
//
// This file contains default templates for XML files we don't yet populated
// based on content.
package excelize
-// XMLHeader define an XML declaration can also contain a standalone declaration.
-const XMLHeader = "\n"
+import "encoding/xml"
+// Source relationship and namespace list, associated prefixes and schema in which it was
+// introduced.
var (
- // XMLHeaderByte define an XML declaration can also contain a standalone
- // declaration.
- XMLHeaderByte = []byte(XMLHeader)
+ NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"}
+ NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
+ NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
+ NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"}
+ NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
+ NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"}
+ NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"}
+ NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
+ NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
+ NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
+ NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
+ NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
+ NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
+ NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"}
+ SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
+ SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
+ SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
+ SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"}
+ SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
)
+// Source relationship and namespace.
+const (
+ ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
+ ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
+ ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
+ ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
+ ContentTypeRelationships = "application/vnd.openxmlformats-package.relationships+xml"
+ ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
+ ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml"
+ ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+xml"
+ ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
+ ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
+ ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
+ ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
+ ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
+ ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
+ ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
+ ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
+ ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
+ ContentTypeVBA = "application/vnd.ms-office.vbaProject"
+ ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
+ NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
+ NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
+ NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
+ NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
+ NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
+ NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
+ NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
+ SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
+ SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
+ SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+ SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
+ SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
+ SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
+ SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
+ SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
+ SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
+ SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
+ SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
+ SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
+ SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
+ SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer"
+ SourceRelationshipSlicerCache = "http://schemas.microsoft.com/office/2007/relationships/slicerCache"
+ SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
+ SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
+ SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
+ StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
+ StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
+ StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
+ StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
+ StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
+ StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
+ StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
+ StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties"
+ StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
+ StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
+ // The following constants defined the extLst child element
+ // ([ISO/IEC29500-1:2016] section 18.2.10) of the workbook and worksheet
+ // elements extended by the addition of new child ext elements.
+ ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"
+ ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
+ ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
+ ExtURIDataField = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}"
+ ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}"
+ ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
+ ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
+ ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}"
+ ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
+ ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
+ ExtURIModelTimeGroupings = "{9835A34E-60A6-4A7C-AAB8-D5F71C897F49}"
+ ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}"
+ ExtURIPivotCachesX14 = "{876F7934-8845-4945-9796-88D515C7AA90}"
+ ExtURIPivotCachesX15 = "{841E416B-1EF1-43b6-AB56-02D37102CBD5}"
+ ExtURIPivotField = "{2946ED86-A175-432a-8AC1-64E0C546D7DE}"
+ ExtURIPivotFilter = "{0605FD5F-26C8-4aeb-8148-2DB25E43C511}"
+ ExtURIPivotHierarchy = "{F1805F06-0CD304483-9156-8803C3D141DF}"
+ ExtURIPivotTableReferences = "{983426D0-5260-488c-9760-48F4B6AC55F4}"
+ ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
+ ExtURISlicerCacheDefinition = "{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}"
+ ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}"
+ ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
+ ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}"
+ ExtURISlicerListX14 = "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"
+ ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
+ ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
+ ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
+ ExtURITimelineCachePivotCaches = "{A2CB5862-8E78-49c6-8D9D-AF26E26ADB89}"
+ ExtURITimelineCacheRefs = "{D0CA8CA8-9F24-4464-BF8E-62219DCF47F9}"
+ ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
+ ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
+ ExtURIWorkbookPrX14 = "{79F54976-1DA5-4618-B147-ACDE4B953A38}"
+ ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}"
+)
+
+// workbookExtURIPriority is the priority of URI in the workbook extension lists.
+var workbookExtURIPriority = []string{
+ ExtURIPivotCachesX14,
+ ExtURISlicerCachesX14,
+ ExtURISlicerCachesX15,
+ ExtURIWorkbookPrX14,
+ ExtURIPivotCachesX15,
+ ExtURIPivotTableReferences,
+ ExtURITimelineCachePivotCaches,
+ ExtURITimelineCacheRefs,
+ ExtURIWorkbookPrX15,
+ ExtURIDataModel,
+ ExtURICalcFeatures,
+ ExtURIExternalLinkPr,
+ ExtURIModelTimeGroupings,
+}
+
+// worksheetExtURIPriority is the priority of URI in the worksheet extension lists.
+var worksheetExtURIPriority = []string{
+ ExtURIConditionalFormattings,
+ ExtURIDataValidations,
+ ExtURISparklineGroups,
+ ExtURISlicerListX14,
+ ExtURIProtectedRanges,
+ ExtURIIgnoredErrors,
+ ExtURIWebExtensions,
+ ExtURISlicerListX15,
+ ExtURITimelineRefs,
+ ExtURIExternalLinkPr,
+}
+
+// Excel specifications and limits
+const (
+ MaxCellStyles = 65430
+ MaxColumns = 16384
+ MaxColumnWidth = 255
+ MaxFieldLength = 255
+ MaxFilePathLength = 207
+ MaxFormControlValue = 30000
+ MaxFontFamilyLength = 31
+ MaxFontSize = 409
+ MaxRowHeight = 409
+ MaxSheetNameLength = 31
+ MinColumns = 1
+ MinFontSize = 1
+ StreamChunkSize = 1 << 24
+ TotalCellChars = 32767
+ TotalRows = 1048576
+ TotalSheetHyperlinks = 65529
+ UnzipSizeLimit = 1000 << 24
+ // pivotTableVersion should be greater than 3. One or more of the
+ // PivotTables chosen are created in a version of Excel earlier than
+ // Excel 2007 or in compatibility mode. Slicer can only be used with
+ // PivotTables created in Excel 2007 or a newer version of Excel.
+ pivotTableVersion = 3
+ pivotTableRefreshedVersion = 8
+ defaultDrawingScale = 1.0
+ defaultChartDimensionWidth = 480
+ defaultChartDimensionHeight = 260
+ defaultSlicerWidth = 200
+ defaultSlicerHeight = 200
+ defaultChartLegendPosition = "bottom"
+ defaultChartShowBlanksAs = "gap"
+ defaultShapeSize = 160
+ defaultShapeLineWidth = 1
+)
+
+// ColorMappingType is the type of color transformation.
+type ColorMappingType byte
+
+// Color transformation types enumeration.
+const (
+ ColorMappingTypeLight1 ColorMappingType = iota
+ ColorMappingTypeDark1
+ ColorMappingTypeLight2
+ ColorMappingTypeDark2
+ ColorMappingTypeAccent1
+ ColorMappingTypeAccent2
+ ColorMappingTypeAccent3
+ ColorMappingTypeAccent4
+ ColorMappingTypeAccent5
+ ColorMappingTypeAccent6
+ ColorMappingTypeHyperlink
+ ColorMappingTypeFollowedHyperlink
+ ColorMappingTypeUnset int = -1
+)
+
+// ChartDataLabelPositionType is the type of chart data labels position.
+type ChartDataLabelPositionType byte
+
+// Chart data labels positions types enumeration.
+const (
+ ChartDataLabelsPositionUnset ChartDataLabelPositionType = iota
+ ChartDataLabelsPositionBestFit
+ ChartDataLabelsPositionBelow
+ ChartDataLabelsPositionCenter
+ ChartDataLabelsPositionInsideBase
+ ChartDataLabelsPositionInsideEnd
+ ChartDataLabelsPositionLeft
+ ChartDataLabelsPositionOutsideEnd
+ ChartDataLabelsPositionRight
+ ChartDataLabelsPositionAbove
+)
+
+// chartDataLabelsPositionTypes defined supported chart data labels position
+// types.
+var chartDataLabelsPositionTypes = map[ChartDataLabelPositionType]string{
+ ChartDataLabelsPositionBestFit: "bestFit",
+ ChartDataLabelsPositionBelow: "b",
+ ChartDataLabelsPositionCenter: "ctr",
+ ChartDataLabelsPositionInsideBase: "inBase",
+ ChartDataLabelsPositionInsideEnd: "inEnd",
+ ChartDataLabelsPositionLeft: "l",
+ ChartDataLabelsPositionOutsideEnd: "outEnd",
+ ChartDataLabelsPositionRight: "r",
+ ChartDataLabelsPositionAbove: "t",
+}
+
+// supportedChartDataLabelsPosition defined supported chart data labels position
+// types for each type of chart.
+var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionType{
+ Bar: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
+ BarStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
+ BarPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
+ Col: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
+ ColStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
+ ColPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
+ Line: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
+ Pie: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
+ Pie3D: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
+ Scatter: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
+ Bubble: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
+ Bubble3D: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
+}
+
+const (
+ defaultTempFileSST = "sharedStrings"
+ defaultXMLMetadata = "xl/metadata.xml"
+ defaultXMLPathCalcChain = "xl/calcChain.xml"
+ defaultXMLPathCellImages = "xl/cellimages.xml"
+ defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
+ defaultXMLPathContentTypes = "[Content_Types].xml"
+ defaultXMLPathDocPropsApp = "docProps/app.xml"
+ defaultXMLPathDocPropsCore = "docProps/core.xml"
+ defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
+ defaultXMLPathStyles = "xl/styles.xml"
+ defaultXMLPathTheme = "xl/theme/theme1.xml"
+ defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
+ defaultXMLPathWorkbook = "xl/workbook.xml"
+ defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
+ defaultXMLRdRichValuePart = "xl/richData/rdrichvalue.xml"
+ defaultXMLRdRichValueRel = "xl/richData/richValueRel.xml"
+ defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels"
+ defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml"
+ defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels"
+)
+
+// IndexedColorMapping is the table of default mappings from indexed color value
+// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards
+// compatibility. A legacy indexing scheme for colors that is still required
+// for some records, and for backwards compatibility with legacy formats. This
+// element contains a sequence of RGB color values that correspond to color
+// indexes (zero-based). When using the default indexed color palette, the
+// values are not written out, but instead are implied. When the color palette
+// has been modified from default, then the entire color palette is written
+// out.
+var IndexedColorMapping = []string{
+ "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
+ "000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
+ "800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080",
+ "9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF",
+ "000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF",
+ "00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99",
+ "3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696",
+ "003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333",
+ "000000", "FFFFFF",
+}
+
+// supportedDefinedNameAtStartCharCodeRange list the valid first character of a
+// defined name ASCII letters.
+var supportedDefinedNameAtStartCharCodeRange = []int{
+ 65, 90, 92, 92, 95, 95, 97, 122, 161, 161, 164, 164,
+ 167, 168, 170, 170, 173, 173, 175, 186, 188, 696, 699, 705,
+ 711, 711, 713, 715, 717, 717, 720, 721, 728, 731, 733, 733,
+ 736, 740, 750, 750, 880, 883, 886, 887, 890, 893, 902, 902,
+ 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1315,
+ 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1610,
+ 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788,
+ 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026,
+ 2036, 2037, 2042, 2042, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401,
+ 2417, 2418, 2427, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480,
+ 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
+ 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611,
+ 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701,
+ 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749,
+ 2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864,
+ 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
+ 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
+ 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084,
+ 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161,
+ 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
+ 3261, 3261, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368,
+ 3370, 3385, 3389, 3389, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505,
+ 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3642, 3648, 3662, 3713, 3714,
+ 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
+ 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763,
+ 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911,
+ 3913, 3948, 3976, 3979, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189,
+ 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293,
+ 4304, 4346, 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680,
+ 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749,
+ 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822,
+ 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
+ 5743, 5750, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905,
+ 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103,
+ 6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6400, 6428, 6480, 6509,
+ 6512, 6516, 6528, 6569, 6593, 6599, 6656, 6678, 6917, 6963, 6981, 6987,
+ 7043, 7072, 7086, 7087, 7168, 7203, 7245, 7247, 7258, 7293, 7424, 7615,
+ 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025,
+ 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126,
+ 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180,
+ 8182, 8188, 8208, 8208, 8211, 8214, 8216, 8216, 8220, 8221, 8224, 8225,
+ 8229, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8305, 8305,
+ 8308, 8308, 8319, 8319, 8321, 8324, 8336, 8340, 8450, 8451, 8453, 8453,
+ 8455, 8455, 8457, 8467, 8469, 8470, 8473, 8477, 8481, 8482, 8484, 8484,
+ 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
+ 8526, 8526, 8531, 8532, 8539, 8542, 8544, 8584, 8592, 8601, 8658, 8658,
+ 8660, 8660, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719,
+ 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741,
+ 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780,
+ 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835,
+ 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978,
+ 9312, 9397, 9424, 9449, 9472, 9547, 9552, 9588, 9601, 9615, 9618, 9621,
+ 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665,
+ 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734,
+ 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794,
+ 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 11264, 11310,
+ 11312, 11358, 11360, 11375, 11377, 11389, 11392, 11492, 11520, 11557, 11568, 11621,
+ 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710,
+ 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12288, 12291, 12293, 12311,
+ 12317, 12319, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447,
+ 12449, 12543, 12549, 12589, 12593, 12686, 12704, 12727, 12784, 12828, 12832, 12841,
+ 12849, 12850, 12857, 12857, 12896, 12923, 12927, 12927, 12963, 12968, 13059, 13059,
+ 13069, 13069, 13076, 13076, 13080, 13080, 13090, 13091, 13094, 13095, 13099, 13099,
+ 13110, 13110, 13115, 13115, 13129, 13130, 13133, 13133, 13137, 13137, 13143, 13143,
+ 13179, 13182, 13184, 13188, 13192, 13258, 13261, 13267, 13269, 13270, 13272, 13272,
+ 13275, 13277, 13312, 19893, 19968, 40899, 40960, 42124, 42240, 42508, 42512, 42527,
+ 42538, 42539, 42560, 42591, 42594, 42606, 42624, 42647, 42786, 42887, 42891, 42892,
+ 43003, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187,
+ 43274, 43301, 43312, 43334, 43520, 43560, 43584, 43586, 43588, 43595, 44032, 55203,
+ 57344, 63560, 63744, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
+ 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321,
+ 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
+ 65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
+ 65136, 65140, 65142, 65276, 65281, 65374, 65377, 65470, 65474, 65479, 65482, 65487,
+ 65490, 65495, 65498, 65500, 65504, 65510,
+}
+
+// supportedDefinedNameAfterStartCharCodeRange list the valid after first
+// character of a defined name ASCII letters.
+var supportedDefinedNameAfterStartCharCodeRange = []int{
+ 46, 46, 48, 57, 63, 63, 65, 90, 92, 92, 95, 95,
+ 97, 122, 161, 161, 164, 164, 167, 168, 170, 170, 173, 173,
+ 175, 186, 188, 887, 890, 893, 900, 902, 904, 906, 908, 908,
+ 910, 929, 931, 1315, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469,
+ 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522,
+ 1536, 1539, 1542, 1544, 1547, 1547, 1550, 1562, 1567, 1567, 1569, 1630,
+ 1632, 1641, 1646, 1747, 1749, 1791, 1807, 1866, 1869, 1969, 1984, 2038,
+ 2042, 2042, 2305, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415,
+ 2417, 2418, 2427, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472,
+ 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510,
+ 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2554, 2561, 2563, 2565, 2570,
+ 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617,
+ 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652,
+ 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728,
+ 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765,
+ 2768, 2768, 2784, 2787, 2790, 2799, 2801, 2801, 2817, 2819, 2821, 2828,
+ 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884,
+ 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2929,
+ 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
+ 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016,
+ 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3066, 3073, 3075, 3077, 3084,
+ 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144,
+ 3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3192, 3199,
+ 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
+ 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299,
+ 3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368,
+ 3370, 3385, 3389, 3396, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3427,
+ 3430, 3445, 3449, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515,
+ 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551,
+ 3570, 3571, 3585, 3642, 3647, 3662, 3664, 3673, 3713, 3714, 3716, 3716,
+ 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747,
+ 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780,
+ 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3843, 3859, 3897,
+ 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028,
+ 4030, 4044, 4046, 4047, 4096, 4169, 4176, 4249, 4254, 4293, 4304, 4346,
+ 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680, 4682, 4685,
+ 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784,
+ 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
+ 4882, 4885, 4888, 4954, 4959, 4960, 4969, 4988, 4992, 5017, 5024, 5108,
+ 5121, 5740, 5743, 5750, 5760, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
+ 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003,
+ 6016, 6099, 6103, 6103, 6107, 6109, 6112, 6121, 6128, 6137, 6155, 6158,
+ 6160, 6169, 6176, 6263, 6272, 6314, 6400, 6428, 6432, 6443, 6448, 6459,
+ 6464, 6464, 6470, 6509, 6512, 6516, 6528, 6569, 6576, 6601, 6608, 6617,
+ 6624, 6683, 6912, 6987, 6992, 7001, 7009, 7036, 7040, 7082, 7086, 7097,
+ 7168, 7223, 7232, 7241, 7245, 7293, 7424, 7654, 7678, 7957, 7960, 7965,
+ 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029,
+ 8031, 8061, 8064, 8116, 8118, 8132, 8134, 8147, 8150, 8155, 8157, 8175,
+ 8178, 8180, 8182, 8190, 8192, 8208, 8211, 8214, 8216, 8216, 8220, 8221,
+ 8224, 8225, 8229, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8260, 8260,
+ 8274, 8274, 8287, 8292, 8298, 8305, 8308, 8316, 8319, 8332, 8336, 8340,
+ 8352, 8373, 8400, 8432, 8448, 8527, 8531, 8584, 8592, 9000, 9003, 9191,
+ 9216, 9254, 9280, 9290, 9312, 9885, 9888, 9916, 9920, 9923, 9985, 9988,
+ 9990, 9993, 9996, 10023, 10025, 10059, 10061, 10061, 10063, 10066, 10070, 10070,
+ 10072, 10078, 10081, 10087, 10102, 10132, 10136, 10159, 10161, 10174, 10176, 10180,
+ 10183, 10186, 10188, 10188, 10192, 10213, 10224, 10626, 10649, 10711, 10716, 10747,
+ 10750, 11084, 11088, 11092, 11264, 11310, 11312, 11358, 11360, 11375, 11377, 11389,
+ 11392, 11498, 11517, 11517, 11520, 11557, 11568, 11621, 11631, 11631, 11648, 11670,
+ 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
+ 11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 11904, 11929, 11931, 12019,
+ 12032, 12245, 12272, 12283, 12288, 12311, 12317, 12335, 12337, 12348, 12350, 12351,
+ 12353, 12438, 12441, 12447, 12449, 12543, 12549, 12589, 12593, 12686, 12688, 12727,
+ 12736, 12771, 12784, 12830, 12832, 12867, 12880, 13054, 13056, 19893, 19904, 40899,
+ 40960, 42124, 42128, 42182, 42240, 42508, 42512, 42539, 42560, 42591, 42594, 42610,
+ 42620, 42621, 42623, 42647, 42752, 42892, 43003, 43051, 43072, 43123, 43136, 43204,
+ 43216, 43225, 43264, 43310, 43312, 43347, 43520, 43574, 43584, 43597, 43600, 43609,
+ 44032, 55203, 55296, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
+ 64285, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
+ 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65021, 65024, 65039, 65056, 65062,
+ 65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
+ 65136, 65140, 65142, 65276, 65279, 65279, 65281, 65374, 65377, 65470, 65474, 65479,
+ 65482, 65487, 65490, 65495, 65498, 65500, 65504, 65510, 65512, 65518, 65529, 65533,
+}
+
+// supportedImageTypes defined supported image types.
+var supportedImageTypes = map[string]string{
+ ".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif",
+ ".jpeg": ".jpeg", ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg",
+ ".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
+}
+
+// supportedCalcMode defined supported formula calculate mode.
+var supportedCalcMode = []string{"manual", "auto", "autoNoTable"}
+
+// supportedRefMode defined supported formula calculate mode.
+var supportedRefMode = []string{"A1", "R1C1"}
+
+// supportedContentTypes defined supported file format types.
+var supportedContentTypes = map[string]string{
+ ".xlam": ContentTypeAddinMacro,
+ ".xlsm": ContentTypeMacro,
+ ".xlsx": ContentTypeSheetML,
+ ".xltm": ContentTypeTemplateMacro,
+ ".xltx": ContentTypeTemplate,
+}
+
+// supportedUnderlineTypes defined supported underline types.
+var supportedUnderlineTypes = []string{"none", "single", "double"}
+
+// supportedDrawingUnderlineTypes defined supported underline types in drawing
+// markup language.
+var supportedDrawingUnderlineTypes = []string{
+ "none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy",
+ "wavyDbl",
+}
+
+// supportedDrawingTextVerticalType defined supported text vertical types in
+// drawing markup language.
+var supportedDrawingTextVerticalType = []string{"horz", "vert", "vert270", "wordArtVert", "eaVert", "mongolianVert", "wordArtVertRtl"}
+
+// supportedPositioning defined supported positioning types.
+var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
+
+// supportedPageOrientation defined supported page setup page orientation.
+var supportedPageOrientation = []string{"portrait", "landscape"}
+
+// supportedPageOrder defined supported page setup page order.
+var supportedPageOrder = []string{"overThenDown", "downThenOver"}
+
+// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix.
+var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"}
+
const templateDocpropsApp = `0Go Excelize`
const templateContentTypes = ``
diff --git a/test/Book1.xlsx b/test/Book1.xlsx
index d5a059121b..ed3e292954 100644
Binary files a/test/Book1.xlsx and b/test/Book1.xlsx differ
diff --git a/test/encryptAES.xlsx b/test/encryptAES.xlsx
new file mode 100644
index 0000000000..27a595adbc
Binary files /dev/null and b/test/encryptAES.xlsx differ
diff --git a/test/encryptSHA1.xlsx b/test/encryptSHA1.xlsx
new file mode 100644
index 0000000000..e7bc852fb7
Binary files /dev/null and b/test/encryptSHA1.xlsx differ
diff --git a/test/images/excel.bmp b/test/images/excel.bmp
new file mode 100755
index 0000000000..cbd3691abc
Binary files /dev/null and b/test/images/excel.bmp differ
diff --git a/test/images/excel.emf b/test/images/excel.emf
new file mode 100644
index 0000000000..9daa6de8b2
Binary files /dev/null and b/test/images/excel.emf differ
diff --git a/test/images/excel.emz b/test/images/excel.emz
new file mode 100644
index 0000000000..bc9480153a
Binary files /dev/null and b/test/images/excel.emz differ
diff --git a/test/images/excel.wmf b/test/images/excel.wmf
new file mode 100644
index 0000000000..fd588c66f9
Binary files /dev/null and b/test/images/excel.wmf differ
diff --git a/test/images/excel.wmz b/test/images/excel.wmz
new file mode 100644
index 0000000000..d608968074
Binary files /dev/null and b/test/images/excel.wmz differ
diff --git a/test/vbaProject.bin b/test/vbaProject.bin
old mode 100755
new mode 100644
index fc15dca28e..77b6bf8292
Binary files a/test/vbaProject.bin and b/test/vbaProject.bin differ
diff --git a/vml.go b/vml.go
new file mode 100644
index 0000000000..7ea3e22a68
--- /dev/null
+++ b/vml.go
@@ -0,0 +1,1220 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+// FormControlType is the type of supported form controls.
+type FormControlType byte
+
+// This section defines the currently supported form control types enumeration.
+const (
+ FormControlNote FormControlType = iota
+ FormControlButton
+ FormControlOptionButton
+ FormControlSpinButton
+ FormControlCheckBox
+ FormControlGroupBox
+ FormControlLabel
+ FormControlScrollBar
+)
+
+// HeaderFooterImagePositionType is the type of header and footer image position.
+type HeaderFooterImagePositionType byte
+
+// Worksheet header and footer image position types enumeration.
+const (
+ HeaderFooterImagePositionLeft HeaderFooterImagePositionType = iota
+ HeaderFooterImagePositionCenter
+ HeaderFooterImagePositionRight
+)
+
+// GetComments retrieves all comments in a worksheet by given worksheet name.
+func (f *File) GetComments(sheet string) ([]Comment, error) {
+ var comments []Comment
+ sheetXMLPath, ok := f.getSheetXMLPath(sheet)
+ if !ok {
+ return comments, ErrSheetNotExist{sheet}
+ }
+ commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
+ if !strings.HasPrefix(commentsXML, "/") {
+ commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
+ }
+ commentsXML = strings.TrimPrefix(commentsXML, "/")
+ cmts, err := f.commentsReader(commentsXML)
+ if err != nil {
+ return comments, err
+ }
+ if cmts != nil {
+ for _, cmt := range cmts.CommentList.Comment {
+ comment := Comment{}
+ if cmt.AuthorID < len(cmts.Authors.Author) {
+ comment.Author = cmts.Authors.Author[cmt.AuthorID]
+ }
+ comment.Cell = cmt.Ref
+ comment.AuthorID = cmt.AuthorID
+ if cmt.Text.T != nil {
+ comment.Text += *cmt.Text.T
+ }
+ for _, text := range cmt.Text.R {
+ if text.T != nil {
+ run := RichTextRun{Text: text.T.Val}
+ if text.RPr != nil {
+ run.Font = newFont(text.RPr)
+ }
+ comment.Paragraph = append(comment.Paragraph, run)
+ }
+ }
+ comments = append(comments, comment)
+ }
+ }
+ return comments, nil
+}
+
+// getSheetComments provides the method to get the target comment reference by
+// given worksheet file path.
+func (f *File) getSheetComments(sheetFile string) string {
+ rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels")
+ if sheetRels := rels; sheetRels != nil {
+ sheetRels.mu.Lock()
+ defer sheetRels.mu.Unlock()
+ for _, v := range sheetRels.Relationships {
+ if v.Type == SourceRelationshipComments {
+ return v.Target
+ }
+ }
+ }
+ return ""
+}
+
+// AddComment provides the method to add comments in a sheet by giving the
+// worksheet name, cell reference, and format set (such as author and text).
+// Note that the maximum author name length is 255 and the max text length is
+// 32512. For example, add a rich-text comment with a specified comments box
+// size in Sheet1!A5:
+//
+// err := f.AddComment("Sheet1", excelize.Comment{
+// Cell: "A5",
+// Author: "Excelize",
+// Paragraph: []excelize.RichTextRun{
+// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}},
+// {Text: "This is a comment."},
+// },
+// Height: 40,
+// Width: 180,
+// })
+func (f *File) AddComment(sheet string, opts Comment) error {
+ return f.addVMLObject(vmlOptions{
+ sheet: sheet, Comment: opts,
+ FormControl: FormControl{
+ Cell: opts.Cell,
+ Type: FormControlNote,
+ Text: opts.Text,
+ Paragraph: opts.Paragraph,
+ Width: opts.Width,
+ Height: opts.Height,
+ },
+ })
+}
+
+// DeleteComment provides the method to delete comment in a worksheet by given
+// worksheet name and cell reference. For example, delete the comment in
+// Sheet1!$A$30:
+//
+// err := f.DeleteComment("Sheet1", "A30")
+func (f *File) DeleteComment(sheet, cell string) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ if ws.LegacyDrawing == nil {
+ return err
+ }
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
+ if !strings.HasPrefix(commentsXML, "/") {
+ commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
+ }
+ commentsXML = strings.TrimPrefix(commentsXML, "/")
+ cmts, err := f.commentsReader(commentsXML)
+ if err != nil {
+ return err
+ }
+ if cmts != nil {
+ for i := 0; i < len(cmts.CommentList.Comment); i++ {
+ cmt := cmts.CommentList.Comment[i]
+ if cmt.Ref != cell {
+ continue
+ }
+ if len(cmts.CommentList.Comment) > 1 {
+ cmts.CommentList.Comment = append(
+ cmts.CommentList.Comment[:i],
+ cmts.CommentList.Comment[i+1:]...,
+ )
+ i--
+ continue
+ }
+ cmts.CommentList.Comment = nil
+ }
+ f.Comments[commentsXML] = cmts
+ }
+ sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
+ return f.deleteFormControl(sheetRelationshipsDrawingVML, cell, true)
+}
+
+// deleteFormControl provides the method to delete shape from
+// xl/drawings/vmlDrawing%d.xml by giving path, cell and shape type.
+func (f *File) deleteFormControl(sheetRelationshipsDrawingVML, cell string, isComment bool) error {
+ col, row, err := CellNameToCoordinates(cell)
+ if err != nil {
+ return err
+ }
+ vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
+ drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
+ vml := f.VMLDrawing[drawingVML]
+ if vml == nil {
+ vml = &vmlDrawing{
+ XMLNSv: "urn:schemas-microsoft-com:vml",
+ XMLNSo: "urn:schemas-microsoft-com:office:office",
+ XMLNSx: "urn:schemas-microsoft-com:office:excel",
+ XMLNSmv: "http://macVmlSchemaUri",
+ ShapeLayout: &xlsxShapeLayout{
+ Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID},
+ },
+ ShapeType: &xlsxShapeType{
+ Stroke: &xlsxStroke{JoinStyle: "miter"},
+ VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"},
+ },
+ }
+ // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml
+ d, err := f.decodeVMLDrawingReader(drawingVML)
+ if err != nil {
+ return err
+ }
+ if d != nil {
+ vml.ShapeType.ID = d.ShapeType.ID
+ vml.ShapeType.CoordSize = d.ShapeType.CoordSize
+ vml.ShapeType.Spt = d.ShapeType.Spt
+ vml.ShapeType.Path = d.ShapeType.Path
+ for _, v := range d.Shape {
+ s := xlsxShape{
+ ID: v.ID,
+ Type: v.Type,
+ Style: v.Style,
+ Button: v.Button,
+ Filled: v.Filled,
+ FillColor: v.FillColor,
+ InsetMode: v.InsetMode,
+ Stroked: v.Stroked,
+ StrokeColor: v.StrokeColor,
+ Val: v.Val,
+ }
+ vml.Shape = append(vml.Shape, s)
+ }
+ }
+ }
+ cond := func(objectType string) bool {
+ if isComment {
+ return objectType == "Note"
+ }
+ return objectType != "Note"
+ }
+ for i, sp := range vml.Shape {
+ var shapeVal decodeShapeVal
+ if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil &&
+ cond(shapeVal.ClientData.ObjectType) && shapeVal.ClientData.Anchor != "" {
+ leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor)
+ if err != nil {
+ return err
+ }
+ if leftCol == col-1 && topRow == row-1 {
+ vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
+ break
+ }
+ }
+ }
+ f.VMLDrawing[drawingVML] = vml
+ return err
+}
+
+// addComment provides a function to create chart as xl/comments%d.xml by
+// given cell and format sets.
+func (f *File) addComment(commentsXML string, opts vmlOptions) error {
+ if opts.Author == "" {
+ opts.Author = "Author"
+ }
+ if len(opts.Author) > MaxFieldLength {
+ opts.Author = opts.Author[:MaxFieldLength]
+ }
+ cmts, err := f.commentsReader(commentsXML)
+ if err != nil {
+ return err
+ }
+ var authorID int
+ if cmts == nil {
+ cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}}
+ }
+ if inStrSlice(cmts.Authors.Author, opts.Author, true) == -1 {
+ cmts.Authors.Author = append(cmts.Authors.Author, opts.Author)
+ authorID = len(cmts.Authors.Author) - 1
+ }
+ defaultFont, err := f.GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ chars, cmt := 0, xlsxComment{
+ Ref: opts.Comment.Cell,
+ AuthorID: authorID,
+ Text: xlsxText{R: []xlsxR{}},
+ }
+ if opts.Comment.Text != "" {
+ if len(opts.Comment.Text) > TotalCellChars {
+ opts.Comment.Text = opts.Comment.Text[:TotalCellChars]
+ }
+ cmt.Text.T = stringPtr(opts.Comment.Text)
+ chars += len(opts.Comment.Text)
+ }
+ for _, run := range opts.Comment.Paragraph {
+ if chars == TotalCellChars {
+ break
+ }
+ if chars+len(run.Text) > TotalCellChars {
+ run.Text = run.Text[:TotalCellChars-chars]
+ }
+ chars += len(run.Text)
+ r := xlsxR{
+ RPr: &xlsxRPr{
+ Sz: &attrValFloat{Val: float64Ptr(9)},
+ Color: &xlsxColor{
+ Indexed: 81,
+ },
+ RFont: &attrValString{Val: stringPtr(defaultFont)},
+ Family: &attrValInt{Val: intPtr(2)},
+ },
+ T: &xlsxT{Val: run.Text, Space: xml.Attr{
+ Name: xml.Name{Space: NameSpaceXML, Local: "space"},
+ Value: "preserve",
+ }},
+ }
+ if run.Font != nil {
+ r.RPr = newRpr(run.Font)
+ }
+ cmt.Text.R = append(cmt.Text.R, r)
+ }
+ cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt)
+ f.Comments[commentsXML] = cmts
+ return err
+}
+
+// countComments provides a function to get comments files count storage in
+// the folder xl.
+func (f *File) countComments() int {
+ comments := map[string]struct{}{}
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/comments") {
+ comments[k.(string)] = struct{}{}
+ }
+ return true
+ })
+ for rel := range f.Comments {
+ if strings.Contains(rel, "xl/comments") {
+ comments[rel] = struct{}{}
+ }
+ }
+ return len(comments)
+}
+
+// commentsReader provides a function to get the pointer to the structure
+// after deserialization of xl/comments%d.xml.
+func (f *File) commentsReader(path string) (*xlsxComments, error) {
+ if f.Comments[path] == nil {
+ content, ok := f.Pkg.Load(path)
+ if ok && content != nil {
+ f.Comments[path] = new(xlsxComments)
+ if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
+ Decode(f.Comments[path]); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ }
+ return f.Comments[path], nil
+}
+
+// commentsWriter provides a function to save xl/comments%d.xml after
+// serialize structure.
+func (f *File) commentsWriter() {
+ for path, c := range f.Comments {
+ if c != nil {
+ v, _ := xml.Marshal(c)
+ f.saveFileList(path, v)
+ }
+ }
+}
+
+// AddFormControl provides the method to add form control button in a worksheet
+// by given worksheet name and form control options. Supported form control
+// type: button, check box, group box, label, option button, scroll bar and
+// spinner. If set macro for the form control, the workbook extension should be
+// XLSM or XLTM. Scroll value must be between 0 and 30000.
+//
+// Example 1, add button form control with macro, rich-text, custom button size,
+// print property on Sheet1!A2, and let the button do not move or size with
+// cells:
+//
+// enable := true
+// err := f.AddFormControl("Sheet1", excelize.FormControl{
+// Cell: "A2",
+// Type: excelize.FormControlButton,
+// Macro: "Button1_Click",
+// Width: 140,
+// Height: 60,
+// Text: "Button 1\r\n",
+// Paragraph: []excelize.RichTextRun{
+// {
+// Font: &excelize.Font{
+// Bold: true,
+// Italic: true,
+// Underline: "single",
+// Family: "Times New Roman",
+// Size: 14,
+// Color: "777777",
+// },
+// Text: "C1=A1+B1",
+// },
+// },
+// Format: excelize.GraphicOptions{
+// PrintObject: &enable,
+// Positioning: "absolute",
+// },
+// })
+//
+// Example 2, add option button form control with checked status and text on
+// Sheet1!A1:
+//
+// err := f.AddFormControl("Sheet1", excelize.FormControl{
+// Cell: "A1",
+// Type: excelize.FormControlOptionButton,
+// Text: "Option Button 1",
+// Checked: true,
+// })
+//
+// Example 3, add spin button form control on Sheet1!B1 to increase or decrease
+// the value of Sheet1!A1:
+//
+// err := f.AddFormControl("Sheet1", excelize.FormControl{
+// Cell: "B1",
+// Type: excelize.FormControlSpinButton,
+// Width: 15,
+// Height: 40,
+// CurrentVal: 7,
+// MinVal: 5,
+// MaxVal: 10,
+// IncChange: 1,
+// CellLink: "A1",
+// })
+//
+// Example 4, add horizontally scroll bar form control on Sheet1!A2 to change
+// the value of Sheet1!A1 by click the scroll arrows or drag the scroll box:
+//
+// err := f.AddFormControl("Sheet1", excelize.FormControl{
+// Cell: "A2",
+// Type: excelize.FormControlScrollBar,
+// Width: 140,
+// Height: 20,
+// CurrentVal: 50,
+// MinVal: 10,
+// MaxVal: 100,
+// IncChange: 1,
+// PageChange: 1,
+// CellLink: "A1",
+// Horizontally: true,
+// })
+func (f *File) AddFormControl(sheet string, opts FormControl) error {
+ return f.addVMLObject(vmlOptions{
+ formCtrl: true, sheet: sheet, FormControl: opts,
+ })
+}
+
+// DeleteFormControl provides the method to delete form control in a worksheet
+// by given worksheet name and cell reference. For example, delete the form
+// control in Sheet1!$A$1:
+//
+// err := f.DeleteFormControl("Sheet1", "A1")
+func (f *File) DeleteFormControl(sheet, cell string) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ if ws.LegacyDrawing == nil {
+ return err
+ }
+ sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
+ return f.deleteFormControl(sheetRelationshipsDrawingVML, cell, false)
+}
+
+// countVMLDrawing provides a function to get VML drawing files count storage
+// in the folder xl/drawings.
+func (f *File) countVMLDrawing() int {
+ drawings := map[string]struct{}{}
+ f.Pkg.Range(func(k, v interface{}) bool {
+ if strings.Contains(k.(string), "xl/drawings/vmlDrawing") {
+ drawings[k.(string)] = struct{}{}
+ }
+ return true
+ })
+ for rel := range f.VMLDrawing {
+ if strings.Contains(rel, "xl/drawings/vmlDrawing") {
+ drawings[rel] = struct{}{}
+ }
+ }
+ return len(drawings)
+}
+
+// decodeVMLDrawingReader provides a function to get the pointer to the
+// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
+func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) {
+ if f.DecodeVMLDrawing[path] == nil {
+ c, ok := f.Pkg.Load(path)
+ if ok && c != nil {
+ f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
+ if err := f.xmlNewDecoder(bytes.NewReader(bytesReplace(namespaceStrictToTransitional(c.([]byte)), []byte("
\r\n"), []byte("
\r\n"), -1))).
+ Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
+ return nil, err
+ }
+ }
+ }
+ return f.DecodeVMLDrawing[path], nil
+}
+
+// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
+// after serialize structure.
+func (f *File) vmlDrawingWriter() {
+ for path, vml := range f.VMLDrawing {
+ if vml != nil {
+ v, _ := xml.Marshal(vml)
+ f.Pkg.Store(path, v)
+ }
+ }
+}
+
+// addVMLObject provides a function to create VML drawing parts and
+// relationships for comments and form controls.
+func (f *File) addVMLObject(opts vmlOptions) error {
+ // Read sheet data
+ ws, err := f.workSheetReader(opts.sheet)
+ if err != nil {
+ return err
+ }
+ vmlID := f.countComments() + 1
+ if opts.formCtrl {
+ if opts.Type > FormControlScrollBar {
+ return ErrParameterInvalid
+ }
+ vmlID = f.countVMLDrawing() + 1
+ }
+ sheetID := f.getSheetID(opts.sheet)
+ drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
+ sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
+ sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
+ if ws.LegacyDrawing != nil {
+ // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
+ sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.sheet, ws.LegacyDrawing.RID)
+ vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
+ drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
+ } else {
+ // Add first VML drawing for given sheet.
+ rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
+ f.addSheetNameSpace(opts.sheet, SourceRelationship)
+ f.addSheetLegacyDrawing(opts.sheet, rID)
+ }
+ if err = f.addDrawingVML(sheetID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil {
+ return err
+ }
+ if !opts.formCtrl {
+ commentsXML := "xl/comments" + strconv.Itoa(vmlID) + ".xml"
+ if err = f.addComment(commentsXML, opts); err != nil {
+ return err
+ }
+ if sheetXMLPath, ok := f.getSheetXMLPath(opts.sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" {
+ sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
+ f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
+ }
+ }
+ return f.addContentTypePart(vmlID, "comments")
+}
+
+// prepareFormCtrlOptions provides a function to parse the format settings of
+// the form control with default value.
+func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions {
+ if opts.Format.ScaleX == 0 {
+ opts.Format.ScaleX = 1
+ }
+ if opts.Format.ScaleY == 0 {
+ opts.Format.ScaleY = 1
+ }
+ if opts.FormControl.Width == 0 {
+ opts.FormControl.Width = 140
+ }
+ if opts.FormControl.Height == 0 {
+ opts.FormControl.Height = 60
+ }
+ return opts
+}
+
+// formCtrlText returns font element in the VML for control form text.
+func formCtrlText(opts *vmlOptions) []vmlFont {
+ var font []vmlFont
+ if opts.FormControl.Text != "" {
+ font = append(font, vmlFont{Content: opts.FormControl.Text})
+ }
+ for _, run := range opts.FormControl.Paragraph {
+ fnt := vmlFont{
+ Content: run.Text + "
\r\n",
+ }
+ if run.Font != nil {
+ fnt.Face = run.Font.Family
+ fnt.Color = run.Font.Color
+ if !strings.HasPrefix(run.Font.Color, "#") {
+ fnt.Color = "#" + fnt.Color
+ }
+ if run.Font.Size != 0 {
+ fnt.Size = uint(run.Font.Size * 20)
+ }
+ if run.Font.Underline == "single" {
+ fnt.Content = "" + fnt.Content + ""
+ }
+ if run.Font.Underline == "double" {
+ fnt.Content = "" + fnt.Content + ""
+ }
+ if run.Font.Italic {
+ fnt.Content = "" + fnt.Content + ""
+ }
+ if run.Font.Bold {
+ fnt.Content = "" + fnt.Content + ""
+ }
+ }
+ font = append(font, fnt)
+ }
+ return font
+}
+
+var formCtrlPresets = map[FormControlType]formCtrlPreset{
+ FormControlNote: {
+ objectType: "Note",
+ autoFill: "True",
+ filled: "",
+ fillColor: "#FBF6D6",
+ stroked: "",
+ strokeColor: "#EDEAA1",
+ strokeButton: "",
+ fill: &vFill{
+ Color2: "#FBFE82",
+ Angle: -180,
+ Type: "gradient",
+ Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
+ },
+ textHAlign: "",
+ textVAlign: "",
+ noThreeD: nil,
+ firstButton: nil,
+ shadow: &vShadow{On: "t", Color: "black", Obscured: "t"},
+ },
+ FormControlButton: {
+ objectType: "Button",
+ autoFill: "True",
+ filled: "",
+ fillColor: "buttonFace [67]",
+ stroked: "",
+ strokeColor: "windowText [64]",
+ strokeButton: "t",
+ fill: &vFill{
+ Color2: "buttonFace [67]",
+ Angle: -180,
+ Type: "gradient",
+ Fill: &oFill{Ext: "view", Type: "gradientUnscaled"},
+ },
+ textHAlign: "Center",
+ textVAlign: "Center",
+ noThreeD: nil,
+ firstButton: nil,
+ shadow: nil,
+ },
+ FormControlCheckBox: {
+ objectType: "Checkbox",
+ autoFill: "True",
+ filled: "f",
+ fillColor: "window [65]",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "Center",
+ noThreeD: stringPtr(""),
+ firstButton: nil,
+ shadow: nil,
+ },
+ FormControlGroupBox: {
+ objectType: "GBox",
+ autoFill: "False",
+ filled: "f",
+ fillColor: "",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "",
+ noThreeD: stringPtr(""),
+ firstButton: nil,
+ shadow: nil,
+ },
+ FormControlLabel: {
+ objectType: "Label",
+ autoFill: "False",
+ filled: "f",
+ fillColor: "window [65]",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "",
+ noThreeD: nil,
+ firstButton: nil,
+ shadow: nil,
+ },
+ FormControlOptionButton: {
+ objectType: "Radio",
+ autoFill: "False",
+ filled: "f",
+ fillColor: "window [65]",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "Center",
+ noThreeD: stringPtr(""),
+ firstButton: stringPtr(""),
+ shadow: nil,
+ },
+ FormControlScrollBar: {
+ objectType: "Scroll",
+ autoFill: "",
+ filled: "",
+ fillColor: "",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "",
+ noThreeD: nil,
+ firstButton: nil,
+ shadow: nil,
+ },
+ FormControlSpinButton: {
+ objectType: "Spin",
+ autoFill: "False",
+ filled: "",
+ fillColor: "",
+ stroked: "f",
+ strokeColor: "windowText [64]",
+ strokeButton: "",
+ fill: nil,
+ textHAlign: "",
+ textVAlign: "",
+ noThreeD: nil,
+ firstButton: nil,
+ shadow: nil,
+ },
+}
+
+// addFormCtrl check and add scroll bar or spinner form control by given options.
+func (sp *encodeShape) addFormCtrl(opts *vmlOptions) error {
+ if opts.Type != FormControlScrollBar && opts.Type != FormControlSpinButton {
+ return nil
+ }
+ if opts.CurrentVal > MaxFormControlValue ||
+ opts.MinVal > MaxFormControlValue ||
+ opts.MaxVal > MaxFormControlValue ||
+ opts.IncChange > MaxFormControlValue ||
+ opts.PageChange > MaxFormControlValue {
+ return ErrFormControlValue
+ }
+ if opts.CellLink != "" {
+ if _, _, err := CellNameToCoordinates(opts.CellLink); err != nil {
+ return err
+ }
+ }
+ sp.ClientData.FmlaLink = opts.CellLink
+ sp.ClientData.Val = opts.CurrentVal
+ sp.ClientData.Min = opts.MinVal
+ sp.ClientData.Max = opts.MaxVal
+ sp.ClientData.Inc = opts.IncChange
+ sp.ClientData.Page = opts.PageChange
+ if opts.Type == FormControlScrollBar {
+ if opts.Horizontally {
+ sp.ClientData.Horiz = stringPtr("")
+ }
+ sp.ClientData.Dx = 15
+ }
+ return nil
+}
+
+// addFormCtrlShape returns a VML shape by given preset and options.
+func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor string, opts *vmlOptions) (*encodeShape, error) {
+ sp := encodeShape{
+ Fill: preset.fill,
+ Shadow: preset.shadow,
+ Path: &vPath{ConnectType: "none"},
+ TextBox: &vTextBox{
+ Style: "mso-direction-alt:auto",
+ Div: &xlsxDiv{Style: "text-align:left"},
+ },
+ ClientData: &xClientData{
+ ObjectType: preset.objectType,
+ Anchor: anchor,
+ AutoFill: preset.autoFill,
+ Row: intPtr(row - 1),
+ Column: intPtr(col - 1),
+ TextHAlign: preset.textHAlign,
+ TextVAlign: preset.textVAlign,
+ NoThreeD: preset.noThreeD,
+ FirstButton: preset.firstButton,
+ },
+ }
+ if opts.Format.PrintObject != nil && !*opts.Format.PrintObject {
+ sp.ClientData.PrintObject = "False"
+ }
+ if opts.Format.Positioning != "" {
+ idx := inStrSlice(supportedPositioning, opts.Format.Positioning, true)
+ if idx == -1 {
+ return &sp, newInvalidOptionalValue("Positioning", opts.Format.Positioning, supportedPositioning)
+ }
+ sp.ClientData.MoveWithCells = []*string{stringPtr(""), nil, nil}[idx]
+ sp.ClientData.SizeWithCells = []*string{stringPtr(""), stringPtr(""), nil}[idx]
+ }
+ if opts.FormControl.Type == FormControlNote {
+ sp.ClientData.MoveWithCells = stringPtr("")
+ sp.ClientData.SizeWithCells = stringPtr("")
+ }
+ if !opts.formCtrl {
+ return &sp, nil
+ }
+ sp.TextBox.Div.Font = formCtrlText(opts)
+ sp.ClientData.FmlaMacro = opts.Macro
+ if (opts.Type == FormControlCheckBox || opts.Type == FormControlOptionButton) && opts.Checked {
+ sp.ClientData.Checked = 1
+ }
+ return &sp, sp.addFormCtrl(opts)
+}
+
+// addDrawingVML provides a function to create VML drawing XML as
+// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The
+// anchor value is a comma-separated list of data written out as: LeftColumn,
+// LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow,
+// BottomOffset.
+func (f *File) addDrawingVML(sheetID int, drawingVML string, opts *vmlOptions) error {
+ col, row, err := CellNameToCoordinates(opts.FormControl.Cell)
+ if err != nil {
+ return err
+ }
+ leftOffset, vmlID, vml, preset := 23, 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type]
+ style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden"
+ if opts.formCtrl {
+ leftOffset, vmlID = 0, 201
+ style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight"
+ }
+ colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.FormControl.Width), int(opts.FormControl.Height))
+ anchor := fmt.Sprintf("%d, %d, %d, 0, %d, %d, %d, %d", colStart, leftOffset, rowStart, colEnd, x2, rowEnd, y2)
+ if vml == nil {
+ vml = &vmlDrawing{
+ XMLNSv: "urn:schemas-microsoft-com:vml",
+ XMLNSo: "urn:schemas-microsoft-com:office:office",
+ XMLNSx: "urn:schemas-microsoft-com:office:excel",
+ XMLNSmv: "http://macVmlSchemaUri",
+ ShapeLayout: &xlsxShapeLayout{
+ Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID},
+ },
+ ShapeType: &xlsxShapeType{
+ ID: fmt.Sprintf("_x0000_t%d", vmlID),
+ CoordSize: "21600,21600",
+ Spt: 202,
+ Path: "m0,0l0,21600,21600,21600,21600,0xe",
+ Stroke: &xlsxStroke{JoinStyle: "miter"},
+ VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"},
+ },
+ }
+ // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml
+ d, err := f.decodeVMLDrawingReader(drawingVML)
+ if err != nil {
+ return err
+ }
+ if d != nil {
+ vml.ShapeType.ID = d.ShapeType.ID
+ vml.ShapeType.CoordSize = d.ShapeType.CoordSize
+ vml.ShapeType.Spt = d.ShapeType.Spt
+ vml.ShapeType.Path = d.ShapeType.Path
+ for _, v := range d.Shape {
+ s := xlsxShape{
+ ID: v.ID,
+ Type: v.Type,
+ Style: v.Style,
+ Button: v.Button,
+ Filled: v.Filled,
+ FillColor: v.FillColor,
+ InsetMode: v.InsetMode,
+ Stroked: v.Stroked,
+ StrokeColor: v.StrokeColor,
+ Val: v.Val,
+ }
+ vml.Shape = append(vml.Shape, s)
+ }
+ }
+ }
+ sp, err := f.addFormCtrlShape(preset, col, row, anchor, opts)
+ if err != nil {
+ return err
+ }
+ s, _ := xml.Marshal(sp)
+ shape := xlsxShape{
+ ID: "_x0000_s1025",
+ Type: fmt.Sprintf("#_x0000_t%d", vmlID),
+ Style: style,
+ Button: preset.strokeButton,
+ Filled: preset.filled,
+ FillColor: preset.fillColor,
+ Stroked: preset.stroked,
+ StrokeColor: preset.strokeColor,
+ Val: string(s[13 : len(s)-14]),
+ }
+ vml.Shape = append(vml.Shape, shape)
+ f.VMLDrawing[drawingVML] = vml
+ return err
+}
+
+// GetFormControls retrieves all form controls in a worksheet by a given
+// worksheet name. Note that, this function does not support getting the width
+// and height of the form controls currently.
+func (f *File) GetFormControls(sheet string) ([]FormControl, error) {
+ var formControls []FormControl
+ // Read sheet data
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return formControls, err
+ }
+ if ws.LegacyDrawing == nil {
+ return formControls, err
+ }
+ target := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
+ drawingVML := strings.ReplaceAll(target, "..", "xl")
+ vml := f.VMLDrawing[drawingVML]
+ if vml == nil {
+ // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml
+ d, err := f.decodeVMLDrawingReader(drawingVML)
+ if err != nil {
+ return formControls, err
+ }
+ for _, sp := range d.Shape {
+ if sp.Type != "#_x0000_t201" {
+ continue
+ }
+ formControl, err := extractFormControl(sp.Val)
+ if err != nil {
+ return formControls, err
+ }
+ if formControl.Type == FormControlNote || formControl.Cell == "" {
+ continue
+ }
+ formControls = append(formControls, formControl)
+ }
+ return formControls, err
+ }
+ for _, sp := range vml.Shape {
+ if sp.Type != "#_x0000_t201" {
+ continue
+ }
+ formControl, err := extractFormControl(sp.Val)
+ if err != nil {
+ return formControls, err
+ }
+ if formControl.Type == FormControlNote || formControl.Cell == "" {
+ continue
+ }
+ formControls = append(formControls, formControl)
+ }
+ return formControls, err
+}
+
+// extractFormControl provides a function to extract form controls for a
+// worksheets by given client data.
+func extractFormControl(clientData string) (FormControl, error) {
+ var (
+ err error
+ formControl FormControl
+ shapeVal decodeShapeVal
+ )
+ if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", clientData)), &shapeVal); err != nil {
+ return formControl, err
+ }
+ for formCtrlType, preset := range formCtrlPresets {
+ if shapeVal.ClientData.ObjectType == preset.objectType && shapeVal.ClientData.Anchor != "" {
+ formControl.Paragraph = extractVMLFont(shapeVal.TextBox.Div.Font)
+ if len(formControl.Paragraph) > 0 && formControl.Paragraph[0].Font == nil {
+ formControl.Text = formControl.Paragraph[0].Text
+ formControl.Paragraph = formControl.Paragraph[1:]
+ }
+ formControl.Type = formCtrlType
+ col, row, err := extractAnchorCell(shapeVal.ClientData.Anchor)
+ if err != nil {
+ return formControl, err
+ }
+ if formControl.Cell, err = CoordinatesToCellName(col+1, row+1); err != nil {
+ return formControl, err
+ }
+ formControl.Macro = shapeVal.ClientData.FmlaMacro
+ formControl.Checked = shapeVal.ClientData.Checked != 0
+ formControl.CellLink = shapeVal.ClientData.FmlaLink
+ formControl.CurrentVal = shapeVal.ClientData.Val
+ formControl.MinVal = shapeVal.ClientData.Min
+ formControl.MaxVal = shapeVal.ClientData.Max
+ formControl.IncChange = shapeVal.ClientData.Inc
+ formControl.PageChange = shapeVal.ClientData.Page
+ formControl.Horizontally = shapeVal.ClientData.Horiz != nil
+ }
+ }
+ return formControl, err
+}
+
+// extractAnchorCell extract left-top cell coordinates from given VML anchor
+// comma-separated list values.
+func extractAnchorCell(anchor string) (int, int, error) {
+ var (
+ leftCol, topRow int
+ err error
+ pos = strings.Split(anchor, ",")
+ )
+ if len(pos) != 8 {
+ return leftCol, topRow, ErrParameterInvalid
+ }
+ leftCol, err = strconv.Atoi(strings.TrimSpace(pos[0]))
+ if err != nil {
+ return leftCol, topRow, ErrColumnNumber
+ }
+ topRow, err = strconv.Atoi(strings.TrimSpace(pos[2]))
+ return leftCol, topRow, err
+}
+
+// extractVMLFont extract rich-text and font format from given VML font element.
+func extractVMLFont(font []decodeVMLFont) []RichTextRun {
+ var runs []RichTextRun
+ extractU := func(u *decodeVMLFontU, run *RichTextRun) {
+ if u == nil {
+ return
+ }
+ run.Text += u.Val
+ if run.Font == nil {
+ run.Font = &Font{}
+ }
+ run.Font.Underline = "single"
+ if u.Class == "font1" {
+ run.Font.Underline = "double"
+ }
+ }
+ extractI := func(i *decodeVMLFontI, run *RichTextRun) {
+ if i == nil {
+ return
+ }
+ extractU(i.U, run)
+ run.Text += i.Val
+ if run.Font == nil {
+ run.Font = &Font{}
+ }
+ run.Font.Italic = true
+ }
+ extractB := func(b *decodeVMLFontB, run *RichTextRun) {
+ if b == nil {
+ return
+ }
+ extractI(b.I, run)
+ run.Text += b.Val
+ if run.Font == nil {
+ run.Font = &Font{}
+ }
+ run.Font.Bold = true
+ }
+ for _, fnt := range font {
+ var run RichTextRun
+ extractB(fnt.B, &run)
+ extractI(fnt.I, &run)
+ extractU(fnt.U, &run)
+ run.Text += fnt.Val
+ if fnt.Face != "" || fnt.Size > 0 || fnt.Color != "" {
+ if run.Font == nil {
+ run.Font = &Font{}
+ }
+ run.Font.Family = fnt.Face
+ run.Font.Size = float64(fnt.Size / 20)
+ run.Font.Color = fnt.Color
+ }
+ runs = append(runs, run)
+ }
+ return runs
+}
+
+// AddHeaderFooterImage provides a mechanism to set the graphics that can be
+// referenced in the header and footer definitions via &G, supported image
+// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ.
+//
+// The extension should be provided with a "." in front, e.g. ".png".
+// The width and height should have units in them, e.g. "100pt".
+func (f *File) AddHeaderFooterImage(sheet string, opts *HeaderFooterImageOptions) error {
+ ws, err := f.workSheetReader(sheet)
+ if err != nil {
+ return err
+ }
+ ext, ok := supportedImageTypes[strings.ToLower(opts.Extension)]
+ if !ok {
+ return ErrImgExt
+ }
+ sheetID := f.getSheetID(sheet)
+ vmlID := f.countVMLDrawing() + 1
+ drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
+ sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
+ sheetXMLPath, _ := f.getSheetXMLPath(sheet)
+ sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
+ if ws.LegacyDrawingHF != nil {
+ // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
+ sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawingHF.RID)
+ vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
+ drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
+ } else {
+ // Add first VML drawing for given sheet.
+ rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
+ f.addSheetNameSpace(sheet, SourceRelationship)
+ f.addSheetLegacyDrawingHF(sheet, rID)
+ }
+
+ shapeID := map[HeaderFooterImagePositionType]string{
+ HeaderFooterImagePositionLeft: "L",
+ HeaderFooterImagePositionCenter: "C",
+ HeaderFooterImagePositionRight: "R",
+ }[opts.Position] +
+ map[bool]string{false: "H", true: "F"}[opts.IsFooter] +
+ map[bool]string{false: "", true: "FIRST"}[opts.FirstPage]
+ vml := f.VMLDrawing[drawingVML]
+ if vml == nil {
+ vml = &vmlDrawing{
+ XMLNSv: "urn:schemas-microsoft-com:vml",
+ XMLNSo: "urn:schemas-microsoft-com:office:office",
+ XMLNSx: "urn:schemas-microsoft-com:office:excel",
+ ShapeLayout: &xlsxShapeLayout{
+ Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID},
+ },
+ ShapeType: &xlsxShapeType{
+ ID: "_x0000_t75",
+ CoordSize: "21600,21600",
+ Spt: 75,
+ PreferRelative: "t",
+ Path: "m@4@5l@4@11@9@11@9@5xe",
+ Filled: "f",
+ Stroked: "f",
+ Stroke: &xlsxStroke{JoinStyle: "miter"},
+ VFormulas: &vFormulas{
+ Formulas: []vFormula{
+ {Equation: "if lineDrawn pixelLineWidth 0"},
+ {Equation: "sum @0 1 0"},
+ {Equation: "sum 0 0 @1"},
+ {Equation: "prod @2 1 2"},
+ {Equation: "prod @3 21600 pixelWidth"},
+ {Equation: "prod @3 21600 pixelHeight"},
+ {Equation: "sum @0 0 1"},
+ {Equation: "prod @6 1 2"},
+ {Equation: "prod @7 21600 pixelWidth"},
+ {Equation: "sum @8 21600 0"},
+ {Equation: "prod @7 21600 pixelHeight"},
+ {Equation: "sum @10 21600 0"},
+ },
+ },
+ VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"},
+ Lock: &oLock{Ext: "edit", AspectRatio: "t"},
+ },
+ }
+ // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml
+ d, err := f.decodeVMLDrawingReader(drawingVML)
+ if err != nil {
+ return err
+ }
+ if d != nil {
+ vml.ShapeType.ID = d.ShapeType.ID
+ vml.ShapeType.CoordSize = d.ShapeType.CoordSize
+ vml.ShapeType.Spt = d.ShapeType.Spt
+ vml.ShapeType.PreferRelative = d.ShapeType.PreferRelative
+ vml.ShapeType.Path = d.ShapeType.Path
+ vml.ShapeType.Filled = d.ShapeType.Filled
+ vml.ShapeType.Stroked = d.ShapeType.Stroked
+ for _, v := range d.Shape {
+ s := xlsxShape{
+ ID: v.ID,
+ SpID: v.SpID,
+ Type: v.Type,
+ Style: v.Style,
+ Val: v.Val,
+ }
+ vml.Shape = append(vml.Shape, s)
+ }
+ }
+ }
+
+ for idx, shape := range vml.Shape {
+ if shape.ID == shapeID {
+ vml.Shape = append(vml.Shape[:idx], vml.Shape[idx+1:]...)
+ }
+ }
+
+ style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", opts.Width, opts.Height)
+ drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels"
+
+ mediaStr := ".." + strings.TrimPrefix(f.addMedia(opts.File, ext), "xl")
+ imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "")
+
+ shape := xlsxShape{
+ ID: shapeID,
+ SpID: "_x0000_s1025",
+ Type: "#_x0000_t75",
+ Style: style,
+ }
+ sp, _ := xml.Marshal(encodeShape{
+ ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)},
+ Lock: &oLock{Ext: "edit", Rotation: "t"},
+ })
+
+ shape.Val = string(sp[13 : len(sp)-14])
+ vml.Shape = append(vml.Shape, shape)
+ f.VMLDrawing[drawingVML] = vml
+
+ if err := f.setContentTypePartImageExtensions(); err != nil {
+ return err
+ }
+ return f.setContentTypePartVMLExtensions()
+}
diff --git a/vmlDrawing.go b/vmlDrawing.go
index 185df28890..44b5ea5f42 100644
--- a/vmlDrawing.go
+++ b/vmlDrawing.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -20,16 +20,16 @@ type vmlDrawing struct {
XMLNSv string `xml:"xmlns:v,attr"`
XMLNSo string `xml:"xmlns:o,attr"`
XMLNSx string `xml:"xmlns:x,attr"`
- XMLNSmv string `xml:"xmlns:mv,attr"`
- Shapelayout *xlsxShapelayout `xml:"o:shapelayout"`
- Shapetype *xlsxShapetype `xml:"v:shapetype"`
+ XMLNSmv string `xml:"xmlns:mv,attr,omitempty"`
+ ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
+ ShapeType *xlsxShapeType `xml:"v:shapetype"`
Shape []xlsxShape `xml:"v:shape"`
}
-// xlsxShapelayout directly maps the shapelayout element. This element contains
+// xlsxShapeLayout directly maps the shapelayout element. This element contains
// child elements that store information used in the editing and layout of
// shapes.
-type xlsxShapelayout struct {
+type xlsxShapeLayout struct {
Ext string `xml:"v:ext,attr"`
IDmap *xlsxIDmap `xml:"o:idmap"`
}
@@ -44,33 +44,60 @@ type xlsxIDmap struct {
type xlsxShape struct {
XMLName xml.Name `xml:"v:shape"`
ID string `xml:"id,attr"`
+ SpID string `xml:"o:spid,attr,omitempty"`
Type string `xml:"type,attr"`
Style string `xml:"style,attr"`
- Fillcolor string `xml:"fillcolor,attr"`
- Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
- Strokecolor string `xml:"strokecolor,attr,omitempty"`
+ Button string `xml:"o:button,attr,omitempty"`
+ Filled string `xml:"filled,attr,omitempty"`
+ FillColor string `xml:"fillcolor,attr,omitempty"`
+ InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
+ Stroked string `xml:"stroked,attr,omitempty"`
+ StrokeColor string `xml:"strokecolor,attr,omitempty"`
Val string `xml:",innerxml"`
}
-// xlsxShapetype directly maps the shapetype element.
-type xlsxShapetype struct {
- ID string `xml:"id,attr"`
- Coordsize string `xml:"coordsize,attr"`
- Spt int `xml:"o:spt,attr"`
- Path string `xml:"path,attr"`
- Stroke *xlsxStroke `xml:"v:stroke"`
- VPath *vPath `xml:"v:path"`
+// xlsxShapeType directly maps the shapetype element.
+type xlsxShapeType struct {
+ ID string `xml:"id,attr"`
+ CoordSize string `xml:"coordsize,attr"`
+ Spt int `xml:"o:spt,attr"`
+ PreferRelative string `xml:"o:preferrelative,attr,omitempty"`
+ Path string `xml:"path,attr"`
+ Filled string `xml:"filled,attr,omitempty"`
+ Stroked string `xml:"stroked,attr,omitempty"`
+ Stroke *xlsxStroke `xml:"v:stroke"`
+ VFormulas *vFormulas `xml:"v:formulas"`
+ VPath *vPath `xml:"v:path"`
+ Lock *oLock `xml:"o:lock"`
}
// xlsxStroke directly maps the stroke element.
type xlsxStroke struct {
- Joinstyle string `xml:"joinstyle,attr"`
+ JoinStyle string `xml:"joinstyle,attr"`
}
// vPath directly maps the v:path element.
type vPath struct {
- Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"`
- Connecttype string `xml:"o:connecttype,attr"`
+ ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"`
+ GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"`
+ ConnectType string `xml:"o:connecttype,attr"`
+}
+
+// oLock directly maps the o:lock element.
+type oLock struct {
+ Ext string `xml:"v:ext,attr"`
+ Rotation string `xml:"rotation,attr,omitempty"`
+ AspectRatio string `xml:"aspectratio,attr,omitempty"`
+}
+
+// vFormulas directly maps to the v:formulas element
+type vFormulas struct {
+ Formulas []vFormula `xml:"v:f"`
+}
+
+// vFormula directly maps to the v:f element
+type vFormula struct {
+ Equation string `xml:"eqn,attr"`
}
// vFill directly maps the v:fill element. This element must be defined within a
@@ -96,16 +123,31 @@ type vShadow struct {
Obscured string `xml:"obscured,attr"`
}
-// vTextbox directly maps the v:textbox element. This element must be defined
+// vTextBox directly maps the v:textbox element. This element must be defined
// within a Shape element.
-type vTextbox struct {
+type vTextBox struct {
Style string `xml:"style,attr"`
Div *xlsxDiv `xml:"div"`
}
+// vImageData directly maps the v:imagedata element. This element must be
+// defined within a Shape element.
+type vImageData struct {
+ RelID string `xml:"o:relid,attr"`
+ Title string `xml:"o:title,attr,omitempty"`
+}
+
// xlsxDiv directly maps the div element.
type xlsxDiv struct {
- Style string `xml:"style,attr"`
+ Style string `xml:"style,attr"`
+ Font []vmlFont `xml:"font"`
+}
+
+type vmlFont struct {
+ Face string `xml:"face,attr,omitempty"`
+ Size uint `xml:"size,attr,omitempty"`
+ Color string `xml:"color,attr,omitempty"`
+ Content string `xml:",innerxml"`
}
// xClientData (Attached Object Data) directly maps the x:ClientData element.
@@ -116,24 +158,129 @@ type xlsxDiv struct {
// child elements is appropriate. Relevant groups are identified for each child
// element.
type xClientData struct {
- ObjectType string `xml:"ObjectType,attr"`
- MoveWithCells string `xml:"x:MoveWithCells,omitempty"`
- SizeWithCells string `xml:"x:SizeWithCells,omitempty"`
- Anchor string `xml:"x:Anchor"`
- AutoFill string `xml:"x:AutoFill"`
- Row int `xml:"x:Row"`
- Column int `xml:"x:Column"`
+ ObjectType string `xml:"ObjectType,attr"`
+ MoveWithCells *string `xml:"x:MoveWithCells"`
+ SizeWithCells *string `xml:"x:SizeWithCells"`
+ Anchor string `xml:"x:Anchor"`
+ Locked string `xml:"x:Locked,omitempty"`
+ PrintObject string `xml:"x:PrintObject,omitempty"`
+ AutoFill string `xml:"x:AutoFill,omitempty"`
+ FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
+ TextHAlign string `xml:"x:TextHAlign,omitempty"`
+ TextVAlign string `xml:"x:TextVAlign,omitempty"`
+ Row *int `xml:"x:Row"`
+ Column *int `xml:"x:Column"`
+ Checked int `xml:"x:Checked,omitempty"`
+ FmlaLink string `xml:"x:FmlaLink,omitempty"`
+ NoThreeD *string `xml:"x:NoThreeD"`
+ FirstButton *string `xml:"x:FirstButton"`
+ Val uint `xml:"x:Val,omitempty"`
+ Min uint `xml:"x:Min,omitempty"`
+ Max uint `xml:"x:Max,omitempty"`
+ Inc uint `xml:"x:Inc,omitempty"`
+ Page uint `xml:"x:Page,omitempty"`
+ Horiz *string `xml:"x:Horiz"`
+ Dx uint `xml:"x:Dx,omitempty"`
}
// decodeVmlDrawing defines the structure used to parse the file
// xl/drawings/vmlDrawing%d.vml.
type decodeVmlDrawing struct {
- Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"`
+ ShapeType decodeShapeType `xml:"urn:schemas-microsoft-com:vml shapetype"`
+ Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"`
+}
+
+// decodeShapeType defines the structure used to parse the shapetype element in
+// the file xl/drawings/vmlDrawing%d.vml.
+type decodeShapeType struct {
+ ID string `xml:"id,attr"`
+ CoordSize string `xml:"coordsize,attr"`
+ Spt int `xml:"spt,attr"`
+ PreferRelative string `xml:"preferrelative,attr,omitempty"`
+ Path string `xml:"path,attr"`
+ Filled string `xml:"filled,attr,omitempty"`
+ Stroked string `xml:"stroked,attr,omitempty"`
}
// decodeShape defines the structure used to parse the particular shape element.
type decodeShape struct {
- Val string `xml:",innerxml"`
+ ID string `xml:"id,attr"`
+ SpID string `xml:"spid,attr,omitempty"`
+ Type string `xml:"type,attr"`
+ Style string `xml:"style,attr"`
+ Button string `xml:"button,attr,omitempty"`
+ Filled string `xml:"filled,attr,omitempty"`
+ FillColor string `xml:"fillcolor,attr,omitempty"`
+ InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
+ Stroked string `xml:"stroked,attr,omitempty"`
+ StrokeColor string `xml:"strokecolor,attr,omitempty"`
+ Val string `xml:",innerxml"`
+}
+
+// decodeShapeVal defines the structure used to parse the sub-element of the
+// shape in the file xl/drawings/vmlDrawing%d.vml.
+type decodeShapeVal struct {
+ TextBox decodeVMLTextBox `xml:"textbox"`
+ ClientData decodeVMLClientData `xml:"ClientData"`
+}
+
+// decodeVMLFontU defines the structure used to parse the u element in the VML.
+type decodeVMLFontU struct {
+ Class string `xml:"class,attr"`
+ Val string `xml:",chardata"`
+}
+
+// decodeVMLFontI defines the structure used to parse the i element in the VML.
+type decodeVMLFontI struct {
+ U *decodeVMLFontU `xml:"u"`
+ Val string `xml:",chardata"`
+}
+
+// decodeVMLFontB defines the structure used to parse the b element in the VML.
+type decodeVMLFontB struct {
+ I *decodeVMLFontI `xml:"i"`
+ U *decodeVMLFontU `xml:"u"`
+ Val string `xml:",chardata"`
+}
+
+// decodeVMLFont defines the structure used to parse the font element in the VML.
+type decodeVMLFont struct {
+ Face string `xml:"face,attr,omitempty"`
+ Size uint `xml:"size,attr,omitempty"`
+ Color string `xml:"color,attr,omitempty"`
+ B *decodeVMLFontB `xml:"b"`
+ I *decodeVMLFontI `xml:"i"`
+ U *decodeVMLFontU `xml:"u"`
+ Val string `xml:",chardata"`
+}
+
+// decodeVMLDiv defines the structure used to parse the div element in the VML.
+type decodeVMLDiv struct {
+ Font []decodeVMLFont `xml:"font"`
+}
+
+// decodeVMLTextBox defines the structure used to parse the v:textbox element in
+// the file xl/drawings/vmlDrawing%d.vml.
+type decodeVMLTextBox struct {
+ Div decodeVMLDiv `xml:"div"`
+}
+
+// decodeVMLClientData defines the structure used to parse the x:ClientData
+// element in the file xl/drawings/vmlDrawing%d.vml.
+type decodeVMLClientData struct {
+ ObjectType string `xml:"ObjectType,attr"`
+ Anchor string
+ FmlaMacro string
+ Column *int
+ Row *int
+ Checked int
+ FmlaLink string
+ Val uint
+ Min uint
+ Max uint
+ Inc uint
+ Page uint
+ Horiz *string
}
// encodeShape defines the structure used to re-serialization shape element.
@@ -141,6 +288,65 @@ type encodeShape struct {
Fill *vFill `xml:"v:fill"`
Shadow *vShadow `xml:"v:shadow"`
Path *vPath `xml:"v:path"`
- Textbox *vTextbox `xml:"v:textbox"`
+ TextBox *vTextBox `xml:"v:textbox"`
+ ImageData *vImageData `xml:"v:imagedata"`
ClientData *xClientData `xml:"x:ClientData"`
+ Lock *oLock `xml:"o:lock"`
+}
+
+// formCtrlPreset defines the structure used to form control presets.
+type formCtrlPreset struct {
+ autoFill string
+ fill *vFill
+ fillColor string
+ filled string
+ firstButton *string
+ noThreeD *string
+ objectType string
+ shadow *vShadow
+ strokeButton string
+ strokeColor string
+ stroked string
+ textHAlign string
+ textVAlign string
+}
+
+// vmlOptions defines the structure used to internal comments and form controls.
+type vmlOptions struct {
+ formCtrl bool
+ sheet string
+ Comment
+ FormControl
+}
+
+// FormControl directly maps the form controls information.
+type FormControl struct {
+ Cell string
+ Macro string
+ Width uint
+ Height uint
+ Checked bool
+ CurrentVal uint
+ MinVal uint
+ MaxVal uint
+ IncChange uint
+ PageChange uint
+ Horizontally bool
+ CellLink string
+ Text string
+ Paragraph []RichTextRun
+ Type FormControlType
+ Format GraphicOptions
+}
+
+// HeaderFooterImageOptions defines the settings for an image to be accessible
+// from the worksheet header and footer options.
+type HeaderFooterImageOptions struct {
+ Position HeaderFooterImagePositionType
+ File []byte
+ IsFooter bool
+ FirstPage bool
+ Extension string
+ Width string
+ Height string
}
diff --git a/vml_test.go b/vml_test.go
new file mode 100644
index 0000000000..dabd374747
--- /dev/null
+++ b/vml_test.go
@@ -0,0 +1,512 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddComment(t *testing.T) {
+ f, err := prepareTestBook1()
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+
+ s := strings.Repeat("c", TotalCellChars+1)
+ assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Paragraph: []RichTextRun{{Text: s}, {Text: s}}}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
+
+ // Test add comment on not exists worksheet
+ assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
+ // Test add comment on with illegal cell reference
+ assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
+ comments, err := f.GetComments("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 2)
+ comments, err = f.GetComments("Sheet2")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 1)
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx")))
+
+ f.Comments["xl/comments2.xml"] = nil
+ f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`Excelize: Excelize: `))
+ comments, err = f.GetComments("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 2)
+ comments, err = f.GetComments("Sheet2")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 1)
+ comments, err = NewFile().GetComments("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 0)
+
+ // Test add comments with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}))
+
+ // Test add comments with unsupported charset
+ f.Comments["xl/comments2.xml"] = nil
+ f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
+ _, err = f.GetComments("Sheet2")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+
+ // Test add comments with unsupported charset
+ f.Comments["xl/comments2.xml"] = nil
+ f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test add comments with unsupported charset style sheet
+ f.Styles = nil
+ f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
+
+ // Test get comments on not exists worksheet
+ comments, err = f.GetComments("SheetN")
+ assert.Len(t, comments, 0)
+ assert.EqualError(t, err, "sheet SheetN does not exist")
+}
+
+func TestDeleteComment(t *testing.T) {
+ f, err := prepareTestBook1()
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
+ assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
+
+ assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
+
+ comments, err := f.GetComments("Sheet2")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 5)
+
+ comments, err = NewFile().GetComments("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, comments, 0)
+
+ // Test delete comment with invalid sheet name
+ assert.Equal(t, ErrSheetNameInvalid, f.DeleteComment("Sheet:1", "A1"))
+ // Test delete all comments in a worksheet
+ assert.NoError(t, f.DeleteComment("Sheet2", "A41"))
+ assert.NoError(t, f.DeleteComment("Sheet2", "C41"))
+ assert.NoError(t, f.DeleteComment("Sheet2", "C42"))
+ comments, err = f.GetComments("Sheet2")
+ assert.NoError(t, err)
+ assert.EqualValues(t, 0, len(comments))
+ // Test delete comment on not exists worksheet
+ assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist")
+ // Test delete comment with worksheet part
+ f.Pkg.Delete("xl/worksheets/sheet1.xml")
+ assert.NoError(t, f.DeleteComment("Sheet1", "A22"))
+
+ f.Comments["xl/comments2.xml"] = nil
+ f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8")
+
+ f = NewFile()
+ // Test delete comment on a no comments worksheet
+ assert.NoError(t, f.DeleteComment("Sheet1", "A1"))
+}
+
+func TestDecodeVMLDrawingReader(t *testing.T) {
+ f := NewFile()
+ path := "xl/drawings/vmlDrawing1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err := f.decodeVMLDrawingReader(path)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestCommentsReader(t *testing.T) {
+ f := NewFile()
+ // Test read comments with unsupported charset
+ path := "xl/comments1.xml"
+ f.Pkg.Store(path, MacintoshCyrillicCharset)
+ _, err := f.commentsReader(path)
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestCountComments(t *testing.T) {
+ f := NewFile()
+ f.Comments["xl/comments1.xml"] = nil
+ assert.Equal(t, f.countComments(), 1)
+}
+
+func TestAddDrawingVML(t *testing.T) {
+ // Test addDrawingVML with illegal cell reference
+ f := NewFile()
+ assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
+
+ f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{sheet: "Sheet1", FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestFormControl(t *testing.T) {
+ f := NewFile()
+ formControls := []FormControl{
+ {
+ Cell: "D1", Type: FormControlButton, Macro: "Button1_Click",
+ },
+ {
+ Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
+ Width: 140, Height: 60, Text: "Button 1\n",
+ Paragraph: []RichTextRun{
+ {
+ Font: &Font{
+ Bold: true,
+ Italic: true,
+ Underline: "single",
+ Family: "Times New Roman",
+ Size: 14,
+ Color: "777777",
+ },
+ Text: "C1=A1+B1",
+ },
+ },
+ Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"},
+ },
+ {
+ Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1",
+ Checked: true, Format: GraphicOptions{
+ PrintObject: boolPtr(false), Positioning: "oneCell",
+ },
+ },
+ {
+ Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2",
+ Format: GraphicOptions{Positioning: "twoCell"},
+ },
+ {
+ Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true,
+ },
+ {
+ Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2",
+ },
+ {
+ Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1",
+ Width: 140, Height: 60,
+ },
+ {
+ Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140,
+ },
+ {
+ Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60,
+ CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2",
+ },
+ {
+ Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20,
+ CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3",
+ },
+ {
+ Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140,
+ CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4",
+ },
+ }
+ for _, formCtrl := range formControls {
+ assert.NoError(t, f.AddFormControl("Sheet1", formCtrl))
+ }
+ // Test get from controls
+ result, err := f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, result, 11)
+ for i, formCtrl := range formControls {
+ assert.Equal(t, formCtrl.Type, result[i].Type)
+ assert.Equal(t, formCtrl.Cell, result[i].Cell)
+ assert.Equal(t, formCtrl.Macro, result[i].Macro)
+ assert.Equal(t, formCtrl.Checked, result[i].Checked)
+ assert.Equal(t, formCtrl.CurrentVal, result[i].CurrentVal)
+ assert.Equal(t, formCtrl.MinVal, result[i].MinVal)
+ assert.Equal(t, formCtrl.MaxVal, result[i].MaxVal)
+ assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
+ assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
+ assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
+ assert.Equal(t, formCtrl.Text, result[i].Text)
+ assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph))
+ }
+ assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
+ file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddVBAProject(file))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddFormControl.xlsm")))
+ assert.NoError(t, f.Close())
+ f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
+ assert.NoError(t, err)
+ // Test get from controls before add form controls
+ result, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, result, 11)
+ // Test add from control to a worksheet which already contains form controls
+ assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
+ Cell: "D4", Type: FormControlButton, Macro: "Button1_Click",
+ Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}},
+ }))
+ // Test get from controls after add form controls
+ result, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, result, 12)
+ // Test add unsupported form control
+ assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
+ Cell: "A1", Type: 0x37, Macro: "Button1_Click",
+ }), ErrParameterInvalid)
+ // Test add form control on not exists worksheet
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddFormControl("SheetN", FormControl{
+ Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
+ }))
+ // Test add form control with invalid positioning types
+ assert.Equal(t, newInvalidOptionalValue("Positioning", "x", supportedPositioning),
+ f.AddFormControl("Sheet1", FormControl{
+ Cell: "A1", Type: FormControlButton,
+ Format: GraphicOptions{Positioning: "x"},
+ }))
+ // Test add spin form control with illegal cell link reference
+ assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
+ Cell: "C5", Type: FormControlSpinButton, CellLink: "*",
+ }), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
+ // Test add spin form control with invalid scroll value
+ assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
+ Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1,
+ }), ErrFormControlValue)
+ assert.NoError(t, f.Close())
+ // Test delete form control
+ f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
+ assert.NoError(t, err)
+ assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
+ assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
+ // Test get from controls after delete form controls
+ result, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, result, 9)
+ // Test delete form control on not exists worksheet
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.DeleteFormControl("SheetN", "A1"))
+ // Test delete form control with illegal cell link reference
+ assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
+ assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
+ assert.NoError(t, f.Close())
+ // Test delete form control with expected element
+ f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
+ assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
+ // Test delete form controls with invalid shape anchor
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: "0"}},
+ }
+ assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1"))
+ assert.NoError(t, f.Close())
+ // Test delete form control on a worksheet without form control
+ f = NewFile()
+ assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
+ // Test get form controls on a worksheet without form control
+ _, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ // Test get form controls on not exists worksheet
+ _, err = f.GetFormControls("SheetN")
+ assert.Equal(t, ErrSheetNotExist{"SheetN"}, err)
+ // Test get form controls with unsupported charset VML drawing
+ f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
+ _, err = f.GetFormControls("Sheet1")
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+ // Test get form controls with unsupported shape type
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "_x0000_t202"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with bold font format
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: "Text
0,0,0,0,0,0,0,0"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.True(t, formControls[0].Paragraph[0].Font.Bold)
+ // Test get form controls with italic font format
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: "Text
0,0,0,0,0,0,0,0"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.True(t, formControls[0].Paragraph[0].Font.Italic)
+ // Test get form controls with font format
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: "Text
0,0,0,0,0,0,0,0"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family)
+ assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size)
+ assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color)
+ // Test get form controls with italic font format
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: "Text
0,0,0,0,0,0,0,0"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.True(t, formControls[0].Paragraph[0].Font.Italic)
+ // Test get form controls with invalid column number
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.Equal(t, err, ErrColumnNumber)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with comment (Note) shape type
+ f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
+ Shape: []decodeShape{{Type: "#_x0000_t201", Val: ""}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with unsupported shape type
+ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
+ Shape: []xlsxShape{{Type: "_x0000_t202"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with invalid column number
+ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
+ Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.Equal(t, err, ErrColumnNumber)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with invalid shape anchor
+ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
+ Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "x,0,0,0,0,0,0,0"}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.Equal(t, ErrColumnNumber, err)
+ assert.Len(t, formControls, 0)
+ // Test get form controls with comment (Note) shape type
+ f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
+ Shape: []xlsxShape{{Type: "#_x0000_t201", Val: ""}},
+ }
+ formControls, err = f.GetFormControls("Sheet1")
+ assert.NoError(t, err)
+ assert.Len(t, formControls, 0)
+ assert.NoError(t, f.Close())
+}
+
+func TestExtractFormControl(t *testing.T) {
+ // Test extract form control with unsupported charset
+ _, err := extractFormControl(string(MacintoshCyrillicCharset))
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestAddHeaderFooterImage(t *testing.T) {
+ f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx")
+ headerFooterOptions := HeaderFooterOptions{
+ DifferentFirst: true,
+ OddHeader: "&L&GExcelize&C&G&R&G",
+ OddFooter: "&L&GExcelize&C&G&R&G",
+ FirstHeader: "&L&GExcelize&C&G&R&G",
+ FirstFooter: "&L&GExcelize&C&G&R&G",
+ }
+ assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions))
+ assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")}))
+ images := map[string][]byte{
+ ".wmf": nil, ".tif": nil, ".png": nil,
+ ".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil,
+ }
+ for ext := range images {
+ img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext))
+ assert.NoError(t, err)
+ images[ext] = img
+ }
+ for _, opt := range []struct {
+ position HeaderFooterImagePositionType
+ file []byte
+ isFooter bool
+ firstPage bool
+ ext string
+ }{
+ {position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"},
+ {position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"},
+ {position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"},
+ {position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"},
+ {position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"},
+ {position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"},
+ {position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"},
+ {position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"},
+ {position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"},
+ {position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"},
+ {position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"},
+ {position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"},
+ } {
+ assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
+ Position: opt.position,
+ File: opt.file,
+ IsFooter: opt.isFooter,
+ FirstPage: opt.firstPage,
+ Extension: opt.ext,
+ Width: "50pt",
+ Height: "32pt",
+ }))
+ }
+ assert.NoError(t, f.SetCellValue(sheet, "A1", "Example"))
+
+ // Test add header footer image with not exist sheet
+ assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist")
+ // Test add header footer image with unsupported file type
+ assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
+ Extension: "jpg",
+ }), ErrImgExt)
+ assert.NoError(t, f.SaveAs(wb))
+ assert.NoError(t, f.Close())
+ // Test change already exist header image with the different image
+ f, err := OpenFile(wb)
+ assert.NoError(t, err)
+ assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
+ File: images[".jpg"],
+ FirstPage: true,
+ Extension: ".jpg",
+ Width: "50pt",
+ Height: "32pt",
+ }))
+ assert.NoError(t, f.Save())
+ assert.NoError(t, f.Close())
+
+ // Test add header image with unsupported charset VML drawing
+ f, err = OpenFile(wb)
+ assert.NoError(t, err)
+ f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
+ File: images[".jpg"],
+ Extension: ".jpg",
+ Width: "50pt",
+ Height: "32pt",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+ // Test set legacy drawing header/footer with unsupported charset content types
+ f = NewFile()
+ f.ContentTypes = nil
+ f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
+ Extension: ".png",
+ File: images[".png"],
+ Width: "50pt",
+ Height: "32pt",
+ }), "XML syntax error on line 1: invalid UTF-8")
+ assert.NoError(t, f.Close())
+}
diff --git a/workbook.go b/workbook.go
new file mode 100644
index 0000000000..d1cb1da45b
--- /dev/null
+++ b/workbook.go
@@ -0,0 +1,432 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import (
+ "bytes"
+ "encoding/xml"
+ "io"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+)
+
+// SetWorkbookProps provides a function to sets workbook properties.
+func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error {
+ if opts == nil {
+ return nil
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb.WorkbookPr == nil {
+ wb.WorkbookPr = new(xlsxWorkbookPr)
+ }
+ setNoPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"},
+ reflect.ValueOf(*opts), reflect.ValueOf(wb.WorkbookPr).Elem())
+ return err
+}
+
+// GetWorkbookProps provides a function to gets workbook properties.
+func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) {
+ var opts WorkbookPropsOptions
+ wb, err := f.workbookReader()
+ if err != nil {
+ return opts, err
+ }
+ if wb.WorkbookPr == nil {
+ return opts, err
+ }
+ setPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"},
+ reflect.ValueOf(*wb.WorkbookPr), reflect.ValueOf(&opts).Elem())
+ return opts, err
+}
+
+// SetCalcProps provides a function to sets calculation properties. Optional
+// value of "CalcMode" property is: "manual", "auto" or "autoNoTable". Optional
+// value of "RefMode" property is: "A1" or "R1C1".
+func (f *File) SetCalcProps(opts *CalcPropsOptions) error {
+ if opts == nil {
+ return nil
+ }
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb.CalcPr == nil {
+ wb.CalcPr = new(xlsxCalcPr)
+ }
+ if opts.CalcMode != nil && inStrSlice(supportedCalcMode, *opts.CalcMode, true) == -1 {
+ return newInvalidOptionalValue("CalcMode", *opts.CalcMode, supportedCalcMode)
+ }
+ if opts.RefMode != nil && inStrSlice(supportedRefMode, *opts.RefMode, true) == -1 {
+ return newInvalidOptionalValue("RefMode", *opts.RefMode, supportedRefMode)
+ }
+ setNoPtrFieldsVal([]string{
+ "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate",
+ "IterateDelta",
+ "CalcMode", "RefMode",
+ }, reflect.ValueOf(*opts), reflect.ValueOf(wb.CalcPr).Elem())
+ if opts.CalcID != nil {
+ wb.CalcPr.CalcID = int(*opts.CalcID)
+ }
+ if opts.ConcurrentManualCount != nil {
+ wb.CalcPr.ConcurrentManualCount = int(*opts.ConcurrentManualCount)
+ }
+ if opts.IterateCount != nil {
+ wb.CalcPr.IterateCount = int(*opts.IterateCount)
+ }
+ wb.CalcPr.ConcurrentCalc = opts.ConcurrentCalc
+ return err
+}
+
+// GetCalcProps provides a function to gets calculation properties.
+func (f *File) GetCalcProps() (CalcPropsOptions, error) {
+ var opts CalcPropsOptions
+ wb, err := f.workbookReader()
+ if err != nil {
+ return opts, err
+ }
+ if wb.CalcPr == nil {
+ return opts, err
+ }
+ setPtrFieldsVal([]string{
+ "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate",
+ "IterateDelta",
+ "CalcMode", "RefMode",
+ }, reflect.ValueOf(*wb.CalcPr), reflect.ValueOf(&opts).Elem())
+ opts.CalcID = uintPtr(uint(wb.CalcPr.CalcID))
+ opts.ConcurrentManualCount = uintPtr(uint(wb.CalcPr.ConcurrentManualCount))
+ opts.IterateCount = uintPtr(uint(wb.CalcPr.IterateCount))
+ opts.ConcurrentCalc = wb.CalcPr.ConcurrentCalc
+ return opts, err
+}
+
+// ProtectWorkbook provides a function to prevent other users from viewing
+// hidden worksheets, adding, moving, deleting, or hiding worksheets, and
+// renaming worksheets in a workbook. The optional field AlgorithmName
+// specified hash algorithm, support XOR, MD4, MD5, SHA-1, SHA2-56, SHA-384,
+// and SHA-512 currently, if no hash algorithm specified, will be using the XOR
+// algorithm as default. The generated workbook only works on Microsoft Office
+// 2007 and later. For example, protect workbook with protection settings:
+//
+// err := f.ProtectWorkbook(&excelize.WorkbookProtectionOptions{
+// Password: "password",
+// LockStructure: true,
+// })
+func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error {
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ if wb.WorkbookProtection == nil {
+ wb.WorkbookProtection = new(xlsxWorkbookProtection)
+ }
+ if opts == nil {
+ opts = &WorkbookProtectionOptions{}
+ }
+ wb.WorkbookProtection = &xlsxWorkbookProtection{
+ LockStructure: opts.LockStructure,
+ LockWindows: opts.LockWindows,
+ }
+ if opts.Password != "" {
+ if opts.AlgorithmName == "" {
+ opts.AlgorithmName = "SHA-512"
+ }
+ hashValue, saltValue, err := genISOPasswdHash(opts.Password, opts.AlgorithmName, "", int(workbookProtectionSpinCount))
+ if err != nil {
+ return err
+ }
+ wb.WorkbookProtection.WorkbookAlgorithmName = opts.AlgorithmName
+ wb.WorkbookProtection.WorkbookSaltValue = saltValue
+ wb.WorkbookProtection.WorkbookHashValue = hashValue
+ wb.WorkbookProtection.WorkbookSpinCount = int(workbookProtectionSpinCount)
+ }
+ return err
+}
+
+// UnprotectWorkbook provides a function to remove protection for workbook,
+// specified the optional password parameter to remove workbook protection with
+// password verification.
+func (f *File) UnprotectWorkbook(password ...string) error {
+ wb, err := f.workbookReader()
+ if err != nil {
+ return err
+ }
+ // password verification
+ if len(password) > 0 {
+ if wb.WorkbookProtection == nil {
+ return ErrUnprotectWorkbook
+ }
+ if wb.WorkbookProtection.WorkbookAlgorithmName != "" {
+ // check with given salt value
+ hashValue, _, err := genISOPasswdHash(password[0], wb.WorkbookProtection.WorkbookAlgorithmName, wb.WorkbookProtection.WorkbookSaltValue, wb.WorkbookProtection.WorkbookSpinCount)
+ if err != nil {
+ return err
+ }
+ if wb.WorkbookProtection.WorkbookHashValue != hashValue {
+ return ErrUnprotectWorkbookPassword
+ }
+ }
+ }
+ wb.WorkbookProtection = nil
+ return err
+}
+
+// setWorkbook update workbook property of the spreadsheet. Maximum 31
+// characters are allowed in sheet title.
+func (f *File) setWorkbook(name string, sheetID, rid int) {
+ wb, _ := f.workbookReader()
+ wb.Sheets.Sheet = append(wb.Sheets.Sheet, xlsxSheet{
+ Name: name,
+ SheetID: sheetID,
+ ID: "rId" + strconv.Itoa(rid),
+ })
+}
+
+// getWorkbookPath provides a function to get the path of the workbook.xml in
+// the spreadsheet.
+func (f *File) getWorkbookPath() (path string) {
+ if rels, _ := f.relsReader("_rels/.rels"); rels != nil {
+ rels.mu.Lock()
+ defer rels.mu.Unlock()
+ for _, rel := range rels.Relationships {
+ if rel.Type == SourceRelationshipOfficeDocument {
+ path = strings.TrimPrefix(rel.Target, "/")
+ return
+ }
+ }
+ }
+ return
+}
+
+// getWorkbookRelsPath provides a function to get the path of the workbook.xml.rels
+// in the spreadsheet.
+func (f *File) getWorkbookRelsPath() (path string) {
+ wbPath := f.getWorkbookPath()
+ wbDir := filepath.Dir(wbPath)
+ if wbDir == "." {
+ path = "_rels/" + filepath.Base(wbPath) + ".rels"
+ return
+ }
+ path = strings.TrimPrefix(filepath.Dir(wbPath)+"/_rels/"+filepath.Base(wbPath)+".rels", "/")
+ return
+}
+
+// deleteWorkbookRels provides a function to delete relationships in
+// xl/_rels/workbook.xml.rels by given type and target.
+func (f *File) deleteWorkbookRels(relType, relTarget string) (string, error) {
+ var rID string
+ rels, err := f.relsReader(f.getWorkbookRelsPath())
+ if err != nil {
+ return rID, err
+ }
+ if rels == nil {
+ rels = &xlsxRelationships{}
+ }
+ for k, v := range rels.Relationships {
+ if v.Type == relType && v.Target == relTarget {
+ rID = v.ID
+ rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
+ }
+ }
+ return rID, err
+}
+
+// workbookReader provides a function to get the pointer to the workbook.xml
+// structure after deserialization.
+func (f *File) workbookReader() (*xlsxWorkbook, error) {
+ var err error
+ if f.WorkBook == nil {
+ wbPath := f.getWorkbookPath()
+ f.WorkBook = new(xlsxWorkbook)
+ if attrs, ok := f.xmlAttr.Load(wbPath); !ok {
+ d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath))))
+ if attrs == nil {
+ attrs = []xml.Attr{}
+ }
+ attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
+ f.xmlAttr.Store(wbPath, attrs)
+ f.addNameSpaces(wbPath, SourceRelationship)
+ }
+ if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))).
+ Decode(f.WorkBook); err != nil && err != io.EOF {
+ return f.WorkBook, err
+ }
+ }
+ return f.WorkBook, err
+}
+
+// workBookWriter provides a function to save workbook.xml after serialize
+// structure.
+func (f *File) workBookWriter() {
+ if f.WorkBook != nil {
+ if f.WorkBook.DecodeAlternateContent != nil {
+ f.WorkBook.AlternateContent = &xlsxAlternateContent{
+ Content: f.WorkBook.DecodeAlternateContent.Content,
+ XMLNSMC: SourceRelationshipCompatibility.Value,
+ }
+ }
+ f.WorkBook.DecodeAlternateContent = nil
+ output, _ := xml.Marshal(f.WorkBook)
+ f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output)))
+ }
+}
+
+// setContentTypePartRelsExtensions provides a function to set the content type
+// for relationship parts and the Main Document part.
+func (f *File) setContentTypePartRelsExtensions() error {
+ var rels bool
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ for _, v := range content.Defaults {
+ if v.Extension == "rels" {
+ rels = true
+ }
+ }
+ if !rels {
+ content.Defaults = append(content.Defaults, xlsxDefault{
+ Extension: "rels",
+ ContentType: ContentTypeRelationships,
+ })
+ }
+ return err
+}
+
+// setContentTypePartImageExtensions provides a function to set the content type
+// for relationship parts and the Main Document part.
+func (f *File) setContentTypePartImageExtensions() error {
+ imageTypes := map[string]string{
+ "bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/",
+ "svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-",
+ "emz": "image/x-", "wmz": "image/x-",
+ }
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
+ for _, file := range content.Defaults {
+ delete(imageTypes, file.Extension)
+ }
+ for extension, prefix := range imageTypes {
+ content.Defaults = append(content.Defaults, xlsxDefault{
+ Extension: extension,
+ ContentType: prefix + extension,
+ })
+ }
+ return err
+}
+
+// setContentTypePartVMLExtensions provides a function to set the content type
+// for relationship parts and the Main Document part.
+func (f *File) setContentTypePartVMLExtensions() error {
+ var vml bool
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
+ for _, v := range content.Defaults {
+ if v.Extension == "vml" {
+ vml = true
+ }
+ }
+ if !vml {
+ content.Defaults = append(content.Defaults, xlsxDefault{
+ Extension: "vml",
+ ContentType: ContentTypeVML,
+ })
+ }
+ return err
+}
+
+// addContentTypePart provides a function to add content type part relationships
+// in the file [Content_Types].xml by given index and content type.
+func (f *File) addContentTypePart(index int, contentType string) error {
+ setContentType := map[string]func() error{
+ "comments": f.setContentTypePartVMLExtensions,
+ "drawings": f.setContentTypePartImageExtensions,
+ }
+ partNames := map[string]string{
+ "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
+ "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
+ "comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
+ "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
+ "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
+ "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
+ "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
+ "sharedStrings": "/xl/sharedStrings.xml",
+ "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
+ "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
+ }
+ contentTypes := map[string]string{
+ "chart": ContentTypeDrawingML,
+ "chartsheet": ContentTypeSpreadSheetMLChartsheet,
+ "comments": ContentTypeSpreadSheetMLComments,
+ "drawings": ContentTypeDrawing,
+ "table": ContentTypeSpreadSheetMLTable,
+ "pivotTable": ContentTypeSpreadSheetMLPivotTable,
+ "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
+ "sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
+ "slicer": ContentTypeSlicer,
+ "slicerCache": ContentTypeSlicerCache,
+ }
+ s, ok := setContentType[contentType]
+ if ok {
+ if err := s(); err != nil {
+ return err
+ }
+ }
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
+ for _, v := range content.Overrides {
+ if v.PartName == partNames[contentType] {
+ return err
+ }
+ }
+ content.Overrides = append(content.Overrides, xlsxOverride{
+ PartName: partNames[contentType],
+ ContentType: contentTypes[contentType],
+ })
+ return f.setContentTypePartRelsExtensions()
+}
+
+// removeContentTypesPart provides a function to remove relationships by given
+// content type and part name in the file [Content_Types].xml.
+func (f *File) removeContentTypesPart(contentType, partName string) error {
+ if !strings.HasPrefix(partName, "/") {
+ partName = "/xl/" + partName
+ }
+ content, err := f.contentTypesReader()
+ if err != nil {
+ return err
+ }
+ content.mu.Lock()
+ defer content.mu.Unlock()
+ for k, v := range content.Overrides {
+ if v.PartName == partName && v.ContentType == contentType {
+ content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
+ }
+ }
+ return err
+}
diff --git a/workbook_test.go b/workbook_test.go
new file mode 100644
index 0000000000..75f137fc2d
--- /dev/null
+++ b/workbook_test.go
@@ -0,0 +1,82 @@
+package excelize
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWorkbookProps(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetWorkbookProps(nil))
+ wb, err := f.workbookReader()
+ assert.NoError(t, err)
+ wb.WorkbookPr = nil
+ expected := WorkbookPropsOptions{
+ Date1904: boolPtr(true),
+ FilterPrivacy: boolPtr(true),
+ CodeName: stringPtr("code"),
+ }
+ assert.NoError(t, f.SetWorkbookProps(&expected))
+ opts, err := f.GetWorkbookProps()
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+ wb.WorkbookPr = nil
+ opts, err = f.GetWorkbookProps()
+ assert.NoError(t, err)
+ assert.Equal(t, WorkbookPropsOptions{}, opts)
+ // Test set workbook properties with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetWorkbookProps(&expected), "XML syntax error on line 1: invalid UTF-8")
+ // Test get workbook properties with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ _, err = f.GetWorkbookProps()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestCalcProps(t *testing.T) {
+ f := NewFile()
+ assert.NoError(t, f.SetCalcProps(nil))
+ wb, err := f.workbookReader()
+ assert.NoError(t, err)
+ wb.CalcPr = nil
+ expected := CalcPropsOptions{
+ FullCalcOnLoad: boolPtr(true),
+ CalcID: uintPtr(122211),
+ ConcurrentManualCount: uintPtr(5),
+ IterateCount: uintPtr(10),
+ ConcurrentCalc: boolPtr(true),
+ }
+ assert.NoError(t, f.SetCalcProps(&expected))
+ opts, err := f.GetCalcProps()
+ assert.NoError(t, err)
+ assert.Equal(t, expected, opts)
+ wb.CalcPr = nil
+ opts, err = f.GetCalcProps()
+ assert.NoError(t, err)
+ assert.Equal(t, CalcPropsOptions{}, opts)
+ // Test set calculation properties with unsupported optional value
+ assert.Equal(t, newInvalidOptionalValue("CalcMode", "AUTO", supportedCalcMode), f.SetCalcProps(&CalcPropsOptions{CalcMode: stringPtr("AUTO")}))
+ assert.Equal(t, newInvalidOptionalValue("RefMode", "a1", supportedRefMode), f.SetCalcProps(&CalcPropsOptions{RefMode: stringPtr("a1")}))
+ // Test set calculation properties with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ assert.EqualError(t, f.SetCalcProps(&expected), "XML syntax error on line 1: invalid UTF-8")
+ // Test get calculation properties with unsupported charset workbook
+ f.WorkBook = nil
+ f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
+ _, err = f.GetCalcProps()
+ assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
+}
+
+func TestDeleteWorkbookRels(t *testing.T) {
+ f := NewFile()
+ // Test delete pivot table without worksheet relationships
+ f.Relationships.Delete("xl/_rels/workbook.xml.rels")
+ f.Pkg.Delete("xl/_rels/workbook.xml.rels")
+ rID, err := f.deleteWorkbookRels("", "")
+ assert.Empty(t, rID)
+ assert.NoError(t, err)
+}
diff --git a/xmlApp.go b/xmlApp.go
index 1d51095254..e82218df17 100644
--- a/xmlApp.go
+++ b/xmlApp.go
@@ -1,50 +1,62 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import "encoding/xml"
+// AppProperties directly maps the document application properties.
+type AppProperties struct {
+ Application string
+ ScaleCrop bool
+ DocSecurity int
+ Company string
+ LinksUpToDate bool
+ HyperlinksChanged bool
+ AppVersion string
+}
+
// xlsxProperties specifies to an OOXML document properties such as the
// template used, the number of pages and words, and the application name and
// version.
type xlsxProperties struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"`
- Template string
- Manager string
- Company string
- Pages int
- Words int
- Characters int
- PresentationFormat string
- Lines int
- Paragraphs int
- Slides int
- Notes int
- TotalTime int
- HiddenSlides int
- MMClips int
- ScaleCrop bool
+ Vt string `xml:"xmlns:vt,attr"`
+ Template string `xml:",omitempty"`
+ Manager string `xml:",omitempty"`
+ Company string `xml:",omitempty"`
+ Pages int `xml:",omitempty"`
+ Words int `xml:",omitempty"`
+ Characters int `xml:",omitempty"`
+ PresentationFormat string `xml:",omitempty"`
+ Lines int `xml:",omitempty"`
+ Paragraphs int `xml:",omitempty"`
+ Slides int `xml:",omitempty"`
+ Notes int `xml:",omitempty"`
+ TotalTime int `xml:",omitempty"`
+ HiddenSlides int `xml:",omitempty"`
+ MMClips int `xml:",omitempty"`
+ ScaleCrop bool `xml:",omitempty"`
HeadingPairs *xlsxVectorVariant
TitlesOfParts *xlsxVectorLpstr
- LinksUpToDate bool
- CharactersWithSpaces int
- SharedDoc bool
- HyperlinkBase string
+ LinksUpToDate bool `xml:",omitempty"`
+ CharactersWithSpaces int `xml:",omitempty"`
+ SharedDoc bool `xml:",omitempty"`
+ HyperlinkBase string `xml:",omitempty"`
HLinks *xlsxVectorVariant
- HyperlinksChanged bool
+ HyperlinksChanged bool `xml:",omitempty"`
DigSig *xlsxDigSig
- Application string
- AppVersion string
- DocSecurity int
+ Application string `xml:",omitempty"`
+ AppVersion string `xml:",omitempty"`
+ DocSecurity int `xml:",omitempty"`
}
// xlsxVectorVariant specifies the set of hyperlinks that were in this
diff --git a/xmlCalcChain.go b/xmlCalcChain.go
index 401bb5e307..472c87faa1 100644
--- a/xmlCalcChain.go
+++ b/xmlCalcChain.go
@@ -1,19 +1,20 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import "encoding/xml"
-// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain.
+// xlsxCalcChain directly maps the calcChain element. This element represents
+// the root of the calculation chain.
type xlsxCalcChain struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"`
C []xlsxCalcChainC `xml:"c"`
@@ -21,65 +22,103 @@ type xlsxCalcChain struct {
// xlsxCalcChainC directly maps the c element.
//
-// Attributes | Attributes
-// --------------------------+----------------------------------------------------------
-// a (Array) | A Boolean flag indicating whether the cell's formula
-// | is an array formula. True if this cell's formula is
-// | an array formula, false otherwise. If there is a
-// | conflict between this attribute and the t attribute
-// | of the f element (§18.3.1.40), the t attribute takes
-// | precedence. The possible values for this attribute
-// | are defined by the W3C XML Schema boolean datatype.
-// |
-// i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is
-// | omitted, it is assumed to be the same as the i value
-// | of the previous cell.The possible values for this
-// | attribute are defined by the W3C XML Schema int datatype.
-// |
-// l (New Dependency Level) | A Boolean flag indicating that the cell's formula
-// | starts a new dependency level. True if the formula
-// | starts a new dependency level, false otherwise.
-// | Starting a new dependency level means that all
-// | concurrent calculations, and child calculations, shall
-// | be completed - and the cells have new values - before
-// | the calc chain can continue. In other words, this
-// | dependency level might depend on levels that came before
-// | it, and any later dependency levels might depend on
-// | this level; but not later dependency levels can have
-// | any calculations started until this dependency level
-// | completes.The possible values for this attribute are
-// | defined by the W3C XML Schema boolean datatype.
-// |
-// r (Cell Reference) | An A-1 style reference to a cell.The possible values
-// | for this attribute are defined by the ST_CellRef
-// | simple type (§18.18.7).
-// |
-// s (Child Chain) | A Boolean flag indicating whether the cell's formula
-// | is on a child chain. True if this cell is part of a
-// | child chain, false otherwise. If this is omitted, it
-// | is assumed to be the same as the s value of the
-// | previous cell .A child chain is a list of calculations
-// | that occur which depend on the parent to the chain.
-// | There shall not be cross dependencies between child
-// | chains. Child chains are not the same as dependency
-// | levels - a child chain and its parent are all on the
-// | same dependency level. Child chains are series of
-// | calculations that can be independently farmed out to
-// | other threads or processors.The possible values for
-// | this attribute are defined by the W3C XML Schema
-// | boolean datatype.
-// |
-// t (New Thread) | A Boolean flag indicating whether the cell's formula
-// | starts a new thread. True if the cell's formula starts
-// | a new thread, false otherwise.The possible values for
-// | this attribute are defined by the W3C XML Schema
-// | boolean datatype.
-//
+// Attributes | Attributes
+// --------------------------+----------------------------------------------------------
+// a (Array) | A Boolean flag indicating whether the cell's formula
+// | is an array formula. True if this cell's formula is
+// | an array formula, false otherwise. If there is a
+// | conflict between this attribute and the t attribute
+// | of the f element (§18.3.1.40), the t attribute takes
+// | precedence. The possible values for this attribute
+// | are defined by the W3C XML Schema boolean datatype.
+// |
+// i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is
+// | omitted, it is assumed to be the same as the i value
+// | of the previous cell.The possible values for this
+// | attribute are defined by the W3C XML Schema int datatype.
+// |
+// l (New Dependency Level) | A Boolean flag indicating that the cell's formula
+// | starts a new dependency level. True if the formula
+// | starts a new dependency level, false otherwise.
+// | Starting a new dependency level means that all
+// | concurrent calculations, and child calculations, shall
+// | be completed - and the cells have new values - before
+// | the calc chain can continue. In other words, this
+// | dependency level might depend on levels that came before
+// | it, and any later dependency levels might depend on
+// | this level; but not later dependency levels can have
+// | any calculations started until this dependency level
+// | completes.The possible values for this attribute are
+// | defined by the W3C XML Schema boolean datatype.
+// |
+// r (Cell Reference) | An A-1 style reference to a cell.The possible values
+// | for this attribute are defined by the ST_CellRef
+// | simple type (§18.18.7).
+// |
+// s (Child Chain) | A Boolean flag indicating whether the cell's formula
+// | is on a child chain. True if this cell is part of a
+// | child chain, false otherwise. If this is omitted, it
+// | is assumed to be the same as the s value of the
+// | previous cell .A child chain is a list of calculations
+// | that occur which depend on the parent to the chain.
+// | There shall not be cross dependencies between child
+// | chains. Child chains are not the same as dependency
+// | levels - a child chain and its parent are all on the
+// | same dependency level. Child chains are series of
+// | calculations that can be independently farmed out to
+// | other threads or processors.The possible values for
+// | this attribute is defined by the W3C XML Schema
+// | boolean datatype.
+// |
+// t (New Thread) | A Boolean flag indicating whether the cell's formula
+// | starts a new thread. True if the cell's formula starts
+// | a new thread, false otherwise.The possible values for
+// | this attribute is defined by the W3C XML Schema
+// | boolean datatype.
type xlsxCalcChainC struct {
R string `xml:"r,attr"`
- I int `xml:"i,attr"`
+ I int `xml:"i,attr,omitempty"`
L bool `xml:"l,attr,omitempty"`
S bool `xml:"s,attr,omitempty"`
T bool `xml:"t,attr,omitempty"`
A bool `xml:"a,attr,omitempty"`
}
+
+// xlsxVolTypes maps the volatileDependencies part provides a cache of data that
+// supports Real Time Data (RTD) and CUBE functions in the workbook.
+type xlsxVolTypes struct {
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"`
+ VolType []xlsxVolType `xml:"volType"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// xlsxVolType represents dependency information for a specific type of external
+// data server.
+type xlsxVolType struct {
+ Type string `xml:"type,attr"`
+ Main []xlsxVolMain `xml:"main"`
+}
+
+// xlsxVolMain represents dependency information for all topics within a
+// volatile dependency type that share the same first string or function
+// argument.
+type xlsxVolMain struct {
+ First string `xml:"first,attr"`
+ Tp []xlsxVolTopic `xml:"tp"`
+}
+
+// xlsxVolTopic represents dependency information for all topics within a
+// volatile dependency type that share the same first string or argument.
+type xlsxVolTopic struct {
+ T string `xml:"t,attr,omitempty"`
+ V string `xml:"v"`
+ Stp []string `xml:"stp"`
+ Tr []xlsxVolTopicRef `xml:"tr"`
+}
+
+// xlsxVolTopicRef represents the reference to a cell that depends on this
+// topic. Each topic can have one or more cells dependencies.
+type xlsxVolTopicRef struct {
+ R string `xml:"r,attr"`
+ S int `xml:"s,attr"`
+}
diff --git a/xmlChart.go b/xmlChart.go
index fae54263d9..abb0e4adbe 100644
--- a/xmlChart.go
+++ b/xmlChart.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -18,10 +18,7 @@ import "encoding/xml"
// charts, pie charts, scatter charts, or other types of charts.
type xlsxChartSpace struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/chart chartSpace"`
- XMLNSc string `xml:"xmlns:c,attr"`
XMLNSa string `xml:"xmlns:a,attr"`
- XMLNSr string `xml:"xmlns:r,attr"`
- XMLNSc16r2 string `xml:"xmlns:c16r2,attr"`
Date1904 *attrValBool `xml:"date1904"`
Lang *attrValString `xml:"lang"`
RoundedCorners *attrValBool `xml:"roundedCorners"`
@@ -77,7 +74,7 @@ type cTx struct {
type cRich struct {
BodyPr aBodyPr `xml:"a:bodyPr,omitempty"`
LstStyle string `xml:"a:lstStyle,omitempty"`
- P aP `xml:"a:p"`
+ P []aP `xml:"a:p"`
}
// aBodyPr (Body Properties) directly maps the a:bodyPr element. This element
@@ -141,25 +138,25 @@ type aSchemeClr struct {
}
// attrValInt directly maps the val element with integer data type as an
-// attribute。
+// attribute.
type attrValInt struct {
Val *int `xml:"val,attr"`
}
// attrValFloat directly maps the val element with float64 data type as an
-// attribute。
+// attribute.
type attrValFloat struct {
Val *float64 `xml:"val,attr"`
}
// attrValBool directly maps the val element with boolean data type as an
-// attribute。
+// attribute.
type attrValBool struct {
Val *bool `xml:"val,attr"`
}
// attrValString directly maps the val element with string data type as an
-// attribute。
+// attribute.
type attrValString struct {
Val *string `xml:"val,attr"`
}
@@ -174,12 +171,11 @@ type aEa struct {
Typeface string `xml:"typeface,attr"`
}
-// aLatin (Latin Font) directly maps the a:latin element. This element
-// specifies that a Latin font be used for a specific run of text. This font is
-// specified with a typeface attribute much like the others but is specifically
-// classified as a Latin font.
-type aLatin struct {
- Typeface string `xml:"typeface,attr"`
+type xlsxCTTextFont struct {
+ Typeface string `xml:"typeface,attr"`
+ Panose string `xml:"panose,attr,omitempty"`
+ PitchFamily string `xml:"pitchFamily,attr,omitempty"`
+ Charset string `xml:"Charset,attr,omitempty"`
}
// aR directly maps the a:r element.
@@ -194,29 +190,29 @@ type aR struct {
// properties are defined as direct formatting, since they are directly applied
// to the run and supersede any formatting from styles.
type aRPr struct {
- AltLang string `xml:"altLang,attr,omitempty"`
- B bool `xml:"b,attr"`
- Baseline int `xml:"baseline,attr"`
- Bmk string `xml:"bmk,attr,omitempty"`
- Cap string `xml:"cap,attr,omitempty"`
- Dirty bool `xml:"dirty,attr,omitempty"`
- Err bool `xml:"err,attr,omitempty"`
- I bool `xml:"i,attr"`
- Kern int `xml:"kern,attr"`
- Kumimoji bool `xml:"kumimoji,attr,omitempty"`
- Lang string `xml:"lang,attr,omitempty"`
- NoProof bool `xml:"noProof,attr,omitempty"`
- NormalizeH bool `xml:"normalizeH,attr,omitempty"`
- SmtClean bool `xml:"smtClean,attr,omitempty"`
- SmtID uint64 `xml:"smtId,attr,omitempty"`
- Spc int `xml:"spc,attr"`
- Strike string `xml:"strike,attr,omitempty"`
- Sz float64 `xml:"sz,attr,omitempty"`
- U string `xml:"u,attr,omitempty"`
- SolidFill *aSolidFill `xml:"a:solidFill"`
- Latin *aLatin `xml:"a:latin"`
- Ea *aEa `xml:"a:ea"`
- Cs *aCs `xml:"a:cs"`
+ AltLang string `xml:"altLang,attr,omitempty"`
+ B bool `xml:"b,attr"`
+ Baseline int `xml:"baseline,attr"`
+ Bmk string `xml:"bmk,attr,omitempty"`
+ Cap string `xml:"cap,attr,omitempty"`
+ Dirty bool `xml:"dirty,attr,omitempty"`
+ Err bool `xml:"err,attr,omitempty"`
+ I bool `xml:"i,attr"`
+ Kern int `xml:"kern,attr"`
+ Kumimoji bool `xml:"kumimoji,attr,omitempty"`
+ Lang string `xml:"lang,attr,omitempty"`
+ NoProof bool `xml:"noProof,attr,omitempty"`
+ NormalizeH bool `xml:"normalizeH,attr,omitempty"`
+ SmtClean bool `xml:"smtClean,attr,omitempty"`
+ SmtID uint64 `xml:"smtId,attr,omitempty"`
+ Spc int `xml:"spc,attr"`
+ Strike string `xml:"strike,attr,omitempty"`
+ Sz float64 `xml:"sz,attr,omitempty"`
+ U string `xml:"u,attr,omitempty"`
+ SolidFill *aSolidFill `xml:"a:solidFill"`
+ Latin *xlsxCTTextFont `xml:"a:latin"`
+ Ea *aEa `xml:"a:ea"`
+ Cs *aCs `xml:"a:cs"`
}
// cSpPr (Shape Properties) directly maps the spPr element. This element
@@ -252,13 +248,13 @@ type aContourClr struct {
// shapes and text. The line allows for the specifying of many different types
// of outlines including even line dashes and bevels.
type aLn struct {
- Algn string `xml:"algn,attr,omitempty"`
- Cap string `xml:"cap,attr,omitempty"`
- Cmpd string `xml:"cmpd,attr,omitempty"`
- W int `xml:"w,attr,omitempty"`
- NoFill string `xml:"a:noFill,omitempty"`
- Round string `xml:"a:round,omitempty"`
- SolidFill *aSolidFill `xml:"a:solidFill"`
+ Algn string `xml:"algn,attr,omitempty"`
+ Cap string `xml:"cap,attr,omitempty"`
+ Cmpd string `xml:"cmpd,attr,omitempty"`
+ W int `xml:"w,attr,omitempty"`
+ NoFill *attrValString `xml:"a:noFill"`
+ Round string `xml:"a:round,omitempty"`
+ SolidFill *aSolidFill `xml:"a:solidFill"`
}
// cTxPr (Text Properties) directly maps the txPr element. This element
@@ -304,25 +300,26 @@ type cView3D struct {
// cPlotArea directly maps the plotArea element. This element specifies the
// plot area of the chart.
type cPlotArea struct {
- Layout *string `xml:"layout"`
- AreaChart *cCharts `xml:"areaChart"`
- Area3DChart *cCharts `xml:"area3DChart"`
- BarChart *cCharts `xml:"barChart"`
- Bar3DChart *cCharts `xml:"bar3DChart"`
- BubbleChart *cCharts `xml:"bubbleChart"`
- DoughnutChart *cCharts `xml:"doughnutChart"`
- LineChart *cCharts `xml:"lineChart"`
- PieChart *cCharts `xml:"pieChart"`
- Pie3DChart *cCharts `xml:"pie3DChart"`
- OfPieChart *cCharts `xml:"ofPieChart"`
- RadarChart *cCharts `xml:"radarChart"`
- ScatterChart *cCharts `xml:"scatterChart"`
- Surface3DChart *cCharts `xml:"surface3DChart"`
- SurfaceChart *cCharts `xml:"surfaceChart"`
- CatAx []*cAxs `xml:"catAx"`
- ValAx []*cAxs `xml:"valAx"`
- SerAx []*cAxs `xml:"serAx"`
- SpPr *cSpPr `xml:"spPr"`
+ Layout *string `xml:"layout"`
+ AreaChart []*cCharts `xml:"areaChart"`
+ Area3DChart []*cCharts `xml:"area3DChart"`
+ BarChart []*cCharts `xml:"barChart"`
+ Bar3DChart []*cCharts `xml:"bar3DChart"`
+ BubbleChart []*cCharts `xml:"bubbleChart"`
+ DoughnutChart []*cCharts `xml:"doughnutChart"`
+ LineChart []*cCharts `xml:"lineChart"`
+ Line3DChart []*cCharts `xml:"line3DChart"`
+ PieChart []*cCharts `xml:"pieChart"`
+ Pie3DChart []*cCharts `xml:"pie3DChart"`
+ OfPieChart []*cCharts `xml:"ofPieChart"`
+ RadarChart []*cCharts `xml:"radarChart"`
+ ScatterChart []*cCharts `xml:"scatterChart"`
+ Surface3DChart []*cCharts `xml:"surface3DChart"`
+ SurfaceChart []*cCharts `xml:"surfaceChart"`
+ CatAx []*cAxs `xml:"catAx"`
+ ValAx []*cAxs `xml:"valAx"`
+ SerAx []*cAxs `xml:"serAx"`
+ SpPr *cSpPr `xml:"spPr"`
}
// cCharts specifies the common element of the chart.
@@ -336,8 +333,10 @@ type cCharts struct {
VaryColors *attrValBool `xml:"varyColors"`
Wireframe *attrValBool `xml:"wireframe"`
Ser *[]cSer `xml:"ser"`
+ SplitPos *attrValInt `xml:"splitPos"`
SerLines *attrValString `xml:"serLines"`
DLbls *cDLbls `xml:"dLbls"`
+ GapWidth *attrValInt `xml:"gapWidth"`
Shape *attrValString `xml:"shape"`
HoleSize *attrValInt `xml:"holeSize"`
Smooth *attrValBool `xml:"smooth"`
@@ -353,6 +352,7 @@ type cAxs struct {
AxPos *attrValString `xml:"axPos"`
MajorGridlines *cChartLines `xml:"majorGridlines"`
MinorGridlines *cChartLines `xml:"minorGridlines"`
+ Title *cTitle `xml:"title"`
NumFmt *cNumFmt `xml:"numFmt"`
MajorTickMark *attrValString `xml:"majorTickMark"`
MinorTickMark *attrValString `xml:"minorTickMark"`
@@ -478,18 +478,22 @@ type cNumCache struct {
PtCount *attrValInt `xml:"ptCount"`
}
-// cDLbls (Data Lables) directly maps the dLbls element. This element serves
+// cDLbls (Data Labels) directly maps the dLbls element. This element serves
// as a root element that specifies the settings for the data labels for an
// entire series or the entire chart. It contains child elements that specify
// the specific formatting and positioning settings.
type cDLbls struct {
- ShowLegendKey *attrValBool `xml:"showLegendKey"`
- ShowVal *attrValBool `xml:"showVal"`
- ShowCatName *attrValBool `xml:"showCatName"`
- ShowSerName *attrValBool `xml:"showSerName"`
- ShowPercent *attrValBool `xml:"showPercent"`
- ShowBubbleSize *attrValBool `xml:"showBubbleSize"`
- ShowLeaderLines *attrValBool `xml:"showLeaderLines"`
+ NumFmt *cNumFmt `xml:"numFmt"`
+ SpPr *cSpPr `xml:"spPr"`
+ TxPr *cTxPr `xml:"txPr"`
+ DLblPos *attrValString `xml:"dLblPos"`
+ ShowLegendKey *attrValBool `xml:"showLegendKey"`
+ ShowVal *attrValBool `xml:"showVal"`
+ ShowCatName *attrValBool `xml:"showCatName"`
+ ShowSerName *attrValBool `xml:"showSerName"`
+ ShowPercent *attrValBool `xml:"showPercent"`
+ ShowBubbleSize *attrValBool `xml:"showBubbleSize"`
+ ShowLeaderLines *attrValBool `xml:"showLeaderLines"`
}
// cLegend (Legend) directly maps the legend element. This element specifies
@@ -521,137 +525,109 @@ type cPageMargins struct {
T float64 `xml:"t,attr"`
}
-// formatChartAxis directly maps the format settings of the chart axis.
-type formatChartAxis struct {
- Crossing string `json:"crossing"`
- MajorGridlines bool `json:"major_grid_lines"`
- MinorGridlines bool `json:"minor_grid_lines"`
- MajorTickMark string `json:"major_tick_mark"`
- MinorTickMark string `json:"minor_tick_mark"`
- MinorUnitType string `json:"minor_unit_type"`
- MajorUnit float64 `json:"major_unit"`
- MajorUnitType string `json:"major_unit_type"`
- TickLabelSkip int `json:"tick_label_skip"`
- DisplayUnits string `json:"display_units"`
- DisplayUnitsVisible bool `json:"display_units_visible"`
- DateAxis bool `json:"date_axis"`
- ReverseOrder bool `json:"reverse_order"`
- Maximum float64 `json:"maximum"`
- Minimum float64 `json:"minimum"`
- NumFormat string `json:"num_format"`
- NumFont struct {
- Color string `json:"color"`
- Bold bool `json:"bold"`
- Italic bool `json:"italic"`
- Underline bool `json:"underline"`
- } `json:"num_font"`
- LogBase float64 `json:"logbase"`
- NameLayout formatLayout `json:"name_layout"`
-}
-
-type formatChartDimension struct {
- Width int `json:"width"`
- Height int `json:"height"`
-}
-
-// formatChart directly maps the format settings of the chart.
-type formatChart struct {
- Type string `json:"type"`
- Series []formatChartSeries `json:"series"`
- Format formatPicture `json:"format"`
- Dimension formatChartDimension `json:"dimension"`
- Legend formatChartLegend `json:"legend"`
- Title formatChartTitle `json:"title"`
- XAxis formatChartAxis `json:"x_axis"`
- YAxis formatChartAxis `json:"y_axis"`
- Chartarea struct {
- Border struct {
- None bool `json:"none"`
- } `json:"border"`
- Fill struct {
- Color string `json:"color"`
- } `json:"fill"`
- Pattern struct {
- Pattern string `json:"pattern"`
- FgColor string `json:"fg_color"`
- BgColor string `json:"bg_color"`
- } `json:"pattern"`
- } `json:"chartarea"`
- Plotarea struct {
- ShowBubbleSize bool `json:"show_bubble_size"`
- ShowCatName bool `json:"show_cat_name"`
- ShowLeaderLines bool `json:"show_leader_lines"`
- ShowPercent bool `json:"show_percent"`
- ShowSerName bool `json:"show_series_name"`
- ShowVal bool `json:"show_val"`
- Gradient struct {
- Colors []string `json:"colors"`
- } `json:"gradient"`
- Border struct {
- Color string `json:"color"`
- Width int `json:"width"`
- DashType string `json:"dash_type"`
- } `json:"border"`
- Fill struct {
- Color string `json:"color"`
- } `json:"fill"`
- Layout formatLayout `json:"layout"`
- } `json:"plotarea"`
- ShowBlanksAs string `json:"show_blanks_as"`
- ShowHiddenData bool `json:"show_hidden_data"`
- SetRotation int `json:"set_rotation"`
- SetHoleSize int `json:"set_hole_size"`
- order int
-}
-
-// formatChartLegend directly maps the format settings of the chart legend.
-type formatChartLegend struct {
- None bool `json:"none"`
- DeleteSeries []int `json:"delete_series"`
- Font Font `json:"font"`
- Layout formatLayout `json:"layout"`
- Position string `json:"position"`
- ShowLegendEntry bool `json:"show_legend_entry"`
- ShowLegendKey bool `json:"show_legend_key"`
-}
-
-// formatChartSeries directly maps the format settings of the chart series.
-type formatChartSeries struct {
- Name string `json:"name"`
- Categories string `json:"categories"`
- Values string `json:"values"`
- Line struct {
- None bool `json:"none"`
- Color string `json:"color"`
- Width float64 `json:"width"`
- } `json:"line"`
- Marker struct {
- Type string `json:"type"`
- Size int `json:"size"`
- Width float64 `json:"width"`
- Border struct {
- Color string `json:"color"`
- None bool `json:"none"`
- } `json:"border"`
- Fill struct {
- Color string `json:"color"`
- None bool `json:"none"`
- } `json:"fill"`
- } `json:"marker"`
-}
-
-// formatChartTitle directly maps the format settings of the chart title.
-type formatChartTitle struct {
- None bool `json:"none"`
- Name string `json:"name"`
- Overlay bool `json:"overlay"`
- Layout formatLayout `json:"layout"`
-}
-
-// formatLayout directly maps the format settings of the element layout.
-type formatLayout struct {
- X float64 `json:"x"`
- Y float64 `json:"y"`
- Width float64 `json:"width"`
- Height float64 `json:"height"`
+// ChartNumFmt directly maps the number format settings of the chart.
+type ChartNumFmt struct {
+ CustomNumFmt string
+ SourceLinked bool
+}
+
+// ChartAxis directly maps the format settings of the chart axis.
+type ChartAxis struct {
+ None bool
+ MajorGridLines bool
+ MinorGridLines bool
+ MajorUnit float64
+ TickLabelPosition ChartTickLabelPositionType
+ TickLabelSkip int
+ ReverseOrder bool
+ Secondary bool
+ Maximum *float64
+ Minimum *float64
+ Alignment Alignment
+ Font Font
+ LogBase float64
+ NumFmt ChartNumFmt
+ Title []RichTextRun
+ axID int
+}
+
+// ChartDimension directly maps the dimension of the chart.
+type ChartDimension struct {
+ Width uint
+ Height uint
+}
+
+// ChartPlotArea directly maps the format settings of the plot area.
+type ChartPlotArea struct {
+ SecondPlotValues int
+ ShowBubbleSize bool
+ ShowCatName bool
+ ShowLeaderLines bool
+ ShowPercent bool
+ ShowSerName bool
+ ShowVal bool
+ Fill Fill
+ NumFmt ChartNumFmt
+}
+
+// Chart directly maps the format settings of the chart.
+type Chart struct {
+ Type ChartType
+ Series []ChartSeries
+ Format GraphicOptions
+ Dimension ChartDimension
+ Legend ChartLegend
+ Title []RichTextRun
+ VaryColors *bool
+ XAxis ChartAxis
+ YAxis ChartAxis
+ PlotArea ChartPlotArea
+ Fill Fill
+ Border ChartLine
+ ShowBlanksAs string
+ BubbleSize int
+ HoleSize int
+ GapWidth *uint
+ Overlap *int
+ order int
+}
+
+// ChartLegend directly maps the format settings of the chart legend.
+type ChartLegend struct {
+ Position string
+ ShowLegendKey bool
+}
+
+// ChartMarker directly maps the format settings of the chart marker.
+type ChartMarker struct {
+ Fill Fill
+ Symbol string
+ Size int
+}
+
+// ChartLine directly maps the format settings of the chart line.
+type ChartLine struct {
+ Type ChartLineType
+ Smooth bool
+ Width float64
+}
+
+// ChartDataLabel directly maps the format settings of the chart labels.
+type ChartDataLabel struct {
+ Alignment Alignment
+ Font Font
+ Fill Fill
+}
+
+// ChartSeries directly maps the format settings of the chart series.
+type ChartSeries struct {
+ Name string
+ Categories string
+ Values string
+ Sizes string
+ Fill Fill
+ Line ChartLine
+ Marker ChartMarker
+ DataLabel ChartDataLabel
+ DataLabelPosition ChartDataLabelPositionType
}
diff --git a/xmlChartSheet.go b/xmlChartSheet.go
index 30a06931fa..401642cfa2 100644
--- a/xmlChartSheet.go
+++ b/xmlChartSheet.go
@@ -1,13 +1,15 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// struct code generated by github.com/xuri/xgen
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX files. Support reads and writes XLSX file generated by
-// Microsoft Excel™ 2007 and later. Support save file without losing original
-// charts of XLSX. This library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -16,27 +18,27 @@ import "encoding/xml"
// xlsxChartsheet directly maps the chartsheet element of Chartsheet Parts in
// a SpreadsheetML document.
type xlsxChartsheet struct {
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"`
- SheetPr []*xlsxChartsheetPr `xml:"sheetPr"`
- SheetViews []*xlsxChartsheetViews `xml:"sheetViews"`
- SheetProtection []*xlsxChartsheetProtection `xml:"sheetProtection"`
- CustomSheetViews []*xlsxCustomChartsheetViews `xml:"customSheetViews"`
- PageMargins *xlsxPageMargins `xml:"pageMargins"`
- PageSetup []*xlsxPageSetUp `xml:"pageSetup"`
- HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"`
- Drawing *xlsxDrawing `xml:"drawing"`
- DrawingHF []*xlsxDrawingHF `xml:"drawingHF"`
- Picture []*xlsxPicture `xml:"picture"`
- WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"`
- ExtLst []*xlsxExtLst `xml:"extLst"`
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"`
+ SheetPr *xlsxChartsheetPr `xml:"sheetPr"`
+ SheetViews *xlsxChartsheetViews `xml:"sheetViews"`
+ SheetProtection *xlsxChartsheetProtection `xml:"sheetProtection"`
+ CustomSheetViews *xlsxCustomChartsheetViews `xml:"customSheetViews"`
+ PageMargins *xlsxPageMargins `xml:"pageMargins"`
+ PageSetup *xlsxPageSetUp `xml:"pageSetup"`
+ HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"`
+ Drawing *xlsxDrawing `xml:"drawing"`
+ DrawingHF *xlsxDrawingHF `xml:"drawingHF"`
+ Picture *xlsxPicture `xml:"picture"`
+ WebPublishItems *xlsxInnerXML `xml:"webPublishItems"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxChartsheetPr specifies chart sheet properties.
type xlsxChartsheetPr struct {
- XMLName xml.Name `xml:"sheetPr"`
- PublishedAttr bool `xml:"published,attr,omitempty"`
- CodeNameAttr string `xml:"codeName,attr,omitempty"`
- TabColor []*xlsxTabColor `xml:"tabColor"`
+ XMLName xml.Name `xml:"sheetPr"`
+ PublishedAttr bool `xml:"published,attr,omitempty"`
+ CodeNameAttr string `xml:"codeName,attr,omitempty"`
+ TabColor *xlsxColor `xml:"tabColor"`
}
// xlsxChartsheetViews specifies chart sheet views.
@@ -71,13 +73,13 @@ type xlsxChartsheetProtection struct {
// xlsxCustomChartsheetViews collection of custom Chart Sheet View
// information.
type xlsxCustomChartsheetViews struct {
- XMLName xml.Name `xml:"customChartsheetViews"`
+ XMLName xml.Name `xml:"customSheetViews"`
CustomSheetView []*xlsxCustomChartsheetView `xml:"customSheetView"`
}
// xlsxCustomChartsheetView defines custom view properties for chart sheets.
type xlsxCustomChartsheetView struct {
- XMLName xml.Name `xml:"customChartsheetView"`
+ XMLName xml.Name `xml:"customSheetView"`
GUIDAttr string `xml:"guid,attr"`
ScaleAttr uint32 `xml:"scale,attr,omitempty"`
StateAttr string `xml:"state,attr,omitempty"`
diff --git a/xmlComments.go b/xmlComments.go
index 0f02b18813..81072ff694 100644
--- a/xmlComments.go
+++ b/xmlComments.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -24,7 +24,7 @@ import "encoding/xml"
// something special about the cell.
type xlsxComments struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main comments"`
- Authors []xlsxAuthor `xml:"authors"`
+ Authors xlsxAuthor `xml:"authors"`
CommentList xlsxCommentList `xml:"commentList"`
}
@@ -33,7 +33,7 @@ type xlsxComments struct {
// have an author. The maximum length of the author string is an implementation
// detail, but a good guideline is 255 chars.
type xlsxAuthor struct {
- Author string `xml:"author"`
+ Author []string `xml:"author"`
}
// xlsxCommentList (List of Comments) directly maps the xlsxCommentList element.
@@ -69,19 +69,16 @@ type xlsxText struct {
type xlsxPhoneticRun struct {
Sb uint32 `xml:"sb,attr"`
Eb uint32 `xml:"eb,attr"`
- T string `xml:"t,attr"`
-}
-
-// formatComment directly maps the format settings of the comment.
-type formatComment struct {
- Author string `json:"author"`
- Text string `json:"text"`
+ T string `xml:"t"`
}
// Comment directly maps the comment information.
type Comment struct {
- Author string `json:"author"`
- AuthorID int `json:"author_id"`
- Ref string `json:"ref"`
- Text string `json:"text"`
+ Author string
+ AuthorID int
+ Cell string
+ Text string
+ Width uint
+ Height uint
+ Paragraph []RichTextRun
}
diff --git a/xmlContentTypes.go b/xmlContentTypes.go
index 458b117311..79708df11f 100644
--- a/xmlContentTypes.go
+++ b/xmlContentTypes.go
@@ -1,25 +1,29 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "encoding/xml"
+import (
+ "encoding/xml"
+ "sync"
+)
-// xlsxTypes directly maps the types element of content types for relationship
+// xlsxTypes directly maps the types' element of content types for relationship
// parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a
// value.
type xlsxTypes struct {
+ mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"`
- Overrides []xlsxOverride `xml:"Override"`
Defaults []xlsxDefault `xml:"Default"`
+ Overrides []xlsxOverride `xml:"Override"`
}
// xlsxOverride directly maps the override element in the namespace
diff --git a/xmlCore.go b/xmlCore.go
index 9d7fc4551d..f726baf47a 100644
--- a/xmlCore.go
+++ b/xmlCore.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -31,61 +31,61 @@ type DocProperties struct {
Version string
}
+// decodeDcTerms directly maps the DCMI metadata terms for the coreProperties.
+type decodeDcTerms struct {
+ Text string `xml:",chardata"`
+ Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
+}
+
// decodeCoreProperties directly maps the root element for a part of this
// content type shall coreProperties. In order to solve the problem that the
// label structure is changed after serialization and deserialization, two
// different structures are defined. decodeCoreProperties just for
// deserialization.
type decodeCoreProperties struct {
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"`
- Title string `xml:"http://purl.org/dc/elements/1.1/ title,omitempty"`
- Subject string `xml:"http://purl.org/dc/elements/1.1/ subject,omitempty"`
- Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
- Keywords string `xml:"keywords,omitempty"`
- Description string `xml:"http://purl.org/dc/elements/1.1/ description,omitempty"`
- LastModifiedBy string `xml:"lastModifiedBy"`
- Language string `xml:"http://purl.org/dc/elements/1.1/ language,omitempty"`
- Identifier string `xml:"http://purl.org/dc/elements/1.1/ identifier,omitempty"`
- Revision string `xml:"revision,omitempty"`
- Created struct {
- Text string `xml:",chardata"`
- Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
- } `xml:"http://purl.org/dc/terms/ created"`
- Modified struct {
- Text string `xml:",chardata"`
- Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
- } `xml:"http://purl.org/dc/terms/ modified"`
- ContentStatus string `xml:"contentStatus,omitempty"`
- Category string `xml:"category,omitempty"`
- Version string `xml:"version,omitempty"`
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"`
+ Title string `xml:"http://purl.org/dc/elements/1.1/ title,omitempty"`
+ Subject string `xml:"http://purl.org/dc/elements/1.1/ subject,omitempty"`
+ Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
+ Keywords string `xml:"keywords,omitempty"`
+ Description string `xml:"http://purl.org/dc/elements/1.1/ description,omitempty"`
+ LastModifiedBy string `xml:"lastModifiedBy"`
+ Language string `xml:"http://purl.org/dc/elements/1.1/ language,omitempty"`
+ Identifier string `xml:"http://purl.org/dc/elements/1.1/ identifier,omitempty"`
+ Revision string `xml:"revision,omitempty"`
+ Created *decodeDcTerms `xml:"http://purl.org/dc/terms/ created"`
+ Modified *decodeDcTerms `xml:"http://purl.org/dc/terms/ modified"`
+ ContentStatus string `xml:"contentStatus,omitempty"`
+ Category string `xml:"category,omitempty"`
+ Version string `xml:"version,omitempty"`
+}
+
+// xlsxDcTerms directly maps the DCMI metadata terms for the coreProperties.
+type xlsxDcTerms struct {
+ Text string `xml:",chardata"`
+ Type string `xml:"xsi:type,attr"`
}
// xlsxCoreProperties directly maps the root element for a part of this
// content type shall coreProperties.
type xlsxCoreProperties struct {
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"`
- Dc string `xml:"xmlns:dc,attr"`
- Dcterms string `xml:"xmlns:dcterms,attr"`
- Dcmitype string `xml:"xmlns:dcmitype,attr"`
- XSI string `xml:"xmlns:xsi,attr"`
- Title string `xml:"dc:title,omitempty"`
- Subject string `xml:"dc:subject,omitempty"`
- Creator string `xml:"dc:creator"`
- Keywords string `xml:"keywords,omitempty"`
- Description string `xml:"dc:description,omitempty"`
- LastModifiedBy string `xml:"lastModifiedBy"`
- Language string `xml:"dc:language,omitempty"`
- Identifier string `xml:"dc:identifier,omitempty"`
- Revision string `xml:"revision,omitempty"`
- Created struct {
- Text string `xml:",chardata"`
- Type string `xml:"xsi:type,attr"`
- } `xml:"dcterms:created"`
- Modified struct {
- Text string `xml:",chardata"`
- Type string `xml:"xsi:type,attr"`
- } `xml:"dcterms:modified"`
- ContentStatus string `xml:"contentStatus,omitempty"`
- Category string `xml:"category,omitempty"`
- Version string `xml:"version,omitempty"`
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"`
+ Dc string `xml:"xmlns:dc,attr"`
+ Dcterms string `xml:"xmlns:dcterms,attr"`
+ Dcmitype string `xml:"xmlns:dcmitype,attr"`
+ XSI string `xml:"xmlns:xsi,attr"`
+ Title string `xml:"dc:title,omitempty"`
+ Subject string `xml:"dc:subject,omitempty"`
+ Creator string `xml:"dc:creator"`
+ Keywords string `xml:"keywords,omitempty"`
+ Description string `xml:"dc:description,omitempty"`
+ LastModifiedBy string `xml:"lastModifiedBy"`
+ Language string `xml:"dc:language,omitempty"`
+ Identifier string `xml:"dc:identifier,omitempty"`
+ Revision string `xml:"revision,omitempty"`
+ Created *xlsxDcTerms `xml:"dcterms:created"`
+ Modified *xlsxDcTerms `xml:"dcterms:modified"`
+ ContentStatus string `xml:"contentStatus,omitempty"`
+ Category string `xml:"category,omitempty"`
+ Version string `xml:"version,omitempty"`
}
diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go
index 8aa22dbfa6..8a20c5d5c6 100644
--- a/xmlDecodeDrawing.go
+++ b/xmlDecodeDrawing.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -18,27 +18,67 @@ import "encoding/xml"
// specifies a two cell anchor placeholder for a group, a shape, or a drawing
// element. It moves with cells and its extents are in EMU units.
type decodeCellAnchor struct {
- EditAs string `xml:"editAs,attr,omitempty"`
- From *decodeFrom `xml:"from"`
- To *decodeTo `xml:"to"`
- Sp *decodeSp `xml:"sp"`
- ClientData *decodeClientData `xml:"clientData"`
- Content string `xml:",innerxml"`
-}
-
-// xdrSp (Shape) directly maps the sp element. This element specifies the
-// existence of a single shape. A shape can either be a preset or a custom
-// geometry, defined using the SpreadsheetDrawingML framework. In addition to
-// a geometry each shape can have both visual and non-visual properties
-// attached. Text and corresponding styling information can also be attached
-// to a shape. This shape is specified along with all other shapes within
-// either the shape tree or group shape elements.
+ EditAs string `xml:"editAs,attr,omitempty"`
+ From *decodeFrom `xml:"from"`
+ To *decodeTo `xml:"to"`
+ Sp *decodeSp `xml:"sp"`
+ Pic *decodePic `xml:"pic"`
+ ClientData *decodeClientData `xml:"clientData"`
+ AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
+ Content string `xml:",innerxml"`
+}
+
+// decodeCellAnchorPos defines the structure used to deserialize the cell anchor
+// for adjust drawing object on inserting/deleting column/rows.
+type decodeCellAnchorPos struct {
+ EditAs string `xml:"editAs,attr,omitempty"`
+ From *xlsxFrom `xml:"from"`
+ To *xlsxTo `xml:"to"`
+ Pos *xlsxInnerXML `xml:"pos"`
+ Ext *xlsxInnerXML `xml:"ext"`
+ Sp *xlsxSp `xml:"sp"`
+ GrpSp *xlsxInnerXML `xml:"grpSp"`
+ GraphicFrame *xlsxInnerXML `xml:"graphicFrame"`
+ CxnSp *xlsxInnerXML `xml:"cxnSp"`
+ Pic *xlsxInnerXML `xml:"pic"`
+ ContentPart *xlsxInnerXML `xml:"contentPart"`
+ AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
+ ClientData *xlsxInnerXML `xml:"clientData"`
+}
+
+// decodeChoice defines the structure used to deserialize the mc:Choice element.
+type decodeChoice struct {
+ XMLName xml.Name `xml:"Choice"`
+ XMLNSA14 string `xml:"a14,attr"`
+ XMLNSSle15 string `xml:"sle15,attr"`
+ Requires string `xml:"Requires,attr"`
+ GraphicFrame decodeGraphicFrame `xml:"graphicFrame"`
+}
+
+// decodeGraphicFrame defines the structure used to deserialize the
+// xdr:graphicFrame element.
+type decodeGraphicFrame struct {
+ Macro string `xml:"macro,attr"`
+ NvGraphicFramePr decodeNvGraphicFramePr `xml:"nvGraphicFramePr"`
+}
+
+// decodeNvGraphicFramePr defines the structure used to deserialize the
+// xdr:nvGraphicFramePr element.
+type decodeNvGraphicFramePr struct {
+ CNvPr decodeCNvPr `xml:"cNvPr"`
+}
+
+// decodeSp defines the structure used to deserialize the sp element.
type decodeSp struct {
- NvSpPr *decodeNvSpPr `xml:"nvSpPr"`
- SpPr *decodeSpPr `xml:"spPr"`
+ Macro string `xml:"macro,attr,omitempty"`
+ TextLink string `xml:"textlink,attr,omitempty"`
+ FLocksText bool `xml:"fLocksText,attr,omitempty"`
+ FPublished *bool `xml:"fPublished,attr"`
+ NvSpPr *decodeNvSpPr `xml:"nvSpPr"`
+ SpPr *decodeSpPr `xml:"spPr"`
}
-// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr
+// decodeNvSpPr (Non-Visual Properties for a Shape) directly maps the nvSpPr
// element. This element specifies all non-visual properties for a shape. This
// element is a container for the non-visual identification properties, shape
// properties and application properties that are to be associated with a
@@ -46,7 +86,7 @@ type decodeSp struct {
// appearance of the shape to be stored.
type decodeNvSpPr struct {
CNvPr *decodeCNvPr `xml:"cNvPr"`
- ExtLst *decodeExt `xml:"extLst"`
+ ExtLst *decodeAExt `xml:"extLst"`
CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"`
}
@@ -63,23 +103,13 @@ type decodeCNvSpPr struct {
// changed after serialization and deserialization, two different structures
// are defined. decodeWsDr just for deserialization.
type decodeWsDr struct {
- A string `xml:"xmlns a,attr"`
- Xdr string `xml:"xmlns xdr,attr"`
- R string `xml:"xmlns r,attr"`
- OneCellAnchor []*decodeCellAnchor `xml:"oneCellAnchor,omitempty"`
- TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"`
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"`
-}
-
-// decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape
-// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element
-// specifies a two cell anchor placeholder for a group, a shape, or a drawing
-// element. It moves with cells and its extents are in EMU units.
-type decodeTwoCellAnchor struct {
- From *decodeFrom `xml:"from"`
- To *decodeTo `xml:"to"`
- Pic *decodePic `xml:"pic,omitempty"`
- ClientData *decodeClientData `xml:"clientData"`
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr"`
+ A string `xml:"xmlns a,attr"`
+ Xdr string `xml:"xmlns xdr,attr"`
+ R string `xml:"xmlns r,attr"`
+ AlternateContent []*xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"`
+ OneCellAnchor []*decodeCellAnchor `xml:"oneCellAnchor,omitempty"`
+ TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"`
}
// decodeCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
@@ -87,10 +117,11 @@ type decodeTwoCellAnchor struct {
// information that does not affect the appearance of the picture to be
// stored.
type decodeCNvPr struct {
- ID int `xml:"id,attr"`
- Name string `xml:"name,attr"`
- Descr string `xml:"descr,attr"`
- Title string `xml:"title,attr,omitempty"`
+ XMLName xml.Name `xml:"cNvPr"`
+ ID int `xml:"id,attr"`
+ Name string `xml:"name,attr"`
+ Descr string `xml:"descr,attr"`
+ Title string `xml:"title,attr,omitempty"`
}
// decodePicLocks directly maps the picLocks (Picture Locks). This element
@@ -111,10 +142,8 @@ type decodePicLocks struct {
NoSelect bool `xml:"noSelect,attr,omitempty"`
}
-// decodeBlip directly maps the blip element in the namespace
-// http://purl.oclc.org/ooxml/officeDoc ument/relationships - This element
-// specifies the existence of an image (binary large image or picture) and
-// contains a reference to the image data.
+// decodeBlip element specifies the existence of an image (binary large image
+// or picture) and contains a reference to the image data.
type decodeBlip struct {
Embed string `xml:"embed,attr"`
Cstate string `xml:"cstate,attr,omitempty"`
@@ -135,8 +164,8 @@ type decodeOff struct {
Y int `xml:"y,attr"`
}
-// decodeExt directly maps the ext element.
-type decodeExt struct {
+// decodeAExt directly maps the a:ext element.
+type decodeAExt struct {
Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"`
}
@@ -154,8 +183,8 @@ type decodePrstGeom struct {
// frame. This transformation is applied to the graphic frame just as it would
// be for a shape or group shape.
type decodeXfrm struct {
- Off decodeOff `xml:"off"`
- Ext decodeExt `xml:"ext"`
+ Off decodeOff `xml:"off"`
+ Ext decodeAExt `xml:"ext"`
}
// decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@@ -200,7 +229,7 @@ type decodeSpPr struct {
// decodePic elements encompass the definition of pictures within the
// DrawingML framework. While pictures are in many ways very similar to shapes
// they have specific properties that are unique in order to optimize for
-// picture- specific scenarios.
+// picture-specific scenarios.
type decodePic struct {
NvPicPr decodeNvPicPr `xml:"nvPicPr"`
BlipFill decodeBlipFill `xml:"blipFill"`
@@ -233,3 +262,15 @@ type decodeClientData struct {
FLocksWithSheet bool `xml:"fLocksWithSheet,attr"`
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
}
+
+// decodeCellImages directly maps the Kingsoft WPS Office embedded cell images.
+type decodeCellImages struct {
+ XMLName xml.Name `xml:"http://www.wps.cn/officeDocument/2017/etCustomData cellImages"`
+ CellImage []decodeCellImage `xml:"cellImage"`
+}
+
+// decodeCellImage defines the structure used to deserialize the Kingsoft WPS
+// Office embedded cell images.
+type decodeCellImage struct {
+ Pic decodePic `xml:"pic"`
+}
diff --git a/xmlDrawing.go b/xmlDrawing.go
index 64d2bc5fbe..f363849014 100644
--- a/xmlDrawing.go
+++ b/xmlDrawing.go
@@ -1,102 +1,21 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "encoding/xml"
-
-// Source relationship and namespace.
-var (
- SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
- SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
- NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
- NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
-)
-
-// Source relationship and namespace.
-const (
- SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
- SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
- SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
- SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
- SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
- SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
- SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
- SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
- SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
- SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
- SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
- SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
- SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
- SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
- SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart"
- SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"
- SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart"
- NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main"
- NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart"
- NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
- NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"
- NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main"
- NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main"
- NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
- NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
- StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
- StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
- StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
- StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
- StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
- NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
- NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
- NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/"
- ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
- ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
- ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
- ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
- ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
- ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
- ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
- ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
- ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
- ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
- ContentTypeVBA = "application/vnd.ms-office.vbaProject"
- ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
- // ExtURIConditionalFormattings is the extLst child element
- // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element
- // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of
- // new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7)
- ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}"
- ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}"
- ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
- ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}"
- ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
- ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
- ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
- ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
- ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
- ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
- ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
- ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
-)
-
-// Excel specifications and limits
-const (
- FileNameLength = 207
- TotalRows = 1048576
- TotalColumns = 16384
- TotalSheetHyperlinks = 65529
- TotalCellChars = 32767
+import (
+ "encoding/xml"
+ "sync"
)
-var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"}
-
// xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
// element specifies non-visual canvas properties. This allows for additional
// information that does not affect the appearance of the picture to be stored.
@@ -141,14 +60,13 @@ type xlsxPicLocks struct {
NoSelect bool `xml:"noSelect,attr,omitempty"`
}
-// xlsxBlip directly maps the blip element in the namespace
-// http://purl.oclc.org/ooxml/officeDoc ument/relationships - This element
-// specifies the existence of an image (binary large image or picture) and
-// contains a reference to the image data.
+// xlsxBlip element specifies the existence of an image (binary large image or
+// picture) and contains a reference to the image data.
type xlsxBlip struct {
- Embed string `xml:"r:embed,attr"`
- Cstate string `xml:"cstate,attr,omitempty"`
- R string `xml:"xmlns:r,attr"`
+ Embed string `xml:"r:embed,attr"`
+ Cstate string `xml:"cstate,attr,omitempty"`
+ R string `xml:"xmlns:r,attr"`
+ ExtList *xlsxEGOfficeArtExtensionList `xml:"a:extLst"`
}
// xlsxStretch directly maps the stretch element. This element specifies that a
@@ -165,8 +83,8 @@ type xlsxOff struct {
Y int `xml:"y,attr"`
}
-// xlsxExt directly maps the ext element.
-type xlsxExt struct {
+// aExt directly maps the a:ext element.
+type aExt struct {
Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"`
}
@@ -185,7 +103,7 @@ type xlsxPrstGeom struct {
// be for a shape or group shape.
type xlsxXfrm struct {
Off xlsxOff `xml:"a:off"`
- Ext xlsxExt `xml:"a:ext"`
+ Ext aExt `xml:"a:ext"`
}
// xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@@ -208,6 +126,28 @@ type xlsxNvPicPr struct {
CNvPicPr xlsxCNvPicPr `xml:"xdr:cNvPicPr"`
}
+// xlsxCTSVGBlip specifies a graphic element in Scalable Vector Graphics (SVG)
+// format.
+type xlsxCTSVGBlip struct {
+ XMLNSaAVG string `xml:"xmlns:asvg,attr"`
+ Embed string `xml:"r:embed,attr"`
+ Link string `xml:"r:link,attr,omitempty"`
+}
+
+// xlsxCTOfficeArtExtension used for future extensibility and is seen elsewhere
+// throughout the drawing area.
+type xlsxCTOfficeArtExtension struct {
+ XMLName xml.Name `xml:"a:ext"`
+ URI string `xml:"uri,attr"`
+ SVGBlip xlsxCTSVGBlip `xml:"asvg:svgBlip"`
+}
+
+// xlsxEGOfficeArtExtensionList used for future extensibility and is seen
+// elsewhere throughout the drawing area.
+type xlsxEGOfficeArtExtensionList struct {
+ Ext []xlsxCTOfficeArtExtension `xml:"ext"`
+}
+
// xlsxBlipFill directly maps the blipFill (Picture Fill). This element
// specifies the kind of picture fill that the picture object has. Because a
// picture has a picture fill already by default, it is possible to have two
@@ -217,14 +157,24 @@ type xlsxBlipFill struct {
Stretch xlsxStretch `xml:"a:stretch"`
}
+// xlsxLineProperties specifies the width of a line in EMUs. This simple type
+// has a minimum value of greater than or equal to 0. This simple type has a
+// maximum value of less than or equal to 20116800.
+type xlsxLineProperties struct {
+ W int `xml:"w,attr,omitempty"`
+ SolidFill *xlsxInnerXML `xml:"a:solidFill"`
+}
+
// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies
// the visual shape properties that can be applied to a picture. These are the
// same properties that are allowed to describe the visual properties of a shape
// but are used here to describe the visual appearance of a picture within a
// document.
type xlsxSpPr struct {
- Xfrm xlsxXfrm `xml:"a:xfrm"`
- PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`
+ Xfrm xlsxXfrm `xml:"a:xfrm"`
+ PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`
+ SolidFill *xlsxInnerXML `xml:"a:solidFill"`
+ Ln xlsxLineProperties `xml:"a:ln"`
}
// xlsxPic elements encompass the definition of pictures within the DrawingML
@@ -264,20 +214,53 @@ type xdrClientData struct {
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
}
-// xdrCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size)
-// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two
-// cell anchor placeholder for a group, a shape, or a drawing element. It moves
-// with cells and its extents are in EMU units.
+// xdrCellAnchor specifies a oneCellAnchor (One Cell Anchor Shape Size) and
+// twoCellAnchor (Two Cell Anchor Shape Size) placeholder for a group, a shape,
+// or a drawing element. It moves with cells and its extents are in EMU units.
type xdrCellAnchor struct {
- EditAs string `xml:"editAs,attr,omitempty"`
- Pos *xlsxPoint2D `xml:"xdr:pos"`
- From *xlsxFrom `xml:"xdr:from"`
- To *xlsxTo `xml:"xdr:to"`
- Ext *xlsxExt `xml:"xdr:ext"`
- Sp *xdrSp `xml:"xdr:sp"`
- Pic *xlsxPic `xml:"xdr:pic,omitempty"`
- GraphicFrame string `xml:",innerxml"`
- ClientData *xdrClientData `xml:"xdr:clientData"`
+ EditAs string `xml:"editAs,attr,omitempty"`
+ Pos *xlsxPoint2D `xml:"xdr:pos"`
+ From *xlsxFrom `xml:"xdr:from"`
+ To *xlsxTo `xml:"xdr:to"`
+ Ext *aExt `xml:"xdr:ext"`
+ Sp *xdrSp `xml:"xdr:sp"`
+ Pic *xlsxPic `xml:"xdr:pic,omitempty"`
+ GraphicFrame string `xml:",innerxml"`
+ AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
+ ClientData *xdrClientData `xml:"xdr:clientData"`
+}
+
+// xlsxCellAnchorPos defines the structure used to serialize the cell anchor for
+// adjust drawing object on inserting/deleting column/rows.
+type xlsxCellAnchorPos struct {
+ EditAs string `xml:"editAs,attr,omitempty"`
+ From *xlsxFrom `xml:"xdr:from"`
+ To *xlsxTo `xml:"xdr:to"`
+ Pos *xlsxInnerXML `xml:"xdr:pos"`
+ Ext *xlsxInnerXML `xml:"xdr:ext"`
+ Sp *xlsxSp `xml:"xdr:sp"`
+ GrpSp *xlsxInnerXML `xml:"xdr:grpSp"`
+ GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"`
+ CxnSp *xlsxInnerXML `xml:"xdr:cxnSp"`
+ Pic *xlsxInnerXML `xml:"xdr:pic"`
+ ContentPart *xlsxInnerXML `xml:"xdr:contentPart"`
+ AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
+ ClientData *xlsxInnerXML `xml:"xdr:clientData"`
+}
+
+// xdrSp (Shape) directly maps the sp element. This element specifies the
+// existence of a single shape. A shape can either be a preset or a custom
+// geometry, defined using the SpreadsheetDrawingML framework. In addition to
+// a geometry each shape can have both visual and non-visual properties
+// attached. Text and corresponding styling information can also be attached
+// to a shape. This shape is specified along with all other shapes within
+// either the shape tree or group shape elements.
+type xlsxSp struct {
+ Macro string `xml:"macro,attr,omitempty"`
+ TextLink string `xml:"textlink,attr,omitempty"`
+ FLocksText bool `xml:"fLocksText,attr,omitempty"`
+ FPublished *bool `xml:"fPublished,attr"`
+ Content string `xml:",innerxml"`
}
// xlsxPoint2D describes the position of a drawing element within a spreadsheet.
@@ -290,13 +273,16 @@ type xlsxPoint2D struct {
// xlsxWsDr directly maps the root element for a part of this content type shall
// wsDr.
type xlsxWsDr struct {
- XMLName xml.Name `xml:"xdr:wsDr"`
- AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"`
- OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"`
- TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"`
- A string `xml:"xmlns:a,attr,omitempty"`
- Xdr string `xml:"xmlns:xdr,attr,omitempty"`
- R string `xml:"xmlns:r,attr,omitempty"`
+ mu sync.Mutex
+ XMLName xml.Name `xml:"xdr:wsDr"`
+ NS string `xml:"xmlns,attr,omitempty"`
+ A string `xml:"xmlns:a,attr,omitempty"`
+ Xdr string `xml:"xmlns:xdr,attr,omitempty"`
+ R string `xml:"xmlns:r,attr,omitempty"`
+ AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
+ AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"`
+ OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"`
+ TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"`
}
// xlsxGraphicFrame (Graphic Frame) directly maps the xdr:graphicFrame element.
@@ -339,6 +325,12 @@ type xlsxGraphic struct {
type xlsxGraphicData struct {
URI string `xml:"uri,attr"`
Chart *xlsxChart `xml:"c:chart,omitempty"`
+ Sle *xlsxSle `xml:"sle:slicer"`
+}
+
+type xlsxSle struct {
+ XMLNS string `xml:"xmlns:sle,attr"`
+ Name string `xml:"name,attr"`
}
// xlsxChart (Chart) directly maps the c:chart element.
@@ -356,6 +348,7 @@ type xlsxChart struct {
// This shape is specified along with all other shapes within either the shape
// tree or group shape elements.
type xdrSp struct {
+ XMLName xml.Name `xml:"xdr:sp"`
Macro string `xml:"macro,attr"`
Textlink string `xml:"textlink,attr"`
NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"`
@@ -429,41 +422,46 @@ type xdrTxBody struct {
P []*aP `xml:"a:p"`
}
-// formatPicture directly maps the format settings of the picture.
-type formatPicture struct {
- FPrintsWithSheet bool `json:"print_obj"`
- FLocksWithSheet bool `json:"locked"`
- NoChangeAspect bool `json:"lock_aspect_ratio"`
- Autofit bool `json:"autofit"`
- OffsetX int `json:"x_offset"`
- OffsetY int `json:"y_offset"`
- XScale float64 `json:"x_scale"`
- YScale float64 `json:"y_scale"`
- Hyperlink string `json:"hyperlink"`
- HyperlinkType string `json:"hyperlink_type"`
- Positioning string `json:"positioning"`
-}
-
-// formatShape directly maps the format settings of the shape.
-type formatShape struct {
- Type string `json:"type"`
- Width int `json:"width"`
- Height int `json:"height"`
- Format formatPicture `json:"format"`
- Color formatShapeColor `json:"color"`
- Paragraph []formatShapeParagraph `json:"paragraph"`
-}
-
-// formatShapeParagraph directly maps the format settings of the paragraph in
-// the shape.
-type formatShapeParagraph struct {
- Font Font `json:"font"`
- Text string `json:"text"`
-}
-
-// formatShapeColor directly maps the color settings of the shape.
-type formatShapeColor struct {
- Line string `json:"line"`
- Fill string `json:"fill"`
- Effect string `json:"effect"`
+// Picture maps the format settings of the picture.
+type Picture struct {
+ Extension string
+ File []byte
+ Format *GraphicOptions
+ InsertType PictureInsertType
+}
+
+// GraphicOptions directly maps the format settings of the picture.
+type GraphicOptions struct {
+ AltText string
+ PrintObject *bool
+ Locked *bool
+ LockAspectRatio bool
+ AutoFit bool
+ AutoFitIgnoreAspect bool
+ OffsetX int
+ OffsetY int
+ ScaleX float64
+ ScaleY float64
+ Hyperlink string
+ HyperlinkType string
+ Positioning string
+}
+
+// Shape directly maps the format settings of the shape.
+type Shape struct {
+ Cell string
+ Type string
+ Macro string
+ Width uint
+ Height uint
+ Format GraphicOptions
+ Fill Fill
+ Line ShapeLine
+ Paragraph []RichTextRun
+}
+
+// ShapeLine directly maps the line settings of the shape.
+type ShapeLine struct {
+ Color string
+ Width *float64
}
diff --git a/xmlMetaData.go b/xmlMetaData.go
new file mode 100644
index 0000000000..90542fbd39
--- /dev/null
+++ b/xmlMetaData.go
@@ -0,0 +1,117 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import "encoding/xml"
+
+// xlsxMetadata directly maps the metadata element. A cell in a spreadsheet
+// application can have metadata associated with it. Metadata is just a set of
+// additional properties about the particular cell, and this metadata is stored
+// in the metadata xml part. There are two types of metadata: cell metadata and
+// value metadata. Cell metadata contains information about the cell itself,
+// and this metadata can be carried along with the cell as it moves
+// (insert, shift, copy/paste, merge, unmerge, etc). Value metadata is
+// information about the value of a particular cell. Value metadata properties
+// can be propagated along with the value as it is referenced in formulas.
+type xlsxMetadata struct {
+ XMLName xml.Name `xml:"metadata"`
+ MetadataTypes *xlsxInnerXML `xml:"metadataTypes"`
+ MetadataStrings *xlsxInnerXML `xml:"metadataStrings"`
+ MdxMetadata *xlsxInnerXML `xml:"mdxMetadata"`
+ FutureMetadata []xlsxFutureMetadata `xml:"futureMetadata"`
+ CellMetadata *xlsxMetadataBlocks `xml:"cellMetadata"`
+ ValueMetadata *xlsxMetadataBlocks `xml:"valueMetadata"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxFutureMetadata directly maps the futureMetadata element. This element
+// represents future metadata information.
+type xlsxFutureMetadata struct {
+ Bk []xlsxFutureMetadataBlock `xml:"bk"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxFutureMetadataBlock directly maps the kb element. This element represents
+// a block of future metadata information. This is a location for storing
+// feature extension information.
+type xlsxFutureMetadataBlock struct {
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxMetadataBlocks directly maps the metadata element. This element
+// represents cell metadata information. Cell metadata is information metadata
+// about a specific cell, and it stays tied to that cell position.
+type xlsxMetadataBlocks struct {
+ Count int `xml:"count,attr,omitempty"`
+ Bk []xlsxMetadataBlock `xml:"bk"`
+}
+
+// xlsxMetadataBlock directly maps the bk element. This element represents a
+// block of metadata records.
+type xlsxMetadataBlock struct {
+ Rc []xlsxMetadataRecord `xml:"rc"`
+}
+
+// xlsxMetadataRecord directly maps the rc element. This element represents a
+// reference to a specific metadata record.
+type xlsxMetadataRecord struct {
+ T int `xml:"t,attr"`
+ V int `xml:"v,attr"`
+}
+
+// xlsxRichValueData directly maps the rvData element that specifies rich value
+// data.
+type xlsxRichValueData struct {
+ XMLName xml.Name `xml:"rvData"`
+ Count int `xml:"count,attr,omitempty"`
+ Rv []xlsxRichValue `xml:"rv"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxRichValue directly maps the rv element that specifies rich value data
+// information for a single rich value
+type xlsxRichValue struct {
+ S int `xml:"s,attr"`
+ V []string `xml:"v"`
+ Fb *xlsxInnerXML `xml:"fb"`
+}
+
+// xlsxRichValueRels directly maps the richValueRels element. This element that
+// specifies a list of rich value relationships.
+type xlsxRichValueRels struct {
+ XMLName xml.Name `xml:"richValueRels"`
+ Rels []xlsxRichValueRelRelationship `xml:"rel"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxRichValueRelRelationship directly maps the rel element. This element
+// specifies a relationship for a rich value property.
+type xlsxRichValueRelRelationship struct {
+ ID string `xml:"id,attr"`
+}
+
+// xlsxWebImagesSupportingRichData directly maps the webImagesSrd element. This
+// element specifies a list of sets of properties associated with web image rich
+// values.
+type xlsxWebImagesSupportingRichData struct {
+ XMLName xml.Name `xml:"webImagesSrd"`
+ WebImageSrd []xlsxWebImageSupportingRichData `xml:"webImageSrd"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxWebImageSupportingRichData directly maps the webImageSrd element. This
+// element specifies a set of properties for a web image rich value.
+type xlsxWebImageSupportingRichData struct {
+ Address xlsxExternalReference `xml:"address"`
+ MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"`
+ Blip xlsxExternalReference `xml:"blip"`
+}
diff --git a/xmlPivotCache.go b/xmlPivotCache.go
index feaec54f03..f634e61a0e 100644
--- a/xmlPivotCache.go
+++ b/xmlPivotCache.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -79,8 +79,7 @@ type xlsxWorksheetSource struct {
// PivotTable is a collection of ranges in the workbook. The ranges are
// specified in the rangeSets collection. The logic for how the application
// consolidates the data in the ranges is application- defined.
-type xlsxConsolidation struct {
-}
+type xlsxConsolidation struct{}
// xlsxCacheFields represents the collection of field definitions in the
// source data.
@@ -121,31 +120,30 @@ type xlsxCacheField struct {
// those values that are referenced in multiple places across all the
// PivotTable parts.
type xlsxSharedItems struct {
- ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"`
- ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"`
- ContainsDate bool `xml:"containsDate,attr,omitempty"`
- ContainsString bool `xml:"containsString,attr,omitempty"`
- ContainsBlank bool `xml:"containsBlank,attr,omitempty"`
- ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"`
- ContainsNumber bool `xml:"containsNumber,attr,omitempty"`
- ContainsInteger bool `xml:"containsInteger,attr,omitempty"`
- MinValue float64 `xml:"minValue,attr,omitempty"`
- MaxValue float64 `xml:"maxValue,attr,omitempty"`
- MinDate string `xml:"minDate,attr,omitempty"`
- MaxDate string `xml:"maxDate,attr,omitempty"`
- Count int `xml:"count,attr"`
- LongText bool `xml:"longText,attr,omitempty"`
- M *xlsxMissing `xml:"m"`
- N *xlsxNumber `xml:"n"`
- B *xlsxBoolean `xml:"b"`
- E *xlsxError `xml:"e"`
- S *xlsxString `xml:"s"`
- D *xlsxDateTime `xml:"d"`
+ ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"`
+ ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"`
+ ContainsDate bool `xml:"containsDate,attr,omitempty"`
+ ContainsString bool `xml:"containsString,attr,omitempty"`
+ ContainsBlank bool `xml:"containsBlank,attr,omitempty"`
+ ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"`
+ ContainsNumber bool `xml:"containsNumber,attr,omitempty"`
+ ContainsInteger bool `xml:"containsInteger,attr,omitempty"`
+ MinValue float64 `xml:"minValue,attr,omitempty"`
+ MaxValue float64 `xml:"maxValue,attr,omitempty"`
+ MinDate string `xml:"minDate,attr,omitempty"`
+ MaxDate string `xml:"maxDate,attr,omitempty"`
+ Count int `xml:"count,attr"`
+ LongText bool `xml:"longText,attr,omitempty"`
+ M []xlsxMissing `xml:"m"`
+ N []xlsxNumber `xml:"n"`
+ B []xlsxBoolean `xml:"b"`
+ E []xlsxError `xml:"e"`
+ S []xlsxString `xml:"s"`
+ D []xlsxDateTime `xml:"d"`
}
// xlsxMissing represents a value that was not specified.
-type xlsxMissing struct {
-}
+type xlsxMissing struct{}
// xlsxNumber represents a numeric value in the PivotTable.
type xlsxNumber struct {
@@ -167,63 +165,78 @@ type xlsxNumber struct {
// xlsxTuples represents members for the OLAP sheet data entry, also known as
// a tuple.
-type xlsxTuples struct {
-}
+type xlsxTuples struct{}
// xlsxBoolean represents a boolean value for an item in the PivotTable.
-type xlsxBoolean struct {
-}
+type xlsxBoolean struct{}
// xlsxError represents an error value. The use of this item indicates that an
// error value is present in the PivotTable source. The error is recorded in
// the value attribute.
-type xlsxError struct {
-}
+type xlsxError struct{}
// xlsxString represents a character value in a PivotTable.
type xlsxString struct {
+ V string `xml:"v,attr"`
+ U bool `xml:"u,attr,omitempty"`
+ F bool `xml:"f,attr,omitempty"`
+ C string `xml:"c,attr,omitempty"`
+ Cp int `xml:"cp,attr,omitempty"`
+ In int `xml:"in,attr,omitempty"`
+ Bc string `xml:"bc,attr,omitempty"`
+ Fc string `xml:"fc,attr,omitempty"`
+ I bool `xml:"i,attr,omitempty"`
+ Un bool `xml:"un,attr,omitempty"`
+ St bool `xml:"st,attr,omitempty"`
+ B bool `xml:"b,attr,omitempty"`
+ Tpls *xlsxTuples `xml:"tpls"`
+ X *attrValInt `xml:"x"`
}
// xlsxDateTime represents a date-time value in the PivotTable.
-type xlsxDateTime struct {
-}
+type xlsxDateTime struct{}
// xlsxFieldGroup represents the collection of properties for a field group.
-type xlsxFieldGroup struct {
-}
+type xlsxFieldGroup struct{}
// xlsxCacheHierarchies represents the collection of OLAP hierarchies in the
// PivotCache.
-type xlsxCacheHierarchies struct {
-}
+type xlsxCacheHierarchies struct{}
// xlsxKpis represents the collection of Key Performance Indicators (KPIs)
// defined on the OLAP server and stored in the PivotCache.
-type xlsxKpis struct {
-}
+type xlsxKpis struct{}
// xlsxTupleCache represents the cache of OLAP sheet data members, or tuples.
-type xlsxTupleCache struct {
-}
+type xlsxTupleCache struct{}
// xlsxCalculatedItems represents the collection of calculated items.
-type xlsxCalculatedItems struct {
-}
+type xlsxCalculatedItems struct{}
// xlsxCalculatedMembers represents the collection of calculated members in an
// OLAP PivotTable.
-type xlsxCalculatedMembers struct {
-}
+type xlsxCalculatedMembers struct{}
// xlsxDimensions represents the collection of PivotTable OLAP dimensions.
-type xlsxDimensions struct {
-}
+type xlsxDimensions struct{}
// xlsxMeasureGroups represents the collection of PivotTable OLAP measure
// groups.
-type xlsxMeasureGroups struct {
-}
+type xlsxMeasureGroups struct{}
// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps.
-type xlsxMaps struct {
+type xlsxMaps struct{}
+
+// xlsxX14PivotCacheDefinition specifies the extended properties of a pivot
+// table cache definition.
+type xlsxX14PivotCacheDefinition struct {
+ XMLName xml.Name `xml:"x14:pivotCacheDefinition"`
+ PivotCacheID int `xml:"pivotCacheId,attr"`
+}
+
+// decodeX14PivotCacheDefinition defines the structure used to parse the
+// x14:pivotCacheDefinition element of a pivot table cache.
+type decodeX14PivotCacheDefinition struct {
+ XMLName xml.Name `xml:"pivotCacheDefinition"`
+ PivotCacheID int `xml:"pivotCacheId,attr"`
}
diff --git a/xmlPivotTable.go b/xmlPivotTable.go
index 657a9e8c34..766c7e1a6c 100644
--- a/xmlPivotTable.go
+++ b/xmlPivotTable.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -31,7 +31,7 @@ type xlsxPivotTableDefinition struct {
DataCaption string `xml:"dataCaption,attr"`
GrandTotalCaption string `xml:"grandTotalCaption,attr,omitempty"`
ErrorCaption string `xml:"errorCaption,attr,omitempty"`
- ShowError bool `xml:"showError,attr,omitempty"`
+ ShowError *bool `xml:"showError,attr"`
MissingCaption string `xml:"missingCaption,attr,omitempty"`
ShowMissing bool `xml:"showMissing,attr,omitempty"`
PageStyle string `xml:"pageStyle,attr,omitempty"`
@@ -48,7 +48,7 @@ type xlsxPivotTableDefinition struct {
VisualTotals bool `xml:"visualTotals,attr,omitempty"`
ShowMultipleLabel bool `xml:"showMultipleLabel,attr,omitempty"`
ShowDataDropDown bool `xml:"showDataDropDown,attr,omitempty"`
- ShowDrill bool `xml:"showDrill,attr,omitempty"`
+ ShowDrill *bool `xml:"showDrill,attr"`
PrintDrill bool `xml:"printDrill,attr,omitempty"`
ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr,omitempty"`
ShowDataTips bool `xml:"showDataTips,attr,omitempty"`
@@ -56,25 +56,25 @@ type xlsxPivotTableDefinition struct {
EnableDrill bool `xml:"enableDrill,attr,omitempty"`
EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"`
PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"`
- UseAutoFormatting bool `xml:"useAutoFormatting,attr,omitempty"`
+ UseAutoFormatting *bool `xml:"useAutoFormatting,attr"`
PageWrap int `xml:"pageWrap,attr,omitempty"`
- PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"`
+ PageOverThenDown *bool `xml:"pageOverThenDown,attr"`
SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"`
- RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"`
- ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"`
+ RowGrandTotals *bool `xml:"rowGrandTotals,attr"`
+ ColGrandTotals *bool `xml:"colGrandTotals,attr"`
FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"`
ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"`
- MergeItem bool `xml:"mergeItem,attr,omitempty"`
+ MergeItem *bool `xml:"mergeItem,attr"`
ShowDropZones bool `xml:"showDropZones,attr,omitempty"`
CreatedVersion int `xml:"createdVersion,attr,omitempty"`
Indent int `xml:"indent,attr,omitempty"`
ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"`
ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"`
ShowHeaders bool `xml:"showHeaders,attr,omitempty"`
- Compact bool `xml:"compact,attr"`
- Outline bool `xml:"outline,attr"`
+ Compact *bool `xml:"compact,attr"`
+ Outline *bool `xml:"outline,attr"`
OutlineData bool `xml:"outlineData,attr,omitempty"`
- CompactData bool `xml:"compactData,attr,omitempty"`
+ CompactData *bool `xml:"compactData,attr"`
Published bool `xml:"published,attr,omitempty"`
GridDropZones bool `xml:"gridDropZones,attr,omitempty"`
Immersive bool `xml:"immersive,attr,omitempty"`
@@ -125,10 +125,10 @@ type xlsxPivotField struct {
ShowDropDowns bool `xml:"showDropDowns,attr,omitempty"`
HiddenLevel bool `xml:"hiddenLevel,attr,omitempty"`
UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"`
- Compact bool `xml:"compact,attr"`
+ Compact *bool `xml:"compact,attr"`
AllDrilled bool `xml:"allDrilled,attr,omitempty"`
NumFmtID string `xml:"numFmtId,attr,omitempty"`
- Outline bool `xml:"outline,attr"`
+ Outline *bool `xml:"outline,attr"`
SubtotalTop bool `xml:"subtotalTop,attr,omitempty"`
DragToRow bool `xml:"dragToRow,attr,omitempty"`
DragToCol bool `xml:"dragToCol,attr,omitempty"`
@@ -150,7 +150,7 @@ type xlsxPivotField struct {
DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"`
NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"`
RankBy int `xml:"rankBy,attr,omitempty"`
- DefaultSubtotal bool `xml:"defaultSubtotal,attr,omitempty"`
+ DefaultSubtotal *bool `xml:"defaultSubtotal,attr"`
SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"`
CountASubtotal bool `xml:"countASubtotal,attr,omitempty"`
AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"`
@@ -189,14 +189,13 @@ type xlsxItem struct {
F bool `xml:"f,attr,omitempty"`
M bool `xml:"m,attr,omitempty"`
C bool `xml:"c,attr,omitempty"`
- X int `xml:"x,attr,omitempty"`
+ X *int `xml:"x,attr,omitempty"`
D bool `xml:"d,attr,omitempty"`
E bool `xml:"e,attr,omitempty"`
}
// xlsxAutoSortScope represents the sorting scope for the PivotTable.
-type xlsxAutoSortScope struct {
-}
+type xlsxAutoSortScope struct{}
// xlsxRowFields represents the collection of row fields for the PivotTable.
type xlsxRowFields struct {
@@ -225,8 +224,7 @@ type xlsxI struct {
}
// xlsxX represents an array of indexes to cached shared item values.
-type xlsxX struct {
-}
+type xlsxX struct{}
// xlsxColFields represents the collection of fields that are on the column
// axis of the PivotTable.
@@ -275,14 +273,13 @@ type xlsxDataField struct {
ShowDataAs string `xml:"showDataAs,attr,omitempty"`
BaseField int `xml:"baseField,attr,omitempty"`
BaseItem int64 `xml:"baseItem,attr,omitempty"`
- NumFmtID string `xml:"numFmtId,attr,omitempty"`
+ NumFmtID int `xml:"numFmtId,attr,omitempty"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxConditionalFormats represents the collection of conditional formats
// applied to a PivotTable.
-type xlsxConditionalFormats struct {
-}
+type xlsxConditionalFormats struct{}
// xlsxPivotTableStyleInfo represent information on style applied to the
// PivotTable.
diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go
index d5fe4a7237..37ea197d85 100644
--- a/xmlSharedStrings.go
+++ b/xmlSharedStrings.go
@@ -1,19 +1,19 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import (
"encoding/xml"
- "strings"
+ "sync"
)
// xlsxSST directly maps the sst element from the namespace
@@ -24,6 +24,7 @@ import (
// is an indexed list of string values, shared across the workbook, which allows
// implementations to store values only once.
type xlsxSST struct {
+ mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"`
Count int `xml:"count,attr"`
UniqueCount int `xml:"uniqueCount,attr"`
@@ -38,25 +39,10 @@ type xlsxSST struct {
// level - then the string item shall consist of multiple rich text runs which
// collectively are used to express the string.
type xlsxSI struct {
- T *xlsxT `xml:"t,omitempty"`
- R []xlsxR `xml:"r"`
-}
-
-// String extracts characters from a string item.
-func (x xlsxSI) String() string {
- if len(x.R) > 0 {
- var rows strings.Builder
- for _, s := range x.R {
- if s.T != nil {
- rows.WriteString(s.T.Val)
- }
- }
- return rows.String()
- }
- if x.T != nil {
- return x.T.Val
- }
- return ""
+ T *xlsxT `xml:"t,omitempty"`
+ R []xlsxR `xml:"r"`
+ RPh []*xlsxPhoneticRun `xml:"rPh"`
+ PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"`
}
// xlsxR represents a run of rich text. A rich text run is a region of text
@@ -64,8 +50,9 @@ func (x xlsxSI) String() string {
// properties are defined in the rPr element, and the text displayed to the
// user is defined in the Text (t) element.
type xlsxR struct {
- RPr *xlsxRPr `xml:"rPr"`
- T *xlsxT `xml:"t"`
+ XMLName xml.Name `xml:"r"`
+ RPr *xlsxRPr `xml:"rPr"`
+ T *xlsxT `xml:"t"`
}
// xlsxT directly maps the t element in the run properties.
@@ -84,13 +71,13 @@ type xlsxRPr struct {
RFont *attrValString `xml:"rFont"`
Charset *attrValInt `xml:"charset"`
Family *attrValInt `xml:"family"`
- B string `xml:"b,omitempty"`
- I string `xml:"i,omitempty"`
- Strike string `xml:"strike,omitempty"`
- Outline string `xml:"outline,omitempty"`
- Shadow string `xml:"shadow,omitempty"`
- Condense string `xml:"condense,omitempty"`
- Extend string `xml:"extend,omitempty"`
+ B *string `xml:"b"`
+ I *string `xml:"i"`
+ Strike *string `xml:"strike"`
+ Outline *string `xml:"outline"`
+ Shadow *string `xml:"shadow"`
+ Condense *string `xml:"condense"`
+ Extend *string `xml:"extend"`
Color *xlsxColor `xml:"color"`
Sz *attrValFloat `xml:"sz"`
U *attrValString `xml:"u"`
diff --git a/xmlSlicers.go b/xmlSlicers.go
new file mode 100644
index 0000000000..4b48bf7f39
--- /dev/null
+++ b/xmlSlicers.go
@@ -0,0 +1,206 @@
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
+
+package excelize
+
+import "encoding/xml"
+
+// xlsxSlicers directly maps the slicers element that specifies a slicer view on
+// the worksheet.
+type xlsxSlicers struct {
+ XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicers"`
+ XMLNSXMC string `xml:"xmlns:mc,attr"`
+ XMLNSX string `xml:"xmlns:x,attr"`
+ XMLNSXR10 string `xml:"xmlns:xr10,attr"`
+ Slicer []xlsxSlicer `xml:"slicer"`
+}
+
+// xlsxSlicer is a complex type that specifies a slicer view.
+type xlsxSlicer struct {
+ Name string `xml:"name,attr"`
+ XR10UID string `xml:"xr10:uid,attr,omitempty"`
+ Cache string `xml:"cache,attr"`
+ Caption string `xml:"caption,attr,omitempty"`
+ StartItem *int `xml:"startItem,attr"`
+ ColumnCount *int `xml:"columnCount,attr"`
+ ShowCaption *bool `xml:"showCaption,attr"`
+ Level int `xml:"level,attr,omitempty"`
+ Style string `xml:"style,attr,omitempty"`
+ LockedPosition bool `xml:"lockedPosition,attr,omitempty"`
+ RowHeight int `xml:"rowHeight,attr"`
+}
+
+// slicerCacheDefinition directly maps the slicerCacheDefinition element that
+// specifies a slicer cache.
+type xlsxSlicerCacheDefinition struct {
+ XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicerCacheDefinition"`
+ XMLNSXMC string `xml:"xmlns:mc,attr"`
+ XMLNSX string `xml:"xmlns:x,attr"`
+ XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"`
+ XMLNSXR10 string `xml:"xmlns:xr10,attr"`
+ Name string `xml:"name,attr"`
+ XR10UID string `xml:"xr10:uid,attr,omitempty"`
+ SourceName string `xml:"sourceName,attr"`
+ PivotTables *xlsxSlicerCachePivotTables `xml:"pivotTables"`
+ Data *xlsxSlicerCacheData `xml:"data"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// xlsxSlicerCachePivotTables is a complex type that specifies a group of
+// pivotTable elements that specify the PivotTable views that are filtered by
+// the slicer cache.
+type xlsxSlicerCachePivotTables struct {
+ PivotTable []xlsxSlicerCachePivotTable `xml:"pivotTable"`
+}
+
+// xlsxSlicerCachePivotTable is a complex type that specifies a PivotTable view
+// filtered by a slicer cache.
+type xlsxSlicerCachePivotTable struct {
+ TabID int `xml:"tabId,attr"`
+ Name string `xml:"name,attr"`
+}
+
+// xlsxSlicerCacheData is a complex type that specifies a data source for the
+// slicer cache.
+type xlsxSlicerCacheData struct {
+ OLAP *xlsxInnerXML `xml:"olap"`
+ Tabular *xlsxTabularSlicerCache `xml:"tabular"`
+}
+
+// xlsxTabularSlicerCache is a complex type that specifies non-OLAP slicer items
+// that are cached within this slicer cache and properties of the slicer cache
+// specific to non-OLAP slicer items.
+type xlsxTabularSlicerCache struct {
+ PivotCacheID int `xml:"pivotCacheId,attr"`
+ SortOrder string `xml:"sortOrder,attr,omitempty"`
+ CustomListSort *bool `xml:"customListSort,attr"`
+ ShowMissing *bool `xml:"showMissing,attr"`
+ CrossFilter string `xml:"crossFilter,attr,omitempty"`
+ Items *xlsxTabularSlicerCacheItems `xml:"items"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// xlsxTabularSlicerCacheItems is a complex type that specifies non-OLAP slicer
+// items that are cached within this slicer cache.
+type xlsxTabularSlicerCacheItems struct {
+ Count int `xml:"count,attr,omitempty"`
+ I []xlsxTabularSlicerCacheItem `xml:"i"`
+}
+
+// xlsxTabularSlicerCacheItem is a complex type that specifies a non-OLAP slicer
+// item that is cached within this slicer cache.
+type xlsxTabularSlicerCacheItem struct {
+ X int `xml:"x,attr"`
+ S bool `xml:"s,attr,omitempty"`
+ ND bool `xml:"nd,attr,omitempty"`
+}
+
+// xlsxTableSlicerCache specifies a table data source for the slicer cache.
+type xlsxTableSlicerCache struct {
+ XMLName xml.Name `xml:"x15:tableSlicerCache"`
+ TableID int `xml:"tableId,attr"`
+ Column int `xml:"column,attr"`
+ SortOrder string `xml:"sortOrder,attr,omitempty"`
+ CustomListSort *bool `xml:"customListSort,attr"`
+ CrossFilter string `xml:"crossFilter,attr,omitempty"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// xlsxX14SlicerList specifies a list of slicer.
+type xlsxX14SlicerList struct {
+ XMLName xml.Name `xml:"x14:slicerList"`
+ Slicer []*xlsxX14Slicer `xml:"x14:slicer"`
+}
+
+// xlsxX14Slicer specifies a slicer view,
+type xlsxX14Slicer struct {
+ XMLName xml.Name `xml:"x14:slicer"`
+ RID string `xml:"r:id,attr"`
+}
+
+// xlsxX14SlicerCaches directly maps the x14:slicerCache element.
+type xlsxX14SlicerCaches struct {
+ XMLName xml.Name `xml:"x14:slicerCaches"`
+ XMLNS string `xml:"xmlns:x14,attr"`
+ Content string `xml:",innerxml"`
+}
+
+// xlsxX15SlicerCaches directly maps the x14:slicerCache element.
+type xlsxX14SlicerCache struct {
+ XMLName xml.Name `xml:"x14:slicerCache"`
+ RID string `xml:"r:id,attr"`
+}
+
+// xlsxX15SlicerCaches directly maps the x15:slicerCaches element.
+type xlsxX15SlicerCaches struct {
+ XMLName xml.Name `xml:"x15:slicerCaches"`
+ XMLNS string `xml:"xmlns:x14,attr"`
+ Content string `xml:",innerxml"`
+}
+
+// decodeTableSlicerCache defines the structure used to parse the
+// x15:tableSlicerCache element of the table slicer cache.
+type decodeTableSlicerCache struct {
+ XMLName xml.Name `xml:"tableSlicerCache"`
+ TableID int `xml:"tableId,attr"`
+ Column int `xml:"column,attr"`
+ SortOrder string `xml:"sortOrder,attr"`
+}
+
+// decodeSlicerList defines the structure used to parse the x14:slicerList
+// element of a list of slicer.
+type decodeSlicerList struct {
+ XMLName xml.Name `xml:"slicerList"`
+ Slicer []*decodeSlicer `xml:"slicer"`
+}
+
+// decodeSlicer defines the structure used to parse the x14:slicer element of a
+// slicer.
+type decodeSlicer struct {
+ RID string `xml:"id,attr"`
+}
+
+// decodeSlicerCaches defines the structure used to parse the
+// x14:slicerCaches and x15:slicerCaches element of a slicer cache.
+type decodeSlicerCaches struct {
+ XMLName xml.Name `xml:"slicerCaches"`
+ Content string `xml:",innerxml"`
+}
+
+// xlsxTimelines is a mechanism for filtering data in pivot table views, cube
+// functions and charts based on non-worksheet pivot tables. In the case of
+// using OLAP Timeline source data, a Timeline is based on a key attribute of
+// an OLAP hierarchy. In the case of using native Timeline source data, a
+// Timeline is based on a data table column.
+type xlsxTimelines struct {
+ XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main timelines"`
+ XMLNSXMC string `xml:"xmlns:mc,attr"`
+ XMLNSX string `xml:"xmlns:x,attr"`
+ XMLNSXR10 string `xml:"xmlns:xr10,attr"`
+ Timeline []xlsxTimeline `xml:"timeline"`
+}
+
+// xlsxTimeline is timeline view specifies the display of a timeline on a
+// worksheet.
+type xlsxTimeline struct {
+ Name string `xml:"name,attr"`
+ XR10UID string `xml:"xr10:uid,attr,omitempty"`
+ Cache string `xml:"cache,attr"`
+ Caption string `xml:"caption,attr,omitempty"`
+ ShowHeader *bool `xml:"showHeader,attr"`
+ ShowSelectionLabel *bool `xml:"showSelectionLabel,attr"`
+ ShowTimeLevel *bool `xml:"showTimeLevel,attr"`
+ ShowHorizontalScrollbar *bool `xml:"showHorizontalScrollbar,attr"`
+ Level int `xml:"level,attr"`
+ SelectionLevel int `xml:"selectionLevel,attr"`
+ ScrollPosition string `xml:"scrollPosition,attr,omitempty"`
+ Style string `xml:"style,attr,omitempty"`
+}
diff --git a/xmlStyles.go b/xmlStyles.go
index 2884800097..93ad33cce3 100644
--- a/xmlStyles.go
+++ b/xmlStyles.go
@@ -1,31 +1,35 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "encoding/xml"
+import (
+ "encoding/xml"
+ "sync"
+)
// xlsxStyleSheet is the root element of the Styles part.
type xlsxStyleSheet struct {
+ mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"`
- NumFmts *xlsxNumFmts `xml:"numFmts,omitempty"`
- Fonts *xlsxFonts `xml:"fonts,omitempty"`
- Fills *xlsxFills `xml:"fills,omitempty"`
- Borders *xlsxBorders `xml:"borders,omitempty"`
- CellStyleXfs *xlsxCellStyleXfs `xml:"cellStyleXfs,omitempty"`
- CellXfs *xlsxCellXfs `xml:"cellXfs,omitempty"`
- CellStyles *xlsxCellStyles `xml:"cellStyles,omitempty"`
- Dxfs *xlsxDxfs `xml:"dxfs,omitempty"`
- TableStyles *xlsxTableStyles `xml:"tableStyles,omitempty"`
- Colors *xlsxStyleColors `xml:"colors,omitempty"`
+ NumFmts *xlsxNumFmts `xml:"numFmts"`
+ Fonts *xlsxFonts `xml:"fonts"`
+ Fills *xlsxFills `xml:"fills"`
+ Borders *xlsxBorders `xml:"borders"`
+ CellStyleXfs *xlsxCellStyleXfs `xml:"cellStyleXfs"`
+ CellXfs *xlsxCellXfs `xml:"cellXfs"`
+ CellStyles *xlsxCellStyles `xml:"cellStyles"`
+ Dxfs *xlsxDxfs `xml:"dxfs"`
+ TableStyles *xlsxTableStyles `xml:"tableStyles"`
+ Colors *xlsxStyleColors `xml:"colors"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
@@ -49,22 +53,22 @@ type xlsxAlignment struct {
// set. The cell protection properties do not take effect unless the sheet has
// been protected.
type xlsxProtection struct {
- Hidden bool `xml:"hidden,attr"`
- Locked bool `xml:"locked,attr"`
+ Hidden *bool `xml:"hidden,attr"`
+ Locked *bool `xml:"locked,attr"`
}
// xlsxLine expresses a single set of cell border.
type xlsxLine struct {
Style string `xml:"style,attr,omitempty"`
- Color *xlsxColor `xml:"color,omitempty"`
+ Color *xlsxColor `xml:"color"`
}
// xlsxColor is a common mapping used for both the fgColor and bgColor elements.
// Foreground color of the cell fill pattern. Cell fill patterns operate with
-// two colors: a background color and a foreground color. These combine together
+// two colors: a background color and a foreground color. These combine
// to make a patterned cell fill. Background color of the cell fill pattern.
// Cell fill patterns operate with two colors: a background color and a
-// foreground color. These combine together to make a patterned cell fill.
+// foreground color. These combine to make a patterned cell fill.
type xlsxColor struct {
Auto bool `xml:"auto,attr,omitempty"`
RGB string `xml:"rgb,attr,omitempty"`
@@ -83,13 +87,13 @@ type xlsxFonts struct {
// xlsxFont directly maps the font element. This element defines the
// properties for one of the fonts used in this workbook.
type xlsxFont struct {
- B *bool `xml:"b,omitempty"`
- I *bool `xml:"i,omitempty"`
- Strike *bool `xml:"strike,omitempty"`
- Outline *bool `xml:"outline,omitempty"`
- Shadow *bool `xml:"shadow,omitempty"`
- Condense *bool `xml:"condense,omitempty"`
- Extend *bool `xml:"extend,omitempty"`
+ B *attrValBool `xml:"b"`
+ I *attrValBool `xml:"i"`
+ Strike *attrValBool `xml:"strike"`
+ Outline *attrValBool `xml:"outline"`
+ Shadow *attrValBool `xml:"shadow"`
+ Condense *attrValBool `xml:"condense"`
+ Extend *attrValBool `xml:"extend"`
U *attrValString `xml:"u"`
Sz *attrValFloat `xml:"sz"`
Color *xlsxColor `xml:"color"`
@@ -99,20 +103,20 @@ type xlsxFont struct {
Scheme *attrValString `xml:"scheme"`
}
-// xlsxFills directly maps the fills element. This element defines the cell
+// xlsxFills directly maps the fills' element. This element defines the cell
// fills portion of the Styles part, consisting of a sequence of fill records. A
// cell fill consists of a background color, foreground color, and pattern to be
// applied across the cell.
type xlsxFills struct {
Count int `xml:"count,attr"`
- Fill []*xlsxFill `xml:"fill,omitempty"`
+ Fill []*xlsxFill `xml:"fill"`
}
// xlsxFill directly maps the fill element. This element specifies fill
// formatting.
type xlsxFill struct {
- PatternFill *xlsxPatternFill `xml:"patternFill,omitempty"`
- GradientFill *xlsxGradientFill `xml:"gradientFill,omitempty"`
+ PatternFill *xlsxPatternFill `xml:"patternFill"`
+ GradientFill *xlsxGradientFill `xml:"gradientFill"`
}
// xlsxPatternFill is used to specify cell fill information for pattern and
@@ -120,9 +124,9 @@ type xlsxFill struct {
// For cell fills with patterns specified, then the cell fill color is
// specified by the bgColor element.
type xlsxPatternFill struct {
- PatternType string `xml:"patternType,attr,omitempty"`
- FgColor xlsxColor `xml:"fgColor,omitempty"`
- BgColor xlsxColor `xml:"bgColor,omitempty"`
+ PatternType string `xml:"patternType,attr,omitempty"`
+ FgColor *xlsxColor `xml:"fgColor"`
+ BgColor *xlsxColor `xml:"bgColor"`
}
// xlsxGradientFill defines a gradient-style cell fill. Gradient cell fills can
@@ -134,7 +138,7 @@ type xlsxGradientFill struct {
Right float64 `xml:"right,attr,omitempty"`
Top float64 `xml:"top,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
- Stop []*xlsxGradientFillStop `xml:"stop,omitempty"`
+ Stop []*xlsxGradientFillStop `xml:"stop"`
}
// xlsxGradientFillStop directly maps the stop element.
@@ -143,26 +147,28 @@ type xlsxGradientFillStop struct {
Color xlsxColor `xml:"color,omitempty"`
}
-// xlsxBorders directly maps the borders element. This element contains borders
+// xlsxBorders directly maps the borders' element. This element contains borders
// formatting information, specifying all border definitions for all cells in
// the workbook.
type xlsxBorders struct {
Count int `xml:"count,attr"`
- Border []*xlsxBorder `xml:"border,omitempty"`
+ Border []*xlsxBorder `xml:"border"`
}
// xlsxBorder directly maps the border element. Expresses a single set of cell
// border formats (left, right, top, bottom, diagonal). Color is optional. When
// missing, 'automatic' is implied.
type xlsxBorder struct {
- DiagonalDown bool `xml:"diagonalDown,attr,omitempty"`
- DiagonalUp bool `xml:"diagonalUp,attr,omitempty"`
- Outline bool `xml:"outline,attr,omitempty"`
- Left xlsxLine `xml:"left,omitempty"`
- Right xlsxLine `xml:"right,omitempty"`
- Top xlsxLine `xml:"top,omitempty"`
- Bottom xlsxLine `xml:"bottom,omitempty"`
- Diagonal xlsxLine `xml:"diagonal,omitempty"`
+ DiagonalDown bool `xml:"diagonalDown,attr,omitempty"`
+ DiagonalUp bool `xml:"diagonalUp,attr,omitempty"`
+ Outline bool `xml:"outline,attr,omitempty"`
+ Left *xlsxLine `xml:"left"`
+ Right *xlsxLine `xml:"right"`
+ Top *xlsxLine `xml:"top"`
+ Bottom *xlsxLine `xml:"bottom"`
+ Diagonal *xlsxLine `xml:"diagonal"`
+ Vertical *xlsxLine `xml:"vertical"`
+ Horizontal *xlsxLine `xml:"horizontal"`
}
// xlsxCellStyles directly maps the cellStyles element. This element contains
@@ -173,7 +179,7 @@ type xlsxBorder struct {
type xlsxCellStyles struct {
XMLName xml.Name `xml:"cellStyles"`
Count int `xml:"count,attr"`
- CellStyle []*xlsxCellStyle `xml:"cellStyle,omitempty"`
+ CellStyle []*xlsxCellStyle `xml:"cellStyle"`
}
// xlsxCellStyle directly maps the cellStyle element. This element represents
@@ -183,17 +189,17 @@ type xlsxCellStyle struct {
XMLName xml.Name `xml:"cellStyle"`
Name string `xml:"name,attr"`
XfID int `xml:"xfId,attr"`
- BuiltInID *int `xml:"builtinId,attr,omitempty"`
- ILevel *int `xml:"iLevel,attr,omitempty"`
- Hidden *bool `xml:"hidden,attr,omitempty"`
- CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"`
+ BuiltInID *int `xml:"builtinId,attr"`
+ ILevel *int `xml:"iLevel,attr"`
+ Hidden *bool `xml:"hidden,attr"`
+ CustomBuiltIn *bool `xml:"customBuiltin,attr"`
}
// xlsxCellStyleXfs directly maps the cellStyleXfs element. This element
// contains the master formatting records (xf's) which define the formatting for
// all named cell styles in this workbook. Master formatting records reference
// individual elements of formatting (e.g., number format, font definitions,
-// cell fills, etc) by specifying a zero-based index into those collections.
+// cell fills, etc.) by specifying a zero-based index into those collections.
// Master formatting records also specify whether to apply or ignore particular
// aspects of formatting.
type xlsxCellStyleXfs struct {
@@ -201,7 +207,7 @@ type xlsxCellStyleXfs struct {
Xf []xlsxXf `xml:"xf,omitempty"`
}
-// xlsxXf directly maps the xf element. A single xf element describes all of the
+// xlsxXf directly maps the xf element. A single xf element describes all the
// formatting for a cell.
type xlsxXf struct {
NumFmtID *int `xml:"numFmtId,attr"`
@@ -232,8 +238,8 @@ type xlsxCellXfs struct {
}
// xlsxDxfs directly maps the dxfs element. This element contains the master
-// differential formatting records (dxf's) which define formatting for all non-
-// cell formatting in this workbook. Whereas xf records fully specify a
+// differential formatting records (dxf's) which define formatting for all
+// non-cell formatting in this workbook. Whereas xf records fully specify a
// particular aspect of formatting (e.g., cell borders) by referencing those
// formatting definitions elsewhere in the Styles part, dxf records specify
// incremental (or differential) aspects of formatting directly inline within
@@ -241,24 +247,19 @@ type xlsxCellXfs struct {
// to any formatting already present on the object using the dxf record.
type xlsxDxfs struct {
Count int `xml:"count,attr"`
- Dxfs []*xlsxDxf `xml:"dxf,omitempty"`
+ Dxfs []*xlsxDxf `xml:"dxf"`
}
// xlsxDxf directly maps the dxf element. A single dxf record, expressing
// incremental formatting to be applied.
type xlsxDxf struct {
- Dxf string `xml:",innerxml"`
-}
-
-// dxf directly maps the dxf element.
-type dxf struct {
Font *xlsxFont `xml:"font"`
NumFmt *xlsxNumFmt `xml:"numFmt"`
Fill *xlsxFill `xml:"fill"`
Alignment *xlsxAlignment `xml:"alignment"`
Border *xlsxBorder `xml:"border"`
Protection *xlsxProtection `xml:"protection"`
- ExtLst *xlsxExt `xml:"extLst"`
+ ExtLst *aExt `xml:"extLst"`
}
// xlsxTableStyles directly maps the tableStyles element. This element
@@ -269,7 +270,7 @@ type xlsxTableStyles struct {
Count int `xml:"count,attr"`
DefaultPivotStyle string `xml:"defaultPivotStyle,attr"`
DefaultTableStyle string `xml:"defaultTableStyle,attr"`
- TableStyles []*xlsxTableStyle `xml:"tableStyle,omitempty"`
+ TableStyles []*xlsxTableStyle `xml:"tableStyle"`
}
// xlsxTableStyle directly maps the tableStyle element. This element represents
@@ -289,80 +290,91 @@ type xlsxTableStyle struct {
// to format and render the numeric value of a cell.
type xlsxNumFmts struct {
Count int `xml:"count,attr"`
- NumFmt []*xlsxNumFmt `xml:"numFmt,omitempty"`
+ NumFmt []*xlsxNumFmt `xml:"numFmt"`
}
// xlsxNumFmt directly maps the numFmt element. This element specifies number
// format properties which indicate how to format and render the numeric value
// of a cell.
type xlsxNumFmt struct {
- NumFmtID int `xml:"numFmtId,attr"`
- FormatCode string `xml:"formatCode,attr,omitempty"`
+ NumFmtID int `xml:"numFmtId,attr"`
+ FormatCode string `xml:"formatCode,attr"`
+ FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
+}
+
+// xlsxIndexedColors directly maps the single ARGB entry for the corresponding
+// color index.
+type xlsxIndexedColors struct {
+ RgbColor []xlsxColor `xml:"rgbColor"`
}
-// xlsxStyleColors directly maps the colors element. Color information
-// associated with this stylesheet. This collection is written whenever the
+// xlsxStyleColors directly maps the colors' element. Color information
+// associated with this style sheet. This collection is written whenever the
// legacy color palette has been modified (backwards compatibility settings) or
// a custom color has been selected while using this workbook.
type xlsxStyleColors struct {
- Color string `xml:",innerxml"`
+ IndexedColors *xlsxIndexedColors `xml:"indexedColors"`
+ MruColors *xlsxInnerXML `xml:"mruColors"`
}
// Alignment directly maps the alignment settings of the cells.
type Alignment struct {
- Horizontal string `json:"horizontal"`
- Indent int `json:"indent"`
- JustifyLastLine bool `json:"justify_last_line"`
- ReadingOrder uint64 `json:"reading_order"`
- RelativeIndent int `json:"relative_indent"`
- ShrinkToFit bool `json:"shrink_to_fit"`
- TextRotation int `json:"text_rotation"`
- Vertical string `json:"vertical"`
- WrapText bool `json:"wrap_text"`
+ Horizontal string
+ Indent int
+ JustifyLastLine bool
+ ReadingOrder uint64
+ RelativeIndent int
+ ShrinkToFit bool
+ TextRotation int
+ Vertical string
+ WrapText bool
}
// Border directly maps the border settings of the cells.
type Border struct {
- Type string `json:"type"`
- Color string `json:"color"`
- Style int `json:"style"`
+ Type string
+ Color string
+ Style int
}
// Font directly maps the font settings of the fonts.
type Font struct {
- Bold bool `json:"bold"`
- Italic bool `json:"italic"`
- Underline string `json:"underline"`
- Family string `json:"family"`
- Size float64 `json:"size"`
- Strike bool `json:"strike"`
- Color string `json:"color"`
+ Bold bool
+ Italic bool
+ Underline string
+ Family string
+ Size float64
+ Strike bool
+ Color string
+ ColorIndexed int
+ ColorTheme *int
+ ColorTint float64
+ VertAlign string
}
// Fill directly maps the fill settings of the cells.
type Fill struct {
- Type string `json:"type"`
- Pattern int `json:"pattern"`
- Color []string `json:"color"`
- Shading int `json:"shading"`
+ Type string
+ Pattern int
+ Color []string
+ Shading int
}
// Protection directly maps the protection settings of the cells.
type Protection struct {
- Hidden bool `json:"hidden"`
- Locked bool `json:"locked"`
+ Hidden bool
+ Locked bool
}
// Style directly maps the style settings of the cells.
type Style struct {
- Border []Border `json:"border"`
- Fill Fill `json:"fill"`
- Font *Font `json:"font"`
- Alignment *Alignment `json:"alignment"`
- Protection *Protection `json:"protection"`
- NumFmt int `json:"number_format"`
- DecimalPlaces int `json:"decimal_places"`
- CustomNumFmt *string `json:"custom_number_format"`
- Lang string `json:"lang"`
- NegRed bool `json:"negred"`
+ Border []Border
+ Fill Fill
+ Font *Font
+ Alignment *Alignment
+ Protection *Protection
+ NumFmt int
+ DecimalPlaces *int
+ CustomNumFmt *string
+ NegRed bool
}
diff --git a/xmlTable.go b/xmlTable.go
index 22d191e349..60bc307ed4 100644
--- a/xmlTable.go
+++ b/xmlTable.go
@@ -1,42 +1,48 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
import "encoding/xml"
// xlsxTable directly maps the table element. A table helps organize and provide
-// structure to lists of information in a worksheet. Tables have clearly labeled
+// structure to list of information in a worksheet. Tables have clearly labeled
// columns, rows, and data regions. Tables make it easier for users to sort,
// analyze, format, manage, add, and delete information. This element is the
// root element for a table that is not a single cell XML table.
type xlsxTable struct {
XMLName xml.Name `xml:"table"`
XMLNS string `xml:"xmlns,attr"`
- DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
- DataDxfID int `xml:"dataDxfId,attr,omitempty"`
- DisplayName string `xml:"displayName,attr,omitempty"`
- HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
- HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
- HeaderRowCount int `xml:"headerRowCount,attr,omitempty"`
- HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
ID int `xml:"id,attr"`
- InsertRow bool `xml:"insertRow,attr,omitempty"`
- InsertRowShift bool `xml:"insertRowShift,attr,omitempty"`
Name string `xml:"name,attr"`
- Published bool `xml:"published,attr,omitempty"`
+ DisplayName string `xml:"displayName,attr,omitempty"`
+ Comment string `xml:"comment,attr,omitempty"`
Ref string `xml:"ref,attr"`
+ TableType string `xml:"tableType,attr,omitempty"`
+ HeaderRowCount *int `xml:"headerRowCount,attr"`
+ InsertRow bool `xml:"insertRow,attr,omitempty"`
+ InsertRowShift bool `xml:"insertRowShift,attr,omitempty"`
TotalsRowCount int `xml:"totalsRowCount,attr,omitempty"`
+ TotalsRowShown *bool `xml:"totalsRowShown,attr"`
+ Published bool `xml:"published,attr,omitempty"`
+ HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
+ DataDxfID int `xml:"dataDxfId,attr,omitempty"`
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
- TotalsRowShown bool `xml:"totalsRowShown,attr"`
+ HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
+ TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"`
+ TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"`
+ HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
+ DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
+ TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
+ ConnectionID int `xml:"connectionId,attr,omitempty"`
AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
TableColumns *xlsxTableColumns `xml:"tableColumns"`
TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`
@@ -46,9 +52,9 @@ type xlsxTable struct {
// applied column by column to a table of data in the worksheet. This collection
// expresses AutoFilter settings.
type xlsxAutoFilter struct {
- XMLName xml.Name `xml:"autoFilter"`
- Ref string `xml:"ref,attr"`
- FilterColumn *xlsxFilterColumn `xml:"filterColumn"`
+ XMLName xml.Name `xml:"autoFilter"`
+ Ref string `xml:"ref,attr"`
+ FilterColumn []*xlsxFilterColumn `xml:"filterColumn"`
}
// xlsxFilterColumn directly maps the filterColumn element. The filterColumn
@@ -171,18 +177,18 @@ type xlsxTableColumns struct {
// xlsxTableColumn directly maps the element representing a single column for
// this table.
type xlsxTableColumn struct {
- DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
- DataDxfID int `xml:"dataDxfId,attr,omitempty"`
- HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
- HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
ID int `xml:"id,attr"`
+ UniqueName string `xml:"uniqueName,attr,omitempty"`
Name string `xml:"name,attr"`
- QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
- TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
- TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
TotalsRowFunction string `xml:"totalsRowFunction,attr,omitempty"`
TotalsRowLabel string `xml:"totalsRowLabel,attr,omitempty"`
- UniqueName string `xml:"uniqueName,attr,omitempty"`
+ QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
+ HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
+ DataDxfID int `xml:"dataDxfId,attr,omitempty"`
+ TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
+ HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
+ DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
+ TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
}
// xlsxTableStyleInfo directly maps the tableStyleInfo element. This element
@@ -196,22 +202,52 @@ type xlsxTableStyleInfo struct {
ShowColumnStripes bool `xml:"showColumnStripes,attr"`
}
-// formatTable directly maps the format settings of the table.
-type formatTable struct {
- TableName string `json:"table_name"`
- TableStyle string `json:"table_style"`
- ShowFirstColumn bool `json:"show_first_column"`
- ShowLastColumn bool `json:"show_last_column"`
- ShowRowStripes bool `json:"show_row_stripes"`
- ShowColumnStripes bool `json:"show_column_stripes"`
-}
-
-// formatAutoFilter directly maps the auto filter settings.
-type formatAutoFilter struct {
- Column string `json:"column"`
- Expression string `json:"expression"`
- FilterList []struct {
- Column string `json:"column"`
- Value []int `json:"value"`
- } `json:"filter_list"`
+// xlsxSingleXMLCells is a single cell table is generated from an XML mapping.
+// These really just look like regular cells to the spreadsheet user, but shall
+// be implemented as Tables "under the covers."
+type xlsxSingleXMLCells struct {
+ XMLName xml.Name `xml:"singleXmlCells"`
+ SingleXmlCell []xlsxSingleXMLCell `xml:"singleXmlCell"`
+}
+
+// xlsxSingleXMLCell is a element represents the table properties for a single
+// cell XML table.
+type xlsxSingleXMLCell struct {
+ XMLName xml.Name `xml:"singleXmlCell"`
+ ID int `xml:"id,attr"`
+ R string `xml:"r,attr"`
+ ConnectionID int `xml:"connectionId,attr"`
+ XMLCellPr xlsxXMLCellPr `xml:"xmlCellPr"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// xlsxXMLCellPr is a element stores the XML properties for the cell of a single
+// cell xml table.
+type xlsxXMLCellPr struct {
+ XMLName xml.Name `xml:"xmlCellPr"`
+ ID int `xml:"id,attr"`
+ UniqueName string `xml:"uniqueName,attr,omitempty"`
+ XMLPr *xlsxInnerXML `xml:"xmlPr"`
+ ExtLst *xlsxInnerXML `xml:"extLst"`
+}
+
+// Table directly maps the format settings of the table.
+type Table struct {
+ tID int
+ rID string
+ tableXML string
+ Range string
+ Name string
+ StyleName string
+ ShowColumnStripes bool
+ ShowFirstColumn bool
+ ShowHeaderRow *bool
+ ShowLastColumn bool
+ ShowRowStripes *bool
+}
+
+// AutoFilterOptions directly maps the auto filter settings.
+type AutoFilterOptions struct {
+ Column string
+ Expression string
}
diff --git a/xmlTheme.go b/xmlTheme.go
index e3588dc5b6..bb48590970 100644
--- a/xmlTheme.go
+++ b/xmlTheme.go
@@ -1,13 +1,13 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
@@ -16,10 +16,57 @@ import "encoding/xml"
// xlsxTheme directly maps the theme element in the namespace
// http://schemas.openxmlformats.org/drawingml/2006/main
type xlsxTheme struct {
- ThemeElements xlsxThemeElements `xml:"themeElements"`
- ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
- ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
- ExtLst *xlsxExtLst `xml:"extLst"`
+ XMLName xml.Name `xml:"a:theme"`
+ XMLNSa string `xml:"xmlns:a,attr"`
+ XMLNSr string `xml:"xmlns:r,attr"`
+ Name string `xml:"name,attr"`
+ ThemeElements xlsxBaseStyles `xml:"a:themeElements"`
+ ObjectDefaults xlsxObjectDefaults `xml:"a:objectDefaults"`
+ ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"a:extraClrSchemeLst"`
+ CustClrLst *xlsxInnerXML `xml:"a:custClrLst"`
+ ExtLst *xlsxExtLst `xml:"a:extLst"`
+}
+
+// xlsxBaseStyles defines the theme elements for a theme, and is the workhorse
+// of the theme. The bulk of the shared theme information that is used by a
+// given document is defined here. Within this complex type is defined a color
+// scheme, a font scheme, and a style matrix (format scheme) that defines
+// different formatting options for different pieces of a document.
+type xlsxBaseStyles struct {
+ ClrScheme xlsxColorScheme `xml:"a:clrScheme"`
+ FontScheme xlsxFontScheme `xml:"a:fontScheme"`
+ FmtScheme xlsxStyleMatrix `xml:"a:fmtScheme"`
+ ExtLst *xlsxExtLst `xml:"a:extLst"`
+}
+
+// xlsxCTColor holds the actual color values that are to be applied to a given
+// diagram and how those colors are to be applied.
+type xlsxCTColor struct {
+ ScrgbClr *xlsxInnerXML `xml:"a:scrgbClr"`
+ SrgbClr *attrValString `xml:"a:srgbClr"`
+ HslClr *xlsxInnerXML `xml:"a:hslClr"`
+ SysClr *xlsxSysClr `xml:"a:sysClr"`
+ SchemeClr *xlsxInnerXML `xml:"a:schemeClr"`
+ PrstClr *xlsxInnerXML `xml:"a:prstClr"`
+}
+
+// xlsxColorScheme defines a set of colors for the theme. The set of colors
+// consists of twelve color slots that can each hold a color of choice.
+type xlsxColorScheme struct {
+ Name string `xml:"name,attr"`
+ Dk1 xlsxCTColor `xml:"a:dk1"`
+ Lt1 xlsxCTColor `xml:"a:lt1"`
+ Dk2 xlsxCTColor `xml:"a:dk2"`
+ Lt2 xlsxCTColor `xml:"a:lt2"`
+ Accent1 xlsxCTColor `xml:"a:accent1"`
+ Accent2 xlsxCTColor `xml:"a:accent2"`
+ Accent3 xlsxCTColor `xml:"a:accent3"`
+ Accent4 xlsxCTColor `xml:"a:accent4"`
+ Accent5 xlsxCTColor `xml:"a:accent5"`
+ Accent6 xlsxCTColor `xml:"a:accent6"`
+ Hlink xlsxCTColor `xml:"a:hlink"`
+ FolHlink xlsxCTColor `xml:"a:folHlink"`
+ ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// objectDefaults element allows for the definition of default shape, line,
@@ -35,24 +82,24 @@ type xlsxExtraClrSchemeLst struct {
ExtraClrSchemeLst string `xml:",innerxml"`
}
-// xlsxThemeElements directly maps the element defines the theme formatting
-// options for the theme and is the workhorse of the theme. This is where the
-// bulk of the shared theme information is contained and used by a document.
-// This element contains the color scheme, font scheme, and format scheme
-// elements which define the different formatting aspects of what a theme
-// defines.
-type xlsxThemeElements struct {
- ClrScheme xlsxClrScheme `xml:"clrScheme"`
- FontScheme xlsxFontScheme `xml:"fontScheme"`
- FmtScheme xlsxFmtScheme `xml:"fmtScheme"`
+// xlsxCTSupplementalFont defines an additional font that is used for language
+// specific fonts in themes. For example, one can specify a font that gets used
+// only within the Japanese language context.
+type xlsxCTSupplementalFont struct {
+ Script string `xml:"script,attr"`
+ Typeface string `xml:"typeface,attr"`
}
-// xlsxClrScheme element specifies the theme color, stored in the document's
-// Theme part to which the value of this theme color shall be mapped. This
-// mapping enables multiple theme colors to be chained together.
-type xlsxClrScheme struct {
- Name string `xml:"name,attr"`
- Children []xlsxClrSchemeEl `xml:",any"`
+// xlsxFontCollection defines a major and minor font which is used in the font
+// scheme. A font collection consists of a font definition for Latin, East
+// Asian, and complex script. On top of these three definitions, one can also
+// define a font for use in a specific language or languages.
+type xlsxFontCollection struct {
+ Latin *xlsxCTTextFont `xml:"a:latin"`
+ Ea *xlsxCTTextFont `xml:"a:ea"`
+ Cs *xlsxCTTextFont `xml:"a:cs"`
+ Font []xlsxCTSupplementalFont `xml:"a:font"`
+ ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// xlsxFontScheme element defines the font scheme within the theme. The font
@@ -61,38 +108,23 @@ type xlsxClrScheme struct {
// document, and the minor font corresponds well with the normal text or
// paragraph areas.
type xlsxFontScheme struct {
- Name string `xml:"name,attr"`
- MajorFont xlsxMajorFont `xml:"majorFont"`
- MinorFont xlsxMinorFont `xml:"minorFont"`
- ExtLst *xlsxExtLst `xml:"extLst"`
-}
-
-// xlsxMajorFont element defines the set of major fonts which are to be used
-// under different languages or locals.
-type xlsxMajorFont struct {
- Children []xlsxFontSchemeEl `xml:",any"`
-}
-
-// xlsxMinorFont element defines the set of minor fonts that are to be used
-// under different languages or locals.
-type xlsxMinorFont struct {
- Children []xlsxFontSchemeEl `xml:",any"`
-}
-
-// xlsxFmtScheme element contains the background fill styles, effect styles,
-// fill styles, and line styles which define the style matrix for a theme. The
-// style matrix consists of subtle, moderate, and intense fills, lines, and
-// effects. The background fills are not generally thought of to directly be
-// associated with the matrix, but do play a role in the style of the overall
-// document. Usually, a given object chooses a single line style, a single
-// fill style, and a single effect style in order to define the overall final
-// look of the object.
-type xlsxFmtScheme struct {
- Name string `xml:"name,attr"`
- FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
- LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
- EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
- BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
+ Name string `xml:"name,attr"`
+ MajorFont xlsxFontCollection `xml:"a:majorFont"`
+ MinorFont xlsxFontCollection `xml:"a:minorFont"`
+ ExtLst *xlsxExtLst `xml:"a:extLst"`
+}
+
+// xlsxStyleMatrix defines a set of formatting options, which can be referenced
+// by documents that apply a certain style to a given part of an object. For
+// example, in a given shape, say a rectangle, one can reference a themed line
+// style, themed effect, and themed fill that would be theme specific and
+// change when the theme is changed.
+type xlsxStyleMatrix struct {
+ Name string `xml:"name,attr,omitempty"`
+ FillStyleLst xlsxFillStyleLst `xml:"a:fillStyleLst"`
+ LnStyleLst xlsxLnStyleLst `xml:"a:lnStyleLst"`
+ EffectStyleLst xlsxEffectStyleLst `xml:"a:effectStyleLst"`
+ BgFillStyleLst xlsxBgFillStyleLst `xml:"a:bgFillStyleLst"`
}
// xlsxFillStyleLst element defines a set of three fill styles that are used
@@ -116,36 +148,98 @@ type xlsxEffectStyleLst struct {
EffectStyleLst string `xml:",innerxml"`
}
-// xlsxBgFillStyleLst element defines a list of background fills that are
+// xlsxBgFillStyleLst element defines a list of background fills that are
// used within a theme. The background fills consist of three fills, arranged
// in order from subtle to moderate to intense.
type xlsxBgFillStyleLst struct {
BgFillStyleLst string `xml:",innerxml"`
}
-// xlsxClrScheme specifies the theme color, stored in the document's Theme
-// part to which the value of this theme color shall be mapped. This mapping
-// enables multiple theme colors to be chained together.
-type xlsxClrSchemeEl struct {
- XMLName xml.Name
- SysClr *xlsxSysClr `xml:"sysClr"`
- SrgbClr *attrValString `xml:"srgbClr"`
-}
-
-// xlsxFontSchemeEl directly maps the major and minor font of the style's font
-// scheme.
-type xlsxFontSchemeEl struct {
- XMLName xml.Name
- Script string `xml:"script,attr,omitempty"`
- Typeface string `xml:"typeface,attr"`
- Panose string `xml:"panose,attr,omitempty"`
- PitchFamily string `xml:"pitchFamily,attr,omitempty"`
- Charset string `xml:"charset,attr,omitempty"`
-}
-
// xlsxSysClr element specifies a color bound to predefined operating system
// elements.
type xlsxSysClr struct {
Val string `xml:"val,attr"`
LastClr string `xml:"lastClr,attr"`
}
+
+// decodeTheme defines the structure used to parse the a:theme element for the
+// theme.
+type decodeTheme struct {
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"`
+ Name string `xml:"name,attr"`
+ ThemeElements decodeBaseStyles `xml:"themeElements"`
+ ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
+ ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
+ CustClrLst *xlsxInnerXML `xml:"custClrLst"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// decodeBaseStyles defines the structure used to parse the theme elements for a
+// theme, and is the workhorse of the theme.
+type decodeBaseStyles struct {
+ ClrScheme decodeColorScheme `xml:"clrScheme"`
+ FontScheme decodeFontScheme `xml:"fontScheme"`
+ FmtScheme decodeStyleMatrix `xml:"fmtScheme"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// decodeColorScheme defines the structure used to parse a set of colors for the
+// theme.
+type decodeColorScheme struct {
+ Name string `xml:"name,attr"`
+ Dk1 decodeCTColor `xml:"dk1"`
+ Lt1 decodeCTColor `xml:"lt1"`
+ Dk2 decodeCTColor `xml:"dk2"`
+ Lt2 decodeCTColor `xml:"lt2"`
+ Accent1 decodeCTColor `xml:"accent1"`
+ Accent2 decodeCTColor `xml:"accent2"`
+ Accent3 decodeCTColor `xml:"accent3"`
+ Accent4 decodeCTColor `xml:"accent4"`
+ Accent5 decodeCTColor `xml:"accent5"`
+ Accent6 decodeCTColor `xml:"accent6"`
+ Hlink decodeCTColor `xml:"hlink"`
+ FolHlink decodeCTColor `xml:"folHlink"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// decodeFontScheme defines the structure used to parse font scheme within the
+// theme.
+type decodeFontScheme struct {
+ Name string `xml:"name,attr"`
+ MajorFont decodeFontCollection `xml:"majorFont"`
+ MinorFont decodeFontCollection `xml:"minorFont"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// decodeFontCollection defines the structure used to parse a major and minor
+// font which is used in the font scheme.
+type decodeFontCollection struct {
+ Latin *xlsxCTTextFont `xml:"latin"`
+ Ea *xlsxCTTextFont `xml:"ea"`
+ Cs *xlsxCTTextFont `xml:"cs"`
+ Font []xlsxCTSupplementalFont `xml:"font"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
+// decodeCTColor defines the structure used to parse the actual color values
+// that are to be applied to a given diagram and how those colors are to be
+// applied.
+type decodeCTColor struct {
+ ScrgbClr *xlsxInnerXML `xml:"scrgbClr"`
+ SrgbClr *attrValString `xml:"srgbClr"`
+ HslClr *xlsxInnerXML `xml:"hslClr"`
+ SysClr *xlsxSysClr `xml:"sysClr"`
+ SchemeClr *xlsxInnerXML `xml:"schemeClr"`
+ PrstClr *xlsxInnerXML `xml:"prstClr"`
+}
+
+// decodeStyleMatrix defines the structure used to parse a set of formatting
+// options, which can be referenced by documents that apply a certain style to
+// a given part of an object.
+type decodeStyleMatrix struct {
+ Name string `xml:"name,attr,omitempty"`
+ FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
+ LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
+ EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
+ BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
+}
diff --git a/xmlWorkbook.go b/xmlWorkbook.go
index 89cacd9253..0b0a4aa4ce 100644
--- a/xmlWorkbook.go
+++ b/xmlWorkbook.go
@@ -1,20 +1,25 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "encoding/xml"
+import (
+ "encoding/xml"
+ "sync"
+)
-// xlsxRelationships describe references from parts to other internal resources in the package or to external resources.
+// xlsxRelationships describe references from parts to other internal resources
+// in the package or to external resources.
type xlsxRelationships struct {
+ mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
Relationships []xlsxRelationship `xml:"Relationship"`
}
@@ -31,19 +36,29 @@ type xlsxRelationship struct {
// content of the workbook. The workbook's child elements each have their own
// subclause references.
type xlsxWorkbook struct {
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"`
- FileVersion *xlsxFileVersion `xml:"fileVersion"`
- WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"`
- WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"`
- BookViews *xlsxBookViews `xml:"bookViews"`
- Sheets xlsxSheets `xml:"sheets"`
- ExternalReferences *xlsxExternalReferences `xml:"externalReferences"`
- DefinedNames *xlsxDefinedNames `xml:"definedNames"`
- CalcPr *xlsxCalcPr `xml:"calcPr"`
- CustomWorkbookViews *xlsxCustomWorkbookViews `xml:"customWorkbookViews"`
- PivotCaches *xlsxPivotCaches `xml:"pivotCaches"`
- ExtLst *xlsxExtLst `xml:"extLst"`
- FileRecoveryPr *xlsxFileRecoveryPr `xml:"fileRecoveryPr"`
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"`
+ Conformance string `xml:"conformance,attr,omitempty"`
+ FileVersion *xlsxFileVersion `xml:"fileVersion"`
+ FileSharing *xlsxExtLst `xml:"fileSharing"`
+ WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"`
+ AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"`
+ DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"`
+ WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"`
+ BookViews *xlsxBookViews `xml:"bookViews"`
+ Sheets xlsxSheets `xml:"sheets"`
+ FunctionGroups *xlsxFunctionGroups `xml:"functionGroups"`
+ ExternalReferences *xlsxExternalReferences `xml:"externalReferences"`
+ DefinedNames *xlsxDefinedNames `xml:"definedNames"`
+ CalcPr *xlsxCalcPr `xml:"calcPr"`
+ OleSize *xlsxExtLst `xml:"oleSize"`
+ CustomWorkbookViews *xlsxCustomWorkbookViews `xml:"customWorkbookViews"`
+ PivotCaches *xlsxPivotCaches `xml:"pivotCaches"`
+ SmartTagPr *xlsxExtLst `xml:"smartTagPr"`
+ SmartTagTypes *xlsxExtLst `xml:"smartTagTypes"`
+ WebPublishing *xlsxExtLst `xml:"webPublishing"`
+ FileRecoveryPr *xlsxFileRecoveryPr `xml:"fileRecoveryPr"`
+ WebPublishObjects *xlsxExtLst `xml:"webPublishObjects"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxFileRecoveryPr maps sheet recovery information. This element defines
@@ -95,24 +110,24 @@ type xlsxFileVersion struct {
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
// defines a collection of workbook properties.
type xlsxWorkbookPr struct {
- AllowRefreshQuery bool `xml:"allowRefreshQuery,attr,omitempty"`
- AutoCompressPictures bool `xml:"autoCompressPictures,attr,omitempty"`
- BackupFile bool `xml:"backupFile,attr,omitempty"`
- CheckCompatibility bool `xml:"checkCompatibility,attr,omitempty"`
- CodeName string `xml:"codeName,attr,omitempty"`
Date1904 bool `xml:"date1904,attr,omitempty"`
- DefaultThemeVersion string `xml:"defaultThemeVersion,attr,omitempty"`
+ ShowObjects string `xml:"showObjects,attr,omitempty"`
+ ShowBorderUnselectedTables *bool `xml:"showBorderUnselectedTables,attr"`
FilterPrivacy bool `xml:"filterPrivacy,attr,omitempty"`
- HidePivotFieldList bool `xml:"hidePivotFieldList,attr,omitempty"`
PromptedSolutions bool `xml:"promptedSolutions,attr,omitempty"`
+ ShowInkAnnotation *bool `xml:"showInkAnnotation,attr"`
+ BackupFile bool `xml:"backupFile,attr,omitempty"`
+ SaveExternalLinkValues *bool `xml:"saveExternalLinkValues,attr"`
+ UpdateLinks string `xml:"updateLinks,attr,omitempty"`
+ CodeName string `xml:"codeName,attr,omitempty"`
+ HidePivotFieldList bool `xml:"hidePivotFieldList,attr,omitempty"`
+ ShowPivotChartFilter bool `xml:"showPivotChartFilter,attr,omitempty"`
+ AllowRefreshQuery bool `xml:"allowRefreshQuery,attr,omitempty"`
PublishItems bool `xml:"publishItems,attr,omitempty"`
+ CheckCompatibility bool `xml:"checkCompatibility,attr,omitempty"`
+ AutoCompressPictures *bool `xml:"autoCompressPictures,attr"`
RefreshAllConnections bool `xml:"refreshAllConnections,attr,omitempty"`
- SaveExternalLinkValues bool `xml:"saveExternalLinkValues,attr,omitempty"`
- ShowBorderUnselectedTables bool `xml:"showBorderUnselectedTables,attr,omitempty"`
- ShowInkAnnotation bool `xml:"showInkAnnotation,attr,omitempty"`
- ShowObjects string `xml:"showObjects,attr,omitempty"`
- ShowPivotChartFilter bool `xml:"showPivotChartFilter,attr,omitempty"`
- UpdateLinks string `xml:"updateLinks,attr,omitempty"`
+ DefaultThemeVersion string `xml:"defaultThemeVersion,attr,omitempty"`
}
// xlsxBookViews directly maps the bookViews element. This element specifies the
@@ -127,19 +142,19 @@ type xlsxBookViews struct {
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
// specifies a single Workbook view.
type xlsxWorkBookView struct {
- ActiveTab int `xml:"activeTab,attr,omitempty"`
- AutoFilterDateGrouping bool `xml:"autoFilterDateGrouping,attr,omitempty"`
- FirstSheet int `xml:"firstSheet,attr,omitempty"`
- Minimized bool `xml:"minimized,attr,omitempty"`
- ShowHorizontalScroll bool `xml:"showHorizontalScroll,attr,omitempty"`
- ShowSheetTabs bool `xml:"showSheetTabs,attr,omitempty"`
- ShowVerticalScroll bool `xml:"showVerticalScroll,attr,omitempty"`
- TabRatio int `xml:"tabRatio,attr,omitempty"`
- Visibility string `xml:"visibility,attr,omitempty"`
- WindowHeight int `xml:"windowHeight,attr,omitempty"`
- WindowWidth int `xml:"windowWidth,attr,omitempty"`
- XWindow string `xml:"xWindow,attr,omitempty"`
- YWindow string `xml:"yWindow,attr,omitempty"`
+ Visibility string `xml:"visibility,attr,omitempty"`
+ Minimized bool `xml:"minimized,attr,omitempty"`
+ ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"`
+ ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
+ ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
+ XWindow string `xml:"xWindow,attr,omitempty"`
+ YWindow string `xml:"yWindow,attr,omitempty"`
+ WindowWidth int `xml:"windowWidth,attr,omitempty"`
+ WindowHeight int `xml:"windowHeight,attr,omitempty"`
+ TabRatio float64 `xml:"tabRatio,attr,omitempty"`
+ FirstSheet int `xml:"firstSheet,attr,omitempty"`
+ ActiveTab int `xml:"activeTab,attr,omitempty"`
+ AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"`
}
// xlsxSheets directly maps the sheets element from the namespace
@@ -157,6 +172,17 @@ type xlsxSheet struct {
State string `xml:"state,attr,omitempty"`
}
+// xlsxFunctionGroup represents a single function group.
+type xlsxFunctionGroup struct {
+ Name string `xml:"name,attr"`
+}
+
+// xlsxFunctionGroups defines the collection of function groups for the workbook.
+type xlsxFunctionGroups struct {
+ BuiltInGroupCount *int `xml:"builtInGroupCount,attr"`
+ FunctionGroup []xlsxFunctionGroup `xml:"functionGroup"`
+}
+
// xlsxExternalReferences directly maps the externalReferences element of the
// external workbook references part.
type xlsxExternalReferences struct {
@@ -187,12 +213,70 @@ type xlsxPivotCache struct {
// document are specified in the markup specification and can be used to store
// extensions to the markup specification, whether those are future version
// extensions of the markup specification or are private extensions implemented
-// independently from the markup specification. Markup within an extension might
+// independently of the markup specification. Markup within an extension might
// not be understood by a consumer.
type xlsxExtLst struct {
Ext string `xml:",innerxml"`
}
+// xlsxExt represents a the future feature data storage area. Each extension
+// within an extension list shall be contained within an ext element.
+// Extensions shall be versioned by namespace, using the uri attribute, and
+// shall be allowed to appear in any order within the extension list. Any
+// number of extensions shall be allowed within an extension list.
+type xlsxExt struct {
+ XMLName xml.Name `xml:"ext"`
+ URI string `xml:"uri,attr"`
+ Content string `xml:",innerxml"`
+ xmlns []xml.Attr
+}
+
+// xlsxAlternateContent is a container for a sequence of multiple
+// representations of a given piece of content. The program reading the file
+// should only process one of these, and the one chosen should be based on
+// which conditions match.
+type xlsxAlternateContent struct {
+ XMLNSMC string `xml:"xmlns:mc,attr,omitempty"`
+ Content string `xml:",innerxml"`
+}
+
+// xlsxChoice element shall be an element in the Markup Compatibility namespace
+// with local name "Choice". Parent elements of Choice elements shall be
+// AlternateContent elements.
+type xlsxChoice struct {
+ XMLName xml.Name `xml:"mc:Choice"`
+ XMLNSA14 string `xml:"xmlns:a14,attr,omitempty"`
+ XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"`
+ Requires string `xml:"Requires,attr,omitempty"`
+ Content string `xml:",innerxml"`
+}
+
+// xlsxFallback element shall be an element in the Markup Compatibility
+// namespace with local name "Fallback". Parent elements of Fallback elements
+// shall be AlternateContent elements.
+type xlsxFallback struct {
+ XMLName xml.Name `xml:"mc:Fallback"`
+ Content string `xml:",innerxml"`
+}
+
+// xlsxInnerXML holds parts of XML content currently not unmarshal.
+type xlsxInnerXML struct {
+ Content string `xml:",innerxml"`
+}
+
+// decodeExtLst defines the structure used to parse the extLst element
+// of the future feature data storage area.
+type decodeExtLst struct {
+ XMLName xml.Name `xml:"extLst"`
+ Ext []*xlsxExt `xml:"ext"`
+}
+
+// decodeExt defines the structure used to parse the ext element.
+type decodeExt struct {
+ URI string `xml:"uri,attr,omitempty"`
+ Content string `xml:",innerxml"`
+}
+
// xlsxDefinedNames directly maps the definedNames element. This element defines
// the collection of defined names for this workbook. Defined names are
// descriptive names to represent cells, ranges of cells, formulas, or constant
@@ -204,8 +288,8 @@ type xlsxDefinedNames struct {
// xlsxDefinedName directly maps the definedName element from the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
// defines a defined name within this workbook. A defined name is descriptive
-// text that is used to represents a cell, range of cells, formula, or constant
-// value. For a descriptions of the attributes see https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname
+// text that is used to represent a cell, range of cells, formula, or constant
+// value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname
type xlsxDefinedName struct {
Comment string `xml:"comment,attr,omitempty"`
CustomMenu string `xml:"customMenu,attr,omitempty"`
@@ -231,7 +315,7 @@ type xlsxDefinedName struct {
// displaying the results as values in the cells that contain the formulas.
type xlsxCalcPr struct {
CalcCompleted bool `xml:"calcCompleted,attr,omitempty"`
- CalcID string `xml:"calcId,attr,omitempty"`
+ CalcID int `xml:"calcId,attr,omitempty"`
CalcMode string `xml:"calcMode,attr,omitempty"`
CalcOnSave bool `xml:"calcOnSave,attr,omitempty"`
ConcurrentCalc *bool `xml:"concurrentCalc,attr"`
@@ -265,30 +349,30 @@ type xlsxCustomWorkbookViews struct {
// to implement configurable display modes, the customWorkbookView element
// should be used to persist the settings for those display modes.
type xlsxCustomWorkbookView struct {
- ActiveSheetID *int `xml:"activeSheetId,attr"`
- AutoUpdate *bool `xml:"autoUpdate,attr"`
- ChangesSavedWin *bool `xml:"changesSavedWin,attr"`
- GUID *string `xml:"guid,attr"`
- IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"`
- IncludePrintSettings *bool `xml:"includePrintSettings,attr"`
- Maximized *bool `xml:"maximized,attr"`
- MergeInterval int `xml:"mergeInterval,attr"`
- Minimized *bool `xml:"minimized,attr"`
- Name *string `xml:"name,attr"`
- OnlySync *bool `xml:"onlySync,attr"`
- PersonalView *bool `xml:"personalView,attr"`
- ShowComments *string `xml:"showComments,attr"`
- ShowFormulaBar *bool `xml:"showFormulaBar,attr"`
- ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"`
- ShowObjects *string `xml:"showObjects,attr"`
- ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
- ShowStatusbar *bool `xml:"showStatusbar,attr"`
- ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
- TabRatio *int `xml:"tabRatio,attr"`
- WindowHeight *int `xml:"windowHeight,attr"`
- WindowWidth *int `xml:"windowWidth,attr"`
- XWindow *int `xml:"xWindow,attr"`
- YWindow *int `xml:"yWindow,attr"`
+ ActiveSheetID *int `xml:"activeSheetId,attr"`
+ AutoUpdate *bool `xml:"autoUpdate,attr"`
+ ChangesSavedWin *bool `xml:"changesSavedWin,attr"`
+ GUID *string `xml:"guid,attr"`
+ IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"`
+ IncludePrintSettings *bool `xml:"includePrintSettings,attr"`
+ Maximized *bool `xml:"maximized,attr"`
+ MergeInterval int `xml:"mergeInterval,attr"`
+ Minimized *bool `xml:"minimized,attr"`
+ Name *string `xml:"name,attr"`
+ OnlySync *bool `xml:"onlySync,attr"`
+ PersonalView *bool `xml:"personalView,attr"`
+ ShowComments *string `xml:"showComments,attr"`
+ ShowFormulaBar *bool `xml:"showFormulaBar,attr"`
+ ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"`
+ ShowObjects *string `xml:"showObjects,attr"`
+ ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
+ ShowStatusbar *bool `xml:"showStatusbar,attr"`
+ ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
+ TabRatio *float64 `xml:"tabRatio,attr"`
+ WindowHeight *int `xml:"windowHeight,attr"`
+ WindowWidth *int `xml:"windowWidth,attr"`
+ XWindow *int `xml:"xWindow,attr"`
+ YWindow *int `xml:"yWindow,attr"`
}
// DefinedName directly maps the name for a cell or cell range on a
@@ -299,3 +383,36 @@ type DefinedName struct {
RefersTo string
Scope string
}
+
+// CalcPropsOptions defines the collection of properties the application uses to
+// record calculation status and details.
+type CalcPropsOptions struct {
+ CalcID *uint
+ CalcMode *string
+ FullCalcOnLoad *bool
+ RefMode *string
+ Iterate *bool
+ IterateCount *uint
+ IterateDelta *float64
+ FullPrecision *bool
+ CalcCompleted *bool
+ CalcOnSave *bool
+ ConcurrentCalc *bool
+ ConcurrentManualCount *uint
+ ForceFullCalc *bool
+}
+
+// WorkbookPropsOptions directly maps the settings of workbook proprieties.
+type WorkbookPropsOptions struct {
+ Date1904 *bool
+ FilterPrivacy *bool
+ CodeName *string
+}
+
+// WorkbookProtectionOptions directly maps the settings of workbook protection.
+type WorkbookProtectionOptions struct {
+ AlgorithmName string
+ Password string
+ LockStructure bool
+ LockWindows bool
+}
diff --git a/xmlWorksheet.go b/xmlWorksheet.go
index 7cd73c4185..dab4caf321 100644
--- a/xmlWorksheet.go
+++ b/xmlWorksheet.go
@@ -1,61 +1,67 @@
-// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
+// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
-// Package excelize providing a set of functions that allow you to write to
-// and read from XLSX / XLSM / XLTM files. Supports reading and writing
-// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports
-// complex components by high compatibility, and provided streaming API for
-// generating or reading data from a worksheet with huge amounts of data. This
-// library needs Go version 1.10 or later.
+// Package excelize providing a set of functions that allow you to write to and
+// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
+// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
+// Supports complex components by high compatibility, and provided streaming
+// API for generating or reading data from a worksheet with huge amounts of
+// data. This library needs Go version 1.23 or later.
package excelize
-import "encoding/xml"
+import (
+ "encoding/xml"
+ "sync"
+)
// xlsxWorksheet directly maps the worksheet element in the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main.
type xlsxWorksheet struct {
- XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
- SheetPr *xlsxSheetPr `xml:"sheetPr"`
- Dimension *xlsxDimension `xml:"dimension"`
- SheetViews *xlsxSheetViews `xml:"sheetViews"`
- SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"`
- Cols *xlsxCols `xml:"cols"`
- SheetData xlsxSheetData `xml:"sheetData"`
- SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"`
- SheetProtection *xlsxSheetProtection `xml:"sheetProtection"`
- ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"`
- Scenarios *xlsxInnerXML `xml:"scenarios"`
- AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
- SortState *xlsxSortState `xml:"sortState"`
- DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"`
- CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"`
- MergeCells *xlsxMergeCells `xml:"mergeCells"`
- PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"`
- ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"`
- DataValidations *xlsxDataValidations `xml:"dataValidations"`
- Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"`
- PrintOptions *xlsxPrintOptions `xml:"printOptions"`
- PageMargins *xlsxPageMargins `xml:"pageMargins"`
- PageSetUp *xlsxPageSetUp `xml:"pageSetup"`
- HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"`
- RowBreaks *xlsxBreaks `xml:"rowBreaks"`
- ColBreaks *xlsxBreaks `xml:"colBreaks"`
- CustomProperties *xlsxInnerXML `xml:"customProperties"`
- CellWatches *xlsxInnerXML `xml:"cellWatches"`
- IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"`
- SmartTags *xlsxInnerXML `xml:"smartTags"`
- Drawing *xlsxDrawing `xml:"drawing"`
- LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"`
- LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"`
- DrawingHF *xlsxDrawingHF `xml:"drawingHF"`
- Picture *xlsxPicture `xml:"picture"`
- OleObjects *xlsxInnerXML `xml:"oleObjects"`
- Controls *xlsxInnerXML `xml:"controls"`
- WebPublishItems *xlsxInnerXML `xml:"webPublishItems"`
- TableParts *xlsxTableParts `xml:"tableParts"`
- ExtLst *xlsxExtLst `xml:"extLst"`
+ mu sync.Mutex
+ XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
+ SheetPr *xlsxSheetPr `xml:"sheetPr"`
+ Dimension *xlsxDimension `xml:"dimension"`
+ SheetViews *xlsxSheetViews `xml:"sheetViews"`
+ SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"`
+ Cols *xlsxCols `xml:"cols"`
+ SheetData xlsxSheetData `xml:"sheetData"`
+ SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"`
+ SheetProtection *xlsxSheetProtection `xml:"sheetProtection"`
+ ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"`
+ Scenarios *xlsxInnerXML `xml:"scenarios"`
+ AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
+ SortState *xlsxSortState `xml:"sortState"`
+ DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"`
+ CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"`
+ MergeCells *xlsxMergeCells `xml:"mergeCells"`
+ PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"`
+ ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"`
+ DataValidations *xlsxDataValidations `xml:"dataValidations"`
+ Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"`
+ PrintOptions *xlsxPrintOptions `xml:"printOptions"`
+ PageMargins *xlsxPageMargins `xml:"pageMargins"`
+ PageSetUp *xlsxPageSetUp `xml:"pageSetup"`
+ HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"`
+ RowBreaks *xlsxRowBreaks `xml:"rowBreaks"`
+ ColBreaks *xlsxColBreaks `xml:"colBreaks"`
+ CustomProperties *xlsxInnerXML `xml:"customProperties"`
+ CellWatches *xlsxInnerXML `xml:"cellWatches"`
+ IgnoredErrors *xlsxIgnoredErrors `xml:"ignoredErrors"`
+ SmartTags *xlsxInnerXML `xml:"smartTags"`
+ Drawing *xlsxDrawing `xml:"drawing"`
+ LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"`
+ LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"`
+ DrawingHF *xlsxDrawingHF `xml:"drawingHF"`
+ Picture *xlsxPicture `xml:"picture"`
+ OleObjects *xlsxInnerXML `xml:"oleObjects"`
+ Controls *xlsxInnerXML `xml:"controls"`
+ WebPublishItems *xlsxInnerXML `xml:"webPublishItems"`
+ AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"`
+ TableParts *xlsxTableParts `xml:"tableParts"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+ DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"`
}
// xlsxDrawing change r:id to rid in the namespace.
@@ -72,18 +78,17 @@ type xlsxDrawing struct {
// footers on the first page can differ from those on odd- and even-numbered
// pages. In the latter case, the first page is not considered an odd page.
type xlsxHeaderFooter struct {
- XMLName xml.Name `xml:"headerFooter"`
- AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"`
- DifferentFirst bool `xml:"differentFirst,attr,omitempty"`
- DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"`
- ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"`
- OddHeader string `xml:"oddHeader,omitempty"`
- OddFooter string `xml:"oddFooter,omitempty"`
- EvenHeader string `xml:"evenHeader,omitempty"`
- EvenFooter string `xml:"evenFooter,omitempty"`
- FirstFooter string `xml:"firstFooter,omitempty"`
- FirstHeader string `xml:"firstHeader,omitempty"`
- DrawingHF *xlsxDrawingHF `xml:"drawingHF"`
+ XMLName xml.Name `xml:"headerFooter"`
+ DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"`
+ DifferentFirst bool `xml:"differentFirst,attr,omitempty"`
+ ScaleWithDoc *bool `xml:"scaleWithDoc,attr"`
+ AlignWithMargins *bool `xml:"alignWithMargins,attr"`
+ OddHeader string `xml:"oddHeader,omitempty"`
+ OddFooter string `xml:"oddFooter,omitempty"`
+ EvenHeader string `xml:"evenHeader,omitempty"`
+ EvenFooter string `xml:"evenFooter,omitempty"`
+ FirstHeader string `xml:"firstHeader,omitempty"`
+ FirstFooter string `xml:"firstFooter,omitempty"`
}
// xlsxDrawingHF (Drawing Reference in Header Footer) specifies the usage of
@@ -107,20 +112,20 @@ type xlsxPageSetUp struct {
Copies int `xml:"copies,attr,omitempty"`
Draft bool `xml:"draft,attr,omitempty"`
Errors string `xml:"errors,attr,omitempty"`
- FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"`
- FitToHeight int `xml:"fitToHeight,attr,omitempty"`
- FitToWidth int `xml:"fitToWidth,attr,omitempty"`
- HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"`
+ FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"`
+ FitToHeight *int `xml:"fitToHeight,attr"`
+ FitToWidth *int `xml:"fitToWidth,attr"`
+ HorizontalDPI string `xml:"horizontalDpi,attr,omitempty"`
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
Orientation string `xml:"orientation,attr,omitempty"`
PageOrder string `xml:"pageOrder,attr,omitempty"`
PaperHeight string `xml:"paperHeight,attr,omitempty"`
- PaperSize int `xml:"paperSize,attr,omitempty"`
+ PaperSize *int `xml:"paperSize,attr"`
PaperWidth string `xml:"paperWidth,attr,omitempty"`
Scale int `xml:"scale,attr,omitempty"`
UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"`
UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"`
- VerticalDPI int `xml:"verticalDpi,attr,omitempty"`
+ VerticalDPI string `xml:"verticalDpi,attr,omitempty"`
}
// xlsxPrintOptions directly maps the printOptions element in the namespace
@@ -141,12 +146,12 @@ type xlsxPrintOptions struct {
// a sheet or a custom sheet view.
type xlsxPageMargins struct {
XMLName xml.Name `xml:"pageMargins"`
- Bottom float64 `xml:"bottom,attr"`
- Footer float64 `xml:"footer,attr"`
- Header float64 `xml:"header,attr"`
Left float64 `xml:"left,attr"`
Right float64 `xml:"right,attr"`
Top float64 `xml:"top,attr"`
+ Bottom float64 `xml:"bottom,attr"`
+ Header float64 `xml:"header,attr"`
+ Footer float64 `xml:"footer,attr"`
}
// xlsxSheetFormatPr directly maps the sheetFormatPr element in the namespace
@@ -187,6 +192,7 @@ type xlsxSheetView struct {
ShowZeros *bool `xml:"showZeros,attr,omitempty"`
RightToLeft bool `xml:"rightToLeft,attr,omitempty"`
TabSelected bool `xml:"tabSelected,attr,omitempty"`
+ ShowRuler *bool `xml:"showRuler,attr,omitempty"`
ShowWhiteSpace *bool `xml:"showWhiteSpace,attr"`
ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr,omitempty"`
DefaultGridColor *bool `xml:"defaultGridColor,attr"`
@@ -230,20 +236,23 @@ type xlsxSheetPr struct {
SyncVertical bool `xml:"syncVertical,attr,omitempty"`
SyncRef string `xml:"syncRef,attr,omitempty"`
TransitionEvaluation bool `xml:"transitionEvaluation,attr,omitempty"`
+ TransitionEntry bool `xml:"transitionEntry,attr,omitempty"`
Published *bool `xml:"published,attr"`
CodeName string `xml:"codeName,attr,omitempty"`
FilterMode bool `xml:"filterMode,attr,omitempty"`
EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"`
- TransitionEntry bool `xml:"transitionEntry,attr,omitempty"`
- TabColor *xlsxTabColor `xml:"tabColor,omitempty"`
- OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"`
- PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"`
+ TabColor *xlsxColor `xml:"tabColor"`
+ OutlinePr *xlsxOutlinePr `xml:"outlinePr"`
+ PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"`
}
// xlsxOutlinePr maps to the outlinePr element. SummaryBelow allows you to
// adjust the direction of grouper controls.
type xlsxOutlinePr struct {
- SummaryBelow bool `xml:"summaryBelow,attr"`
+ ApplyStyles *bool `xml:"applyStyles,attr"`
+ SummaryBelow *bool `xml:"summaryBelow,attr"`
+ SummaryRight *bool `xml:"summaryRight,attr"`
+ ShowOutlineSymbols *bool `xml:"showOutlineSymbols,attr"`
}
// xlsxPageSetUpPr expresses page setup properties of the worksheet.
@@ -252,13 +261,6 @@ type xlsxPageSetUpPr struct {
FitToPage bool `xml:"fitToPage,attr,omitempty"`
}
-// xlsxTabColor represents background color of the sheet tab.
-type xlsxTabColor struct {
- RGB string `xml:"rgb,attr,omitempty"`
- Theme int `xml:"theme,attr,omitempty"`
- Tint float64 `xml:"tint,attr,omitempty"`
-}
-
// xlsxCols defines column width and column formatting for one or more columns
// of the worksheet.
type xlsxCols struct {
@@ -269,16 +271,16 @@ type xlsxCols struct {
// xlsxCol directly maps the col (Column Width & Formatting). Defines column
// width and column formatting for one or more columns of the worksheet.
type xlsxCol struct {
- BestFit bool `xml:"bestFit,attr,omitempty"`
- Collapsed bool `xml:"collapsed,attr,omitempty"`
- CustomWidth bool `xml:"customWidth,attr,omitempty"`
- Hidden bool `xml:"hidden,attr,omitempty"`
- Max int `xml:"max,attr"`
- Min int `xml:"min,attr"`
- OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"`
- Phonetic bool `xml:"phonetic,attr,omitempty"`
- Style int `xml:"style,attr,omitempty"`
- Width float64 `xml:"width,attr,omitempty"`
+ BestFit bool `xml:"bestFit,attr,omitempty"`
+ Collapsed bool `xml:"collapsed,attr,omitempty"`
+ CustomWidth bool `xml:"customWidth,attr,omitempty"`
+ Hidden bool `xml:"hidden,attr,omitempty"`
+ Max int `xml:"max,attr"`
+ Min int `xml:"min,attr"`
+ OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"`
+ Phonetic bool `xml:"phonetic,attr,omitempty"`
+ Style int `xml:"style,attr,omitempty"`
+ Width *float64 `xml:"width,attr"`
}
// xlsxDimension directly maps the dimension element in the namespace
@@ -305,19 +307,19 @@ type xlsxSheetData struct {
// about an entire row of a worksheet, and contains all cell definitions for a
// particular row in the worksheet.
type xlsxRow struct {
- Collapsed bool `xml:"collapsed,attr,omitempty"`
- CustomFormat bool `xml:"customFormat,attr,omitempty"`
- CustomHeight bool `xml:"customHeight,attr,omitempty"`
- Hidden bool `xml:"hidden,attr,omitempty"`
- Ht float64 `xml:"ht,attr,omitempty"`
- OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"`
- Ph bool `xml:"ph,attr,omitempty"`
- R int `xml:"r,attr,omitempty"`
- S int `xml:"s,attr,omitempty"`
- Spans string `xml:"spans,attr,omitempty"`
- ThickBot bool `xml:"thickBot,attr,omitempty"`
- ThickTop bool `xml:"thickTop,attr,omitempty"`
- C []xlsxC `xml:"c"`
+ C []xlsxC `xml:"c"`
+ R int `xml:"r,attr,omitempty"`
+ Spans string `xml:"spans,attr,omitempty"`
+ S int `xml:"s,attr,omitempty"`
+ CustomFormat bool `xml:"customFormat,attr,omitempty"`
+ Ht *float64 `xml:"ht,attr"`
+ Hidden bool `xml:"hidden,attr,omitempty"`
+ CustomHeight bool `xml:"customHeight,attr,omitempty"`
+ OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"`
+ Collapsed bool `xml:"collapsed,attr,omitempty"`
+ ThickTop bool `xml:"thickTop,attr,omitempty"`
+ ThickBot bool `xml:"thickBot,attr,omitempty"`
+ Ph bool `xml:"ph,attr,omitempty"`
}
// xlsxSortState directly maps the sortState element. This collection
@@ -347,6 +349,18 @@ type xlsxBrk struct {
Pt bool `xml:"pt,attr,omitempty"`
}
+// xlsxRowBreaks directly maps a collection of the row breaks.
+type xlsxRowBreaks struct {
+ XMLName xml.Name `xml:"rowBreaks"`
+ xlsxBreaks
+}
+
+// xlsxRowBreaks directly maps a collection of the column breaks.
+type xlsxColBreaks struct {
+ XMLName xml.Name `xml:"colBreaks"`
+ xlsxBreaks
+}
+
// xlsxBreaks directly maps a collection of the row or column breaks.
type xlsxBreaks struct {
Brk []*xlsxBrk `xml:"brk"`
@@ -390,7 +404,8 @@ type xlsxCustomSheetView struct {
// xlsxMergeCell directly maps the mergeCell element. A single merged cell.
type xlsxMergeCell struct {
- Ref string `xml:"ref,attr,omitempty"`
+ Ref string `xml:"ref,attr,omitempty"`
+ rect []int
}
// xlsxMergeCells directly maps the mergeCells element. This collection
@@ -404,31 +419,32 @@ type xlsxMergeCells struct {
// xlsxDataValidations expresses all data validation information for cells in a
// sheet which have data validation features applied.
type xlsxDataValidations struct {
- XMLName xml.Name `xml:"dataValidations"`
- Count int `xml:"count,attr,omitempty"`
- DisablePrompts bool `xml:"disablePrompts,attr,omitempty"`
- XWindow int `xml:"xWindow,attr,omitempty"`
- YWindow int `xml:"yWindow,attr,omitempty"`
- DataValidation []*DataValidation `xml:"dataValidation"`
+ XMLName xml.Name `xml:"dataValidations"`
+ Count int `xml:"count,attr,omitempty"`
+ DisablePrompts bool `xml:"disablePrompts,attr,omitempty"`
+ XWindow int `xml:"xWindow,attr,omitempty"`
+ YWindow int `xml:"yWindow,attr,omitempty"`
+ DataValidation []*xlsxDataValidation `xml:"dataValidation"`
}
-// DataValidation directly maps the a single item of data validation defined
+// DataValidation directly maps the single item of data validation defined
// on a range of the worksheet.
-type DataValidation struct {
- AllowBlank bool `xml:"allowBlank,attr"`
- Error *string `xml:"error,attr"`
- ErrorStyle *string `xml:"errorStyle,attr"`
- ErrorTitle *string `xml:"errorTitle,attr"`
- Operator string `xml:"operator,attr,omitempty"`
- Prompt *string `xml:"prompt,attr"`
- PromptTitle *string `xml:"promptTitle,attr"`
- ShowDropDown bool `xml:"showDropDown,attr,omitempty"`
- ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
- ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
- Sqref string `xml:"sqref,attr"`
- Type string `xml:"type,attr,omitempty"`
- Formula1 string `xml:",innerxml"`
- Formula2 string `xml:",innerxml"`
+type xlsxDataValidation struct {
+ AllowBlank bool `xml:"allowBlank,attr"`
+ Error *string `xml:"error,attr"`
+ ErrorStyle *string `xml:"errorStyle,attr"`
+ ErrorTitle *string `xml:"errorTitle,attr"`
+ Operator string `xml:"operator,attr,omitempty"`
+ Prompt *string `xml:"prompt,attr"`
+ PromptTitle *string `xml:"promptTitle,attr"`
+ ShowDropDown bool `xml:"showDropDown,attr,omitempty"`
+ ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
+ ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
+ Sqref string `xml:"sqref,attr"`
+ XMSqref string `xml:"sqref,omitempty"`
+ Type string `xml:"type,attr,omitempty"`
+ Formula1 *xlsxInnerXML `xml:"formula1"`
+ Formula2 *xlsxInnerXML `xml:"formula2"`
}
// xlsxC collection represents a cell in the worksheet. Information about the
@@ -437,39 +453,50 @@ type DataValidation struct {
//
// This simple type is restricted to the values listed in the following table:
//
-// Enumeration Value | Description
-// ---------------------------+---------------------------------
-// b (Boolean) | Cell containing a boolean.
-// d (Date) | Cell contains a date in the ISO 8601 format.
-// e (Error) | Cell containing an error.
-// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e., one not in the shared string table. If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element).
-// n (Number) | Cell containing a number.
-// s (Shared String) | Cell containing a shared string.
-// str (String) | Cell containing a formula string.
-//
+// Enumeration Value | Description
+// ---------------------------+---------------------------------
+// b (Boolean) | Cell containing a boolean.
+// d (Date) | Cell contains a date in the ISO 8601 format.
+// e (Error) | Cell containing an error.
+// inlineStr (Inline String) | Cell containing an (inline) rich string, i.e.,
+// | one not in the shared string table. If this
+// | cell type is used, then the cell value is in
+// | the is element rather than the v element in
+// | the cell (c element).
+// n (Number) | Cell containing a number.
+// s (Shared String) | Cell containing a shared string.
+// str (String) | Cell containing a formula string.
type xlsxC struct {
XMLName xml.Name `xml:"c"`
XMLSpace xml.Attr `xml:"space,attr,omitempty"`
R string `xml:"r,attr,omitempty"` // Cell ID, e.g. A1
- S int `xml:"s,attr,omitempty"` // Style reference.
- // Str string `xml:"str,attr,omitempty"` // Style reference.
- T string `xml:"t,attr,omitempty"` // Type.
- F *xlsxF `xml:"f,omitempty"` // Formula
- V string `xml:"v,omitempty"` // Value
- IS *xlsxSI `xml:"is"`
-}
-
-func (c *xlsxC) hasValue() bool {
- return c.S != 0 || c.V != "" || c.F != nil || c.T != ""
+ S int `xml:"s,attr,omitempty"` // Style reference
+ T string `xml:"t,attr,omitempty"` // Type
+ Cm *uint `xml:"cm,attr"`
+ Vm *uint `xml:"vm,attr"`
+ Ph *bool `xml:"ph,attr"`
+ F *xlsxF `xml:"f"` // Formula
+ V string `xml:"v,omitempty"` // Value
+ IS *xlsxSI `xml:"is"`
+ f string
}
// xlsxF represents a formula for the cell. The formula expression is
// contained in the character node of this element.
type xlsxF struct {
Content string `xml:",chardata"`
- T string `xml:"t,attr,omitempty"` // Formula type
+ T string `xml:"t,attr,omitempty"` // Formula type
+ Aca bool `xml:"aca,attr,omitempty"`
Ref string `xml:"ref,attr,omitempty"` // Shared formula ref
- Si string `xml:"si,attr,omitempty"` // Shared formula index
+ Dt2D bool `xml:"dt2D,attr,omitempty"`
+ Dtr bool `xml:"dtr,attr,omitempty"`
+ Del1 bool `xml:"del1,attr,omitempty"`
+ Del2 bool `xml:"del2,attr,omitempty"`
+ R1 string `xml:"r1,attr,omitempty"`
+ R2 string `xml:"r2,attr,omitempty"`
+ Ca bool `xml:"ca,attr,omitempty"`
+ Si *int `xml:"si,attr"` // Shared formula index
+ Bx bool `xml:"bx,attr,omitempty"`
}
// xlsxSheetProtection collection expresses the sheet protection options to
@@ -519,6 +546,7 @@ type xlsxPhoneticPr struct {
// applied to a particular cell or range.
type xlsxConditionalFormatting struct {
XMLName xml.Name `xml:"conditionalFormatting"`
+ Pivot bool `xml:"pivot,attr,omitempty"`
SQRef string `xml:"sqref,attr,omitempty"`
CfRule []*xlsxCfRule `xml:"cfRule"`
}
@@ -526,19 +554,19 @@ type xlsxConditionalFormatting struct {
// xlsxCfRule (Conditional Formatting Rule) represents a description of a
// conditional formatting rule.
type xlsxCfRule struct {
- AboveAverage *bool `xml:"aboveAverage,attr"`
- Bottom bool `xml:"bottom,attr,omitempty"`
+ Type string `xml:"type,attr,omitempty"`
DxfID *int `xml:"dxfId,attr"`
- EqualAverage bool `xml:"equalAverage,attr,omitempty"`
- Operator string `xml:"operator,attr,omitempty"`
- Percent bool `xml:"percent,attr,omitempty"`
Priority int `xml:"priority,attr,omitempty"`
- Rank int `xml:"rank,attr,omitempty"`
- StdDev int `xml:"stdDev,attr,omitempty"`
StopIfTrue bool `xml:"stopIfTrue,attr,omitempty"`
+ AboveAverage *bool `xml:"aboveAverage,attr"`
+ Percent bool `xml:"percent,attr,omitempty"`
+ Bottom bool `xml:"bottom,attr,omitempty"`
+ Operator string `xml:"operator,attr,omitempty"`
Text string `xml:"text,attr,omitempty"`
TimePeriod string `xml:"timePeriod,attr,omitempty"`
- Type string `xml:"type,attr,omitempty"`
+ Rank int `xml:"rank,attr,omitempty"`
+ StdDev int `xml:"stdDev,attr,omitempty"`
+ EqualAverage bool `xml:"equalAverage,attr,omitempty"`
Formula []string `xml:"formula,omitempty"`
ColorScale *xlsxColorScale `xml:"colorScale"`
DataBar *xlsxDataBar `xml:"dataBar"`
@@ -557,7 +585,7 @@ type xlsxColorScale struct {
type xlsxDataBar struct {
MaxLength int `xml:"maxLength,attr,omitempty"`
MinLength int `xml:"minLength,attr,omitempty"`
- ShowValue bool `xml:"showValue,attr,omitempty"`
+ ShowValue *bool `xml:"showValue,attr"`
Cfvo []*xlsxCfvo `xml:"cfvo"`
Color []*xlsxColor `xml:"color"`
}
@@ -566,7 +594,7 @@ type xlsxDataBar struct {
type xlsxIconSet struct {
Cfvo []*xlsxCfvo `xml:"cfvo"`
IconSet string `xml:"iconSet,attr,omitempty"`
- ShowValue bool `xml:"showValue,attr,omitempty"`
+ ShowValue *bool `xml:"showValue,attr"`
Percent bool `xml:"percent,attr,omitempty"`
Reverse bool `xml:"reverse,attr,omitempty"`
}
@@ -595,6 +623,7 @@ type xlsxHyperlink struct {
Ref string `xml:"ref,attr"`
Location string `xml:"location,attr,omitempty"`
Display string `xml:"display,attr,omitempty"`
+ Tooltip string `xml:"tooltip,attr,omitempty"`
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
}
@@ -622,13 +651,12 @@ type xlsxHyperlink struct {
// size of the sample. To reference the table, just add the tableParts element,
// of course after having created and stored the table part. For example:
//
-//
-// ...
-//
-//
-//
-//
-//
+//
+// ...
+//
+//
+//
+//
type xlsxTableParts struct {
XMLName xml.Name `xml:"tableParts"`
Count int `xml:"count,attr,omitempty"`
@@ -645,13 +673,34 @@ type xlsxTablePart struct {
// http://schemas.openxmlformats.org/spreadsheetml/2006/main - Background sheet
// image. For example:
//
-//
-//
+//
type xlsxPicture struct {
XMLName xml.Name `xml:"picture"`
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
}
+// xlsxIgnoredError specifies a single ignored error for a range of cells.
+type xlsxIgnoredError struct {
+ XMLName xml.Name `xml:"ignoredError"`
+ Sqref string `xml:"sqref,attr"`
+ EvalError bool `xml:"evalError,attr,omitempty"`
+ TwoDigitTextYear bool `xml:"twoDigitTextYear,attr,omitempty"`
+ NumberStoredAsText bool `xml:"numberStoredAsText,attr,omitempty"`
+ Formula bool `xml:"formula,attr,omitempty"`
+ FormulaRange bool `xml:"formulaRange,attr,omitempty"`
+ UnlockedFormula bool `xml:"unlockedFormula,attr,omitempty"`
+ EmptyCellReference bool `xml:"emptyCellReference,attr,omitempty"`
+ ListDataValidation bool `xml:"listDataValidation,attr,omitempty"`
+ CalculatedColumn bool `xml:"calculatedColumn,attr,omitempty"`
+}
+
+// xlsxIgnoredErrors specifies a collection of ignored errors, by cell range.
+type xlsxIgnoredErrors struct {
+ XMLName xml.Name `xml:"ignoredErrors"`
+ IgnoredError []xlsxIgnoredError `xml:"ignoredError"`
+ ExtLst *xlsxExtLst `xml:"extLst"`
+}
+
// xlsxLegacyDrawing directly maps the legacyDrawing element in the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main - A comment is a
// rich text note that is attached to, and associated with, a cell, separate
@@ -674,30 +723,100 @@ type xlsxLegacyDrawingHF struct {
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
}
-type xlsxInnerXML struct {
- Content string `xml:",innerxml"`
+// decodeX14SparklineGroups directly maps the sparklineGroups element.
+type decodeX14SparklineGroups struct {
+ XMLName xml.Name `xml:"sparklineGroups"`
+ XMLNSXM string `xml:"xmlns:xm,attr"`
+ Content string `xml:",innerxml"`
}
-// xlsxWorksheetExt directly maps the ext element in the worksheet.
-type xlsxWorksheetExt struct {
+// decodeX14ConditionalFormattingExt directly maps the ext element.
+type decodeX14ConditionalFormattingExt struct {
XMLName xml.Name `xml:"ext"`
- URI string `xml:"uri,attr"`
+ ID string `xml:"id"`
+}
+
+// decodeX14ConditionalFormattings directly maps the conditionalFormattings
+// element.
+type decodeX14ConditionalFormattings struct {
+ XMLName xml.Name `xml:"conditionalFormattings"`
+ XMLNSXM string `xml:"xmlns:xm,attr"`
Content string `xml:",innerxml"`
}
-// decodeWorksheetExt directly maps the ext element.
-type decodeWorksheetExt struct {
- XMLName xml.Name `xml:"extLst"`
- Ext []*xlsxWorksheetExt `xml:"ext"`
+// decodeX14ConditionalFormattingRules directly maps the conditionalFormattings
+// element.
+type decodeX14ConditionalFormattingRules struct {
+ XMLName xml.Name `xml:"conditionalFormattings"`
+ XMLNSXM string `xml:"xmlns:xm,attr"`
+ CondFmt []decodeX14ConditionalFormatting `xml:"conditionalFormatting"`
}
-// decodeX14SparklineGroups directly maps the sparklineGroups element.
-type decodeX14SparklineGroups struct {
- XMLName xml.Name `xml:"sparklineGroups"`
- XMLNSXM string `xml:"xmlns:xm,attr"`
+// decodeX14ConditionalFormatting directly maps the conditionalFormatting
+// element.
+type decodeX14ConditionalFormatting struct {
+ XMLName xml.Name `xml:"conditionalFormatting"`
+ CfRule []*decodeX14CfRule `xml:"cfRule"`
+}
+
+// decodeX14CfRule directly maps the cfRule element.
+type decodeX14CfRule struct {
+ XMLName xml.Name `xml:"cfRule"`
+ Type string `xml:"type,attr,omitempty"`
+ ID string `xml:"id,attr,omitempty"`
+ DataBar *decodeX14DataBar `xml:"dataBar"`
+}
+
+// decodeX14DataBar directly maps the dataBar element.
+type decodeX14DataBar struct {
+ XMLName xml.Name `xml:"dataBar"`
+ MaxLength int `xml:"maxLength,attr"`
+ MinLength int `xml:"minLength,attr"`
+ Border bool `xml:"border,attr,omitempty"`
+ Gradient *bool `xml:"gradient,attr"`
+ ShowValue bool `xml:"showValue,attr,omitempty"`
+ Direction string `xml:"direction,attr,omitempty"`
+ Cfvo []*xlsxCfvo `xml:"cfvo"`
+ BorderColor *xlsxColor `xml:"borderColor"`
+ NegativeFillColor *xlsxColor `xml:"negativeFillColor"`
+ AxisColor *xlsxColor `xml:"axisColor"`
+}
+
+// xlsxX14ConditionalFormattings directly maps the conditionalFormattings
+// element.
+type xlsxX14ConditionalFormattings struct {
+ XMLName xml.Name `xml:"x14:conditionalFormattings"`
Content string `xml:",innerxml"`
}
+// xlsxX14ConditionalFormatting directly maps the conditionalFormatting element.
+type xlsxX14ConditionalFormatting struct {
+ XMLName xml.Name `xml:"x14:conditionalFormatting"`
+ XMLNSXM string `xml:"xmlns:xm,attr"`
+ CfRule []*xlsxX14CfRule `xml:"x14:cfRule"`
+}
+
+// xlsxX14CfRule directly maps the cfRule element.
+type xlsxX14CfRule struct {
+ Type string `xml:"type,attr,omitempty"`
+ ID string `xml:"id,attr,omitempty"`
+ DataBar *xlsx14DataBar `xml:"x14:dataBar"`
+}
+
+// xlsx14DataBar directly maps the dataBar element.
+type xlsx14DataBar struct {
+ MaxLength int `xml:"maxLength,attr"`
+ MinLength int `xml:"minLength,attr"`
+ Border bool `xml:"border,attr"`
+ Gradient bool `xml:"gradient,attr"`
+ ShowValue bool `xml:"showValue,attr,omitempty"`
+ Direction string `xml:"direction,attr,omitempty"`
+ Cfvo []*xlsxCfvo `xml:"x14:cfvo"`
+ BorderColor *xlsxColor `xml:"x14:borderColor"`
+ NegativeFillColor *xlsxColor `xml:"x14:negativeFillColor"`
+ AxisColor *xlsxColor `xml:"x14:axisColor"`
+}
+
// xlsxX14SparklineGroups directly maps the sparklineGroups element.
type xlsxX14SparklineGroups struct {
XMLName xml.Name `xml:"x14:sparklineGroups"`
@@ -726,14 +845,14 @@ type xlsxX14SparklineGroup struct {
MinAxisType string `xml:"minAxisType,attr,omitempty"`
MaxAxisType string `xml:"maxAxisType,attr,omitempty"`
RightToLeft bool `xml:"rightToLeft,attr,omitempty"`
- ColorSeries *xlsxTabColor `xml:"x14:colorSeries"`
- ColorNegative *xlsxTabColor `xml:"x14:colorNegative"`
+ ColorSeries *xlsxColor `xml:"x14:colorSeries"`
+ ColorNegative *xlsxColor `xml:"x14:colorNegative"`
ColorAxis *xlsxColor `xml:"x14:colorAxis"`
- ColorMarkers *xlsxTabColor `xml:"x14:colorMarkers"`
- ColorFirst *xlsxTabColor `xml:"x14:colorFirst"`
- ColorLast *xlsxTabColor `xml:"x14:colorLast"`
- ColorHigh *xlsxTabColor `xml:"x14:colorHigh"`
- ColorLow *xlsxTabColor `xml:"x14:colorLow"`
+ ColorMarkers *xlsxColor `xml:"x14:colorMarkers"`
+ ColorFirst *xlsxColor `xml:"x14:colorFirst"`
+ ColorLast *xlsxColor `xml:"x14:colorLast"`
+ ColorHigh *xlsxColor `xml:"x14:colorHigh"`
+ ColorLow *xlsxColor `xml:"x14:colorLow"`
Sparklines xlsxX14Sparklines `xml:"x14:sparklines"`
}
@@ -748,8 +867,26 @@ type xlsxX14Sparkline struct {
Sqref string `xml:"xm:sqref"`
}
-// SparklineOption directly maps the settings of the sparkline.
-type SparklineOption struct {
+// DataValidation directly maps the settings of the data validation rule.
+type DataValidation struct {
+ AllowBlank bool
+ Error *string
+ ErrorStyle *string
+ ErrorTitle *string
+ Operator string
+ Prompt *string
+ PromptTitle *string
+ ShowDropDown bool
+ ShowErrorMessage bool
+ ShowInputMessage bool
+ Sqref string
+ Type string
+ Formula1 string
+ Formula2 string
+}
+
+// SparklineOptions directly maps the settings of the sparkline.
+type SparklineOptions struct {
Location []string
Range []string
Max int
@@ -779,48 +916,55 @@ type SparklineOption struct {
EmptyCells string
}
-// formatPanes directly maps the settings of the panes.
-type formatPanes struct {
- Freeze bool `json:"freeze"`
- Split bool `json:"split"`
- XSplit int `json:"x_split"`
- YSplit int `json:"y_split"`
- TopLeftCell string `json:"top_left_cell"`
- ActivePane string `json:"active_pane"`
- Panes []struct {
- SQRef string `json:"sqref"`
- ActiveCell string `json:"active_cell"`
- Pane string `json:"pane"`
- } `json:"panes"`
-}
-
-// formatConditional directly maps the conditional format settings of the cells.
-type formatConditional struct {
- Type string `json:"type"`
- AboveAverage bool `json:"above_average"`
- Percent bool `json:"percent"`
- Format int `json:"format"`
- Criteria string `json:"criteria"`
- Value string `json:"value,omitempty"`
- Minimum string `json:"minimum,omitempty"`
- Maximum string `json:"maximum,omitempty"`
- MinType string `json:"min_type,omitempty"`
- MidType string `json:"mid_type,omitempty"`
- MaxType string `json:"max_type,omitempty"`
- MinValue string `json:"min_value,omitempty"`
- MidValue string `json:"mid_value,omitempty"`
- MaxValue string `json:"max_value,omitempty"`
- MinColor string `json:"min_color,omitempty"`
- MidColor string `json:"mid_color,omitempty"`
- MaxColor string `json:"max_color,omitempty"`
- MinLength string `json:"min_length,omitempty"`
- MaxLength string `json:"max_length,omitempty"`
- MultiRange string `json:"multi_range,omitempty"`
- BarColor string `json:"bar_color,omitempty"`
-}
-
-// FormatSheetProtection directly maps the settings of worksheet protection.
-type FormatSheetProtection struct {
+// Selection directly maps the settings of the worksheet selection.
+type Selection struct {
+ SQRef string
+ ActiveCell string
+ Pane string
+}
+
+// Panes directly maps the settings of the panes.
+type Panes struct {
+ Freeze bool
+ Split bool
+ XSplit int
+ YSplit int
+ TopLeftCell string
+ ActivePane string
+ Selection []Selection
+}
+
+// ConditionalFormatOptions directly maps the conditional format settings of the cells.
+type ConditionalFormatOptions struct {
+ Type string
+ AboveAverage bool
+ Percent bool
+ Format *int
+ Criteria string
+ Value string
+ MinType string
+ MidType string
+ MaxType string
+ MinValue string
+ MidValue string
+ MaxValue string
+ MinColor string
+ MidColor string
+ MaxColor string
+ BarColor string
+ BarBorderColor string
+ BarDirection string
+ BarOnly bool
+ BarSolid bool
+ IconStyle string
+ ReverseIcons bool
+ IconsOnly bool
+ StopIfTrue bool
+}
+
+// SheetProtectionOptions directly maps the settings of worksheet protection.
+type SheetProtectionOptions struct {
+ AlgorithmName string
AutoFilter bool
DeleteColumns bool
DeleteRows bool
@@ -839,26 +983,200 @@ type FormatSheetProtection struct {
Sort bool
}
-// FormatHeaderFooter directly maps the settings of header and footer.
-type FormatHeaderFooter struct {
- AlignWithMargins bool
+// HeaderFooterOptions directly maps the settings of header and footer.
+type HeaderFooterOptions struct {
+ AlignWithMargins *bool
DifferentFirst bool
DifferentOddEven bool
- ScaleWithDoc bool
+ ScaleWithDoc *bool
OddHeader string
OddFooter string
EvenHeader string
EvenFooter string
- FirstFooter string
FirstHeader string
+ FirstFooter string
+}
+
+// PageLayoutMarginsOptions directly maps the settings of page layout margins.
+type PageLayoutMarginsOptions struct {
+ Bottom *float64
+ Footer *float64
+ Header *float64
+ Left *float64
+ Right *float64
+ Top *float64
+ Horizontally *bool
+ Vertically *bool
+}
+
+// PageLayoutOptions directly maps the settings of page layout.
+type PageLayoutOptions struct {
+ // Size defines the paper size of the worksheet.
+ Size *int
+ // Orientation defines the orientation of page layout for a worksheet.
+ Orientation *string
+ // FirstPageNumber specified the first printed page number. If no value is
+ // specified, then 'automatic' is assumed.
+ FirstPageNumber *uint
+ // AdjustTo defines the print scaling. This attribute is restricted to
+ // value ranging from 10 (10%) to 400 (400%). This setting is overridden
+ // when fitToWidth and/or fitToHeight are in use.
+ AdjustTo *uint
+ // FitToHeight specified the number of vertical pages to fit on.
+ FitToHeight *int
+ // FitToWidth specified the number of horizontal pages to fit on.
+ FitToWidth *int
+ // BlackAndWhite specified print black and white.
+ BlackAndWhite *bool
+ // PageOrder specifies the ordering of multiple pages. Values
+ // accepted: overThenDown and downThenOver
+ PageOrder *string
+}
+
+// ViewOptions directly maps the settings of sheet view.
+type ViewOptions struct {
+ // DefaultGridColor indicating that the consuming application should use
+ // the default grid lines color(system dependent). Overrides any color
+ // specified in colorId.
+ DefaultGridColor *bool
+ // RightToLeft indicating whether the sheet is in 'right to left' display
+ // mode. When in this mode, Column A is on the far right, Column B; is one
+ // column left of Column A, and so on. Also, information in cells is
+ // displayed in the Right to Left format.
+ RightToLeft *bool
+ // ShowFormulas indicating whether this sheet should display formulas.
+ ShowFormulas *bool
+ // ShowGridLines indicating whether this sheet should display grid lines.
+ ShowGridLines *bool
+ // ShowRowColHeaders indicating whether the sheet should display row and
+ // column headings.
+ ShowRowColHeaders *bool
+ // ShowRuler indicating this sheet should display ruler.
+ ShowRuler *bool
+ // ShowZeros indicating whether to "show a zero in cells that have zero
+ // value". When using a formula to reference another cell which is empty,
+ // the referenced value becomes 0 when the flag is true. (Default setting
+ // is true.)
+ ShowZeros *bool
+ // TopLeftCell specifies a location of the top left visible cell Location
+ // of the top left visible cell in the bottom right pane (when in
+ // Left-to-Right mode).
+ TopLeftCell *string
+ // View indicating how sheet is displayed, by default it uses empty string
+ // available options: normal, pageLayout, pageBreakPreview
+ View *string
+ // ZoomScale specifies a window zoom magnification for current view
+ // representing percent values. This attribute is restricted to values
+ // ranging from 10 to 400. Horizontal & Vertical scale together.
+ ZoomScale *float64
}
-// FormatPageMargins directly maps the settings of page margins
-type FormatPageMargins struct {
- Bottom string
- Footer string
- Header string
- Left string
- Right string
- Top string
+// SheetPropsOptions provides a function to set worksheet properties. There 4
+// kinds of presets "Custom Scaling Options" in the spreadsheet applications, if
+// you need to set those kind of scaling options, please using the
+// "SetSheetProps" and "SetPageLayout" functions to approach these 4 scaling
+// options:
+//
+// 1. No Scaling (Print sheets at their actual size):
+//
+// disable := false
+// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
+// FitToPage: &disable,
+// }); err != nil {
+// fmt.Println(err)
+// }
+//
+// 2. Fit Sheet on One Page (Shrink the printout so that it fits on one page):
+//
+// enable := true
+// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
+// FitToPage: &enable,
+// }); err != nil {
+// fmt.Println(err)
+// }
+//
+// 3. Fit All Columns on One Page (Shrink the printout so that it is one page
+// wide):
+//
+// enable, zero := true, 0
+// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
+// FitToPage: &enable,
+// }); err != nil {
+// fmt.Println(err)
+// }
+// if err := f.SetPageLayout("Sheet1", &excelize.PageLayoutOptions{
+// FitToHeight: &zero,
+// }); err != nil {
+// fmt.Println(err)
+// }
+//
+// 4. Fit All Rows on One Page (Shrink the printout so that it is one page
+// high):
+//
+// enable, zero := true, 0
+// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{
+// FitToPage: &enable,
+// }); err != nil {
+// fmt.Println(err)
+// }
+// if err := f.SetPageLayout("Sheet1", &excelize.PageLayoutOptions{
+// FitToWidth: &zero,
+// }); err != nil {
+// fmt.Println(err)
+// }
+type SheetPropsOptions struct {
+ // Specifies a stable name of the sheet, which should not change over time,
+ // and does not change from user input. This name should be used by code
+ // to reference a particular sheet.
+ CodeName *string
+ // EnableFormatConditionsCalculation indicating whether the conditional
+ // formatting calculations shall be evaluated. If set to false, then the
+ // min/max values of color scales or data bars or threshold values in Top N
+ // rules shall not be updated. Essentially the conditional
+ // formatting "calc" is off.
+ EnableFormatConditionsCalculation *bool
+ // Published indicating whether the worksheet is published.
+ Published *bool
+ // AutoPageBreaks indicating whether the sheet displays Automatic Page
+ // Breaks.
+ AutoPageBreaks *bool
+ // FitToPage indicating whether the Fit to Page print option is enabled.
+ FitToPage *bool
+ // TabColorIndexed represents the indexed color value.
+ TabColorIndexed *int
+ // TabColorRGB represents the standard Alpha Red Green Blue color value.
+ TabColorRGB *string
+ // TabColorTheme represents the zero-based index into the collection,
+ // referencing a particular value expressed in the Theme part.
+ TabColorTheme *int
+ // TabColorTint specifies the tint value applied to the color.
+ TabColorTint *float64
+ // OutlineSummaryBelow indicating whether summary rows appear below detail
+ // in an outline, when applying an outline.
+ OutlineSummaryBelow *bool
+ // OutlineSummaryRight indicating whether summary columns appear to the
+ // right of detail in an outline, when applying an outline.
+ OutlineSummaryRight *bool
+ // BaseColWidth specifies the number of characters of the maximum digit
+ // width of the normal style's font. This value does not include margin
+ // padding or extra padding for grid lines. It is only the number of
+ // characters.
+ BaseColWidth *uint8
+ // DefaultColWidth specifies the default column width measured as the
+ // number of characters of the maximum digit width of the normal style's
+ // font.
+ DefaultColWidth *float64
+ // DefaultRowHeight specifies the default row height measured in point
+ // size. Optimization so we don't have to write the height on all rows.
+ // This can be written out if most rows have custom height, to achieve the
+ // optimization.
+ DefaultRowHeight *float64
+ // CustomHeight specifies the custom height.
+ CustomHeight *bool
+ // ZeroHeight specifies if rows are hidden.
+ ZeroHeight *bool
+ // ThickTop specifies if rows have a thick top border by default.
+ ThickTop *bool
+ // ThickBottom specifies if rows have a thick bottom border by default.
+ ThickBottom *bool
}